Upgrade dependencies

This commit is contained in:
Ingo Oppermann 2024-05-28 14:32:25 +02:00
parent 3ee4876290
commit 32ccfc24ee
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
260 changed files with 15224 additions and 6147 deletions

53
go.mod
View File

@ -1,24 +1,24 @@
module github.com/datarhei/core/v16
go 1.21
go 1.21.0
toolchain go1.22.1
require (
github.com/99designs/gqlgen v0.17.45
github.com/99designs/gqlgen v0.17.47
github.com/Masterminds/semver/v3 v3.2.1
github.com/adhocore/gronx v1.8.1
github.com/andybalholm/brotli v1.1.0
github.com/atrox/haikunatorgo/v2 v2.0.1
github.com/caddyserver/certmagic v0.20.0
github.com/casbin/casbin/v2 v2.88.0
github.com/caddyserver/certmagic v0.21.2
github.com/casbin/casbin/v2 v2.89.0
github.com/datarhei/core-client-go/v16 v16.11.1-0.20240429143858-23ad5985b894
github.com/datarhei/gosrt v0.6.0
github.com/datarhei/joy4 v0.0.0-20240229100136-43bcaf8ef5e7
github.com/datarhei/joy4 v0.0.0-20240528121836-da80d79b6435
github.com/fujiwara/shapeio v1.0.0
github.com/go-playground/validator/v10 v10.19.0
github.com/go-playground/validator/v10 v10.20.0
github.com/gobwas/glob v0.2.3
github.com/goccy/go-json v0.10.2
github.com/goccy/go-json v0.10.3
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/gops v0.3.28
github.com/google/uuid v1.6.0
@ -35,18 +35,18 @@ require (
github.com/mattn/go-isatty v0.0.20
github.com/minio/minio-go/v7 v7.0.70
github.com/prep/average v0.0.0-20200506183628-d26c465f48c3
github.com/prometheus/client_golang v1.19.0
github.com/prometheus/client_golang v1.19.1
github.com/puzpuzpuz/xsync/v3 v3.1.0
github.com/shirou/gopsutil/v3 v3.24.3
github.com/shirou/gopsutil/v3 v3.24.4
github.com/stretchr/testify v1.9.0
github.com/swaggo/echo-swagger v1.4.1
github.com/swaggo/swag v1.16.3
github.com/vektah/gqlparser/v2 v2.5.11
github.com/vektah/gqlparser/v2 v2.5.12
github.com/xeipuuv/gojsonschema v1.2.0
go.etcd.io/bbolt v1.3.9
go.etcd.io/bbolt v1.3.10
go.uber.org/automaxprocs v1.5.3
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.22.0
golang.org/x/crypto v0.23.0
golang.org/x/mod v0.17.0
)
@ -59,13 +59,14 @@ require (
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boltdb/bolt v1.3.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/casbin/govaluate v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
@ -86,10 +87,10 @@ require (
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/lufia/plan9stats v0.0.0-20240408141607-282e7b5d6b74 // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mholt/acmez v1.2.0 // indirect
github.com/mholt/acmez/v2 v2.0.1 // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -98,30 +99,30 @@ require (
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.53.0 // indirect
github.com/prometheus/procfs v0.14.0 // indirect
github.com/prometheus/procfs v0.15.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sosodev/duration v1.3.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/urfave/cli/v2 v2.27.1 // indirect
github.com/urfave/cli/v2 v2.27.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.20.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/tools v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

108
go.sum
View File

@ -1,12 +1,12 @@
github.com/99designs/gqlgen v0.17.45 h1:bH0AH67vIJo8JKNKPJP+pOPpQhZeuVRQLf53dKIpDik=
github.com/99designs/gqlgen v0.17.45/go.mod h1:Bas0XQ+Jiu/Xm5E33jC8sES3G+iC2esHBMXcq0fUPs0=
github.com/99designs/gqlgen v0.17.47 h1:M9DTK8X3+3ATNBfZlHBwMwNngn4hhZWDxNmTiuQU5tQ=
github.com/99designs/gqlgen v0.17.47/go.mod h1:ejVkldSdtmuudqmtfaiqjwlGXWAhIv0DKXGXFY25F04=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/adhocore/gronx v1.8.1 h1:F2mLTG5sB11z7vplwD4iydz3YCEjstSfYmCrdSm3t6A=
github.com/adhocore/gronx v1.8.1/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
@ -35,10 +35,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
github.com/casbin/casbin/v2 v2.88.0 h1:JFHId/aIFvNvPnTwUP+tTtVAjSh3eidslFzy+5LpSeU=
github.com/casbin/casbin/v2 v2.88.0/go.mod h1:jX8uoN4veP85O/n2674r2qtfSXI6myvxW85f6TH50fw=
github.com/caddyserver/certmagic v0.21.2 h1:O18LtaYBGDooyy257cYePnhp4lPfz6TaJELil6Q1fDg=
github.com/caddyserver/certmagic v0.21.2/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/casbin/casbin/v2 v2.89.0 h1:XpgheobgazzxruVClvyNRMyAn+l1g9O4LY6XAgtaDkg=
github.com/casbin/casbin/v2 v2.89.0/go.mod h1:jX8uoN4veP85O/n2674r2qtfSXI6myvxW85f6TH50fw=
github.com/casbin/govaluate v1.1.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.1.1 h1:J1rFKIBhiC5xr0APd5HP6rDL+xt+BRoyq1pa4o2i/5c=
github.com/casbin/govaluate v1.1.1/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@ -47,16 +49,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20240424105158-86a7f261b92c h1:RIzMclqmSYwpMZxyW7nLg0XyKjY6prQWcuIdgm97U8o=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20240424105158-86a7f261b92c/go.mod h1:7XrUOUlB165Gs8JUE4lzVuNve6HW90Yz3/+lTY2EV4A=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20240429143858-23ad5985b894 h1:ZQCTobOGpzfuZxgMWsZviFSXfI5QuttkTgPQz1PKbhU=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20240429143858-23ad5985b894/go.mod h1:Mu2bHqvGJe46KvAhY2ElohuQYhHB64PZeaCNDv6C5b8=
github.com/datarhei/gosrt v0.6.0 h1:HrrXAw90V78ok4WMIhX6se1aTHPCn82Sg2hj+PhdmGc=
github.com/datarhei/gosrt v0.6.0/go.mod h1:fsOWdLSHUHShHjgi/46h6wjtdQrtnSdAQFnlas8ONxs=
github.com/datarhei/joy4 v0.0.0-20240229100136-43bcaf8ef5e7 h1:MG5XQMTTDPcuvvRzc1c37QbwgDbYPhKmPFo9gSaPdBE=
github.com/datarhei/joy4 v0.0.0-20240229100136-43bcaf8ef5e7/go.mod h1:Jcw/6jZDQQmPx8A7INEkXmuEF7E9jjBbSTfVSLwmiQw=
github.com/datarhei/joy4 v0.0.0-20240528121836-da80d79b6435 h1:bXcqdyQWtKyb1i82qLMqi4DxbVrZMpk0oVmKtWJjWhg=
github.com/datarhei/joy4 v0.0.0-20240528121836-da80d79b6435/go.mod h1:Jcw/6jZDQQmPx8A7INEkXmuEF7E9jjBbSTfVSLwmiQw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -66,12 +66,12 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fujiwara/shapeio v1.0.0 h1:xG5D9oNqCSUUbryZ/jQV3cqe1v2suEjwPIcEg1gKM8M=
github.com/fujiwara/shapeio v1.0.0/go.mod h1:LmEmu6L/8jetyj1oewewFb7bZCNRwE7wLCUNzDLaLVA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -95,13 +95,13 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
@ -194,8 +194,8 @@ github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfs
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20240408141607-282e7b5d6b74 h1:1KuuSOy4ZNgW0KA2oYIngXVFhQcXxhLqCVK7cBcldkk=
github.com/lufia/plan9stats v0.0.0-20240408141607-282e7b5d6b74/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -208,8 +208,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k=
github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
@ -242,8 +242,8 @@ github.com/prep/average v0.0.0-20200506183628-d26c465f48c3/go.mod h1:0ZE5gcyWKS1
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -256,8 +256,8 @@ github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@ -268,16 +268,16 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE=
github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg=
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sosodev/duration v1.3.0 h1:g3E6mto+hFdA2uZXeNDYff8LYeg7v5D4YKP/Ng/NUkE=
github.com/sosodev/duration v1.3.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@ -306,14 +306,14 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
github.com/vektah/gqlparser/v2 v2.5.12 h1:COMhVVnql6RoaF7+aTBWiTADdpLGyZWU3K/NwW0ph98=
github.com/vektah/gqlparser/v2 v2.5.12/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -321,8 +321,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
@ -331,8 +331,8 @@ github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -343,15 +343,15 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -376,21 +376,21 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -82,13 +82,15 @@ func New(config Config) (Manager, error) {
func (am *access) HasPolicy(name, domain string, types []string, resource string, actions []string) bool {
policy := []string{name, domain, EncodeResource(types, resource), EncodeActions(actions)}
return am.enforcer.HasPolicy(policy)
hasPolicy, _ := am.enforcer.HasPolicy(policy)
return hasPolicy
}
func (am *access) AddPolicy(name, domain string, types []string, resource string, actions []string) error {
policy := []string{name, domain, EncodeResource(types, resource), EncodeActions(actions)}
if am.enforcer.HasPolicy(policy) {
if hasPolicy, _ := am.enforcer.HasPolicy(policy); hasPolicy {
return nil
}
@ -98,8 +100,12 @@ func (am *access) AddPolicy(name, domain string, types []string, resource string
}
func (am *access) RemovePolicy(name, domain string, types []string, resource string, actions []string) error {
policies := am.enforcer.GetFilteredPolicy(0, name, domain, EncodeResource(types, resource), EncodeActions(actions))
_, err := am.enforcer.RemovePolicies(policies)
policies, err := am.enforcer.GetFilteredPolicy(0, name, domain, EncodeResource(types, resource), EncodeActions(actions))
if err != nil {
return err
}
_, err = am.enforcer.RemovePolicies(policies)
return err
}
@ -107,7 +113,10 @@ func (am *access) RemovePolicy(name, domain string, types []string, resource str
func (am *access) ListPolicies(name, domain string, types []string, resource string, actions []string) []Policy {
policies := []Policy{}
ps := am.enforcer.GetFilteredPolicy(0, name, domain, EncodeResource(types, resource), EncodeActions(actions))
ps, err := am.enforcer.GetFilteredPolicy(0, name, domain, EncodeResource(types, resource), EncodeActions(actions))
if err != nil {
return policies
}
for _, p := range ps {
types, resource := DecodeResource(p[2])

View File

@ -18,3 +18,8 @@ gqlgen
*.exe
node_modules
# generated files
/api/testdata/default/graph/generated.go
/api/testdata/federation2/graph/federation.go
/api/testdata/federation2/graph/generated.go

View File

@ -1,11 +1,25 @@
run:
tests: true
skip-dirs:
- bin
linters-settings:
errcheck:
ignore: fmt:.*,[rR]ead|[wW]rite|[cC]lose,io:Copy
exclude-functions:
- (io.Writer).Write
- io.Copy
- io.WriteString
revive:
enable-all-rules: false
rules:
- name: empty-lines
testifylint:
disable-all: true
enable:
- bool-compare
- compares
- error-is-as
- error-nil
- expected-actual
- nil-compare
linters:
disable-all: true
@ -22,14 +36,19 @@ linters:
- misspell
- nakedret
- prealloc
- revive
- staticcheck
- testifylint
- typecheck
- unconvert
- unused
issues:
exclude-dirs:
- bin
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- dupl
- errcheck

File diff suppressed because it is too large Load Diff

View File

@ -20,9 +20,9 @@ Still not convinced enough to use **gqlgen**? Compare **gqlgen** with other Go g
cd example
go mod init example
2. Add `github.com/99designs/gqlgen` to your [project's tools.go](https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module)
2. Add `github.com/99designs/gqlgen` to your [project's tools.go](https://go.dev/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module)
printf '// +build tools\npackage tools\nimport (_ "github.com/99designs/gqlgen"\n _ "github.com/99designs/gqlgen/graphql/introspection")' | gofmt > tools.go
printf '//go:build tools\npackage tools\nimport (_ "github.com/99designs/gqlgen"\n _ "github.com/99designs/gqlgen/graphql/introspection")' | gofmt > tools.go
go mod tidy
@ -135,6 +135,7 @@ models:
model:
- github.com/99designs/gqlgen/graphql.IntID # a go integer
- github.com/99designs/gqlgen/graphql.ID # or a go string
- github.com/99designs/gqlgen/graphql.UintID # or a go uint
```
This means gqlgen will be able to automatically bind to strings or ints for models you have written yourself, but the

View File

@ -6,10 +6,9 @@ Assuming the next version is $NEW_VERSION=v0.16.0 or something like that.
./bin/release $NEW_VERSION
```
2. git-chglog -o CHANGELOG.md
3. go generate ./...; cd _examples; go generate ./...; cd ..
3. go generate ./...
4. git commit and push the CHANGELOG.md
5. Go to https://github.com/99designs/gqlgen/releases and draft new release, autogenerate the release notes, and Create a discussion for this release
6. Comment on the release discussion with any really important notes (breaking changes)
I used https://github.com/git-chglog/git-chglog to automate the changelog maintenance process for now. We could just as easily use go releaser to make the whole thing automated.

View File

@ -13,6 +13,11 @@ import (
"github.com/99designs/gqlgen/plugin/resolvergen"
)
var (
urlRegex = regexp.MustCompile(`(?s)@link.*\(.*url:.*?"(.*?)"[^)]+\)`) // regex to grab the url of a link directive, should it exist
versionRegex = regexp.MustCompile(`v(\d+).(\d+)$`) // regex to grab the version number from a url
)
func Generate(cfg *config.Config, option ...Option) error {
_ = syscall.Unlink(cfg.Exec.Filename)
if cfg.Model.IsDefined() {
@ -26,8 +31,6 @@ func Generate(cfg *config.Config, option ...Option) error {
plugins = append(plugins, resolvergen.New())
if cfg.Federation.IsDefined() {
if cfg.Federation.Version == 0 { // default to using the user's choice of version, but if unset, try to sort out which federation version to use
urlRegex := regexp.MustCompile(`(?s)@link.*\(.*url:.*?"(.*?)"[^)]+\)`) // regex to grab the url of a link directive, should it exist
versionRegex := regexp.MustCompile(`v(\d+).(\d+)$`) // regex to grab the version number from a url
// check the sources, and if one is marked as federation v2, we mark the entirety to be generated using that format
for _, v := range cfg.Sources {
cfg.Federation.Version = 1

View File

@ -5,6 +5,7 @@ import (
"fmt"
"go/token"
"go/types"
"strings"
"github.com/vektah/gqlparser/v2/ast"
"golang.org/x/tools/go/packages"
@ -204,6 +205,7 @@ type TypeReference struct {
IsContext bool // Is the Marshaler/Unmarshaller the context version; applies to either the method or interface variety.
PointersInUmarshalInput bool // Inverse values and pointers in return.
IsRoot bool // Is the type a root level definition such as Query, Mutation or Subscription
EnumValues []EnumValueReference
}
func (ref *TypeReference) Elem() *TypeReference {
@ -321,6 +323,10 @@ func (ref *TypeReference) IsTargetNilable() bool {
return IsNilable(ref.Target)
}
func (ref *TypeReference) HasEnumValues() bool {
return len(ref.EnumValues) > 0
}
func (b *Binder) PushRef(ret *TypeReference) {
b.References = append(b.References, ret)
}
@ -428,7 +434,12 @@ func (b *Binder) TypeReference(schemaType *ast.Type, bindTarget types.Type) (ret
return nil, err
}
if fun, isFunc := obj.(*types.Func); isFunc {
if values := b.enumValues(def); len(values) > 0 {
err = b.enumReference(ref, obj, values)
if err != nil {
return nil, err
}
} else if fun, isFunc := obj.(*types.Func); isFunc {
ref.GO = fun.Type().(*types.Signature).Params().At(0).Type()
ref.IsContext = fun.Type().(*types.Signature).Results().At(0).Type().String() == "github.com/99designs/gqlgen/graphql.ContextMarshaler"
ref.Marshaler = fun
@ -548,3 +559,81 @@ func basicUnderlying(it types.Type) *types.Basic {
return nil
}
type EnumValueReference struct {
Definition *ast.EnumValueDefinition
Object types.Object
}
func (b *Binder) enumValues(def *ast.Definition) map[string]EnumValue {
if def.Kind != ast.Enum {
return nil
}
if strings.HasPrefix(def.Name, "__") {
return nil
}
model, ok := b.cfg.Models[def.Name]
if !ok {
return nil
}
return model.EnumValues
}
func (b *Binder) enumReference(ref *TypeReference, obj types.Object, values map[string]EnumValue) error {
if len(ref.Definition.EnumValues) != len(values) {
return fmt.Errorf("not all enum values are binded for %v", ref.Definition.Name)
}
if fn, ok := obj.Type().(*types.Signature); ok {
ref.GO = fn.Params().At(0).Type()
} else {
ref.GO = obj.Type()
}
str, err := b.TypeReference(&ast.Type{NamedType: "String"}, nil)
if err != nil {
return err
}
ref.Marshaler = str.Marshaler
ref.Unmarshaler = str.Unmarshaler
ref.EnumValues = make([]EnumValueReference, 0, len(values))
for _, value := range ref.Definition.EnumValues {
v, ok := values[value.Name]
if !ok {
return fmt.Errorf("enum value not found for: %v, of enum: %v", value.Name, ref.Definition.Name)
}
pkgName, typeName := code.PkgAndType(v.Value)
if pkgName == "" {
return fmt.Errorf("missing package name for %v", value.Name)
}
valueObj, err := b.FindObject(pkgName, typeName)
if err != nil {
return err
}
if !types.AssignableTo(valueObj.Type(), ref.GO) {
return fmt.Errorf("wrong type: %v, for enum value: %v, expected type: %v, of enum: %v",
valueObj.Type(), value.Name, ref.GO, ref.Definition.Name)
}
switch valueObj.(type) {
case *types.Const, *types.Var:
ref.EnumValues = append(ref.EnumValues, EnumValueReference{
Definition: value,
Object: valueObj,
})
default:
return fmt.Errorf("unsupported enum value for: %v, of enum: %v, only const and var allowed",
value.Name, ref.Definition.Name)
}
}
return nil
}

View File

@ -275,6 +275,10 @@ func (c *Config) injectTypesFromSchema() error {
SkipRuntime: true,
}
c.Directives["goExtraField"] = DirectiveConfig{
SkipRuntime: true,
}
c.Directives["goField"] = DirectiveConfig{
SkipRuntime: true,
}
@ -283,6 +287,10 @@ func (c *Config) injectTypesFromSchema() error {
SkipRuntime: true,
}
c.Directives["goEnum"] = DirectiveConfig{
SkipRuntime: true,
}
for _, schemaType := range c.Schema.Types {
if c.IsRoot(schemaType) {
continue
@ -342,6 +350,82 @@ func (c *Config) injectTypesFromSchema() error {
}
}
}
if efds := schemaType.Directives.ForNames("goExtraField"); len(efds) != 0 {
for _, efd := range efds {
if fn := efd.Arguments.ForName("name"); fn != nil {
extraFieldName := ""
if fnv, err := fn.Value.Value(nil); err == nil {
extraFieldName = fnv.(string)
}
if extraFieldName == "" {
return fmt.Errorf(
"argument 'name' for directive @goExtraField (src: %s, line: %d) cannot by empty",
efd.Position.Src.Name,
efd.Position.Line,
)
}
extraField := ModelExtraField{}
if t := efd.Arguments.ForName("type"); t != nil {
if tv, err := t.Value.Value(nil); err == nil {
extraField.Type = tv.(string)
}
}
if extraField.Type == "" {
return fmt.Errorf(
"argument 'type' for directive @goExtraField (src: %s, line: %d) cannot by empty",
efd.Position.Src.Name,
efd.Position.Line,
)
}
if ot := efd.Arguments.ForName("overrideTags"); ot != nil {
if otv, err := ot.Value.Value(nil); err == nil {
extraField.OverrideTags = otv.(string)
}
}
if d := efd.Arguments.ForName("description"); d != nil {
if dv, err := d.Value.Value(nil); err == nil {
extraField.Description = dv.(string)
}
}
typeMapEntry := c.Models[schemaType.Name]
if typeMapEntry.ExtraFields == nil {
typeMapEntry.ExtraFields = make(map[string]ModelExtraField)
}
c.Models[schemaType.Name] = typeMapEntry
c.Models[schemaType.Name].ExtraFields[extraFieldName] = extraField
}
}
}
}
if schemaType.Kind == ast.Enum && !strings.HasPrefix(schemaType.Name, "__") {
values := make(map[string]EnumValue)
for _, value := range schemaType.EnumValues {
if directive := value.Directives.ForName("goEnum"); directive != nil {
if arg := directive.Arguments.ForName("value"); arg != nil {
if v, err := arg.Value.Value(nil); err == nil {
values[value.Name] = EnumValue{
Value: v.(string),
}
}
}
}
}
if len(values) > 0 {
model := c.Models[schemaType.Name]
model.EnumValues = values
c.Models[schemaType.Name] = model
}
}
}
@ -352,6 +436,7 @@ type TypeMapEntry struct {
Model StringList `yaml:"model,omitempty"`
ForceGenerate bool `yaml:"forceGenerate,omitempty"`
Fields map[string]TypeMapField `yaml:"fields,omitempty"`
EnumValues map[string]EnumValue `yaml:"enum_values,omitempty"`
// Key is the Go name of the field.
ExtraFields map[string]ModelExtraField `yaml:"extraFields,omitempty"`
@ -363,6 +448,10 @@ type TypeMapField struct {
GeneratedMethod string `yaml:"-"`
}
type EnumValue struct {
Value string
}
type ModelExtraField struct {
// Type is the Go type of the field.
//
@ -518,6 +607,14 @@ func (tm TypeMap) Check() error {
return fmt.Errorf("model %s: invalid type specifier \"%s\" - you need to specify a struct to map to", typeName, entry.Model)
}
}
if len(entry.Model) == 0 {
for enum, v := range entry.EnumValues {
if v.Value != "" {
return fmt.Errorf("model is empty for: %v, but enum value is specified for %v", typeName, enum)
}
}
}
}
return nil
}

View File

@ -124,7 +124,6 @@ func (b *builder) getDirectives(list ast.DirectiveList) ([]*Directive, error) {
DirectiveDefinition: list[i].Definition,
Builtin: b.Config.Directives[d.Name].SkipRuntime,
}
}
return dirs, nil

View File

@ -287,7 +287,7 @@ func (b *builder) findBindStructTagTarget(in types.Type, name string) (types.Obj
tags := reflect.StructTag(t.Tag(i))
if val, ok := tags.Lookup(b.Config.StructTag); ok && equalFieldName(val, name) {
if found != nil {
return nil, fmt.Errorf("tag %s is ambigious; multiple fields have the same tag value of %s", b.Config.StructTag, val)
return nil, fmt.Errorf("tag %s is ambiguous; multiple fields have the same tag value of %s", b.Config.StructTag, val)
}
found = field

View File

@ -21,7 +21,7 @@ func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Contex
res := &{{ $field.TypeReference.Elem.GO | ref }}{}
{{- else }}
res := {{ $field.TypeReference.GO | ref }}{}
{{- end }}
{{- end }}
fc.Result = res
return ec.{{ $field.TypeReference.MarshalFunc }}(ctx, field.Selections, res)
{{- else}}
@ -72,7 +72,7 @@ func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Contex
{{- end }}
}
func (ec *executionContext) {{ $field.FieldContextFunc }}(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
func (ec *executionContext) {{ $field.FieldContextFunc }}({{ if not $field.Args }}_{{ else }}ctx{{ end }} context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: {{quote $field.Object.Name}},
Field: field,

View File

@ -48,7 +48,7 @@ func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.Selec
{{- if $field.IsConcurrent }}
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
innerFunc := func(ctx context.Context, {{ if $field.TypeReference.GQL.NonNull }}fs{{ else }}_{{ end }} *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))

View File

@ -201,6 +201,7 @@ func Funcs() template.FuncMap {
"rawQuote": rawQuote,
"dump": Dump,
"ref": ref,
"obj": obj,
"ts": TypeIdentifier,
"call": Call,
"prefixLines": prefixLines,
@ -247,6 +248,15 @@ func ref(p types.Type) string {
return CurrentImports.LookupType(p)
}
func obj(obj types.Object) string {
pkg := CurrentImports.Lookup(obj.Pkg().Path())
if pkg != "" {
pkg += "."
}
return pkg + obj.Name()
}
func Call(p *types.Func) string {
pkg := CurrentImports.Lookup(p.Pkg().Path())

View File

@ -34,7 +34,10 @@
return &pres, nil
{{- else }}
{{- if $type.Unmarshaler }}
{{- if $type.CastType }}
{{- if $type.HasEnumValues }}
tmp, err := {{ $type.Unmarshaler | call }}(v)
res := {{ $type.UnmarshalFunc }}[tmp]
{{- else if $type.CastType }}
{{- if $type.IsContext }}
tmp, err := {{ $type.Unmarshaler | call }}(ctx, v)
{{- else }}
@ -170,7 +173,12 @@
{{- else if and (not $type.IsTargetNilable) $type.IsNilable }}
{{- $v = "*v" }}
{{- end }}
res := {{ $type.Marshaler | call }}({{- if $type.CastType }}{{ $type.CastType | ref }}({{ $v }}){{else}}{{ $v }}{{- end }})
{{- if $type.HasEnumValues }}
{{- $v = printf "%v[%v]" $type.MarshalFunc $v }}
{{- else if $type.CastType }}
{{- $v = printf "%v(%v)" ($type.CastType | ref) $v}}
{{- end }}
res := {{ $type.Marshaler | call }}({{ $v }})
{{- if $type.GQL.NonNull }}
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@ -196,4 +204,23 @@
{{- end }}
}
{{- end }}
{{- if $type.HasEnumValues }}
{{- $enum := $type.GO }}
{{- if $type.IsNilable }}
{{- $enum = $type.GO.Elem }}
{{- end }}
var (
{{ $type.UnmarshalFunc }} = map[string]{{ $enum | ref }}{
{{- range $value := $type.EnumValues }}
"{{ $value.Definition.Name }}": {{ $value.Object | obj }},
{{- end }}
}
{{ $type.MarshalFunc }} = map[{{ $enum | ref }}]string{
{{- range $value := $type.EnumValues }}
{{ $value.Object | obj }}: "{{ $value.Definition.Name }}",
{{- end }}
}
)
{{- end }}
{{- end }}

View File

@ -1,3 +0,0 @@
#!/usr/bin/env sh
cd ./_examples
go generate ./... || return 0

View File

@ -34,7 +34,6 @@ func (fic *PathContext) Path() ast.Path {
if fic.ParentField != nil {
fieldPath := fic.ParentField.Path()
return append(fieldPath, path...)
}
return path

View File

@ -107,7 +107,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
resp := &graphql.Response{Errors: []*gqlerror.Error{gqlErr}}
b, _ := json.Marshal(resp)
w.WriteHeader(http.StatusUnprocessableEntity)
w.Write(b)
_, _ = w.Write(b)
}
}()
@ -128,7 +128,7 @@ func sendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) {
if err != nil {
panic(err)
}
w.Write(b)
_, _ = w.Write(b)
}
func sendErrorf(w http.ResponseWriter, code int, format string, args ...interface{}) {

View File

@ -18,7 +18,7 @@ func SendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) {
if err != nil {
panic(err)
}
w.Write(b)
_, _ = w.Write(b)
}
// SendErrorf wraps SendError to add formatted messages

View File

@ -103,7 +103,7 @@ func (t Websocket) Do(w http.ResponseWriter, r *http.Request, exec graphql.Graph
switch ws.Subprotocol() {
default:
msg := websocket.FormatCloseMessage(websocket.CloseProtocolError, fmt.Sprintf("unsupported negotiated subprotocol %s", ws.Subprotocol()))
ws.WriteMessage(websocket.CloseMessage, msg)
_ = ws.WriteMessage(websocket.CloseMessage, msg)
return
case graphqlwsSubprotocol, "":
// clients are required to send a subprotocol, to be backward compatible with the previous implementation we select
@ -272,7 +272,7 @@ func (c *wsConnection) run() {
if !c.MissingPongOk {
// Note: when the connection is closed by this deadline, the client
// will receive an "invalid close code"
c.conn.SetReadDeadline(time.Now().UTC().Add(2 * c.PingPongInterval))
_ = c.conn.SetReadDeadline(time.Now().UTC().Add(2 * c.PingPongInterval))
}
go c.ping(ctx)
}
@ -312,7 +312,7 @@ func (c *wsConnection) run() {
c.receivedPong = true
c.mu.Unlock()
// Clear ReadTimeout -- 0 time val clears.
c.conn.SetReadDeadline(time.Time{})
_ = c.conn.SetReadDeadline(time.Time{})
default:
c.sendConnectionError("unexpected message %s", m.t)
c.close(websocket.CloseProtocolError, "unexpected message")
@ -357,7 +357,7 @@ func (c *wsConnection) ping(ctx context.Context) {
// if we have not yet received a pong, don't reset the deadline.
c.mu.Lock()
if !c.MissingPongOk && c.receivedPong {
c.conn.SetReadDeadline(time.Now().UTC().Add(2 * c.PingPongInterval))
_ = c.conn.SetReadDeadline(time.Now().UTC().Add(2 * c.PingPongInterval))
}
c.receivedPong = false
c.mu.Unlock()

View File

@ -56,3 +56,32 @@ func UnmarshalIntID(v interface{}) (int, error) {
return 0, fmt.Errorf("%T is not an int", v)
}
}
func MarshalUintID(i uint) Marshaler {
return WriterFunc(func(w io.Writer) {
writeQuotedString(w, strconv.FormatUint(uint64(i), 10))
})
}
func UnmarshalUintID(v interface{}) (uint, error) {
switch v := v.(type) {
case string:
result, err := strconv.ParseUint(v, 10, 64)
return uint(result), err
case int:
return uint(v), nil
case int64:
return uint(v), nil
case int32:
return uint(v), nil
case uint32:
return uint(v), nil
case uint64:
return uint(v), nil
case json.Number:
result, err := strconv.ParseUint(string(v), 10, 64)
return uint(result), err
default:
return 0, fmt.Errorf("%T is not an uint", v)
}
}

View File

@ -1,3 +1,3 @@
package graphql
const Version = "v0.17.45"
const Version = "v0.17.47"

View File

@ -21,12 +21,10 @@ var (
var mode = packages.NeedName |
packages.NeedFiles |
packages.NeedImports |
packages.NeedTypes |
packages.NeedSyntax |
packages.NeedTypesInfo |
packages.NeedModule |
packages.NeedDeps
packages.NeedModule
type (
// Packages is a wrapper around x/tools/go/packages that maintains a (hopefully prewarmed) cache of packages
@ -135,11 +133,6 @@ func (p *Packages) LoadAll(importPaths ...string) []*packages.Package {
func (p *Packages) addToCache(pkg *packages.Package) {
imp := NormalizeVendor(pkg.PkgPath)
p.packages[imp] = pkg
for _, imp := range pkg.Imports {
if _, found := p.packages[NormalizeVendor(imp.PkgPath)]; !found {
p.addToCache(imp)
}
}
}
// Load works the same as LoadAll, except a single package at a time.
@ -220,18 +213,9 @@ func (p *Packages) NameForPackage(importPath string) string {
return pkg.Name
}
// Evict removes a given package import path from the cache, along with any packages that depend on it. Further calls
// to Load will fetch it from disk.
// Evict removes a given package import path from the cache. Further calls to Load will fetch it from disk.
func (p *Packages) Evict(importPath string) {
delete(p.packages, importPath)
for _, pkg := range p.packages {
for _, imported := range pkg.Imports {
if imported.PkgPath == importPath {
p.Evict(pkg.PkgPath)
}
}
}
}
func (p *Packages) ModTidy() error {

View File

@ -63,7 +63,6 @@ func (r *Rewriter) getFile(filename string) string {
}
r.files[filename] = string(b)
}
return r.files[filename]

View File

@ -1,6 +1,6 @@
package main
//go:generate sh generate_examples.sh
//go:generate sh -c "cd _examples && go generate ./..."
import (
"bytes"

View File

@ -182,7 +182,6 @@ func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source {
var entities, resolvers, entityResolverInputDefinitions string
for _, e := range f.Entities {
if e.Def.Kind != ast.Interface {
if entities != "" {
entities += " | "
@ -329,7 +328,6 @@ func (f *federation) GenerateCode(data *codegen.Data) error {
// add type info to entity
e.Type = obj.Type
}
}
@ -416,7 +414,6 @@ func (f *federation) GenerateCode(data *codegen.Data) error {
if err != nil {
return err
}
}
return templates.Render(templates.Options{

View File

@ -253,19 +253,30 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati
ok bool
)
_ = val
// if all of the KeyFields values for this resolver are null,
// we shouldn't use use it
allNull := true
{{- range $_, $keyField := .KeyFields }}
m = rep
{{- range $i, $field := .Field }}
if {{ if (ne $i $keyField.Field.LastIndex ) -}}val{{- else -}}_{{- end -}}, ok = m["{{.}}"]; !ok {
val, ok = m["{{.}}"]
if !ok {
break
}
{{- if (ne $i $keyField.Field.LastIndex ) }}
if m, ok = val.(map[string]interface{}); !ok {
break
}
{{- else}}
if allNull {
allNull = val == nil
}
{{- end}}
{{- end}}
{{- end }}
if allNull {
break
}
return "{{.ResolverName}}", nil
}
{{- end }}

View File

@ -176,7 +176,6 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error {
uniqueMap[iface] = true
}
}
}
b.Models = append(b.Models, it)

View File

@ -33,5 +33,5 @@ type LateSourceInjector interface {
// ResolverImplementer is used to generate code inside resolvers
type ResolverImplementer interface {
Implement(field *codegen.Field) string
Implement(prevImplementation string, field *codegen.Field) string
}

View File

@ -127,14 +127,9 @@ func (m *Plugin) generatePerSchema(data *codegen.Data) error {
if !f.IsResolver {
continue
}
structName := templates.LcFirst(o.Name) + templates.UcFirst(data.Config.Resolver.Type)
comment := strings.TrimSpace(strings.TrimLeft(rewriter.GetMethodComment(structName, f.GoFieldName), `\`))
implementation := strings.TrimSpace(rewriter.GetMethodBody(structName, f.GoFieldName))
if implementation == "" {
// use default implementation, if no implementation was previously used
implementation = fmt.Sprintf("panic(fmt.Errorf(\"not implemented: %v - %v\"))", f.GoFieldName, f.Name)
}
resolver := Resolver{o, f, rewriter.GetPrevDecl(structName, f.GoFieldName), comment, implementation, nil}
var implExists bool
for _, p := range data.Plugins {
@ -257,13 +252,20 @@ type Resolver struct {
PrevDecl *ast.FuncDecl
Comment string
ImplementationStr string
ImplementationRender func(r *codegen.Field) string
ImplementationRender func(prevImplementation string, r *codegen.Field) string
}
func (r *Resolver) Implementation() string {
if r.ImplementationRender != nil {
return r.ImplementationRender(r.Field)
// use custom implementation
return r.ImplementationRender(r.ImplementationStr, r.Field)
}
// if not implementation was previously used, use default implementation
if r.ImplementationStr == "" {
// use default implementation, if no implementation was previously used
return fmt.Sprintf("panic(fmt.Errorf(\"not implemented: %v - %v\"))", r.Field.GoFieldName, r.Field.Name)
}
// use previously used implementation
return r.ImplementationStr
}

View File

@ -60,13 +60,16 @@ CertMagic - Automatic HTTPS using Let's Encrypt
- [Advanced use](#advanced-use)
- [Wildcard Certificates](#wildcard-certificates)
- [Behind a load balancer (or in a cluster)](#behind-a-load-balancer-or-in-a-cluster)
- [The ACME Challenges](#the-acme-challenges)
- [HTTP Challenge](#http-challenge)
- [TLS-ALPN Challenge](#tls-alpn-challenge)
- [DNS Challenge](#dns-challenge)
- [On-Demand TLS](#on-demand-tls)
- [Storage](#storage)
- [Cache](#cache)
- [The ACME Challenges](#the-acme-challenges)
- [HTTP Challenge](#http-challenge)
- [TLS-ALPN Challenge](#tls-alpn-challenge)
- [DNS Challenge](#dns-challenge)
- [On-Demand TLS](#on-demand-tls)
- [Storage](#storage)
- [Cache](#cache)
- [Events](#events)
- [ZeroSSL](#zerossl)
- [FAQ](#faq)
- [Contributing](#contributing)
- [Project History](#project-history)
- [Credits and License](#credits-and-license)
@ -87,7 +90,7 @@ CertMagic - Automatic HTTPS using Let's Encrypt
- Exponential backoff with carefully-tuned intervals
- Retries with optional test/staging CA endpoint instead of production, to avoid rate limits
- Written in Go, a language with memory-safety guarantees
- Powered by [ACMEz](https://github.com/mholt/acmez), _the_ premier ACME client library for Go
- Powered by [ACMEz](https://github.com/mholt/acmez/v2), _the_ premier ACME client library for Go
- All [libdns](https://github.com/libdns) DNS providers work out-of-the-box
- Pluggable storage backends (default: file system)
- Pluggable key sources
@ -110,6 +113,7 @@ CertMagic - Automatic HTTPS using Let's Encrypt
- Cross-platform support! Mac, Windows, Linux, BSD, Android...
- Scales to hundreds of thousands of names/certificates per instance
- Use in conjunction with your own certificates
- Full support for [draft-ietf-acme-ari](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/) (ACME Renewal Information; ARI) extension
## Requirements
@ -125,6 +129,7 @@ CertMagic - Automatic HTTPS using Let's Encrypt
4. Persistent storage
- Typically the local file system (default)
- Other integrations available/possible
5. Go 1.21 or newer
**_Before using this library, your domain names MUST be pointed (A/AAAA records) at your server (unless you use the DNS challenge)!_**
@ -292,7 +297,7 @@ tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos..
// we can simply set its GetCertificate field and append the
// TLS-ALPN challenge protocol to the NextProtos
myTLSConfig.GetCertificate = magic.GetCertificate
myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, tlsalpn01.ACMETLS1Protocol)
myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, acmez.ACMETLS1Protocol)
// the HTTP challenge has to be handled by your HTTP server;
// if you don't have one, you should have disabled it earlier
@ -379,7 +384,7 @@ Or make two simple changes to an existing `tls.Config`:
```go
myTLSConfig.GetCertificate = magic.GetCertificate
myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, tlsalpn01.ACMETLS1Protocol}
myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, acmez.ACMETLS1Protocol}
```
Then just make sure your TLS listener is listening on port 443:
@ -401,8 +406,10 @@ To enable it, just set the `DNS01Solver` field on a `certmagic.ACMEIssuer` struc
import "github.com/libdns/cloudflare"
certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &cloudflare.Provider{
APIToken: "topsecret",
DNSManager: certmagic.DNSManager{
DNSProvider: &cloudflare.Provider{
APIToken: "topsecret",
},
},
}
```
@ -504,6 +511,26 @@ CertMagic emits events when possible things of interest happen. Set the [`OnEven
`OnEvent` can return an error. Some events may be aborted by returning an error. For example, returning an error from `cert_obtained` can cancel obtaining the certificate. Only return an error from `OnEvent` if you want to abort program flow.
## ZeroSSL
ZeroSSL has both ACME and HTTP API services for getting certificates. CertMagic works with both of them.
To use ZeroSSL's ACME server, configure CertMagic with an [`ACMEIssuer`](https://pkg.go.dev/github.com/caddyserver/certmagic#ACMEIssuer) like you would with any other ACME CA (just adjust the directory URL). External Account Binding (EAB) is required for ZeroSSL. You can use the [ZeroSSL API](https://pkg.go.dev/github.com/caddyserver/zerossl) to generate one, or your account dashboard.
To use ZeroSSL's API instead, use the [`ZeroSSLIssuer`](https://pkg.go.dev/github.com/caddyserver/certmagic#ZeroSSLIssuer). Here is a simple example:
```go
magic := certmagic.NewDefault()
magic.Issuers = []certmagic.Issuer{
certmagic.ZeroSSLIssuer{
APIKey: "<your ZeroSSL API key>",
}),
}
err := magic.ManageSync(ctx, []string{"example.com"})
```
## FAQ
### Can I use some of my own certificates while using CertMagic?
@ -540,7 +567,7 @@ We welcome your contributions! Please see our **[contributing guidelines](https:
## Project History
CertMagic is the core of Caddy's advanced TLS automation code, extracted into a library. The underlying ACME client implementation is [ACMEz](https://github.com/mholt/acmez). CertMagic's code was originally a central part of Caddy even before Let's Encrypt entered public beta in 2015.
CertMagic is the core of Caddy's advanced TLS automation code, extracted into a library. The underlying ACME client implementation is [ACMEz](https://github.com/mholt/acmez/v2). CertMagic's code was originally a central part of Caddy even before Let's Encrypt entered public beta in 2015.
In the years since then, Caddy's TLS automation techniques have been widely adopted, tried and tested in production, and served millions of sites and secured trillions of connections.

View File

@ -32,7 +32,7 @@ import (
"strings"
"sync"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v2/acme"
)
// getAccount either loads or creates a new account, depending on if
@ -88,11 +88,18 @@ func (*ACMEIssuer) newAccount(email string) (acme.Account, error) {
// If it does not exist in storage, it will be retrieved from the ACME server and added to storage.
// The account must already exist; it does not create a new account.
func (am *ACMEIssuer) GetAccount(ctx context.Context, privateKeyPEM []byte) (acme.Account, error) {
account, err := am.loadAccountByKey(ctx, privateKeyPEM)
if errors.Is(err, fs.ErrNotExist) {
account, err = am.lookUpAccount(ctx, privateKeyPEM)
email := am.getEmail()
if email == "" {
if account, err := am.loadAccountByKey(ctx, privateKeyPEM); err == nil {
return account, nil
}
} else {
keyBytes, err := am.config.Storage.Load(ctx, am.storageKeyUserPrivateKey(am.CA, email))
if err == nil && bytes.Equal(bytes.TrimSpace(keyBytes), bytes.TrimSpace(privateKeyPEM)) {
return am.loadAccount(ctx, am.CA, email)
}
}
return account, err
return am.lookUpAccount(ctx, privateKeyPEM)
}
// loadAccountByKey loads the account with the given private key from storage, if it exists.
@ -107,9 +114,14 @@ func (am *ACMEIssuer) loadAccountByKey(ctx context.Context, privateKeyPEM []byte
email := path.Base(accountFolderKey)
keyBytes, err := am.config.Storage.Load(ctx, am.storageKeyUserPrivateKey(am.CA, email))
if err != nil {
return acme.Account{}, err
// Try the next account: This one is missing its private key, if it turns out to be the one we're looking
// for we will try to save it again after confirming with the ACME server.
continue
}
if bytes.Equal(bytes.TrimSpace(keyBytes), bytes.TrimSpace(privateKeyPEM)) {
// Found the account with the correct private key, try loading it. If this fails we we will follow
// the same procedure as if the private key was not found and confirm with the ACME server before saving
// it again.
return am.loadAccount(ctx, am.CA, email)
}
}
@ -171,6 +183,16 @@ func (am *ACMEIssuer) saveAccount(ctx context.Context, ca string, account acme.A
return storeTx(ctx, am.config.Storage, all)
}
// deleteAccountLocally deletes the registration info and private key of the account
// for the given CA from storage.
func (am *ACMEIssuer) deleteAccountLocally(ctx context.Context, ca string, account acme.Account) error {
primaryContact := getPrimaryContact(account)
if err := am.config.Storage.Delete(ctx, am.storageKeyUserReg(ca, primaryContact)); err != nil {
return err
}
return am.config.Storage.Delete(ctx, am.storageKeyUserPrivateKey(ca, primaryContact))
}
// setEmail does everything it can to obtain an email address
// from the user within the scope of memory and storage to use
// for ACME TLS. If it cannot get an email address, it does nothing

View File

@ -18,23 +18,19 @@ import (
"context"
"crypto/x509"
"fmt"
weakrand "math/rand"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
)
func init() {
weakrand.Seed(time.Now().UnixNano())
}
// acmeClient holds state necessary to perform ACME operations
// for certificate management with an ACME account. Call
// ACMEIssuer.newACMEClientWithAccount() to get a valid one.
@ -141,44 +137,21 @@ func (iss *ACMEIssuer) newACMEClientWithAccount(ctx context.Context, useTestCA,
// independent of any particular ACME account. If useTestCA is true, am.TestCA
// will be used if it is set; otherwise, the primary CA will be used.
func (iss *ACMEIssuer) newACMEClient(useTestCA bool) (*acmez.Client, error) {
// ensure defaults are filled in
var caURL string
if useTestCA {
caURL = iss.TestCA
client, err := iss.newBasicACMEClient()
if err != nil {
return nil, err
}
if caURL == "" {
caURL = iss.CA
}
if caURL == "" {
caURL = DefaultACME.CA
// fill in a little more beyond a basic client
if useTestCA && iss.TestCA != "" {
client.Client.Directory = iss.TestCA
}
certObtainTimeout := iss.CertObtainTimeout
if certObtainTimeout == 0 {
certObtainTimeout = DefaultACME.CertObtainTimeout
}
// ensure endpoint is secure (assume HTTPS if scheme is missing)
if !strings.Contains(caURL, "://") {
caURL = "https://" + caURL
}
u, err := url.Parse(caURL)
if err != nil {
return nil, err
}
if u.Scheme != "https" && !isLoopback(u.Host) && !isInternal(u.Host) {
return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL)
}
client := &acmez.Client{
Client: &acme.Client{
Directory: caURL,
PollTimeout: certObtainTimeout,
UserAgent: buildUAString(),
HTTPClient: iss.httpClient,
},
ChallengeSolvers: make(map[string]acmez.Solver),
}
client.Logger = iss.Logger.Named("acme_client")
client.Client.PollTimeout = certObtainTimeout
client.ChallengeSolvers = make(map[string]acmez.Solver)
// configure challenges (most of the time, DNS challenge is
// exclusive of other ones because it is usually only used
@ -186,38 +159,24 @@ func (iss *ACMEIssuer) newACMEClient(useTestCA bool) (*acmez.Client, error) {
if iss.DNS01Solver == nil {
// enable HTTP-01 challenge
if !iss.DisableHTTPChallenge {
useHTTPPort := HTTPChallengePort
if HTTPPort > 0 && HTTPPort != HTTPChallengePort {
useHTTPPort = HTTPPort
}
if iss.AltHTTPPort > 0 {
useHTTPPort = iss.AltHTTPPort
}
client.ChallengeSolvers[acme.ChallengeTypeHTTP01] = distributedSolver{
storage: iss.config.Storage,
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
solver: &httpSolver{
acmeIssuer: iss,
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(useHTTPPort)),
handler: iss.HTTPChallengeHandler(http.NewServeMux()),
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getHTTPPort())),
},
}
}
// enable TLS-ALPN-01 challenge
if !iss.DisableTLSALPNChallenge {
useTLSALPNPort := TLSALPNChallengePort
if HTTPSPort > 0 && HTTPSPort != TLSALPNChallengePort {
useTLSALPNPort = HTTPSPort
}
if iss.AltTLSALPNPort > 0 {
useTLSALPNPort = iss.AltTLSALPNPort
}
client.ChallengeSolvers[acme.ChallengeTypeTLSALPN01] = distributedSolver{
storage: iss.config.Storage,
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
solver: &tlsALPNSolver{
config: iss.config,
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(useTLSALPNPort)),
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getTLSALPNPort())),
},
}
}
@ -248,6 +207,64 @@ func (iss *ACMEIssuer) newACMEClient(useTestCA bool) (*acmez.Client, error) {
return client, nil
}
// newBasicACMEClient sets up a basically-functional ACME client that is not capable
// of solving challenges but can provide basic interactions with the server.
func (iss *ACMEIssuer) newBasicACMEClient() (*acmez.Client, error) {
caURL := iss.CA
if caURL == "" {
caURL = DefaultACME.CA
}
// ensure endpoint is secure (assume HTTPS if scheme is missing)
if !strings.Contains(caURL, "://") {
caURL = "https://" + caURL
}
u, err := url.Parse(caURL)
if err != nil {
return nil, err
}
if u.Scheme != "https" && !SubjectIsInternal(u.Host) {
return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required for non-internal CA)", caURL)
}
return &acmez.Client{
Client: &acme.Client{
Directory: caURL,
UserAgent: buildUAString(),
HTTPClient: iss.httpClient,
Logger: iss.Logger.Named("acme_client"),
},
}, nil
}
func (iss *ACMEIssuer) getRenewalInfo(ctx context.Context, cert Certificate) (acme.RenewalInfo, error) {
acmeClient, err := iss.newBasicACMEClient()
if err != nil {
return acme.RenewalInfo{}, err
}
return acmeClient.GetRenewalInfo(ctx, cert.Certificate.Leaf)
}
func (iss *ACMEIssuer) getHTTPPort() int {
useHTTPPort := HTTPChallengePort
if HTTPPort > 0 && HTTPPort != HTTPChallengePort {
useHTTPPort = HTTPPort
}
if iss.AltHTTPPort > 0 {
useHTTPPort = iss.AltHTTPPort
}
return useHTTPPort
}
func (iss *ACMEIssuer) getTLSALPNPort() int {
useTLSALPNPort := TLSALPNChallengePort
if HTTPSPort > 0 && HTTPSPort != TLSALPNChallengePort {
useTLSALPNPort = HTTPSPort
}
if iss.AltTLSALPNPort > 0 {
useTLSALPNPort = iss.AltTLSALPNPort
}
return useTLSALPNPort
}
func (c *acmeClient) throttle(ctx context.Context, names []string) error {
email := c.iss.getEmail()

View File

@ -1,3 +1,17 @@
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
@ -14,8 +28,8 @@ import (
"sync"
"time"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
)
@ -55,6 +69,13 @@ type ACMEIssuer struct {
// with this ACME account
ExternalAccount *acme.EAB
// Optionally specify the validity period of
// the certificate(s) here as offsets from the
// approximate time of certificate issuance,
// but note that not all CAs support this
// (EXPERIMENTAL: Subject to change)
NotBefore, NotAfter time.Duration
// Disable all HTTP challenges
DisableHTTPChallenge bool
@ -169,6 +190,12 @@ func NewACMEIssuer(cfg *Config, template ACMEIssuer) *ACMEIssuer {
if template.ExternalAccount == nil {
template.ExternalAccount = DefaultACME.ExternalAccount
}
if template.NotBefore == 0 {
template.NotBefore = DefaultACME.NotBefore
}
if template.NotAfter == 0 {
template.NotAfter = DefaultACME.NotAfter
}
if !template.DisableHTTPChallenge {
template.DisableHTTPChallenge = DefaultACME.DisableHTTPChallenge
}
@ -296,14 +323,32 @@ func (iss *ACMEIssuer) isAgreed() bool {
// PreCheck performs a few simple checks before obtaining or
// renewing a certificate with ACME, and returns whether this
// batch is eligible for certificates if using Let's Encrypt.
// It also ensures that an email address is available.
// batch is eligible for certificates. It also ensures that an
// email address is available if possible.
//
// IP certificates via ACME are defined in RFC 8738.
func (am *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error {
publicCA := strings.Contains(am.CA, "api.letsencrypt.org") || strings.Contains(am.CA, "acme.zerossl.com") || strings.Contains(am.CA, "api.pki.goog")
publicCAsAndIPCerts := map[string]bool{ // map of public CAs to whether they support IP certificates (last updated: Q1 2024)
"api.letsencrypt.org": false, // https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
"acme.zerossl.com": false, // only supported via their API, not ACME endpoint
"api.pki.goog": true, // https://pki.goog/faq/#faq-IPCerts
"api.buypass.com": false, // https://community.buypass.com/t/h7hm76w/buypass-support-for-rfc-8738
"acme.ssl.com": false,
}
var publicCA, ipCertAllowed bool
for caSubstr, ipCert := range publicCAsAndIPCerts {
if strings.Contains(am.CA, caSubstr) {
publicCA, ipCertAllowed = true, ipCert
break
}
}
if publicCA {
for _, name := range names {
if !SubjectQualifiesForPublicCert(name) {
return fmt.Errorf("subject does not qualify for a public certificate: %s", name)
return fmt.Errorf("subject '%s' does not qualify for a public certificate", name)
}
if !ipCertAllowed && SubjectIsIP(name) {
return fmt.Errorf("subject '%s' cannot have public IP certificate from %s (if CA's policy has changed, please notify the developers in an issue)", name, am.CA)
}
}
}
@ -317,12 +362,13 @@ func (am *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (
panic("missing config pointer (must use NewACMEIssuer)")
}
var isRetry bool
if attempts, ok := ctx.Value(AttemptsCtxKey).(*int); ok {
isRetry = *attempts > 0
var attempts int
if attemptsPtr, ok := ctx.Value(AttemptsCtxKey).(*int); ok {
attempts = *attemptsPtr
}
isRetry := attempts > 0
cert, usedTestCA, err := am.doIssue(ctx, csr, isRetry)
cert, usedTestCA, err := am.doIssue(ctx, csr, attempts)
if err != nil {
return nil, err
}
@ -350,7 +396,7 @@ func (am *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (
// other endpoint. This is more likely to happen if a user is testing with
// the staging CA as the main CA, then changes their configuration once they
// think they are ready for the production endpoint.
cert, _, err = am.doIssue(ctx, csr, false)
cert, _, err = am.doIssue(ctx, csr, 0)
if err != nil {
// succeeded with test CA but failed just now with the production CA;
// either we are observing differing internal states of each CA that will
@ -378,7 +424,8 @@ func (am *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (
return cert, err
}
func (am *ACMEIssuer) doIssue(ctx context.Context, csr *x509.CertificateRequest, useTestCA bool) (*IssuedCertificate, bool, error) {
func (am *ACMEIssuer) doIssue(ctx context.Context, csr *x509.CertificateRequest, attempts int) (*IssuedCertificate, bool, error) {
useTestCA := attempts > 0
client, err := am.newACMEClientWithAccount(ctx, useTestCA, false)
if err != nil {
return nil, false, err
@ -393,12 +440,72 @@ func (am *ACMEIssuer) doIssue(ctx context.Context, csr *x509.CertificateRequest,
}
}
certChains, err := client.acmeClient.ObtainCertificateUsingCSR(ctx, client.account, csr)
params, err := acmez.OrderParametersFromCSR(client.account, csr)
if err != nil {
return nil, usingTestCA, fmt.Errorf("%v %w (ca=%s)", nameSet, err, client.acmeClient.Directory)
return nil, false, fmt.Errorf("generating order parameters from CSR: %v", err)
}
if len(certChains) == 0 {
return nil, usingTestCA, fmt.Errorf("no certificate chains")
if am.NotBefore != 0 {
params.NotBefore = time.Now().Add(am.NotBefore)
}
if am.NotAfter != 0 {
params.NotAfter = time.Now().Add(am.NotAfter)
}
// Notify the ACME server we are replacing a certificate (if the caller says we are),
// only if the following conditions are met:
// - The caller has set a Replaces value in the context, indicating this is a renewal.
// - Not using test CA. This should be obvious, but a test CA should be in a separate
// environment from production, and thus not have knowledge of the cert being replaced.
// - Not a certain attempt number. We skip setting Replaces once early on in the retries
// in case the reason the order is failing is only because there is a state inconsistency
// between client and server or some sort of bookkeeping error with regards to the certID
// and the server is rejecting the ARI certID. In any case, an invalid certID may cause
// orders to fail. So try once without setting it.
if !usingTestCA && attempts != 2 {
if replacing, ok := ctx.Value(ctxKeyARIReplaces).(*x509.Certificate); ok {
params.Replaces = replacing
}
}
// do this in a loop because there's an error case that may necessitate a retry, but not more than once
var certChains []acme.Certificate
for i := 0; i < 2; i++ {
am.Logger.Info("using ACME account",
zap.String("account_id", params.Account.Location),
zap.Strings("account_contact", params.Account.Contact))
certChains, err = client.acmeClient.ObtainCertificate(ctx, params)
if err != nil {
var prob acme.Problem
if errors.As(err, &prob) && prob.Type == acme.ProblemTypeAccountDoesNotExist {
am.Logger.Warn("ACME account does not exist on server; attempting to recreate",
zap.String("account_id", client.account.Location),
zap.Strings("account_contact", client.account.Contact),
zap.String("key_location", am.storageKeyUserPrivateKey(client.acmeClient.Directory, am.getEmail())),
zap.Object("problem", prob))
// the account we have no longer exists on the CA, so we need to create a new one;
// we could use the same key pair, but this is a good opportunity to rotate keys
// (see https://caddy.community/t/acme-account-is-not-regenerated-when-acme-server-gets-reinstalled/22627)
// (basically this happens if the CA gets reset or reinstalled; usually just internal PKI)
err := am.deleteAccountLocally(ctx, client.iss.CA, client.account)
if err != nil {
return nil, usingTestCA, fmt.Errorf("%v ACME account no longer exists on CA, but resetting our local copy of the account info failed: %v", nameSet, err)
}
// recreate account and try again
client, err = am.newACMEClientWithAccount(ctx, useTestCA, false)
if err != nil {
return nil, false, err
}
continue
}
return nil, usingTestCA, fmt.Errorf("%v %w (ca=%s)", nameSet, err, client.acmeClient.Directory)
}
if len(certChains) == 0 {
return nil, usingTestCA, fmt.Errorf("no certificate chains")
}
break
}
preferredChain := am.selectPreferredChain(certChains)
@ -408,6 +515,8 @@ func (am *ACMEIssuer) doIssue(ctx context.Context, csr *x509.CertificateRequest,
Metadata: preferredChain,
}
am.Logger.Debug("selected certificate chain", zap.String("url", preferredChain.URL))
return ic, usingTestCA, nil
}
@ -523,16 +632,27 @@ var DefaultACME = ACMEIssuer{
HTTPProxy: http.ProxyFromEnvironment,
}
// Some well-known CA endpoints available to use.
// Some well-known CA endpoints available to use. See
// the documentation for each service; some may require
// External Account Binding (EAB) and possibly payment.
// COMPATIBILITY NOTICE: These constants refer to external
// resources and are thus subject to change or removal
// without a major version bump.
const (
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
ZeroSSLProductionCA = "https://acme.zerossl.com/v2/DV90"
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory" // https://letsencrypt.org/docs/staging-environment/
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory" // https://letsencrypt.org/getting-started/
ZeroSSLProductionCA = "https://acme.zerossl.com/v2/DV90" // https://zerossl.com/documentation/acme/
GoogleTrustStagingCA = "https://dv.acme-v02.test-api.pki.goog/directory" // https://cloud.google.com/certificate-manager/docs/public-ca-tutorial
GoogleTrustProductionCA = "https://dv.acme-v02.api.pki.goog/directory" // https://cloud.google.com/certificate-manager/docs/public-ca-tutorial
)
// prefixACME is the storage key prefix used for ACME-specific assets.
const prefixACME = "acme"
type ctxKey string
const ctxKeyARIReplaces = ctxKey("ari_replaces")
// Interface guards
var (
_ PreChecker = (*ACMEIssuer)(nil)

View File

@ -16,7 +16,7 @@ package certmagic
import (
"fmt"
weakrand "math/rand" // seeded elsewhere
weakrand "math/rand"
"strings"
"sync"
"time"
@ -394,17 +394,26 @@ func (certCache *Cache) AllMatchingCertificates(name string) []Certificate {
return certs
}
// SubjectIssuer pairs a subject name with an issuer ID/key.
type SubjectIssuer struct {
Subject, IssuerKey string
}
// RemoveManaged removes managed certificates for the given subjects from the cache.
// This effectively stops maintenance of those certificates.
func (certCache *Cache) RemoveManaged(subjects []string) {
// This effectively stops maintenance of those certificates. If an IssuerKey is
// specified alongside the subject, only certificates for that subject from the
// specified issuer will be removed.
func (certCache *Cache) RemoveManaged(subjects []SubjectIssuer) {
deleteQueue := make([]string, 0, len(subjects))
for _, subject := range subjects {
certs := certCache.getAllMatchingCerts(subject) // does NOT expand wildcards; exact matches only
for _, subj := range subjects {
certs := certCache.getAllMatchingCerts(subj.Subject) // does NOT expand wildcards; exact matches only
for _, cert := range certs {
if !cert.managed {
continue
}
deleteQueue = append(deleteQueue, cert.hash)
if subj.IssuerKey == "" || cert.issuerKey == subj.IssuerKey {
deleteQueue = append(deleteQueue, cert.hash)
}
}
}
certCache.Remove(deleteQueue)

View File

@ -18,12 +18,15 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"math/rand"
"net"
"os"
"strings"
"time"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
"golang.org/x/crypto/ocsp"
)
@ -56,6 +59,9 @@ type Certificate struct {
// The unique string identifying the issuer of this certificate.
issuerKey string
// ACME Renewal Information, if available
ari acme.RenewalInfo
}
// Empty returns true if the certificate struct is not filled out; at
@ -67,10 +73,106 @@ func (cert Certificate) Empty() bool {
// Hash returns a checksum of the certificate chain's DER-encoded bytes.
func (cert Certificate) Hash() string { return cert.hash }
// NeedsRenewal returns true if the certificate is
// expiring soon (according to cfg) or has expired.
// NeedsRenewal returns true if the certificate is expiring
// soon (according to ARI and/or cfg) or has expired.
func (cert Certificate) NeedsRenewal(cfg *Config) bool {
return currentlyInRenewalWindow(cert.Leaf.NotBefore, expiresAt(cert.Leaf), cfg.RenewalWindowRatio)
return cfg.certNeedsRenewal(cert.Leaf, cert.ari, true)
}
// certNeedsRenewal consults ACME Renewal Info (ARI) and certificate expiration to determine
// whether the leaf certificate needs to be renewed yet. If true is returned, the certificate
// should be renewed as soon as possible. The reasoning for a true return value is logged
// unless emitLogs is false; this can be useful to suppress noisy logs in the case where you
// first call this to determine if a cert in memory needs renewal, and then right after you
// call it again to see if the cert in storage still needs renewal -- you probably don't want
// to log the second time for checking the cert in storage which is mainly for synchronization.
func (cfg *Config) certNeedsRenewal(leaf *x509.Certificate, ari acme.RenewalInfo, emitLogs bool) bool {
expiration := expiresAt(leaf)
var logger *zap.Logger
if emitLogs {
logger = cfg.Logger.With(
zap.Strings("subjects", leaf.DNSNames),
zap.Time("expiration", expiration),
zap.String("ari_cert_id", ari.UniqueIdentifier),
zap.Timep("next_ari_update", ari.RetryAfter),
zap.Duration("renew_check_interval", cfg.certCache.options.RenewCheckInterval),
zap.Time("window_start", ari.SuggestedWindow.Start),
zap.Time("window_end", ari.SuggestedWindow.End))
} else {
logger = zap.NewNop()
}
// first check ARI: if it says it's time to renew, it's time to renew
// (notice that we don't strictly require an ARI window to also exist; we presume
// that if a time has been selected, a window does or did exist, even if it didn't
// get stored/encoded for some reason - but also: this allows administrators to
// manually or explicitly schedule a renewal time indepedently of ARI which could
// be useful)
selectedTime := ari.SelectedTime
// if, for some reason a random time in the window hasn't been selected yet, but an ARI
// window does exist, we can always improvise one... even if this is called repeatedly,
// a random time is a random time, whether you generate it once or more :D
// (code borrowed from our acme package)
if selectedTime.IsZero() &&
(!ari.SuggestedWindow.Start.IsZero() && !ari.SuggestedWindow.End.IsZero()) {
start, end := ari.SuggestedWindow.Start.Unix()+1, ari.SuggestedWindow.End.Unix()
selectedTime = time.Unix(rand.Int63n(end-start)+start, 0).UTC()
logger.Warn("no renewal time had been selected with ARI; chose an ephemeral one for now",
zap.Time("ephemeral_selected_time", selectedTime))
}
// if a renewal time has been selected, start with that
if !selectedTime.IsZero() {
// ARI spec recommends an algorithm that renews after the randomly-selected
// time OR just before it if the next waking time would be after it; this
// cutoff can actually be before the start of the renewal window, but the spec
// author says that's OK: https://github.com/aarongable/draft-acme-ari/issues/71
cutoff := ari.SelectedTime.Add(-cfg.certCache.options.RenewCheckInterval)
if time.Now().After(cutoff) {
logger.Info("certificate needs renewal based on ARI window",
zap.Time("selected_time", selectedTime),
zap.Time("renewal_cutoff", cutoff))
return true
}
// according to ARI, we are not ready to renew; however, we do not rely solely on
// ARI calculations... what if there is a bug in our implementation, or in the
// server's, or the stored metadata? for redundancy, give credence to the expiration
// date; ignore ARI if we are past a "dangerously close" limit, to avoid any
// possibility of a bug in ARI compromising a site's uptime: we should always always
// always give heed to actual validity period
if currentlyInRenewalWindow(leaf.NotBefore, expiration, 1.0/20.0) {
logger.Warn("certificate is in emergency renewal window; superceding ARI",
zap.Duration("remaining", time.Until(expiration)),
zap.Time("renewal_cutoff", cutoff))
return true
}
}
// the normal check, in the absence of ARI, is to determine if we're near enough (or past)
// the expiration date based on the configured remaining:lifetime ratio
if currentlyInRenewalWindow(leaf.NotBefore, expiration, cfg.RenewalWindowRatio) {
logger.Info("certificate is in configured renewal window based on expiration date",
zap.Duration("remaining", time.Until(expiration)))
return true
}
// finally, if the certificate is expiring imminently, always attempt a renewal;
// we check both a (very low) lifetime ratio and also a strict difference between
// the time until expiration and the interval at which we run the standard maintenance
// routine to check for renewals, to accommodate both exceptionally long and short
// cert lifetimes
if currentlyInRenewalWindow(leaf.NotBefore, expiration, 1.0/50.0) ||
time.Until(expiration) < cfg.certCache.options.RenewCheckInterval*5 {
logger.Warn("certificate is in emergency renewal window; expiration imminent",
zap.Duration("remaining", time.Until(expiration)))
return true
}
return false
}
// Expired returns true if the certificate has expired.
@ -85,10 +187,12 @@ func (cert Certificate) Expired() bool {
return time.Now().After(expiresAt(cert.Leaf))
}
// currentlyInRenewalWindow returns true if the current time is
// within the renewal window, according to the given start/end
// currentlyInRenewalWindow returns true if the current time is within
// (or after) the renewal window, according to the given start/end
// dates and the ratio of the renewal window. If true is returned,
// the certificate being considered is due for renewal.
// the certificate being considered is due for renewal. The ratio
// is remaining:total time, i.e. 1/3 = 1/3 of lifetime remaining,
// or 9/10 = 9/10 of time lifetime remaining.
func currentlyInRenewalWindow(notBefore, notAfter time.Time, renewalWindowRatio float64) bool {
if notAfter.IsZero() {
return false
@ -130,6 +234,7 @@ func expiresAt(cert *x509.Certificate) time.Time {
//
// This method is safe for concurrent use.
func (cfg *Config) CacheManagedCertificate(ctx context.Context, domain string) (Certificate, error) {
domain = cfg.transformSubject(ctx, nil, domain)
cert, err := cfg.loadManagedCertificate(ctx, domain)
if err != nil {
return cert, err
@ -153,16 +258,44 @@ func (cfg *Config) loadManagedCertificate(ctx context.Context, domain string) (C
}
cert.managed = true
cert.issuerKey = certRes.issuerKey
if ari, err := certRes.getARI(); err == nil && ari != nil {
cert.ari = *ari
}
return cert, nil
}
// getARI unpacks ACME Renewal Information from the issuer data, if available.
// It is only an error if there is invalid JSON.
func (certRes CertificateResource) getARI() (*acme.RenewalInfo, error) {
acmeData, err := certRes.getACMEData()
if err != nil {
return nil, err
}
return acmeData.RenewalInfo, nil
}
// getACMEData returns the ACME certificate metadata from the IssuerData, but
// note that a non-ACME-issued certificate may return an empty value and nil
// since the JSON may still decode successfully but just not match any or all
// of the fields. Remember that the IssuerKey is used to store and access the
// cert files in the first place (it is part of the path) so in theory if you
// load a CertificateResource from an ACME issuer it should work as expected.
func (certRes CertificateResource) getACMEData() (acme.Certificate, error) {
if len(certRes.IssuerData) == 0 {
return acme.Certificate{}, nil
}
var acmeCert acme.Certificate
err := json.Unmarshal(certRes.IssuerData, &acmeCert)
return acmeCert, err
}
// CacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
// and keyFile, which must be in PEM format. It stores the certificate in
// the in-memory cache and returns the hash, useful for removing from the cache.
//
// This method is safe for concurrent use.
func (cfg *Config) CacheUnmanagedCertificatePEMFile(ctx context.Context, certFile, keyFile string, tags []string) (string, error) {
cert, err := cfg.makeCertificateFromDiskWithOCSP(ctx, cfg.Storage, certFile, keyFile)
cert, err := cfg.makeCertificateFromDiskWithOCSP(ctx, certFile, keyFile)
if err != nil {
return "", err
}
@ -185,6 +318,15 @@ func (cfg *Config) CacheUnmanagedTLSCertificate(ctx context.Context, tlsCert tls
if err != nil {
return "", err
}
if time.Now().After(cert.Leaf.NotAfter) {
cfg.Logger.Warn("unmanaged certificate has expired",
zap.Time("not_after", cert.Leaf.NotAfter),
zap.Strings("sans", cert.Names))
} else if time.Until(cert.Leaf.NotAfter) < 24*time.Hour {
cfg.Logger.Warn("unmanaged certificate expires within 1 day",
zap.Time("not_after", cert.Leaf.NotAfter),
zap.Strings("sans", cert.Names))
}
err = stapleOCSP(ctx, cfg.OCSP, cfg.Storage, &cert, nil)
if err != nil {
cfg.Logger.Warn("stapling OCSP", zap.Error(err))
@ -215,7 +357,7 @@ func (cfg *Config) CacheUnmanagedCertificatePEMBytes(ctx context.Context, certBy
// certificate and key files. It fills out all the fields in
// the certificate except for the Managed and OnDemand flags.
// (It is up to the caller to set those.) It staples OCSP.
func (cfg Config) makeCertificateFromDiskWithOCSP(ctx context.Context, storage Storage, certFile, keyFile string) (Certificate, error) {
func (cfg Config) makeCertificateFromDiskWithOCSP(ctx context.Context, certFile, keyFile string) (Certificate, error) {
certPEMBlock, err := os.ReadFile(certFile)
if err != nil {
return Certificate{}, err
@ -319,21 +461,22 @@ func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
return nil
}
// managedCertInStorageExpiresSoon returns true if cert (being a
// managed certificate) is expiring within RenewDurationBefore.
// It returns false if there was an error checking the expiration
// of the certificate as found in storage, or if the certificate
// in storage is NOT expiring soon. A certificate that is expiring
// managedCertInStorageNeedsRenewal returns true if cert (being a
// managed certificate) is expiring soon (according to cfg) or if
// ACME Renewal Information (ARI) is available and says that it is
// time to renew (it uses existing ARI; it does not update it).
// It returns false if there was an error, the cert is not expiring
// soon, and ARI window is still future. A certificate that is expiring
// soon in our cache but is not expiring soon in storage probably
// means that another instance renewed the certificate in the
// meantime, and it would be a good idea to simply load the cert
// into our cache rather than repeating the renewal process again.
func (cfg *Config) managedCertInStorageExpiresSoon(ctx context.Context, cert Certificate) (bool, error) {
func (cfg *Config) managedCertInStorageNeedsRenewal(ctx context.Context, cert Certificate) (bool, error) {
certRes, err := cfg.loadCertResourceAnyIssuer(ctx, cert.Names[0])
if err != nil {
return false, err
}
_, needsRenew := cfg.managedCertNeedsRenewal(certRes)
_, _, needsRenew := cfg.managedCertNeedsRenewal(certRes, false)
return needsRenew, nil
}
@ -376,8 +519,8 @@ func SubjectQualifiesForCert(subj string) bool {
// SubjectQualifiesForPublicCert returns true if the subject
// name appears eligible for automagic TLS with a public
// CA such as Let's Encrypt. For example: localhost and IP
// addresses are not eligible because we cannot obtain certs
// CA such as Let's Encrypt. For example: internal IP addresses
// and localhost are not eligible because we cannot obtain certs
// for those names with a public CA. Wildcard names are
// allowed, as long as they conform to CABF requirements (only
// one wildcard label, and it must be the left-most label).
@ -385,13 +528,9 @@ func SubjectQualifiesForPublicCert(subj string) bool {
// must at least qualify for a certificate
return SubjectQualifiesForCert(subj) &&
// localhost, .localhost TLD, and .local TLD are ineligible
// loopback hosts and internal IPs are ineligible
!SubjectIsInternal(subj) &&
// cannot be an IP address (as of yet), see
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
!SubjectIsIP(subj) &&
// only one wildcard label allowed, and it must be left-most, with 3+ labels
(!strings.Contains(subj, "*") ||
(strings.Count(subj, "*") == 1 &&
@ -406,12 +545,55 @@ func SubjectIsIP(subj string) bool {
}
// SubjectIsInternal returns true if subj is an internal-facing
// hostname or address.
// hostname or address, including localhost/loopback hosts.
// Ports are ignored, if present.
func SubjectIsInternal(subj string) bool {
subj = strings.ToLower(strings.TrimSuffix(hostOnly(subj), "."))
return subj == "localhost" ||
strings.HasSuffix(subj, ".localhost") ||
strings.HasSuffix(subj, ".local") ||
strings.HasSuffix(subj, ".home.arpa")
strings.HasSuffix(subj, ".home.arpa") ||
isInternalIP(subj)
}
// isInternalIP returns true if the IP of addr
// belongs to a private network IP range. addr
// must only be an IP or an IP:port combination.
func isInternalIP(addr string) bool {
privateNetworks := []string{
"127.0.0.0/8", // IPv4 loopback
"0.0.0.0/16",
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"169.254.0.0/16", // RFC3927 link-local
"::1/7", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fc00::/7", // IPv6 unique local addr
}
host := hostOnly(addr)
ip := net.ParseIP(host)
if ip == nil {
return false
}
for _, privateNetwork := range privateNetworks {
_, ipnet, _ := net.ParseCIDR(privateNetwork)
if ipnet.Contains(ip) {
return true
}
}
return false
}
// hostOnly returns only the host portion of hostport.
// If there is no port or if there is an error splitting
// the port off, the whole input string is returned.
func hostOnly(hostport string) string {
host, _, err := net.SplitHostPort(hostport)
if err != nil {
return hostport // OK; probably had no port to begin with
}
return host
}
// MatchWildcard returns true if subject (a candidate DNS name)

View File

@ -39,6 +39,7 @@ import (
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"log"
"net"
@ -302,52 +303,6 @@ type OnDemandConfig struct {
hostAllowlist map[string]struct{}
}
// isLoopback returns true if the hostname of addr looks
// explicitly like a common local hostname. addr must only
// be a host or a host:port combination.
func isLoopback(addr string) bool {
host := hostOnly(addr)
return host == "localhost" ||
strings.Trim(host, "[]") == "::1" ||
strings.HasPrefix(host, "127.")
}
// isInternal returns true if the IP of addr
// belongs to a private network IP range. addr
// must only be an IP or an IP:port combination.
// Loopback addresses are considered false.
func isInternal(addr string) bool {
privateNetworks := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7",
}
host := hostOnly(addr)
ip := net.ParseIP(host)
if ip == nil {
return false
}
for _, privateNetwork := range privateNetworks {
_, ipnet, _ := net.ParseCIDR(privateNetwork)
if ipnet.Contains(ip) {
return true
}
}
return false
}
// hostOnly returns only the host portion of hostport.
// If there is no port or if there is an error splitting
// the port off, the whole input string is returned.
func hostOnly(hostport string) string {
host, _, err := net.SplitHostPort(hostport)
if err != nil {
return hostport // OK; probably had no port to begin with
}
return host
}
// PreChecker is an interface that can be optionally implemented by
// Issuers. Pre-checks are performed before each call (or batch of
// identical calls) to Issue(), giving the issuer the option to ensure
@ -394,7 +349,12 @@ type Revoker interface {
type Manager interface {
// GetCertificate returns the certificate to use to complete the handshake.
// Since this is called during every TLS handshake, it must be very fast and not block.
// Returning (nil, nil) is valid and is simply treated as a no-op.
// Returning any non-nil value indicates that this Manager manages a certificate
// for the described handshake. Returning (nil, nil) is valid and is simply treated as
// a no-op Return (nil, nil) when the Manager has no certificate for this handshake.
// Return an error or a certificate only if the Manager is supposed to get a certificate
// for this handshake. Returning (nil, nil) other Managers or Issuers to try to get
// a certificate for the handshake.
GetCertificate(context.Context, *tls.ClientHelloInfo) (*tls.Certificate, error)
}
@ -429,7 +389,8 @@ type IssuedCertificate struct {
Certificate []byte
// Any extra information to serialize alongside the
// certificate in storage.
// certificate in storage. It MUST be serializable
// as JSON in order to be preserved.
Metadata any
}
@ -450,7 +411,7 @@ type CertificateResource struct {
// Any extra information associated with the certificate,
// usually provided by the issuer implementation.
IssuerData any `json:"issuer_data,omitempty"`
IssuerData json.RawMessage `json:"issuer_data,omitempty"`
// The unique string identifying the issuer of the
// certificate; internally useful for storage access.

View File

@ -24,17 +24,19 @@ import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/fs"
weakrand "math/rand"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
"golang.org/x/crypto/ocsp"
"golang.org/x/net/idna"
@ -50,6 +52,7 @@ type Config struct {
// it should be renewed; for most certificates, the
// global default is good, but for extremely short-
// lived certs, you may want to raise this to ~0.5.
// Ratio is remaining:total lifetime.
RenewalWindowRatio float64
// An optional event callback clients can set
@ -135,9 +138,17 @@ type Config struct {
// storage is properly configured and has sufficient
// space, you can disable this check to reduce I/O
// if that is expensive for you.
// EXPERIMENTAL: Option subject to change or removal.
// EXPERIMENTAL: Subject to change or removal.
DisableStorageCheck bool
// SubjectTransformer is a hook that can transform the
// subject (SAN) of a certificate being loaded or issued.
// For example, a common use case is to replace the
// left-most label with an asterisk (*) to become a
// wildcard certificate.
// EXPERIMENTAL: Subject to change or removal.
SubjectTransformer func(ctx context.Context, domain string) string
// Set a logger to enable logging. If not set,
// a default logger will be created.
Logger *zap.Logger
@ -436,6 +447,15 @@ func (cfg *Config) manageOne(ctx context.Context, domainName string, async bool)
return err
}
// ensure ARI is updated before we check whether the cert needs renewing
// (we ignore the second return value because we already check if needs renewing anyway)
if cert.ari.NeedsRefresh() {
cert, _, err = cfg.updateARI(ctx, cert, cfg.Logger)
if err != nil {
cfg.Logger.Error("updating ARI upon managing", zap.Error(err))
}
}
// otherwise, simply renew the certificate if needed
if cert.NeedsRenewal(cfg) {
var err error
@ -484,6 +504,10 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
return fmt.Errorf("no issuers configured; impossible to obtain or check for existing certificate in storage")
}
log := cfg.Logger.Named("obtain")
name = cfg.transformSubject(ctx, log, name)
// if storage has all resources for this certificate, obtain is a no-op
if cfg.storageHasCertResourcesAnyIssuer(ctx, name) {
return nil
@ -496,8 +520,6 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
return fmt.Errorf("failed storage check: %v - storage is probably misconfigured", err)
}
log := cfg.Logger.Named("obtain")
log.Info("acquiring lock", zap.String("identifier", name))
// ensure idempotency of the obtain operation for this name
@ -561,7 +583,7 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
}
}
csr, err := cfg.generateCSR(privKey, []string{name})
csr, err := cfg.generateCSR(privKey, []string{name}, false)
if err != nil {
return err
}
@ -583,7 +605,19 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
}
}
issuedCert, err = issuer.Issue(ctx, csr)
// TODO: ZeroSSL's API currently requires CommonName to be set, and requires it be
// distinct from SANs. If this was a cert it would violate the BRs, but their certs
// are compliant, so their CSR requirements just needlessly add friction, complexity,
// and inefficiency for clients. CommonName has been deprecated for 25+ years.
useCSR := csr
if issuer.IssuerKey() == zerosslIssuerKey {
useCSR, err = cfg.generateCSR(privKey, []string{name}, true)
if err != nil {
return err
}
}
issuedCert, err = issuer.Issue(ctx, useCSR)
if err == nil {
issuerUsed = issuer
break
@ -615,11 +649,15 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
issuerKey := issuerUsed.IssuerKey()
// success - immediately save the certificate resource
metaJSON, err := json.Marshal(issuedCert.Metadata)
if err != nil {
log.Error("unable to encode certificate metadata", zap.Error(err))
}
certRes := CertificateResource{
SANs: namesFromCSR(csr),
CertificatePEM: issuedCert.Certificate,
PrivateKeyPEM: privKeyPEM,
IssuerData: issuedCert.Metadata,
IssuerData: metaJSON,
issuerKey: issuerUsed.IssuerKey(),
}
err = cfg.saveCertResource(ctx, issuerUsed, certRes)
@ -627,7 +665,9 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
return fmt.Errorf("[%s] Obtain: saving assets: %v", name, err)
}
log.Info("certificate obtained successfully", zap.String("identifier", name))
log.Info("certificate obtained successfully",
zap.String("identifier", name),
zap.String("issuer", issuerUsed.IssuerKey()))
certKey := certRes.NamesKey()
@ -639,6 +679,10 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
"private_key_path": StorageKeys.SitePrivateKey(issuerKey, certKey),
"certificate_path": StorageKeys.SiteCert(issuerKey, certKey),
"metadata_path": StorageKeys.SiteMeta(issuerKey, certKey),
"csr_pem": pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr.Raw,
}),
})
return nil
@ -723,6 +767,10 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
return fmt.Errorf("no issuers configured; impossible to renew or check existing certificate in storage")
}
log := cfg.Logger.Named("renew")
name = cfg.transformSubject(ctx, log, name)
// ensure storage is writeable and readable
// TODO: this is not necessary every time; should only perform check once every so often for each storage, which may require some global state...
err := cfg.checkStorage(ctx)
@ -730,8 +778,6 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
return fmt.Errorf("failed storage check: %v - storage is probably misconfigured", err)
}
log := cfg.Logger.Named("renew")
log.Info("acquiring lock", zap.String("identifier", name))
// ensure idempotency of the renew operation for this name
@ -760,7 +806,7 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
}
// check if renew is still needed - might have been renewed while waiting for lock
timeLeft, needsRenew := cfg.managedCertNeedsRenewal(certRes)
timeLeft, leaf, needsRenew := cfg.managedCertNeedsRenewal(certRes, false)
if !needsRenew {
if force {
log.Info("certificate does not need to be renewed, but renewal is being forced",
@ -807,7 +853,7 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
}
}
csr, err := cfg.generateCSR(privateKey, []string{name})
csr, err := cfg.generateCSR(privateKey, []string{name}, false)
if err != nil {
return err
}
@ -817,6 +863,18 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
var issuerUsed Issuer
var issuerKeys []string
for _, issuer := range cfg.Issuers {
// TODO: ZeroSSL's API currently requires CommonName to be set, and requires it be
// distinct from SANs. If this was a cert it would violate the BRs, but their certs
// are compliant, so their CSR requirements just needlessly add friction, complexity,
// and inefficiency for clients. CommonName has been deprecated for 25+ years.
useCSR := csr
if _, ok := issuer.(*ZeroSSLIssuer); ok {
useCSR, err = cfg.generateCSR(privateKey, []string{name}, true)
if err != nil {
return err
}
}
issuerKeys = append(issuerKeys, issuer.IssuerKey())
if prechecker, ok := issuer.(PreChecker); ok {
err = prechecker.PreCheck(ctx, []string{name}, interactive)
@ -825,7 +883,19 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
}
}
issuedCert, err = issuer.Issue(ctx, csr)
// if we're renewing with the same ACME CA as before, have the ACME
// client tell the server we are replacing a certificate (but doing
// this on the wrong CA, or when the CA doesn't recognize the certID,
// can fail the order)
if acmeData, err := certRes.getACMEData(); err == nil && acmeData.CA != "" {
if acmeIss, ok := issuer.(*ACMEIssuer); ok {
if acmeIss.CA == acmeData.CA {
ctx = context.WithValue(ctx, ctxKeyARIReplaces, leaf)
}
}
}
issuedCert, err = issuer.Issue(ctx, useCSR)
if err == nil {
issuerUsed = issuer
break
@ -858,11 +928,15 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
issuerKey := issuerUsed.IssuerKey()
// success - immediately save the renewed certificate resource
metaJSON, err := json.Marshal(issuedCert.Metadata)
if err != nil {
log.Error("unable to encode certificate metadata", zap.Error(err))
}
newCertRes := CertificateResource{
SANs: namesFromCSR(csr),
CertificatePEM: issuedCert.Certificate,
PrivateKeyPEM: certRes.PrivateKeyPEM,
IssuerData: issuedCert.Metadata,
IssuerData: metaJSON,
issuerKey: issuerKey,
}
err = cfg.saveCertResource(ctx, issuerUsed, newCertRes)
@ -870,7 +944,9 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
return fmt.Errorf("[%s] Renew: saving assets: %v", name, err)
}
log.Info("certificate renewed successfully", zap.String("identifier", name))
log.Info("certificate renewed successfully",
zap.String("identifier", name),
zap.String("issuer", issuerKey))
certKey := newCertRes.NamesKey()
@ -883,6 +959,10 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
"private_key_path": StorageKeys.SitePrivateKey(issuerKey, certKey),
"certificate_path": StorageKeys.SiteCert(issuerKey, certKey),
"metadata_path": StorageKeys.SiteMeta(issuerKey, certKey),
"csr_pem": pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr.Raw,
}),
})
return nil
@ -897,10 +977,16 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
return err
}
func (cfg *Config) generateCSR(privateKey crypto.PrivateKey, sans []string) (*x509.CertificateRequest, error) {
// generateCSR generates a CSR for the given SANs. If useCN is true, CommonName will get the first SAN (TODO: this is only a temporary hack for ZeroSSL API support).
func (cfg *Config) generateCSR(privateKey crypto.PrivateKey, sans []string, useCN bool) (*x509.CertificateRequest, error) {
csrTemplate := new(x509.CertificateRequest)
for _, name := range sans {
// TODO: This is a temporary hack to support ZeroSSL API...
if useCN && csrTemplate.Subject.CommonName == "" && len(name) <= 64 {
csrTemplate.Subject.CommonName = name
continue
}
if ip := net.ParseIP(name); ip != nil {
csrTemplate.IPAddresses = append(csrTemplate.IPAddresses, ip)
} else if strings.Contains(name, "@") {
@ -1052,6 +1138,19 @@ func (cfg *Config) getChallengeInfo(ctx context.Context, identifier string) (Cha
return Challenge{Challenge: chalInfo}, true, nil
}
func (cfg *Config) transformSubject(ctx context.Context, logger *zap.Logger, name string) string {
if cfg.SubjectTransformer == nil {
return name
}
transformedName := cfg.SubjectTransformer(ctx, name)
if logger != nil && transformedName != name {
logger.Debug("transformed subject name",
zap.String("original", name),
zap.String("transformed", transformedName))
}
return transformedName
}
// checkStorage tests the storage by writing random bytes
// to a random key, and then loading those bytes and
// comparing the loaded value. If this fails, the provided
@ -1137,14 +1236,19 @@ func (cfg *Config) lockKey(op, domainName string) string {
// managedCertNeedsRenewal returns true if certRes is expiring soon or already expired,
// or if the process of decoding the cert and checking its expiration returned an error.
func (cfg *Config) managedCertNeedsRenewal(certRes CertificateResource) (time.Duration, bool) {
// If there wasn't an error, the leaf cert is also returned, so it can be reused if
// necessary, since we are parsing the PEM bundle anyway.
func (cfg *Config) managedCertNeedsRenewal(certRes CertificateResource, emitLogs bool) (time.Duration, *x509.Certificate, bool) {
certChain, err := parseCertsFromPEMBundle(certRes.CertificatePEM)
if err != nil {
return 0, true
if err != nil || len(certChain) == 0 {
return 0, nil, true
}
var ari acme.RenewalInfo
if ariPtr, err := certRes.getARI(); err == nil && ariPtr != nil {
ari = *ariPtr
}
remaining := time.Until(expiresAt(certChain[0]))
needsRenew := currentlyInRenewalWindow(certChain[0].NotBefore, expiresAt(certChain[0]), cfg.RenewalWindowRatio)
return remaining, needsRenew
return remaining, certChain[0], cfg.certNeedsRenewal(certChain[0], ari, emitLogs)
}
func (cfg *Config) emit(ctx context.Context, eventName string, data map[string]any) error {
@ -1173,6 +1277,10 @@ type OCSPConfig struct {
// embedded in certificates. Mapping to an empty
// URL will disable OCSP from that responder.
ResponderOverrides map[string]string
// Optionally specify a function that can return the URL
// for an HTTP proxy to use for OCSP-related HTTP requests.
HTTPProxy func(*http.Request) (*url.URL, error)
}
// certIssueLockOp is the name of the operation used

View File

@ -280,6 +280,11 @@ func hashCertificateChain(certChain [][]byte) string {
func namesFromCSR(csr *x509.CertificateRequest) []string {
var nameSet []string
// TODO: CommonName should not be used (it has been deprecated for 25+ years,
// but ZeroSSL CA still requires it to be filled out and not overlap SANs...)
if csr.Subject.CommonName != "" {
nameSet = append(nameSet, csr.Subject.CommonName)
}
nameSet = append(nameSet, csr.DNSNames...)
nameSet = append(nameSet, csr.EmailAddresses...)
for _, v := range csr.IPAddresses {

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/miekg/dns"
"go.uber.org/zap"
)
// Code in this file adapted from go-acme/lego, July 2020:
@ -19,19 +20,21 @@ import (
// findZoneByFQDN determines the zone apex for the given fqdn by recursing
// up the domain labels until the nameserver returns a SOA record in the
// answer section.
func findZoneByFQDN(fqdn string, nameservers []string) (string, error) {
// answer section. The logger must be non-nil.
func findZoneByFQDN(logger *zap.Logger, fqdn string, nameservers []string) (string, error) {
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
soa, err := lookupSoaByFqdn(fqdn, nameservers)
soa, err := lookupSoaByFqdn(logger, fqdn, nameservers)
if err != nil {
return "", err
}
return soa.zone, nil
}
func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
func lookupSoaByFqdn(logger *zap.Logger, fqdn string, nameservers []string) (*soaCacheEntry, error) {
logger = logger.Named("soa_lookup")
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
@ -41,10 +44,11 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error)
// prefer cached version if fresh
if ent := fqdnSOACache[fqdn]; ent != nil && !ent.isExpired() {
logger.Debug("using cached SOA result", zap.String("entry", ent.zone))
return ent, nil
}
ent, err := fetchSoaByFqdn(fqdn, nameservers)
ent, err := fetchSoaByFqdn(logger, fqdn, nameservers)
if err != nil {
return nil, err
}
@ -62,7 +66,7 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error)
return ent, nil
}
func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
func fetchSoaByFqdn(logger *zap.Logger, fqdn string, nameservers []string) (*soaCacheEntry, error) {
var err error
var in *dns.Msg
@ -77,6 +81,7 @@ func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
if in == nil {
continue
}
logger.Debug("fetched SOA", zap.String("msg", in.String()))
switch in.Rcode {
case dns.RcodeSuccess:
@ -210,36 +215,46 @@ func populateNameserverPorts(servers []string) {
}
}
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn, value string, resolvers []string) (bool, error) {
// checkDNSPropagation checks if the expected record has been propagated to all authoritative nameservers.
func checkDNSPropagation(logger *zap.Logger, fqdn string, recType uint16, expectedValue string, checkAuthoritativeServers bool, resolvers []string) (bool, error) {
logger = logger.Named("propagation")
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
// Initial attempt to resolve at the recursive NS
r, err := dnsQuery(fqdn, dns.TypeTXT, resolvers, true)
if err != nil {
return false, err
// Initial attempt to resolve at the recursive NS - but do not actually
// dereference (follow) a CNAME record if we are targeting a CNAME record
// itself
if recType != dns.TypeCNAME {
r, err := dnsQuery(fqdn, recType, resolvers, true)
if err != nil {
return false, fmt.Errorf("CNAME dns query: %v", err)
}
if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
}
}
if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
if checkAuthoritativeServers {
authoritativeServers, err := lookupNameservers(logger, fqdn, resolvers)
if err != nil {
return false, fmt.Errorf("looking up authoritative nameservers: %v", err)
}
populateNameserverPorts(authoritativeServers)
resolvers = authoritativeServers
}
logger.Debug("checking authoritative nameservers", zap.Strings("resolvers", resolvers))
authoritativeNss, err := lookupNameservers(fqdn, resolvers)
if err != nil {
return false, err
}
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
return checkAuthoritativeNss(fqdn, recType, expectedValue, resolvers)
}
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
// checkAuthoritativeNss queries each of the given nameservers for the expected record.
func checkAuthoritativeNss(fqdn string, recType uint16, expectedValue string, nameservers []string) (bool, error) {
for _, ns := range nameservers {
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, true)
r, err := dnsQuery(fqdn, recType, []string{ns}, true)
if err != nil {
return false, err
return false, fmt.Errorf("querying authoritative nameservers: %v", err)
}
if r.Rcode != dns.RcodeSuccess {
@ -252,37 +267,43 @@ func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, erro
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
}
var found bool
for _, rr := range r.Answer {
if txt, ok := rr.(*dns.TXT); ok {
record := strings.Join(txt.Txt, "")
if record == value {
found = true
break
switch recType {
case dns.TypeTXT:
if txt, ok := rr.(*dns.TXT); ok {
record := strings.Join(txt.Txt, "")
if record == expectedValue {
return true, nil
}
}
case dns.TypeCNAME:
if cname, ok := rr.(*dns.CNAME); ok {
// TODO: whether a DNS provider assumes a trailing dot or not varies, and we may have to standardize this in libdns packages
if strings.TrimSuffix(cname.Target, ".") == strings.TrimSuffix(expectedValue, ".") {
return true, nil
}
}
default:
return false, fmt.Errorf("unsupported record type: %d", recType)
}
}
if !found {
return false, nil
}
}
return true, nil
return false, nil
}
// lookupNameservers returns the authoritative nameservers for the given fqdn.
func lookupNameservers(fqdn string, resolvers []string) ([]string, error) {
func lookupNameservers(logger *zap.Logger, fqdn string, resolvers []string) ([]string, error) {
var authoritativeNss []string
zone, err := findZoneByFQDN(fqdn, resolvers)
zone, err := findZoneByFQDN(logger, fqdn, resolvers)
if err != nil {
return nil, fmt.Errorf("could not determine the zone: %w", err)
return nil, fmt.Errorf("could not determine the zone for '%s': %w", fqdn, err)
}
r, err := dnsQuery(zone, dns.TypeNS, resolvers, true)
if err != nil {
return nil, err
return nil, fmt.Errorf("querying NS resolver for zone '%s' recursively: %v", zone, err)
}
for _, rr := range r.Answer {

View File

@ -92,7 +92,7 @@ func (s *FileStorage) Load(_ context.Context, key string) ([]byte, error) {
// Delete deletes the value at key.
func (s *FileStorage) Delete(_ context.Context, key string) error {
return os.Remove(s.Filename(key))
return os.RemoveAll(s.Filename(key))
}
// List returns all keys that match prefix.

View File

@ -25,7 +25,7 @@ import (
"sync"
"time"
"github.com/mholt/acmez"
"github.com/mholt/acmez/v2"
"go.uber.org/zap"
"golang.org/x/crypto/ocsp"
)
@ -65,24 +65,27 @@ func (cfg *Config) GetCertificateWithContext(ctx context.Context, clientHello *t
ctx = context.WithValue(ctx, ClientHelloInfoCtxKey, clientHello)
// special case: serve up the certificate for a TLS-ALPN ACME challenge
// (https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05)
for _, proto := range clientHello.SupportedProtos {
if proto == acmez.ACMETLS1Protocol {
challengeCert, distributed, err := cfg.getTLSALPNChallengeCert(clientHello)
if err != nil {
cfg.Logger.Error("tls-alpn challenge",
zap.String("remote_addr", clientHello.Conn.RemoteAddr().String()),
zap.String("server_name", clientHello.ServerName),
zap.Error(err))
return nil, err
}
cfg.Logger.Info("served key authentication certificate",
// (https://www.rfc-editor.org/rfc/rfc8737.html)
// "The ACME server MUST provide an ALPN extension with the single protocol
// name "acme-tls/1" and an SNI extension containing only the domain name
// being validated during the TLS handshake."
if clientHello.ServerName != "" &&
len(clientHello.SupportedProtos) == 1 &&
clientHello.SupportedProtos[0] == acmez.ACMETLS1Protocol {
challengeCert, distributed, err := cfg.getTLSALPNChallengeCert(clientHello)
if err != nil {
cfg.Logger.Error("tls-alpn challenge",
zap.String("remote_addr", clientHello.Conn.RemoteAddr().String()),
zap.String("server_name", clientHello.ServerName),
zap.String("challenge", "tls-alpn-01"),
zap.String("remote", clientHello.Conn.RemoteAddr().String()),
zap.Bool("distributed", distributed))
return challengeCert, nil
zap.Error(err))
return nil, err
}
cfg.Logger.Info("served key authentication certificate",
zap.String("server_name", clientHello.ServerName),
zap.String("challenge", "tls-alpn-01"),
zap.String("remote", clientHello.Conn.RemoteAddr().String()),
zap.Bool("distributed", distributed))
return challengeCert, nil
}
// get the certificate and serve it up
@ -120,7 +123,7 @@ func (cfg *Config) getCertificateFromCache(hello *tls.ClientHelloInfo) (cert Cer
}
}
// fall back to a "default" certificate, if specified
// use a "default" certificate by name, if specified
if cfg.DefaultServerName != "" {
normDefault := normalizedName(cfg.DefaultServerName)
cert, defaulted = cfg.selectCert(hello, normDefault)
@ -316,13 +319,6 @@ func (cfg *Config) getCertDuringHandshake(ctx context.Context, hello *tls.Client
}()
}
// Make sure a certificate is allowed for the given name. If not, it doesn't
// make sense to try loading one from storage (issue #185), getting it from a
// certificate manager, or obtaining one from an issuer.
if err := cfg.checkIfCertShouldBeObtained(ctx, name, false); err != nil {
return Certificate{}, fmt.Errorf("certificate is not allowed for server name %s: %w", name, err)
}
// If an external Manager is configured, try to get it from them.
// Only continue to use our own logic if it returns empty+nil.
externalCert, err := cfg.getCertFromAnyCertManager(ctx, hello, logger)
@ -333,6 +329,12 @@ func (cfg *Config) getCertDuringHandshake(ctx context.Context, hello *tls.Client
return externalCert, nil
}
// Make sure a certificate is allowed for the given name. If not, it doesn't make sense
// to try loading one from storage (issue #185) or obtaining one from an issuer.
if err := cfg.checkIfCertShouldBeObtained(ctx, name, false); err != nil {
return Certificate{}, fmt.Errorf("certificate is not allowed for server name %s: %w", name, err)
}
// We might be able to load or obtain a needed certificate. Load from
// storage if OnDemand is enabled, or if there is the possibility that
// a statically-managed cert was evicted from a full cache.
@ -547,11 +549,11 @@ func (cfg *Config) obtainOnDemandCertificate(ctx context.Context, hello *tls.Cli
//
// This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) handshakeMaintenance(ctx context.Context, hello *tls.ClientHelloInfo, cert Certificate) (Certificate, error) {
log := cfg.Logger.Named("on_demand")
logger := cfg.Logger.Named("on_demand")
// Check OCSP staple validity
if cert.ocsp != nil && !freshOCSP(cert.ocsp) {
log.Debug("OCSP response needs refreshing",
logger.Debug("OCSP response needs refreshing",
zap.Strings("identifiers", cert.Names),
zap.Int("ocsp_status", cert.ocsp.Status),
zap.Time("this_update", cert.ocsp.ThisUpdate),
@ -561,12 +563,12 @@ func (cfg *Config) handshakeMaintenance(ctx context.Context, hello *tls.ClientHe
if err != nil {
// An error with OCSP stapling is not the end of the world, and in fact, is
// quite common considering not all certs have issuer URLs that support it.
log.Warn("stapling OCSP",
logger.Warn("stapling OCSP",
zap.String("server_name", hello.ServerName),
zap.Strings("sans", cert.Names),
zap.Error(err))
} else {
log.Debug("successfully stapled new OCSP response",
logger.Debug("successfully stapled new OCSP response",
zap.Strings("identifiers", cert.Names),
zap.Int("ocsp_status", cert.ocsp.Status),
zap.Time("this_update", cert.ocsp.ThisUpdate),
@ -579,10 +581,20 @@ func (cfg *Config) handshakeMaintenance(ctx context.Context, hello *tls.ClientHe
cfg.certCache.mu.Unlock()
}
// Check ARI status
if cert.ari.NeedsRefresh() {
// we ignore the second return value here because we go on to check renewal status below regardless
var err error
cert, _, err = cfg.updateARI(ctx, cert, logger)
if err != nil {
logger.Error("updated ARI", zap.Error(err))
}
}
// We attempt to replace any certificates that were revoked.
// Crucially, this happens OUTSIDE a lock on the certCache.
if certShouldBeForceRenewed(cert) {
log.Warn("on-demand certificate's OCSP status is REVOKED; will try to forcefully renew",
logger.Warn("on-demand certificate's OCSP status is REVOKED; will try to forcefully renew",
zap.Strings("identifiers", cert.Names),
zap.Int("ocsp_status", cert.ocsp.Status),
zap.Time("revoked_at", cert.ocsp.RevokedAt),
@ -592,14 +604,13 @@ func (cfg *Config) handshakeMaintenance(ctx context.Context, hello *tls.ClientHe
}
// Check cert expiration
if currentlyInRenewalWindow(cert.Leaf.NotBefore, expiresAt(cert.Leaf), cfg.RenewalWindowRatio) {
if cfg.certNeedsRenewal(cert.Leaf, cert.ari, true) {
// Check if the certificate still exists on disk. If not, we need to obtain a new one.
// This can happen if the certificate was cleaned up by the storage cleaner, but still
// remains in the in-memory cache.
if !cfg.storageHasCertResourcesAnyIssuer(ctx, cert.Names[0]) {
log.Debug("certificate not found on disk; obtaining new certificate",
logger.Debug("certificate not found on disk; obtaining new certificate",
zap.Strings("identifiers", cert.Names))
return cfg.obtainOnDemandCertificate(ctx, hello)
}
// Otherwise, renew the certificate.
@ -621,7 +632,7 @@ func (cfg *Config) handshakeMaintenance(ctx context.Context, hello *tls.ClientHe
//
// This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) renewDynamicCertificate(ctx context.Context, hello *tls.ClientHelloInfo, currentCert Certificate) (Certificate, error) {
log := logWithRemote(cfg.Logger.Named("on_demand"), hello)
logger := logWithRemote(cfg.Logger.Named("on_demand"), hello)
name := cfg.getNameFromClientHello(hello)
timeLeft := time.Until(expiresAt(currentCert.Leaf))
@ -638,7 +649,7 @@ func (cfg *Config) renewDynamicCertificate(ctx context.Context, hello *tls.Clien
// renewing it, so we might as well serve what we have without blocking, UNLESS
// we're forcing renewal, in which case the current certificate is not usable
if timeLeft > 0 && !revoked {
log.Debug("certificate expires soon but is already being renewed; serving current certificate",
logger.Debug("certificate expires soon but is already being renewed; serving current certificate",
zap.Strings("subjects", currentCert.Names),
zap.Duration("remaining", timeLeft))
return currentCert, nil
@ -647,7 +658,7 @@ func (cfg *Config) renewDynamicCertificate(ctx context.Context, hello *tls.Clien
// otherwise, we'll have to wait for the renewal to finish so we don't serve
// a revoked or expired certificate
log.Debug("certificate has expired, but is already being renewed; waiting for renewal to complete",
logger.Debug("certificate has expired, but is already being renewed; waiting for renewal to complete",
zap.Strings("subjects", currentCert.Names),
zap.Time("expired", expiresAt(currentCert.Leaf)),
zap.Bool("revoked", revoked))
@ -678,7 +689,7 @@ func (cfg *Config) renewDynamicCertificate(ctx context.Context, hello *tls.Clien
obtainCertWaitChansMu.Unlock()
}
log = log.With(
logger = logger.With(
zap.String("server_name", name),
zap.Strings("subjects", currentCert.Names),
zap.Time("expiration", expiresAt(currentCert.Leaf)),
@ -699,19 +710,19 @@ func (cfg *Config) renewDynamicCertificate(ctx context.Context, hello *tls.Clien
cfg.certCache.mu.Unlock()
unblockWaiters()
if log != nil {
log.Error("certificate should not be obtained", zap.Error(err))
if logger != nil {
logger.Error("certificate should not be obtained", zap.Error(err))
}
return Certificate{}, err
}
log.Info("attempting certificate renewal")
logger.Info("attempting certificate renewal")
// otherwise, renew with issuer, etc.
var newCert Certificate
if revoked {
newCert, err = cfg.forceRenew(ctx, log, currentCert)
newCert, err = cfg.forceRenew(ctx, logger, currentCert)
} else {
err = cfg.RenewCertAsync(ctx, name, false)
if err == nil {
@ -726,7 +737,7 @@ func (cfg *Config) renewDynamicCertificate(ctx context.Context, hello *tls.Clien
unblockWaiters()
if err != nil {
log.Error("renewing and reloading certificate", zap.String("server_name", name), zap.Error(err))
logger.Error("renewing and reloading certificate", zap.String("server_name", name), zap.Error(err))
}
return newCert, err
@ -753,16 +764,16 @@ func (cfg *Config) getCertFromAnyCertManager(ctx context.Context, hello *tls.Cli
return Certificate{}, nil
}
var upstreamCert *tls.Certificate
// try all the GetCertificate methods on external managers; use first one that returns a certificate
var upstreamCert *tls.Certificate
var err error
for i, certManager := range cfg.OnDemand.Managers {
var err error
upstreamCert, err = certManager.GetCertificate(ctx, hello)
if err != nil {
logger.Error("getting certificate from external certificate manager",
logger.Error("external certificate manager",
zap.String("sni", hello.ServerName),
zap.Int("cert_manager", i),
zap.String("cert_manager", fmt.Sprintf("%T", certManager)),
zap.Int("cert_manager_idx", i),
zap.Error(err))
continue
}
@ -770,14 +781,16 @@ func (cfg *Config) getCertFromAnyCertManager(ctx context.Context, hello *tls.Cli
break
}
}
if err != nil {
return Certificate{}, fmt.Errorf("external certificate manager indicated that it is unable to yield certificate: %v", err)
}
if upstreamCert == nil {
logger.Debug("all external certificate managers yielded no certificates and no errors", zap.String("sni", hello.ServerName))
return Certificate{}, nil
}
var cert Certificate
err := fillCertFromLeaf(&cert, *upstreamCert)
if err != nil {
if err = fillCertFromLeaf(&cert, *upstreamCert); err != nil {
return Certificate{}, fmt.Errorf("external certificate manager: %s: filling cert from leaf: %v", hello.ServerName, err)
}
@ -822,10 +835,13 @@ func (cfg *Config) getTLSALPNChallengeCert(clientHello *tls.ClientHelloInfo) (*t
// getNameFromClientHello returns a normalized form of hello.ServerName.
// If hello.ServerName is empty (i.e. client did not use SNI), then the
// associated connection's local address is used to extract an IP address.
func (*Config) getNameFromClientHello(hello *tls.ClientHelloInfo) string {
func (cfg *Config) getNameFromClientHello(hello *tls.ClientHelloInfo) string {
if name := normalizedName(hello.ServerName); name != "" {
return name
}
if cfg.DefaultServerName != "" {
return normalizedName(cfg.DefaultServerName)
}
return localIPFromConn(hello.Conn)
}

View File

@ -16,9 +16,10 @@ package certmagic
import (
"net/http"
"net/url"
"strings"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
)
@ -91,7 +92,7 @@ func solveHTTPChallenge(logger *zap.Logger, w http.ResponseWriter, r *http.Reque
challengeReqPath := challenge.HTTP01ResourcePath()
if r.URL.Path == challengeReqPath &&
strings.EqualFold(hostOnly(r.Host), challenge.Identifier.Value) && // mitigate DNS rebinding attacks
r.Method == "GET" {
r.Method == http.MethodGet {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(challenge.KeyAuthorization))
r.Close = true
@ -116,7 +117,94 @@ func SolveHTTPChallenge(logger *zap.Logger, w http.ResponseWriter, r *http.Reque
// LooksLikeHTTPChallenge returns true if r looks like an ACME
// HTTP challenge request from an ACME server.
func LooksLikeHTTPChallenge(r *http.Request) bool {
return r.Method == "GET" && strings.HasPrefix(r.URL.Path, challengeBasePath)
return r.Method == http.MethodGet &&
strings.HasPrefix(r.URL.Path, acmeHTTPChallengeBasePath)
}
const challengeBasePath = "/.well-known/acme-challenge"
// LooksLikeZeroSSLHTTPValidation returns true if the request appears to be
// domain validation from a ZeroSSL/Sectigo CA. NOTE: This API is
// non-standard and is subject to change.
func LooksLikeZeroSSLHTTPValidation(r *http.Request) bool {
return r.Method == http.MethodGet &&
strings.HasPrefix(r.URL.Path, zerosslHTTPValidationBasePath)
}
// HTTPValidationHandler wraps the ZeroSSL HTTP validation handler such that
// it can pass verification checks from ZeroSSL's API.
//
// If a request is not a ZeroSSL HTTP validation request, h will be invoked.
func (iss *ZeroSSLIssuer) HTTPValidationHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if iss.HandleZeroSSLHTTPValidation(w, r) {
return
}
h.ServeHTTP(w, r)
})
}
// HandleZeroSSLHTTPValidation is to ZeroSSL API HTTP validation requests like HandleHTTPChallenge
// is to ACME HTTP challenge requests.
func (iss *ZeroSSLIssuer) HandleZeroSSLHTTPValidation(w http.ResponseWriter, r *http.Request) bool {
if iss == nil {
return false
}
if !LooksLikeZeroSSLHTTPValidation(r) {
return false
}
return iss.distributedHTTPValidationAnswer(w, r)
}
func (iss *ZeroSSLIssuer) distributedHTTPValidationAnswer(w http.ResponseWriter, r *http.Request) bool {
if iss == nil {
return false
}
logger := iss.Logger
if logger == nil {
logger = zap.NewNop()
}
host := hostOnly(r.Host)
valInfo, distributed, err := iss.getDistributedValidationInfo(r.Context(), host)
if err != nil {
logger.Error("looking up info for HTTP validation",
zap.String("host", host),
zap.String("remote_addr", r.RemoteAddr),
zap.String("user_agent", r.Header.Get("User-Agent")),
zap.Error(err))
return false
}
return answerHTTPValidation(logger, w, r, valInfo, distributed)
}
func answerHTTPValidation(logger *zap.Logger, rw http.ResponseWriter, req *http.Request, valInfo acme.Challenge, distributed bool) bool {
// ensure URL matches
validationURL, err := url.Parse(valInfo.URL)
if err != nil {
logger.Error("got invalid URL from CA",
zap.String("file_validation_url", valInfo.URL),
zap.Error(err))
rw.WriteHeader(http.StatusInternalServerError)
return true
}
if req.URL.Path != validationURL.Path {
rw.WriteHeader(http.StatusNotFound)
return true
}
rw.Header().Add("Content-Type", "text/plain")
req.Close = true
rw.Write([]byte(valInfo.Token))
logger.Info("served HTTP validation credential",
zap.String("validation_path", valInfo.URL),
zap.String("challenge", "http-01"),
zap.String("remote", req.RemoteAddr),
zap.Bool("distributed", distributed))
return true
}
const (
acmeHTTPChallengeBasePath = "/.well-known/acme-challenge"
zerosslHTTPValidationBasePath = "/.well-known/pki-validation/"
)

View File

@ -27,7 +27,7 @@ import (
"strings"
"time"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
"golang.org/x/crypto/ocsp"
)
@ -92,7 +92,7 @@ func (certCache *Cache) maintainAssets(panicCount int) {
func (certCache *Cache) RenewManagedCertificates(ctx context.Context) error {
log := certCache.logger.Named("maintenance")
// configs will hold a map of certificate name to the config
// configs will hold a map of certificate hash to the config
// to use when managing that certificate
configs := make(map[string]*Config)
@ -102,7 +102,7 @@ func (certCache *Cache) RenewManagedCertificates(ctx context.Context) error {
// words, our first iteration through the certificate cache does NOT
// perform any operations--only queues them--so that more fine-grained
// write locks may be obtained during the actual operations.
var renewQueue, reloadQueue, deleteQueue []Certificate
var renewQueue, reloadQueue, deleteQueue, ariQueue certList
certCache.mu.RLock()
for certKey, cert := range certCache.cache {
@ -135,22 +135,28 @@ func (certCache *Cache) RenewManagedCertificates(ctx context.Context) error {
continue
}
// ACME-specific: see if if ACME Renewal Info (ARI) window needs refreshing
if cert.ari.NeedsRefresh() {
configs[cert.hash] = cfg
ariQueue = append(ariQueue, cert)
}
// if time is up or expires soon, we need to try to renew it
if cert.NeedsRenewal(cfg) {
configs[cert.Names[0]] = cfg
configs[cert.hash] = cfg
// see if the certificate in storage has already been renewed, possibly by another
// instance that didn't coordinate with this one; if so, just load it (this
// might happen if another instance already renewed it - kinda sloppy but checking disk
// first is a simple way to possibly drastically reduce rate limit problems)
storedCertExpiring, err := cfg.managedCertInStorageExpiresSoon(ctx, cert)
storedCertNeedsRenew, err := cfg.managedCertInStorageNeedsRenewal(ctx, cert)
if err != nil {
// hmm, weird, but not a big deal, maybe it was deleted or something
log.Warn("error while checking if stored certificate is also expiring soon",
zap.Strings("identifiers", cert.Names),
zap.Error(err))
} else if !storedCertExpiring {
// if the certificate is NOT expiring soon and there was no error, then we
} else if !storedCertNeedsRenew {
// if the certificate does NOT need renewal and there was no error, then we
// are good to just reload the certificate from storage instead of repeating
// a likely-unnecessary renewal procedure
reloadQueue = append(reloadQueue, cert)
@ -161,11 +167,30 @@ func (certCache *Cache) RenewManagedCertificates(ctx context.Context) error {
// NOTE: It is super-important to note that the TLS-ALPN challenge requires
// a write lock on the cache in order to complete its challenge, so it is extra
// vital that this renew operation does not happen inside our read lock!
renewQueue = append(renewQueue, cert)
renewQueue.insert(cert)
}
}
certCache.mu.RUnlock()
// Update ARI, and then for any certs where the ARI window changed,
// be sure to queue them for renewal if necessary
for _, cert := range ariQueue {
cfg := configs[cert.hash]
cert, changed, err := cfg.updateARI(ctx, cert, log)
if err != nil {
log.Error("updating ARI", zap.Error(err))
}
if changed && cert.NeedsRenewal(cfg) {
// it's theoretically possible that another instance already got the memo
// on the changed ARI and even renewed the cert already, and thus doing it
// here is wasteful, but I have never heard of this happening in reality,
// so to save some cycles for now I think we'll just queue it for renewal
// (notice how we use 'insert' to avoid duplicates, in case it was already
// scheduled for renewal anyway)
renewQueue.insert(cert)
}
}
// Reload certificates that merely need to be updated in memory
for _, oldCert := range reloadQueue {
timeLeft := expiresAt(oldCert.Leaf).Sub(time.Now().UTC())
@ -173,7 +198,7 @@ func (certCache *Cache) RenewManagedCertificates(ctx context.Context) error {
zap.Strings("identifiers", oldCert.Names),
zap.Duration("remaining", timeLeft))
cfg := configs[oldCert.Names[0]]
cfg := configs[oldCert.hash]
// crucially, this happens OUTSIDE a lock on the certCache
_, err := cfg.reloadManagedCertificate(ctx, oldCert)
@ -187,7 +212,7 @@ func (certCache *Cache) RenewManagedCertificates(ctx context.Context) error {
// Renewal queue
for _, oldCert := range renewQueue {
cfg := configs[oldCert.Names[0]]
cfg := configs[oldCert.hash]
err := certCache.queueRenewalTask(ctx, oldCert, cfg)
if err != nil {
log.Error("queueing renewal task",
@ -390,6 +415,171 @@ func (certCache *Cache) updateOCSPStaples(ctx context.Context) {
}
}
// storageHasNewerARI returns true if the configured storage has ARI that is newer
// than that of a certificate that is already loaded, along with the value from
// storage.
func (cfg *Config) storageHasNewerARI(ctx context.Context, cert Certificate) (bool, acme.RenewalInfo, error) {
storedCertData, err := cfg.loadStoredACMECertificateMetadata(ctx, cert)
if err != nil || storedCertData.RenewalInfo == nil {
return false, acme.RenewalInfo{}, err
}
// prefer stored info if it has a window and the loaded one doesn't,
// or if the one in storage has a later RetryAfter (though I suppose
// it's not guaranteed, typically those will move forward in time)
if (!cert.ari.HasWindow() && storedCertData.RenewalInfo.HasWindow()) ||
storedCertData.RenewalInfo.RetryAfter.After(*cert.ari.RetryAfter) {
return true, *storedCertData.RenewalInfo, nil
}
return false, acme.RenewalInfo{}, nil
}
// loadStoredACMECertificateMetadata loads the stored ACME certificate data
// from the cert's sidecar JSON file.
func (cfg *Config) loadStoredACMECertificateMetadata(ctx context.Context, cert Certificate) (acme.Certificate, error) {
metaBytes, err := cfg.Storage.Load(ctx, StorageKeys.SiteMeta(cert.issuerKey, cert.Names[0]))
if err != nil {
return acme.Certificate{}, fmt.Errorf("loading cert metadata: %w", err)
}
var certRes CertificateResource
if err = json.Unmarshal(metaBytes, &certRes); err != nil {
return acme.Certificate{}, fmt.Errorf("unmarshaling cert metadata: %w", err)
}
var acmeCert acme.Certificate
if err = json.Unmarshal(certRes.IssuerData, &acmeCert); err != nil {
return acme.Certificate{}, fmt.Errorf("unmarshaling potential ACME issuer metadata: %v", err)
}
return acmeCert, nil
}
// updateARI updates the cert's ACME renewal info, first by checking storage for a newer
// one, or getting it from the CA if needed. The updated info is stored in storage and
// updated in the cache. The certificate with the updated ARI is returned. If true is
// returned, the ARI window or selected time has changed, and the caller should check if
// the cert needs to be renewed now, even if there is an error.
func (cfg *Config) updateARI(ctx context.Context, cert Certificate, logger *zap.Logger) (updatedCert Certificate, changed bool, err error) {
logger = logger.With(
zap.Strings("identifiers", cert.Names),
zap.String("cert_hash", cert.hash),
zap.String("ari_unique_id", cert.ari.UniqueIdentifier),
zap.Time("cert_expiry", cert.Leaf.NotAfter))
updatedCert = cert
oldARI := cert.ari
// see if the stored value has been refreshed already by another instance
gotNewARI, newARI, err := cfg.storageHasNewerARI(ctx, cert)
// when we're all done, log if something about the schedule is different
// ("WARN" level because ARI window changing may be a sign of external trouble
// and we want to draw their attention to a potential explanation URL)
defer func() {
changed = !newARI.SameWindow(oldARI)
if changed {
logger.Warn("ARI window or selected renewal time changed",
zap.Time("prev_start", oldARI.SuggestedWindow.Start),
zap.Time("next_start", newARI.SuggestedWindow.Start),
zap.Time("prev_end", oldARI.SuggestedWindow.End),
zap.Time("next_end", newARI.SuggestedWindow.End),
zap.Time("prev_selected_time", oldARI.SelectedTime),
zap.Time("next_selected_time", newARI.SelectedTime),
zap.String("explanation_url", newARI.ExplanationURL))
}
}()
if err == nil && gotNewARI {
// great, storage has a newer one we can use
cfg.certCache.mu.Lock()
updatedCert = cfg.certCache.cache[cert.hash]
updatedCert.ari = newARI
cfg.certCache.cache[cert.hash] = updatedCert
cfg.certCache.mu.Unlock()
logger.Info("reloaded ARI with newer one in storage",
zap.Timep("next_refresh", newARI.RetryAfter),
zap.Time("renewal_time", newARI.SelectedTime))
return
}
if err != nil {
logger.Error("error while checking storage for updated ARI; updating ARI now", zap.Error(err))
}
// of the issuers configured, hopefully one of them is the ACME CA we got the cert from
for _, iss := range cfg.Issuers {
if acmeIss, ok := iss.(*ACMEIssuer); ok {
newARI, err = acmeIss.getRenewalInfo(ctx, cert) // be sure to use existing newARI variable so we can compare against old value in the defer
if err != nil {
// could be anything, but a common error might simply be the "wrong" ACME CA
// (meaning, different from the one that issued the cert, thus the only one
// that would have any ARI for it) if multiple ACME CAs are configured
logger.Error("failed updating renewal info from ACME CA",
zap.String("issuer", iss.IssuerKey()),
zap.Error(err))
continue
}
// when we get the latest ARI, the acme package will select a time within the window
// for us; of course, since it's random, it's likely different from the previously-
// selected time; but if the window doesn't change, there's no need to change the
// selected time (the acme package doesn't know the previous window to know better)
// ... so if the window hasn't changed we'll just put back the selected time
if newARI.SameWindow(oldARI) && !oldARI.SelectedTime.IsZero() {
newARI.SelectedTime = oldARI.SelectedTime
}
// then store the updated ARI (even if the window didn't change, the Retry-After
// likely did) in cache and storage
// be sure we get the cert from the cache while inside a lock to avoid logical races
cfg.certCache.mu.Lock()
updatedCert = cfg.certCache.cache[cert.hash]
updatedCert.ari = newARI
cfg.certCache.cache[cert.hash] = updatedCert
cfg.certCache.mu.Unlock()
// update the ARI value in storage
var certData acme.Certificate
certData, err = cfg.loadStoredACMECertificateMetadata(ctx, cert)
if err != nil {
err = fmt.Errorf("got new ARI from %s, but failed loading stored certificate metadata: %v", iss.IssuerKey(), err)
return
}
certData.RenewalInfo = &newARI
var certDataBytes, certResBytes []byte
certDataBytes, err = json.Marshal(certData)
if err != nil {
err = fmt.Errorf("got new ARI from %s, but failed marshaling certificate ACME metadata: %v", iss.IssuerKey(), err)
return
}
certResBytes, err = json.MarshalIndent(CertificateResource{
SANs: cert.Names,
IssuerData: certDataBytes,
}, "", "\t")
if err != nil {
err = fmt.Errorf("got new ARI from %s, but could not re-encode certificate metadata: %v", iss.IssuerKey(), err)
return
}
if err = cfg.Storage.Store(ctx, StorageKeys.SiteMeta(cert.issuerKey, cert.Names[0]), certResBytes); err != nil {
err = fmt.Errorf("got new ARI from %s, but could not store it with certificate metadata: %v", iss.IssuerKey(), err)
return
}
logger.Info("updated ACME renewal information",
zap.Time("selected_time", newARI.SelectedTime),
zap.Timep("next_update", newARI.RetryAfter),
zap.String("explanation_url", newARI.ExplanationURL))
return
}
}
err = fmt.Errorf("could not fully update ACME renewal info: either no ACME issuer configured for certificate, or all failed (make sure the ACME CA that issued the certificate is configured)")
return
}
// CleanStorageOptions specifies how to clean up a storage unit.
type CleanStorageOptions struct {
// Optional custom logger.
@ -452,7 +642,7 @@ func CleanStorage(ctx context.Context, storage Storage, opts CleanStorageOptions
lastTLSClean := lastClean["tls"]
if time.Since(lastTLSClean.Timestamp) < opts.Interval {
nextTime := time.Now().Add(opts.Interval)
opts.Logger.Warn("storage cleaning happened too recently; skipping for now",
opts.Logger.Info("storage cleaning happened too recently; skipping for now",
zap.String("instance", lastTLSClean.InstanceID),
zap.Time("try_again", nextTime),
zap.Duration("try_again_in", time.Until(nextTime)),
@ -725,6 +915,19 @@ func certShouldBeForceRenewed(cert Certificate) bool {
cert.ocsp.Status == ocsp.Revoked
}
type certList []Certificate
// insert appends cert to the list if it is not already in the list.
// Efficiency: O(n)
func (certs *certList) insert(cert Certificate) {
for _, c := range *certs {
if c.hash == cert.hash {
return
}
}
*certs = append(*certs, cert)
}
const (
// DefaultRenewCheckInterval is how often to check certificates for expiration.
// Scans are very lightweight, so this can be semi-frequent. This default should

View File

@ -168,12 +168,24 @@ func getOCSPForCert(ocspConfig OCSPConfig, bundle []byte) ([]byte, *ocsp.Respons
return nil, nil, fmt.Errorf("override disables querying OCSP responder: %v", issuedCert.OCSPServer[0])
}
// configure HTTP client if necessary
httpClient := http.DefaultClient
if ocspConfig.HTTPProxy != nil {
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: ocspConfig.HTTPProxy,
},
Timeout: 30 * time.Second,
}
}
// get issuer certificate if needed
if len(certificates) == 1 {
if len(issuedCert.IssuingCertificateURL) == 0 {
return nil, nil, fmt.Errorf("no URL to issuing certificate")
}
resp, err := http.Get(issuedCert.IssuingCertificateURL[0])
resp, err := httpClient.Get(issuedCert.IssuingCertificateURL[0])
if err != nil {
return nil, nil, fmt.Errorf("getting issuer certificate: %v", err)
}
@ -202,7 +214,7 @@ func getOCSPForCert(ocspConfig OCSPConfig, bundle []byte) ([]byte, *ocsp.Respons
}
reader := bytes.NewReader(ocspReq)
req, err := http.Post(respURL, "application/ocsp-request", reader)
req, err := httpClient.Post(respURL, "application/ocsp-request", reader)
if err != nil {
return nil, nil, fmt.Errorf("making OCSP request: %v", err)
}

View File

@ -30,9 +30,10 @@ import (
"time"
"github.com/libdns/libdns"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
"github.com/miekg/dns"
"go.uber.org/zap"
)
// httpSolver solves the HTTP challenge. It must be
@ -46,9 +47,9 @@ import (
// can access the keyAuth material is by loading it
// from storage, which is done by distributedSolver.
type httpSolver struct {
closed int32 // accessed atomically
acmeIssuer *ACMEIssuer
address string
closed int32 // accessed atomically
handler http.Handler
address string
}
// Present starts an HTTP server if none is already listening on s.address.
@ -88,7 +89,7 @@ func (s *httpSolver) serve(ctx context.Context, si *solverInfo) {
}()
defer close(si.done)
httpServer := &http.Server{
Handler: s.acmeIssuer.HTTPChallengeHandler(http.NewServeMux()),
Handler: s.handler,
BaseContext: func(listener net.Listener) context.Context { return ctx },
}
httpServer.SetKeepAlivesEnabled(false)
@ -250,9 +251,92 @@ func (s *tlsALPNSolver) CleanUp(_ context.Context, chal acme.Challenge) error {
// DNS provider APIs and implementations of the libdns interfaces must also
// support multiple same-named TXT records.
type DNS01Solver struct {
DNSManager
}
// Present creates the DNS TXT record for the given ACME challenge.
func (s *DNS01Solver) Present(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
zrec, err := s.DNSManager.createRecord(ctx, dnsName, "TXT", keyAuth)
if err != nil {
return err
}
// remember the record and zone we got so we can clean up more efficiently
s.saveDNSPresentMemory(dnsPresentMemory{
dnsName: dnsName,
zoneRec: zrec,
})
return nil
}
// Wait blocks until the TXT record created in Present() appears in
// authoritative lookups, i.e. until it has propagated, or until
// timeout, whichever is first.
func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error {
// prepare for the checks by determining what to look for
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// wait for the record to propagate
memory, err := s.getDNSPresentMemory(dnsName, "TXT", keyAuth)
if err != nil {
return err
}
return s.DNSManager.wait(ctx, memory.zoneRec)
}
// CleanUp deletes the DNS TXT record created in Present().
//
// We ignore the context because cleanup is often/likely performed after
// a context cancellation, and properly-implemented DNS providers should
// honor cancellation, which would result in cleanup being aborted.
// Cleanup must always occur.
func (s *DNS01Solver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// always forget about the record so we don't leak memory
defer s.deleteDNSPresentMemory(dnsName, keyAuth)
// recall the record we created and zone we looked up
memory, err := s.getDNSPresentMemory(dnsName, "TXT", keyAuth)
if err != nil {
return err
}
if err := s.DNSManager.cleanUpRecord(ctx, memory.zoneRec); err != nil {
return err
}
return nil
}
// DNSManager is a type that makes libdns providers usable for performing
// DNS verification. See https://github.com/libdns/libdns
//
// Note that records may be manipulated concurrently by some clients (such as
// acmez, which CertMagic uses), meaning that multiple records may be created
// in a DNS zone simultaneously, and in some cases distinct records of the same
// type may have the same name. For example, solving ACME challenges for both example.com
// and *.example.com create a TXT record named _acme_challenge.example.com,
// but with different tokens as their values. This solver distinguishes between
// different records with the same type and name by looking at their values.
type DNSManager struct {
// The implementation that interacts with the DNS
// provider to set or delete records. (REQUIRED)
DNSProvider ACMEDNSProvider
DNSProvider DNSProvider
// The TTL for the temporary challenge records.
TTL time.Duration
@ -274,6 +358,9 @@ type DNS01Solver struct {
// that the solver doesn't follow CNAME/NS record.
OverrideDomain string
// An optional logger.
Logger *zap.Logger
// Remember DNS records while challenges are active; i.e.
// records we have presented and not yet cleaned up.
// This lets us clean them up quickly and efficiently.
@ -285,83 +372,81 @@ type DNS01Solver struct {
// the value of their TXT records, which should contain
// unique challenge tokens.
// See https://github.com/caddyserver/caddy/issues/3474.
txtRecords map[string][]dnsPresentMemory
txtRecordsMu sync.Mutex
records map[string][]dnsPresentMemory
recordsMu sync.Mutex
}
// Present creates the DNS TXT record for the given ACME challenge.
func (s *DNS01Solver) Present(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
func (m *DNSManager) createRecord(ctx context.Context, dnsName, recordType, recordValue string) (zoneRecord, error) {
logger := m.logger()
zone, err := findZoneByFQDN(dnsName, recursiveNameservers(s.Resolvers))
zone, err := findZoneByFQDN(logger, dnsName, recursiveNameservers(m.Resolvers))
if err != nil {
return fmt.Errorf("could not determine zone for domain %q: %v", dnsName, err)
return zoneRecord{}, fmt.Errorf("could not determine zone for domain %q: %v", dnsName, err)
}
rec := libdns.Record{
Type: "TXT",
Type: recordType,
Name: libdns.RelativeName(dnsName+".", zone),
Value: keyAuth,
TTL: s.TTL,
Value: recordValue,
TTL: m.TTL,
}
results, err := s.DNSProvider.AppendRecords(ctx, zone, []libdns.Record{rec})
logger.Debug("creating DNS record",
zap.String("dns_name", dnsName),
zap.String("zone", zone),
zap.String("record_name", rec.Name),
zap.String("record_type", rec.Type),
zap.String("record_value", rec.Value),
zap.Duration("record_ttl", rec.TTL))
results, err := m.DNSProvider.AppendRecords(ctx, zone, []libdns.Record{rec})
if err != nil {
return fmt.Errorf("adding temporary record for zone %q: %w", zone, err)
return zoneRecord{}, fmt.Errorf("adding temporary record for zone %q: %w", zone, err)
}
if len(results) != 1 {
return fmt.Errorf("expected one record, got %d: %v", len(results), results)
return zoneRecord{}, fmt.Errorf("expected one record, got %d: %v", len(results), results)
}
// remember the record and zone we got so we can clean up more efficiently
s.saveDNSPresentMemory(dnsPresentMemory{
dnsZone: zone,
dnsName: dnsName,
rec: results[0],
})
return nil
return zoneRecord{zone, results[0]}, nil
}
// Wait blocks until the TXT record created in Present() appears in
// wait blocks until the TXT record created in Present() appears in
// authoritative lookups, i.e. until it has propagated, or until
// timeout, whichever is first.
func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error {
func (m *DNSManager) wait(ctx context.Context, zrec zoneRecord) error {
logger := m.logger()
// if configured to, pause before doing propagation checks
// (even if they are disabled, the wait might be desirable on its own)
if s.PropagationDelay > 0 {
if m.PropagationDelay > 0 {
select {
case <-time.After(s.PropagationDelay):
case <-time.After(m.PropagationDelay):
case <-ctx.Done():
return ctx.Err()
}
}
// skip propagation checks if configured to do so
if s.PropagationTimeout == -1 {
if m.PropagationTimeout == -1 {
return nil
}
// prepare for the checks by determining what to look for
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// timings
timeout := s.PropagationTimeout
timeout := m.PropagationTimeout
if timeout == 0 {
timeout = defaultDNSPropagationTimeout
}
const interval = 2 * time.Second
// how we'll do the checks
resolvers := recursiveNameservers(s.Resolvers)
checkAuthoritativeServers := len(m.Resolvers) == 0
resolvers := recursiveNameservers(m.Resolvers)
recType := dns.TypeTXT
if zrec.record.Type == "CNAME" {
recType = dns.TypeCNAME
}
absName := libdns.AbsoluteName(zrec.record.Name, zrec.zone)
var err error
start := time.Now()
@ -371,10 +456,17 @@ func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error
case <-ctx.Done():
return ctx.Err()
}
logger.Debug("checking DNS propagation",
zap.String("fqdn", absName),
zap.String("record_type", zrec.record.Type),
zap.String("expected_value", zrec.record.Value),
zap.Strings("resolvers", resolvers))
var ready bool
ready, err = checkDNSPropagation(dnsName, keyAuth, resolvers)
ready, err = checkDNSPropagation(logger, absName, recType, zrec.record.Value, checkAuthoritativeServers, resolvers)
if err != nil {
return fmt.Errorf("checking DNS propagation of %q: %w", dnsName, err)
return fmt.Errorf("checking DNS propagation of %q (relative=%s zone=%s resolvers=%v): %w", absName, zrec.record.Name, zrec.zone, resolvers, err)
}
if ready {
return nil
@ -384,101 +476,110 @@ func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error
return fmt.Errorf("timed out waiting for record to fully propagate; verify DNS provider configuration is correct - last error: %v", err)
}
type zoneRecord struct {
zone string
record libdns.Record
}
// CleanUp deletes the DNS TXT record created in Present().
//
// We ignore the context because cleanup is often/likely performed after
// a context cancellation, and properly-implemented DNS providers should
// honor cancellation, which would result in cleanup being aborted.
// Cleanup must always occur.
func (s *DNS01Solver) CleanUp(_ context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// always forget about the record so we don't leak memory
defer s.deleteDNSPresentMemory(dnsName, keyAuth)
// recall the record we created and zone we looked up
memory, err := s.getDNSPresentMemory(dnsName, keyAuth)
if err != nil {
return err
}
func (m *DNSManager) cleanUpRecord(_ context.Context, zrec zoneRecord) error {
logger := m.logger()
// clean up the record - use a different context though, since
// one common reason cleanup is performed is because a context
// was canceled, and if so, any HTTP requests by this provider
// should fail if the provider is properly implemented
// (see issue #200)
timeout := s.PropagationTimeout
timeout := m.PropagationTimeout
if timeout <= 0 {
timeout = defaultDNSPropagationTimeout
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
_, err = s.DNSProvider.DeleteRecords(ctx, memory.dnsZone, []libdns.Record{memory.rec})
if err != nil {
return fmt.Errorf("deleting temporary record for name %q in zone %q: %w", memory.dnsName, memory.dnsZone, err)
}
logger.Debug("deleting DNS record",
zap.String("zone", zrec.zone),
zap.String("record_id", zrec.record.ID),
zap.String("record_name", zrec.record.Name),
zap.String("record_type", zrec.record.Type),
zap.String("record_value", zrec.record.Value))
_, err := m.DNSProvider.DeleteRecords(ctx, zrec.zone, []libdns.Record{zrec.record})
if err != nil {
return fmt.Errorf("deleting temporary record for name %q in zone %q: %w", zrec.zone, zrec.record, err)
}
return nil
}
func (m *DNSManager) logger() *zap.Logger {
logger := m.Logger
if logger == nil {
logger = zap.NewNop()
}
return logger.Named("dns_manager")
}
const defaultDNSPropagationTimeout = 2 * time.Minute
// dnsPresentMemory associates a created DNS record with its zone
// (since libdns Records are zone-relative and do not include zone).
type dnsPresentMemory struct {
dnsZone string
dnsName string
rec libdns.Record
zoneRec zoneRecord
}
func (s *DNS01Solver) saveDNSPresentMemory(mem dnsPresentMemory) {
s.txtRecordsMu.Lock()
if s.txtRecords == nil {
s.txtRecords = make(map[string][]dnsPresentMemory)
func (s *DNSManager) saveDNSPresentMemory(mem dnsPresentMemory) {
s.recordsMu.Lock()
if s.records == nil {
s.records = make(map[string][]dnsPresentMemory)
}
s.txtRecords[mem.dnsName] = append(s.txtRecords[mem.dnsName], mem)
s.txtRecordsMu.Unlock()
s.records[mem.dnsName] = append(s.records[mem.dnsName], mem)
s.recordsMu.Unlock()
}
func (s *DNS01Solver) getDNSPresentMemory(dnsName, keyAuth string) (dnsPresentMemory, error) {
s.txtRecordsMu.Lock()
defer s.txtRecordsMu.Unlock()
func (s *DNSManager) getDNSPresentMemory(dnsName, recType, value string) (dnsPresentMemory, error) {
s.recordsMu.Lock()
defer s.recordsMu.Unlock()
var memory dnsPresentMemory
for _, mem := range s.txtRecords[dnsName] {
if mem.rec.Value == keyAuth {
for _, mem := range s.records[dnsName] {
if mem.zoneRec.record.Type == recType && mem.zoneRec.record.Value == value {
memory = mem
break
}
}
if memory.rec.Name == "" {
if memory.zoneRec.record.Name == "" {
return dnsPresentMemory{}, fmt.Errorf("no memory of presenting a DNS record for %q (usually OK if presenting also failed)", dnsName)
}
return memory, nil
}
func (s *DNS01Solver) deleteDNSPresentMemory(dnsName, keyAuth string) {
s.txtRecordsMu.Lock()
defer s.txtRecordsMu.Unlock()
func (s *DNSManager) deleteDNSPresentMemory(dnsName, keyAuth string) {
s.recordsMu.Lock()
defer s.recordsMu.Unlock()
for i, mem := range s.txtRecords[dnsName] {
if mem.rec.Value == keyAuth {
s.txtRecords[dnsName] = append(s.txtRecords[dnsName][:i], s.txtRecords[dnsName][i+1:]...)
for i, mem := range s.records[dnsName] {
if mem.zoneRec.record.Value == keyAuth {
s.records[dnsName] = append(s.records[dnsName][:i], s.records[dnsName][i+1:]...)
return
}
}
}
// ACMEDNSProvider defines the set of operations required for
// ACME challenges. A DNS provider must be able to append and
// delete records in order to solve ACME challenges. Find one
// you can use at https://github.com/libdns. If your provider
// isn't implemented yet, feel free to contribute!
type ACMEDNSProvider interface {
// DNSProvider defines the set of operations required for
// ACME challenges or other sorts of domain verification.
// A DNS provider must be able to append and delete records
// in order to solve ACME challenges. Find one you can use
// at https://github.com/libdns. If your provider isn't
// implemented yet, feel free to contribute!
type DNSProvider interface {
libdns.RecordAppender
libdns.RecordDeleter
}

View File

@ -289,7 +289,7 @@ func acquireLock(ctx context.Context, storage Storage, lockKey string) error {
}
func releaseLock(ctx context.Context, storage Storage, lockKey string) error {
err := storage.Unlock(context.TODO(), lockKey) // TODO: in Go 1.21, use WithoutCancel (see #247)
err := storage.Unlock(context.WithoutCancel(ctx), lockKey)
if err == nil {
locksMu.Lock()
delete(locks, lockKey)

View File

@ -0,0 +1,304 @@
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"context"
"crypto/x509"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/caddyserver/zerossl"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
)
// ZeroSSLIssuer can get certificates from ZeroSSL's API. (To use ZeroSSL's ACME
// endpoint, use the ACMEIssuer instead.) Note that use of the API is restricted
// by payment tier.
type ZeroSSLIssuer struct {
// The API key (or "access key") for using the ZeroSSL API.
// REQUIRED.
APIKey string
// How many days the certificate should be valid for.
ValidityDays int
// The host to bind to when opening a listener for
// verifying domain names (or IPs).
ListenHost string
// If HTTP is forwarded from port 80, specify the
// forwarded port here.
AltHTTPPort int
// To use CNAME validation instead of HTTP
// validation, set this field.
CNAMEValidation *DNSManager
// Where to store verification material temporarily.
// Set this on all instances in a cluster to the same
// value to enable distributed verification.
Storage Storage
// An optional (but highly recommended) logger.
Logger *zap.Logger
}
// Issue obtains a certificate for the given csr.
func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*IssuedCertificate, error) {
client := iss.getClient()
identifiers := namesFromCSR(csr)
if len(identifiers) == 0 {
return nil, fmt.Errorf("no identifiers on CSR")
}
logger := iss.Logger
if logger == nil {
logger = zap.NewNop()
}
logger = logger.With(zap.Strings("identifiers", identifiers))
logger.Info("creating certificate")
cert, err := client.CreateCertificate(ctx, csr, iss.ValidityDays)
if err != nil {
return nil, fmt.Errorf("creating certificate: %v", err)
}
logger = logger.With(zap.String("cert_id", cert.ID))
logger.Info("created certificate")
defer func(certID string) {
if err != nil {
err := client.CancelCertificate(context.WithoutCancel(ctx), certID)
if err == nil {
logger.Info("canceled certificate")
} else {
logger.Error("unable to cancel certificate", zap.Error(err))
}
}
}(cert.ID)
var verificationMethod zerossl.VerificationMethod
if iss.CNAMEValidation == nil {
verificationMethod = zerossl.HTTPVerification
logger = logger.With(zap.String("verification_method", string(verificationMethod)))
httpVerifier := &httpSolver{
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getHTTPPort())),
handler: iss.HTTPValidationHandler(http.NewServeMux()),
}
var solver acmez.Solver = httpVerifier
if iss.Storage != nil {
solver = distributedSolver{
storage: iss.Storage,
storageKeyIssuerPrefix: iss.IssuerKey(),
solver: httpVerifier,
}
}
// since the distributed solver was originally designed for ACME,
// the API is geared around ACME challenges. ZeroSSL's HTTP validation
// is very similar to the HTTP challenge, but not quite compatible,
// so we kind of shim the ZeroSSL validation data into a Challenge
// object... it is not a perfect use of this type but it's pretty close
valInfo := cert.Validation.OtherMethods[identifiers[0]]
fakeChallenge := acme.Challenge{
Identifier: acme.Identifier{
Value: identifiers[0], // used for storage key
},
URL: valInfo.FileValidationURLHTTP,
Token: strings.Join(cert.Validation.OtherMethods[identifiers[0]].FileValidationContent, "\n"),
}
if err = solver.Present(ctx, fakeChallenge); err != nil {
return nil, fmt.Errorf("presenting validation file for verification: %v", err)
}
defer solver.CleanUp(ctx, fakeChallenge)
} else {
verificationMethod = zerossl.CNAMEVerification
logger = logger.With(zap.String("verification_method", string(verificationMethod)))
// create the CNAME record(s)
records := make(map[string]zoneRecord, len(cert.Validation.OtherMethods))
for name, verifyInfo := range cert.Validation.OtherMethods {
zr, err := iss.CNAMEValidation.createRecord(ctx, verifyInfo.CnameValidationP1, "CNAME", verifyInfo.CnameValidationP2)
if err != nil {
return nil, fmt.Errorf("creating CNAME record: %v", err)
}
defer func(name string, zr zoneRecord) {
if err := iss.CNAMEValidation.cleanUpRecord(ctx, zr); err != nil {
logger.Warn("cleaning up temporary validation record failed",
zap.String("dns_name", name),
zap.Error(err))
}
}(name, zr)
records[name] = zr
}
// wait for them to propagate
for name, zr := range records {
if err := iss.CNAMEValidation.wait(ctx, zr); err != nil {
// allow it, since the CA will ultimately decide, but definitely log it
logger.Warn("failed CNAME record propagation check", zap.String("domain", name), zap.Error(err))
}
}
}
logger.Info("validating identifiers")
cert, err = client.VerifyIdentifiers(ctx, cert.ID, verificationMethod, nil)
if err != nil {
return nil, fmt.Errorf("verifying identifiers: %v", err)
}
switch cert.Status {
case "pending_validation":
logger.Info("validations succeeded; waiting for certificate to be issued")
cert, err = iss.waitForCertToBeIssued(ctx, client, cert)
if err != nil {
return nil, fmt.Errorf("waiting for certificate to be issued: %v", err)
}
case "issued":
logger.Info("validations succeeded; downloading certificate bundle")
default:
return nil, fmt.Errorf("unexpected certificate status: %s", cert.Status)
}
bundle, err := client.DownloadCertificate(ctx, cert.ID, false)
if err != nil {
return nil, fmt.Errorf("downloading certificate: %v", err)
}
logger.Info("successfully downloaded issued certificate")
return &IssuedCertificate{
Certificate: []byte(bundle.CertificateCrt + bundle.CABundleCrt),
Metadata: cert,
}, nil
}
func (*ZeroSSLIssuer) waitForCertToBeIssued(ctx context.Context, client zerossl.Client, cert zerossl.CertificateObject) (zerossl.CertificateObject, error) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return cert, ctx.Err()
case <-ticker.C:
var err error
cert, err = client.GetCertificate(ctx, cert.ID)
if err != nil {
return cert, err
}
if cert.Status == "issued" {
return cert, nil
}
if cert.Status != "pending_validation" {
return cert, fmt.Errorf("unexpected certificate status: %s", cert.Status)
}
}
}
}
func (iss *ZeroSSLIssuer) getClient() zerossl.Client {
return zerossl.Client{AccessKey: iss.APIKey}
}
func (iss *ZeroSSLIssuer) getHTTPPort() int {
useHTTPPort := HTTPChallengePort
if HTTPPort > 0 && HTTPPort != HTTPChallengePort {
useHTTPPort = HTTPPort
}
if iss.AltHTTPPort > 0 {
useHTTPPort = iss.AltHTTPPort
}
return useHTTPPort
}
// IssuerKey returns the unique issuer key for ZeroSSL.
func (iss *ZeroSSLIssuer) IssuerKey() string { return zerosslIssuerKey }
// Revoke revokes the given certificate. Only do this if there is a security or trust
// concern with the certificate.
func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert CertificateResource, reason int) error {
r := zerossl.UnspecifiedReason
switch reason {
case acme.ReasonKeyCompromise:
r = zerossl.KeyCompromise
case acme.ReasonAffiliationChanged:
r = zerossl.AffiliationChanged
case acme.ReasonSuperseded:
r = zerossl.Superseded
case acme.ReasonCessationOfOperation:
r = zerossl.CessationOfOperation
default:
return fmt.Errorf("unsupported reason: %d", reason)
}
var certObj zerossl.CertificateObject
if err := json.Unmarshal(cert.IssuerData, &certObj); err != nil {
return err
}
return iss.getClient().RevokeCertificate(ctx, certObj.ID, r)
}
func (iss *ZeroSSLIssuer) getDistributedValidationInfo(ctx context.Context, identifier string) (acme.Challenge, bool, error) {
ds := distributedSolver{
storage: iss.Storage,
storageKeyIssuerPrefix: StorageKeys.Safe(iss.IssuerKey()),
}
tokenKey := ds.challengeTokensKey(identifier)
valObjectBytes, err := iss.Storage.Load(ctx, tokenKey)
if err != nil {
return acme.Challenge{}, false, fmt.Errorf("opening distributed challenge token file %s: %v", tokenKey, err)
}
if len(valObjectBytes) == 0 {
return acme.Challenge{}, false, fmt.Errorf("no information found to solve challenge for identifier: %s", identifier)
}
// since the distributed solver's API is geared around ACME challenges,
// we crammed the validation info into a Challenge object
var chal acme.Challenge
if err = json.Unmarshal(valObjectBytes, &chal); err != nil {
return acme.Challenge{}, false, fmt.Errorf("decoding HTTP validation token file %s (corrupted?): %v", tokenKey, err)
}
return chal, true, nil
}
const (
zerosslAPIBase = "https://" + zerossl.BaseURL + "/acme"
zerosslValidationPathPrefix = "/.well-known/pki-validation/"
zerosslIssuerKey = "zerossl"
)
// Interface guards
var (
_ Issuer = (*ZeroSSLIssuer)(nil)
_ Revoker = (*ZeroSSLIssuer)(nil)
)

2
vendor/github.com/caddyserver/zerossl/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
_gitignore
.DS_Store

21
vendor/github.com/caddyserver/zerossl/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Matthew Holt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
vendor/github.com/caddyserver/zerossl/README.md generated vendored Normal file
View File

@ -0,0 +1,6 @@
ZeroSSL API client [![Go Reference](https://pkg.go.dev/badge/github.com/caddyserver/zerossl.svg)](https://pkg.go.dev/github.com/caddyserver/zerossl)
==================
This package implements the [ZeroSSL REST API](https://zerossl.com/documentation/api/) in Go.
The REST API is distinct from the [ACME endpoint](https://zerossl.com/documentation/acme/), which is a standardized way of obtaining certificates.

170
vendor/github.com/caddyserver/zerossl/client.go generated vendored Normal file
View File

@ -0,0 +1,170 @@
package zerossl
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// Client acts as a ZeroSSL API client. It facilitates ZeroSSL certificate operations.
type Client struct {
// REQUIRED: Your ZeroSSL account access key.
AccessKey string `json:"access_key"`
// Optionally adjust the base URL of the API.
// Default: https://api.zerossl.com
BaseURL string `json:"base_url,omitempty"`
// Optionally configure a custom HTTP client.
HTTPClient *http.Client `json:"-"`
}
func (c Client) httpGet(ctx context.Context, endpoint string, qs url.Values, target any) error {
url := c.url(endpoint, qs)
return c.httpRequest(ctx, http.MethodGet, url, nil, target)
}
func (c Client) httpPost(ctx context.Context, endpoint string, qs url.Values, payload, target any) error {
var reqBody io.Reader
if payload != nil {
payloadJSON, err := json.Marshal(payload)
if err != nil {
return err
}
reqBody = bytes.NewReader(payloadJSON)
}
url := c.url(endpoint, qs)
return c.httpRequest(ctx, http.MethodPost, url, reqBody, target)
}
func (c Client) httpRequest(ctx context.Context, method, reqURL string, reqBody io.Reader, target any) error {
r, err := http.NewRequestWithContext(ctx, method, reqURL, reqBody)
if err != nil {
return err
}
if reqBody != nil {
r.Header.Set("Content-Type", "application/json")
}
resp, err := c.httpClient().Do(r)
if err != nil {
return err
}
defer resp.Body.Close()
// because the ZeroSSL API doesn't use HTTP status codes to indicate an error,
// nor does each response body have a consistent way of detecting success/error,
// we have to implement a hack: download the entire response body and try
// decoding it as JSON in a way that errors if there's any unknown fields
// (such as "success"), because if there is an unkown field, either our model
// is outdated, or there was an error payload in the response instead of the
// expected structure, so we then try again to decode to an error struct
respBytes, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024*2))
if err != nil {
return fmt.Errorf("failed reading response body: %v", err)
}
// assume success first by trying to decode payload into output target
dec := json.NewDecoder(bytes.NewReader(respBytes))
dec.DisallowUnknownFields() // important hacky hack so we can detect an error payload
originalDecodeErr := dec.Decode(&target)
if originalDecodeErr == nil {
return nil
}
// could have gotten any kind of error, really; but assuming valid JSON,
// most likely it is an error payload
var apiError APIError
if err := json.NewDecoder(bytes.NewReader(respBytes)).Decode(&apiError); err != nil {
return fmt.Errorf("request succeeded, but decoding JSON response failed: %v (raw=%s)", err, respBytes)
}
// successfully got an error! or did we?
if apiError.Success {
return apiError // ummm... why are we getting an error if it was successful ??? is this not really an error?
}
// remove access_key from URL so it doesn't leak into logs
u, err := url.Parse(reqURL)
if err != nil {
reqURL = fmt.Sprintf("<invalid url: %v>", err)
}
if u != nil {
q, err := url.ParseQuery(u.RawQuery)
if err == nil {
q.Set(accessKeyParam, "redacted")
u.RawQuery = q.Encode()
reqURL = u.String()
}
}
return fmt.Errorf("%s %s: HTTP %d: %v (raw=%s decode_error=%v)", method, reqURL, resp.StatusCode, apiError, respBytes, originalDecodeErr)
}
func (c Client) url(endpoint string, qs url.Values) string {
baseURL := c.BaseURL
if baseURL == "" {
baseURL = BaseURL
}
// for consistency, ensure endpoint starts with /
// and base URL does NOT end with /.
if !strings.HasPrefix(endpoint, "/") {
endpoint = "/" + endpoint
}
baseURL = strings.TrimSuffix(baseURL, "/")
if qs == nil {
qs = url.Values{}
}
qs.Set(accessKeyParam, c.AccessKey)
return fmt.Sprintf("%s%s?%s", baseURL, endpoint, qs.Encode())
}
func (c Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return httpClient
}
var httpClient = &http.Client{
Timeout: 2 * time.Minute,
}
// anyBool is a hacky type that accepts true or 1 (or their string variants),
// or "yes" or "y", and any casing variants of the same, as a boolean true when
// unmarshaling JSON. Everything else is boolean false.
//
// This is needed due to type inconsistencies in ZeroSSL's API with "success" values.
type anyBool bool
// UnmarshalJSON satisfies json.Unmarshaler according to
// this type's documentation.
func (ab *anyBool) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return io.EOF
}
switch strings.ToLower(string(b)) {
case `true`, `"true"`, `1`, `"1"`, `"yes"`, `"y"`:
*ab = true
}
return nil
}
// MarshalJSON marshals ab to either true or false.
func (ab *anyBool) MarshalJSON() ([]byte, error) {
if ab != nil && *ab {
return []byte("true"), nil
}
return []byte("false"), nil
}
const accessKeyParam = "access_key"

270
vendor/github.com/caddyserver/zerossl/endpoints.go generated vendored Normal file
View File

@ -0,0 +1,270 @@
package zerossl
import (
"context"
"crypto/x509"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
// CreateCertificate creates a certificate. After creating a certificate, its identifiers must be verified before
// the certificate can be downloaded. The CSR must have been fully created using x509.CreateCertificateRequest
// (its Raw field must be filled out).
func (c Client) CreateCertificate(ctx context.Context, csr *x509.CertificateRequest, validityDays int) (CertificateObject, error) {
payload := struct {
CertificateDomains string `json:"certificate_domains"`
CertificateCSR string `json:"certificate_csr"`
CertificateValidityDays int `json:"certificate_validity_days,omitempty"`
StrictDomains int `json:"strict_domains,omitempty"`
ReplacementForCertificate string `json:"replacement_for_certificate,omitempty"`
}{
CertificateDomains: strings.Join(identifiersFromCSR(csr), ","),
CertificateCSR: csr2pem(csr.Raw),
CertificateValidityDays: validityDays,
StrictDomains: 1,
}
var result CertificateObject
if err := c.httpPost(ctx, "/certificates", nil, payload, &result); err != nil {
return CertificateObject{}, err
}
return result, nil
}
// VerifyIdentifiers tells ZeroSSL that you are ready to prove control over your domain/IP using the method specified.
// The credentials from CreateCertificate must be used to verify identifiers. At least one email is required if using
// email verification method.
func (c Client) VerifyIdentifiers(ctx context.Context, certificateID string, method VerificationMethod, emails []string) (CertificateObject, error) {
payload := struct {
ValidationMethod VerificationMethod `json:"validation_method"`
ValidationEmail string `json:"validation_email,omitempty"`
}{
ValidationMethod: method,
}
if method == EmailVerification && len(emails) > 0 {
payload.ValidationEmail = strings.Join(emails, ",")
}
endpoint := fmt.Sprintf("/certificates/%s/challenges", url.QueryEscape(certificateID))
var result CertificateObject
if err := c.httpPost(ctx, endpoint, nil, payload, &result); err != nil {
return CertificateObject{}, err
}
return result, nil
}
// DownloadCertificateFile writes the certificate bundle as a zip file to the provided output writer.
func (c Client) DownloadCertificateFile(ctx context.Context, certificateID string, includeCrossSigned bool, output io.Writer) error {
endpoint := fmt.Sprintf("/certificates/%s/download", url.QueryEscape(certificateID))
qs := url.Values{}
if includeCrossSigned {
qs.Set("include_cross_signed", "1")
}
url := c.url(endpoint, qs)
r, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := c.httpClient().Do(r)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: HTTP %d", resp.StatusCode)
}
if _, err := io.Copy(output, resp.Body); err != nil {
return err
}
return nil
}
func (c Client) DownloadCertificate(ctx context.Context, certificateID string, includeCrossSigned bool) (CertificateBundle, error) {
endpoint := fmt.Sprintf("/certificates/%s/download/return", url.QueryEscape(certificateID))
qs := url.Values{}
if includeCrossSigned {
qs.Set("include_cross_signed", "1")
}
var result CertificateBundle
if err := c.httpGet(ctx, endpoint, qs, &result); err != nil {
return CertificateBundle{}, err
}
return result, nil
}
func (c Client) GetCertificate(ctx context.Context, certificateID string) (CertificateObject, error) {
endpoint := fmt.Sprintf("/certificates/%s", url.QueryEscape(certificateID))
var result CertificateObject
if err := c.httpGet(ctx, endpoint, nil, &result); err != nil {
return CertificateObject{}, err
}
return result, nil
}
// ListCertificateParameters specifies how to search or list certificates on the account.
// An empty set of parameters will return no results.
type ListCertificatesParameters struct {
// Return certificates with this status.
Status string
// Return these types of certificates.
Type string
// The CommonName or SAN.
Search string
// The page number. Default: 1
Page int
// How many per page. Default: 100
Limit int
}
func (c Client) ListCertificates(ctx context.Context, params ListCertificatesParameters) (CertificateList, error) {
qs := url.Values{}
if params.Status != "" {
qs.Set("certificate_status", params.Status)
}
if params.Type != "" {
qs.Set("certificate_type", params.Type)
}
if params.Search != "" {
qs.Set("search", params.Search)
}
if params.Limit != 0 {
qs.Set("limit", strconv.Itoa(params.Limit))
}
if params.Page != 0 {
qs.Set("page", strconv.Itoa(params.Page))
}
var result CertificateList
if err := c.httpGet(ctx, "/certificates", qs, &result); err != nil {
return CertificateList{}, err
}
return result, nil
}
func (c Client) VerificationStatus(ctx context.Context, certificateID string) (ValidationStatus, error) {
endpoint := fmt.Sprintf("/certificates/%s/status", url.QueryEscape(certificateID))
var result ValidationStatus
if err := c.httpGet(ctx, endpoint, nil, &result); err != nil {
return ValidationStatus{}, err
}
return result, nil
}
func (c Client) ResendVerificationEmail(ctx context.Context, certificateID string) error {
endpoint := fmt.Sprintf("/certificates/%s/challenges/email", url.QueryEscape(certificateID))
var result struct {
Success anyBool `json:"success"`
}
if err := c.httpGet(ctx, endpoint, nil, &result); err != nil {
return err
}
if !result.Success {
return fmt.Errorf("got %v without any error status", result)
}
return nil
}
// Only revoke a certificate if the private key is compromised, the certificate was a mistake, or
// the identifiers are no longer in use. Do not revoke a certificate when renewing it.
func (c Client) RevokeCertificate(ctx context.Context, certificateID string, reason RevocationReason) error {
endpoint := fmt.Sprintf("/certificates/%s/revoke", url.QueryEscape(certificateID))
qs := url.Values{"reason": []string{string(reason)}}
var result struct {
Success anyBool `json:"success"`
}
if err := c.httpGet(ctx, endpoint, qs, &result); err != nil {
return err
}
if !result.Success {
return fmt.Errorf("got %v without any error status", result)
}
return nil
}
// CancelCertificate cancels a certificate that has not been issued yet (is in draft or pending_validation state).
func (c Client) CancelCertificate(ctx context.Context, certificateID string) error {
endpoint := fmt.Sprintf("/certificates/%s/cancel", url.QueryEscape(certificateID))
var result struct {
Success anyBool `json:"success"`
}
if err := c.httpPost(ctx, endpoint, nil, nil, &result); err != nil {
return err
}
if !result.Success {
return fmt.Errorf("got %v without any error status", result)
}
return nil
}
// ValidateCSR sends the CSR to ZeroSSL for validation. Pass in the ASN.1 DER-encoded bytes;
// this is found in x509.CertificateRequest.Raw after calling x5p9.CreateCertificateRequest.
func (c Client) ValidateCSR(ctx context.Context, csrASN1DER []byte) error {
payload := struct {
CSR string `json:"csr"`
}{
CSR: csr2pem(csrASN1DER),
}
var result struct {
Valid bool `json:"valid"`
Error any `json:"error"`
}
if err := c.httpPost(ctx, "/validation/csr", nil, payload, &result); err != nil {
return err
}
if !result.Valid {
return fmt.Errorf("invalid CSR: %v", result.Error)
}
return nil
}
func (c Client) GenerateEABCredentials(ctx context.Context) (keyID, hmacKey string, err error) {
var result struct {
APIError
EABKID string `json:"eab_kid"`
EABHMACKey string `json:"eab_hmac_key"`
}
err = c.httpPost(ctx, "/acme/eab-credentials", nil, nil, &result)
if err != nil {
return
}
if !result.Success {
err = fmt.Errorf("failed to create EAB credentials: %v", result.APIError)
}
return result.EABKID, result.EABHMACKey, err
}

94
vendor/github.com/caddyserver/zerossl/models.go generated vendored Normal file
View File

@ -0,0 +1,94 @@
package zerossl
import "fmt"
type APIError struct {
Success anyBool `json:"success"`
ErrorInfo struct {
Code int `json:"code"`
Type string `json:"type"`
// for domain verification only; each domain is grouped into its
// www and non-www variant for CNAME validation, or its URL
// for HTTP validation
Details map[string]map[string]ValidationError `json:"details"`
} `json:"error"`
}
func (ae APIError) Error() string {
if ae.ErrorInfo.Code == 0 && ae.ErrorInfo.Type == "" && len(ae.ErrorInfo.Details) == 0 {
return "<missing error info>"
}
return fmt.Sprintf("API error %d: %s (details=%v)",
ae.ErrorInfo.Code, ae.ErrorInfo.Type, ae.ErrorInfo.Details)
}
type ValidationError struct {
CNAMEValidationError
HTTPValidationError
}
type CNAMEValidationError struct {
CNAMEFound int `json:"cname_found"`
RecordCorrect int `json:"record_correct"`
TargetHost string `json:"target_host"`
TargetRecord string `json:"target_record"`
ActualRecord string `json:"actual_record"`
}
type HTTPValidationError struct {
FileFound int `json:"file_found"`
Error bool `json:"error"`
ErrorSlug string `json:"error_slug"`
ErrorInfo string `json:"error_info"`
}
type CertificateObject struct {
ID string `json:"id"` // "certificate hash"
Type string `json:"type"`
CommonName string `json:"common_name"`
AdditionalDomains string `json:"additional_domains"`
Created string `json:"created"`
Expires string `json:"expires"`
Status string `json:"status"`
ValidationType *string `json:"validation_type,omitempty"`
ValidationEmails *string `json:"validation_emails,omitempty"`
ReplacementFor string `json:"replacement_for,omitempty"`
FingerprintSHA1 *string `json:"fingerprint_sha1"`
BrandValidation any `json:"brand_validation"`
Validation *struct {
EmailValidation map[string][]string `json:"email_validation,omitempty"`
OtherMethods map[string]ValidationObject `json:"other_methods,omitempty"`
} `json:"validation,omitempty"`
}
type ValidationObject struct {
FileValidationURLHTTP string `json:"file_validation_url_http"`
FileValidationURLHTTPS string `json:"file_validation_url_https"`
FileValidationContent []string `json:"file_validation_content"`
CnameValidationP1 string `json:"cname_validation_p1"`
CnameValidationP2 string `json:"cname_validation_p2"`
}
type CertificateBundle struct {
CertificateCrt string `json:"certificate.crt"`
CABundleCrt string `json:"ca_bundle.crt"`
}
type CertificateList struct {
TotalCount int `json:"total_count"`
ResultCount int `json:"result_count"`
Page string `json:"page"` // don't ask me why this is a string
Limit int `json:"limit"`
ACMEUsageLevel string `json:"acmeUsageLevel"`
ACMELocked bool `json:"acmeLocked"`
Results []CertificateObject `json:"results"`
}
type ValidationStatus struct {
ValidationCompleted int `json:"validation_completed"`
Details map[string]struct {
Method string `json:"method"`
Status string `json:"status"`
} `json:"details"`
}

64
vendor/github.com/caddyserver/zerossl/zerossl.go generated vendored Normal file
View File

@ -0,0 +1,64 @@
// Package zerossl implements the ZeroSSL REST API.
// See the API documentation on the ZeroSSL website: https://zerossl.com/documentation/api/
package zerossl
import (
"crypto/x509"
"encoding/base64"
"fmt"
)
// The base URL to the ZeroSSL API.
const BaseURL = "https://api.zerossl.com"
// ListAllCertificates returns parameters that lists all the certificates on the account;
// be sure to set Page and Limit if paginating.
func ListAllCertificates() ListCertificatesParameters {
return ListCertificatesParameters{
Status: "draft,pending_validation,issued,cancelled,revoked,expired",
}
}
func identifiersFromCSR(csr *x509.CertificateRequest) []string {
var identifiers []string
if csr.Subject.CommonName != "" {
// deprecated for like 20 years, but oh well
identifiers = append(identifiers, csr.Subject.CommonName)
}
identifiers = append(identifiers, csr.DNSNames...)
identifiers = append(identifiers, csr.EmailAddresses...)
for _, ip := range csr.IPAddresses {
identifiers = append(identifiers, ip.String())
}
for _, uri := range csr.URIs {
identifiers = append(identifiers, uri.String())
}
return identifiers
}
func csr2pem(csrASN1DER []byte) string {
return fmt.Sprintf("-----BEGIN CERTIFICATE REQUEST-----\n%s\n-----END CERTIFICATE REQUEST-----",
base64.StdEncoding.EncodeToString(csrASN1DER))
}
// VerificationMethod represents a way of verifying identifiers with ZeroSSL.
type VerificationMethod string
// Verification methods.
const (
EmailVerification VerificationMethod = "EMAIL"
CNAMEVerification VerificationMethod = "CNAME_CSR_HASH"
HTTPVerification VerificationMethod = "HTTP_CSR_HASH"
HTTPSVerification VerificationMethod = "HTTPS_CSR_HASH"
)
// RevocationReason represents various reasons for revoking a certificate.
type RevocationReason string
const (
UnspecifiedReason RevocationReason = "unspecified" // default
KeyCompromise RevocationReason = "keyCompromise" // lost control of private key
AffiliationChanged RevocationReason = "affiliationChanged" // identify information changed
Superseded RevocationReason = "Superseded" // certificate replaced -- do not revoke for this reason, however
CessationOfOperation RevocationReason = "cessationOfOperation" // domains are no longer in use
)

View File

@ -34,19 +34,25 @@ func (d *DistributedEnforcer) AddPoliciesSelf(shouldPersist func() bool, sec str
if shouldPersist != nil && shouldPersist() {
var noExistsPolicy [][]string
for _, rule := range rules {
if !d.model.HasPolicy(sec, ptype, rule) {
var hasPolicy bool
hasPolicy, err = d.model.HasPolicy(sec, ptype, rule)
if err != nil {
return nil, err
}
if !hasPolicy {
noExistsPolicy = append(noExistsPolicy, rule)
}
}
if err := d.adapter.(persist.BatchAdapter).AddPolicies(sec, ptype, noExistsPolicy); err != nil {
if err.Error() != notImplemented {
return nil, err
}
if err = d.adapter.(persist.BatchAdapter).AddPolicies(sec, ptype, noExistsPolicy); err != nil && err.Error() != notImplemented {
return nil, err
}
}
affected = d.model.AddPoliciesWithAffected(sec, ptype, rules)
affected, err = d.model.AddPoliciesWithAffected(sec, ptype, rules)
if err != nil {
return affected, err
}
if sec == "g" {
err := d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, affected)
@ -71,7 +77,10 @@ func (d *DistributedEnforcer) RemovePoliciesSelf(shouldPersist func() bool, sec
}
}
affected = d.model.RemovePoliciesWithAffected(sec, ptype, rules)
affected, err = d.model.RemovePoliciesWithAffected(sec, ptype, rules)
if err != nil {
return affected, err
}
if sec == "g" {
err = d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, affected)
@ -89,14 +98,17 @@ func (d *DistributedEnforcer) RemoveFilteredPolicySelf(shouldPersist func() bool
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
if err := d.adapter.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...); err != nil {
if err = d.adapter.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...); err != nil {
if err.Error() != notImplemented {
return nil, err
}
}
}
_, affected = d.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
_, affected, err = d.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
if err != nil {
return affected, err
}
if sec == "g" {
err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, affected)
@ -129,15 +141,15 @@ func (d *DistributedEnforcer) UpdatePolicySelf(shouldPersist func() bool, sec st
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
err := d.adapter.(persist.UpdatableAdapter).UpdatePolicy(sec, ptype, oldRule, newRule)
err = d.adapter.(persist.UpdatableAdapter).UpdatePolicy(sec, ptype, oldRule, newRule)
if err != nil {
return false, err
}
}
ruleUpdated := d.model.UpdatePolicy(sec, ptype, oldRule, newRule)
if !ruleUpdated {
return ruleUpdated, nil
ruleUpdated, err := d.model.UpdatePolicy(sec, ptype, oldRule, newRule)
if !ruleUpdated || err != nil {
return ruleUpdated, err
}
if sec == "g" {
@ -159,15 +171,15 @@ func (d *DistributedEnforcer) UpdatePoliciesSelf(shouldPersist func() bool, sec
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
err := d.adapter.(persist.UpdatableAdapter).UpdatePolicies(sec, ptype, oldRules, newRules)
err = d.adapter.(persist.UpdatableAdapter).UpdatePolicies(sec, ptype, oldRules, newRules)
if err != nil {
return false, err
}
}
ruleUpdated := d.model.UpdatePolicies(sec, ptype, oldRules, newRules)
if !ruleUpdated {
return ruleUpdated, nil
ruleUpdated, err := d.model.UpdatePolicies(sec, ptype, oldRules, newRules)
if !ruleUpdated || err != nil {
return ruleUpdated, err
}
if sec == "g" {
@ -199,8 +211,14 @@ func (d *DistributedEnforcer) UpdateFilteredPoliciesSelf(shouldPersist func() bo
}
}
ruleChanged := !d.model.RemovePolicies(sec, ptype, oldRules)
d.model.AddPolicies(sec, ptype, newRules)
ruleChanged, err := d.model.RemovePolicies(sec, ptype, oldRules)
if err != nil {
return ruleChanged, err
}
err = d.model.AddPolicies(sec, ptype, newRules)
if err != nil {
return ruleChanged, err
}
ruleChanged = ruleChanged && len(newRules) != 0
if !ruleChanged {
return ruleChanged, nil

View File

@ -70,7 +70,7 @@ type IEnforcer interface {
DeletePermissionForUser(user string, permission ...string) (bool, error)
DeletePermissionsForUser(user string) (bool, error)
GetPermissionsForUser(user string, domain ...string) ([][]string, error)
HasPermissionForUser(user string, permission ...string) bool
HasPermissionForUser(user string, permission ...string) (bool, error)
GetImplicitRolesForUser(name string, domain ...string) ([]string, error)
GetImplicitPermissionsForUser(user string, domain ...string) ([][]string, error)
GetImplicitUsersForPermission(permission ...string) ([]string, error)
@ -86,32 +86,32 @@ type IEnforcer interface {
GetPermissionsForUserInDomain(user string, domain string) [][]string
AddRoleForUserInDomain(user string, role string, domain string) (bool, error)
DeleteRoleForUserInDomain(user string, role string, domain string) (bool, error)
GetAllUsersByDomain(domain string) []string
GetAllUsersByDomain(domain string) ([]string, error)
DeleteRolesForUserInDomain(user string, domain string) (bool, error)
DeleteAllUsersByDomain(domain string) (bool, error)
DeleteDomains(domains ...string) (bool, error)
GetAllDomains() ([]string, error)
GetAllRolesByDomain(domain string) []string
GetAllRolesByDomain(domain string) ([]string, error)
/* Management API */
GetAllSubjects() []string
GetAllNamedSubjects(ptype string) []string
GetAllObjects() []string
GetAllNamedObjects(ptype string) []string
GetAllActions() []string
GetAllNamedActions(ptype string) []string
GetAllRoles() []string
GetAllNamedRoles(ptype string) []string
GetPolicy() [][]string
GetFilteredPolicy(fieldIndex int, fieldValues ...string) [][]string
GetNamedPolicy(ptype string) [][]string
GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) [][]string
GetGroupingPolicy() [][]string
GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) [][]string
GetNamedGroupingPolicy(ptype string) [][]string
GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) [][]string
HasPolicy(params ...interface{}) bool
HasNamedPolicy(ptype string, params ...interface{}) bool
GetAllSubjects() ([]string, error)
GetAllNamedSubjects(ptype string) ([]string, error)
GetAllObjects() ([]string, error)
GetAllNamedObjects(ptype string) ([]string, error)
GetAllActions() ([]string, error)
GetAllNamedActions(ptype string) ([]string, error)
GetAllRoles() ([]string, error)
GetAllNamedRoles(ptype string) ([]string, error)
GetPolicy() ([][]string, error)
GetFilteredPolicy(fieldIndex int, fieldValues ...string) ([][]string, error)
GetNamedPolicy(ptype string) ([][]string, error)
GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error)
GetGroupingPolicy() ([][]string, error)
GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) ([][]string, error)
GetNamedGroupingPolicy(ptype string) ([][]string, error)
GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error)
HasPolicy(params ...interface{}) (bool, error)
HasNamedPolicy(ptype string, params ...interface{}) (bool, error)
AddPolicy(params ...interface{}) (bool, error)
AddPolicies(rules [][]string) (bool, error)
AddNamedPolicy(ptype string, params ...interface{}) (bool, error)
@ -124,8 +124,8 @@ type IEnforcer interface {
RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error)
RemoveNamedPolicies(ptype string, rules [][]string) (bool, error)
RemoveFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error)
HasGroupingPolicy(params ...interface{}) bool
HasNamedGroupingPolicy(ptype string, params ...interface{}) bool
HasGroupingPolicy(params ...interface{}) (bool, error)
HasNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error)
AddGroupingPolicy(params ...interface{}) (bool, error)
AddGroupingPolicies(rules [][]string) (bool, error)
AddGroupingPoliciesEx(rules [][]string) (bool, error)

View File

@ -233,126 +233,126 @@ func (e *SyncedEnforcer) BatchEnforceWithMatcher(matcher string, requests [][]in
}
// GetAllSubjects gets the list of subjects that show up in the current policy.
func (e *SyncedEnforcer) GetAllSubjects() []string {
func (e *SyncedEnforcer) GetAllSubjects() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllSubjects()
}
// GetAllNamedSubjects gets the list of subjects that show up in the current named policy.
func (e *SyncedEnforcer) GetAllNamedSubjects(ptype string) []string {
func (e *SyncedEnforcer) GetAllNamedSubjects(ptype string) ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllNamedSubjects(ptype)
}
// GetAllObjects gets the list of objects that show up in the current policy.
func (e *SyncedEnforcer) GetAllObjects() []string {
func (e *SyncedEnforcer) GetAllObjects() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllObjects()
}
// GetAllNamedObjects gets the list of objects that show up in the current named policy.
func (e *SyncedEnforcer) GetAllNamedObjects(ptype string) []string {
func (e *SyncedEnforcer) GetAllNamedObjects(ptype string) ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllNamedObjects(ptype)
}
// GetAllActions gets the list of actions that show up in the current policy.
func (e *SyncedEnforcer) GetAllActions() []string {
func (e *SyncedEnforcer) GetAllActions() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllActions()
}
// GetAllNamedActions gets the list of actions that show up in the current named policy.
func (e *SyncedEnforcer) GetAllNamedActions(ptype string) []string {
func (e *SyncedEnforcer) GetAllNamedActions(ptype string) ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllNamedActions(ptype)
}
// GetAllRoles gets the list of roles that show up in the current policy.
func (e *SyncedEnforcer) GetAllRoles() []string {
func (e *SyncedEnforcer) GetAllRoles() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllRoles()
}
// GetAllNamedRoles gets the list of roles that show up in the current named policy.
func (e *SyncedEnforcer) GetAllNamedRoles(ptype string) []string {
func (e *SyncedEnforcer) GetAllNamedRoles(ptype string) ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllNamedRoles(ptype)
}
// GetPolicy gets all the authorization rules in the policy.
func (e *SyncedEnforcer) GetPolicy() [][]string {
func (e *SyncedEnforcer) GetPolicy() ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetPolicy()
}
// GetFilteredPolicy gets all the authorization rules in the policy, field filters can be specified.
func (e *SyncedEnforcer) GetFilteredPolicy(fieldIndex int, fieldValues ...string) [][]string {
func (e *SyncedEnforcer) GetFilteredPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetFilteredPolicy(fieldIndex, fieldValues...)
}
// GetNamedPolicy gets all the authorization rules in the named policy.
func (e *SyncedEnforcer) GetNamedPolicy(ptype string) [][]string {
func (e *SyncedEnforcer) GetNamedPolicy(ptype string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetNamedPolicy(ptype)
}
// GetFilteredNamedPolicy gets all the authorization rules in the named policy, field filters can be specified.
func (e *SyncedEnforcer) GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) [][]string {
func (e *SyncedEnforcer) GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetFilteredNamedPolicy(ptype, fieldIndex, fieldValues...)
}
// GetGroupingPolicy gets all the role inheritance rules in the policy.
func (e *SyncedEnforcer) GetGroupingPolicy() [][]string {
func (e *SyncedEnforcer) GetGroupingPolicy() ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetGroupingPolicy()
}
// GetFilteredGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified.
func (e *SyncedEnforcer) GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) [][]string {
func (e *SyncedEnforcer) GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetFilteredGroupingPolicy(fieldIndex, fieldValues...)
}
// GetNamedGroupingPolicy gets all the role inheritance rules in the policy.
func (e *SyncedEnforcer) GetNamedGroupingPolicy(ptype string) [][]string {
func (e *SyncedEnforcer) GetNamedGroupingPolicy(ptype string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetNamedGroupingPolicy(ptype)
}
// GetFilteredNamedGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified.
func (e *SyncedEnforcer) GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) [][]string {
func (e *SyncedEnforcer) GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetFilteredNamedGroupingPolicy(ptype, fieldIndex, fieldValues...)
}
// HasPolicy determines whether an authorization rule exists.
func (e *SyncedEnforcer) HasPolicy(params ...interface{}) bool {
func (e *SyncedEnforcer) HasPolicy(params ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasPolicy(params...)
}
// HasNamedPolicy determines whether a named authorization rule exists.
func (e *SyncedEnforcer) HasNamedPolicy(ptype string, params ...interface{}) bool {
func (e *SyncedEnforcer) HasNamedPolicy(ptype string, params ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasNamedPolicy(ptype, params...)
@ -493,14 +493,14 @@ func (e *SyncedEnforcer) RemoveFilteredNamedPolicy(ptype string, fieldIndex int,
}
// HasGroupingPolicy determines whether a role inheritance rule exists.
func (e *SyncedEnforcer) HasGroupingPolicy(params ...interface{}) bool {
func (e *SyncedEnforcer) HasGroupingPolicy(params ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasGroupingPolicy(params...)
}
// HasNamedGroupingPolicy determines whether a named role inheritance rule exists.
func (e *SyncedEnforcer) HasNamedGroupingPolicy(ptype string, params ...interface{}) bool {
func (e *SyncedEnforcer) HasNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasNamedGroupingPolicy(ptype, params...)

View File

@ -27,7 +27,10 @@ func CasbinJsGetPermissionForUser(e IEnforcer, user string) (string, error) {
pRules := [][]string{}
for ptype := range model["p"] {
policies := model.GetPolicy("p", ptype)
policies, err := model.GetPolicy("p", ptype)
if err != nil {
return "", err
}
for _, rules := range policies {
pRules = append(pRules, append([]string{ptype}, rules...))
}
@ -36,7 +39,10 @@ func CasbinJsGetPermissionForUser(e IEnforcer, user string) (string, error) {
gRules := [][]string{}
for ptype := range model["g"] {
policies := model.GetPolicy("g", ptype)
policies, err := model.GetPolicy("g", ptype)
if err != nil {
return "", err
}
for _, rules := range policies {
gRules = append(gRules, append([]string{ptype}, rules...))
}

View File

@ -40,19 +40,23 @@ func (e *Enforcer) addPolicyWithoutNotify(sec string, ptype string, rule []strin
return true, e.dispatcher.AddPolicies(sec, ptype, [][]string{rule})
}
if e.model.HasPolicy(sec, ptype, rule) {
return false, nil
hasPolicy, err := e.model.HasPolicy(sec, ptype, rule)
if hasPolicy || err != nil {
return hasPolicy, err
}
if e.shouldPersist() {
if err := e.adapter.AddPolicy(sec, ptype, rule); err != nil {
if err = e.adapter.AddPolicy(sec, ptype, rule); err != nil {
if err.Error() != notImplemented {
return false, err
}
}
}
e.model.AddPolicy(sec, ptype, rule)
err = e.model.AddPolicy(sec, ptype, rule)
if err != nil {
return false, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{rule})
@ -72,8 +76,11 @@ func (e *Enforcer) addPoliciesWithoutNotify(sec string, ptype string, rules [][]
return true, e.dispatcher.AddPolicies(sec, ptype, rules)
}
if !autoRemoveRepeat && e.model.HasPolicies(sec, ptype, rules) {
return false, nil
if !autoRemoveRepeat {
hasPolicies, err := e.model.HasPolicies(sec, ptype, rules)
if hasPolicies || err != nil {
return false, err
}
}
if e.shouldPersist() {
@ -84,7 +91,10 @@ func (e *Enforcer) addPoliciesWithoutNotify(sec string, ptype string, rules [][]
}
}
e.model.AddPolicies(sec, ptype, rules)
err := e.model.AddPolicies(sec, ptype, rules)
if err != nil {
return false, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, rules)
@ -115,9 +125,9 @@ func (e *Enforcer) removePolicyWithoutNotify(sec string, ptype string, rule []st
}
}
ruleRemoved := e.model.RemovePolicy(sec, ptype, rule)
if !ruleRemoved {
return ruleRemoved, nil
ruleRemoved, err := e.model.RemovePolicy(sec, ptype, rule)
if !ruleRemoved || err != nil {
return ruleRemoved, err
}
if sec == "g" {
@ -142,9 +152,9 @@ func (e *Enforcer) updatePolicyWithoutNotify(sec string, ptype string, oldRule [
}
}
}
ruleUpdated := e.model.UpdatePolicy(sec, ptype, oldRule, newRule)
if !ruleUpdated {
return ruleUpdated, nil
ruleUpdated, err := e.model.UpdatePolicy(sec, ptype, oldRule, newRule)
if !ruleUpdated || err != nil {
return ruleUpdated, err
}
if sec == "g" {
@ -178,9 +188,9 @@ func (e *Enforcer) updatePoliciesWithoutNotify(sec string, ptype string, oldRule
}
}
ruleUpdated := e.model.UpdatePolicies(sec, ptype, oldRules, newRules)
if !ruleUpdated {
return ruleUpdated, nil
ruleUpdated, err := e.model.UpdatePolicies(sec, ptype, oldRules, newRules)
if !ruleUpdated || err != nil {
return ruleUpdated, err
}
if sec == "g" {
@ -199,8 +209,8 @@ func (e *Enforcer) updatePoliciesWithoutNotify(sec string, ptype string, oldRule
// removePolicies removes rules from the current policy.
func (e *Enforcer) removePoliciesWithoutNotify(sec string, ptype string, rules [][]string) (bool, error) {
if !e.model.HasPolicies(sec, ptype, rules) {
return false, nil
if hasPolicies, err := e.model.HasPolicies(sec, ptype, rules); !hasPolicies || err != nil {
return hasPolicies, err
}
if e.dispatcher != nil && e.autoNotifyDispatcher {
@ -215,9 +225,9 @@ func (e *Enforcer) removePoliciesWithoutNotify(sec string, ptype string, rules [
}
}
rulesRemoved := e.model.RemovePolicies(sec, ptype, rules)
if !rulesRemoved {
return rulesRemoved, nil
rulesRemoved, err := e.model.RemovePolicies(sec, ptype, rules)
if !rulesRemoved || err != nil {
return rulesRemoved, err
}
if sec == "g" {
@ -247,9 +257,9 @@ func (e *Enforcer) removeFilteredPolicyWithoutNotify(sec string, ptype string, f
}
}
ruleRemoved, effects := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
if !ruleRemoved {
return ruleRemoved, nil
ruleRemoved, effects, err := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
if !ruleRemoved || err != nil {
return ruleRemoved, err
}
if sec == "g" {
@ -268,6 +278,10 @@ func (e *Enforcer) updateFilteredPoliciesWithoutNotify(sec string, ptype string,
err error
)
if _, err = e.model.GetAssertion(sec, ptype); err != nil {
return oldRules, err
}
if e.shouldPersist() {
if oldRules, err = e.adapter.(persist.UpdatableAdapter).UpdateFilteredPolicies(sec, ptype, newRules, fieldIndex, fieldValues...); err != nil {
if err.Error() != notImplemented {
@ -286,8 +300,14 @@ func (e *Enforcer) updateFilteredPoliciesWithoutNotify(sec string, ptype string,
return oldRules, e.dispatcher.UpdateFilteredPolicies(sec, ptype, oldRules, newRules)
}
ruleChanged := e.model.RemovePolicies(sec, ptype, oldRules)
e.model.AddPolicies(sec, ptype, newRules)
ruleChanged, err := e.model.RemovePolicies(sec, ptype, oldRules)
if err != nil {
return oldRules, err
}
err = e.model.AddPolicies(sec, ptype, newRules)
if err != nil {
return oldRules, err
}
ruleChanged = ruleChanged && len(newRules) != 0
if !ruleChanged {
return make([][]string, 0), nil

View File

@ -24,82 +24,82 @@ import (
)
// GetAllSubjects gets the list of subjects that show up in the current policy.
func (e *Enforcer) GetAllSubjects() []string {
func (e *Enforcer) GetAllSubjects() ([]string, error) {
return e.model.GetValuesForFieldInPolicyAllTypes("p", 0)
}
// GetAllNamedSubjects gets the list of subjects that show up in the current named policy.
func (e *Enforcer) GetAllNamedSubjects(ptype string) []string {
func (e *Enforcer) GetAllNamedSubjects(ptype string) ([]string, error) {
return e.model.GetValuesForFieldInPolicy("p", ptype, 0)
}
// GetAllObjects gets the list of objects that show up in the current policy.
func (e *Enforcer) GetAllObjects() []string {
func (e *Enforcer) GetAllObjects() ([]string, error) {
return e.model.GetValuesForFieldInPolicyAllTypes("p", 1)
}
// GetAllNamedObjects gets the list of objects that show up in the current named policy.
func (e *Enforcer) GetAllNamedObjects(ptype string) []string {
func (e *Enforcer) GetAllNamedObjects(ptype string) ([]string, error) {
return e.model.GetValuesForFieldInPolicy("p", ptype, 1)
}
// GetAllActions gets the list of actions that show up in the current policy.
func (e *Enforcer) GetAllActions() []string {
func (e *Enforcer) GetAllActions() ([]string, error) {
return e.model.GetValuesForFieldInPolicyAllTypes("p", 2)
}
// GetAllNamedActions gets the list of actions that show up in the current named policy.
func (e *Enforcer) GetAllNamedActions(ptype string) []string {
func (e *Enforcer) GetAllNamedActions(ptype string) ([]string, error) {
return e.model.GetValuesForFieldInPolicy("p", ptype, 2)
}
// GetAllRoles gets the list of roles that show up in the current policy.
func (e *Enforcer) GetAllRoles() []string {
func (e *Enforcer) GetAllRoles() ([]string, error) {
return e.model.GetValuesForFieldInPolicyAllTypes("g", 1)
}
// GetAllNamedRoles gets the list of roles that show up in the current named policy.
func (e *Enforcer) GetAllNamedRoles(ptype string) []string {
func (e *Enforcer) GetAllNamedRoles(ptype string) ([]string, error) {
return e.model.GetValuesForFieldInPolicy("g", ptype, 1)
}
// GetPolicy gets all the authorization rules in the policy.
func (e *Enforcer) GetPolicy() [][]string {
func (e *Enforcer) GetPolicy() ([][]string, error) {
return e.GetNamedPolicy("p")
}
// GetFilteredPolicy gets all the authorization rules in the policy, field filters can be specified.
func (e *Enforcer) GetFilteredPolicy(fieldIndex int, fieldValues ...string) [][]string {
func (e *Enforcer) GetFilteredPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) {
return e.GetFilteredNamedPolicy("p", fieldIndex, fieldValues...)
}
// GetNamedPolicy gets all the authorization rules in the named policy.
func (e *Enforcer) GetNamedPolicy(ptype string) [][]string {
func (e *Enforcer) GetNamedPolicy(ptype string) ([][]string, error) {
return e.model.GetPolicy("p", ptype)
}
// GetFilteredNamedPolicy gets all the authorization rules in the named policy, field filters can be specified.
func (e *Enforcer) GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) [][]string {
func (e *Enforcer) GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) {
return e.model.GetFilteredPolicy("p", ptype, fieldIndex, fieldValues...)
}
// GetGroupingPolicy gets all the role inheritance rules in the policy.
func (e *Enforcer) GetGroupingPolicy() [][]string {
func (e *Enforcer) GetGroupingPolicy() ([][]string, error) {
return e.GetNamedGroupingPolicy("g")
}
// GetFilteredGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified.
func (e *Enforcer) GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) [][]string {
func (e *Enforcer) GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) {
return e.GetFilteredNamedGroupingPolicy("g", fieldIndex, fieldValues...)
}
// GetNamedGroupingPolicy gets all the role inheritance rules in the policy.
func (e *Enforcer) GetNamedGroupingPolicy(ptype string) [][]string {
func (e *Enforcer) GetNamedGroupingPolicy(ptype string) ([][]string, error) {
return e.model.GetPolicy("g", ptype)
}
// GetFilteredNamedGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified.
func (e *Enforcer) GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) [][]string {
func (e *Enforcer) GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) {
return e.model.GetFilteredPolicy("g", ptype, fieldIndex, fieldValues...)
}
@ -182,12 +182,12 @@ func (e *Enforcer) GetFilteredNamedPolicyWithMatcher(ptype string, matcher strin
}
// HasPolicy determines whether an authorization rule exists.
func (e *Enforcer) HasPolicy(params ...interface{}) bool {
func (e *Enforcer) HasPolicy(params ...interface{}) (bool, error) {
return e.HasNamedPolicy("p", params...)
}
// HasNamedPolicy determines whether a named authorization rule exists.
func (e *Enforcer) HasNamedPolicy(ptype string, params ...interface{}) bool {
func (e *Enforcer) HasNamedPolicy(ptype string, params ...interface{}) (bool, error) {
if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
return e.model.HasPolicy("p", ptype, strSlice)
}
@ -316,12 +316,12 @@ func (e *Enforcer) RemoveFilteredNamedPolicy(ptype string, fieldIndex int, field
}
// HasGroupingPolicy determines whether a role inheritance rule exists.
func (e *Enforcer) HasGroupingPolicy(params ...interface{}) bool {
func (e *Enforcer) HasGroupingPolicy(params ...interface{}) (bool, error) {
return e.HasNamedGroupingPolicy("g", params...)
}
// HasNamedGroupingPolicy determines whether a named role inheritance rule exists.
func (e *Enforcer) HasNamedGroupingPolicy(ptype string, params ...interface{}) bool {
func (e *Enforcer) HasNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) {
if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
return e.model.HasPolicy("g", ptype, strSlice)
}

View File

@ -212,6 +212,16 @@ func (model Model) hasSection(sec string) bool {
return section != nil
}
func (model Model) GetAssertion(sec string, ptype string) (*Assertion, error) {
if model[sec] == nil {
return nil, fmt.Errorf("missing required section %s", sec)
}
if model[sec][ptype] == nil {
return nil, fmt.Errorf("missiong required definition %s in section %s", ptype, sec)
}
return model[sec][ptype], nil
}
// PrintModel prints the model to the log.
func (model Model) PrintModel() {
if !model.GetLogger().IsEnabled() {
@ -236,6 +246,10 @@ func (model Model) SortPoliciesBySubjectHierarchy() error {
if model["e"]["e"].Value != constant.SubjectPriorityEffect {
return nil
}
g, err := model.GetAssertion("g", "g")
if err != nil {
return err
}
subIndex := 0
for ptype, assertion := range model["p"] {
domainIndex, err := model.GetFieldIndex(ptype, constant.DomainIndex)
@ -243,7 +257,7 @@ func (model Model) SortPoliciesBySubjectHierarchy() error {
domainIndex = -1
}
policies := assertion.Policy
subjectHierarchyMap, err := getSubjectHierarchyMap(model["g"]["g"].Policy)
subjectHierarchyMap, err := getSubjectHierarchyMap(g.Policy)
if err != nil {
return err
}

View File

@ -38,6 +38,10 @@ const DefaultSep = ","
// BuildIncrementalRoleLinks provides incremental build the role inheritance relations.
func (model Model) BuildIncrementalRoleLinks(rmMap map[string]rbac.RoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error {
if sec == "g" && rmMap[ptype] != nil {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return err
}
return model[sec][ptype].buildIncrementalRoleLinks(rmMap[ptype], op, rules)
}
return nil
@ -61,6 +65,10 @@ func (model Model) BuildRoleLinks(rmMap map[string]rbac.RoleManager) error {
// BuildIncrementalConditionalRoleLinks provides incremental build the role inheritance relations.
func (model Model) BuildIncrementalConditionalRoleLinks(condRmMap map[string]rbac.ConditionalRoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error {
if sec == "g" && condRmMap[ptype] != nil {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return err
}
return model[sec][ptype].buildIncrementalConditionalRoleLinks(condRmMap[ptype], op, rules)
}
return nil
@ -126,12 +134,20 @@ func (model Model) ClearPolicy() {
}
// GetPolicy gets all rules in a policy.
func (model Model) GetPolicy(sec string, ptype string) [][]string {
return model[sec][ptype].Policy
func (model Model) GetPolicy(sec string, ptype string) ([][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
return model[sec][ptype].Policy, nil
}
// GetFilteredPolicy gets rules based on field filters from a policy.
func (model Model) GetFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) [][]string {
func (model Model) GetFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
res := [][]string{}
for _, rule := range model[sec][ptype].Policy {
@ -148,12 +164,15 @@ func (model Model) GetFilteredPolicy(sec string, ptype string, fieldIndex int, f
}
}
return res
return res, nil
}
// HasPolicyEx determines whether a model has the specified policy rule with error.
func (model Model) HasPolicyEx(sec string, ptype string, rule []string) (bool, error) {
assertion := model[sec][ptype]
assertion, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
switch sec {
case "p":
if len(rule) != len(assertion.Tokens) {
@ -172,29 +191,40 @@ func (model Model) HasPolicyEx(sec string, ptype string, rule []string) (bool, e
rule)
}
}
return model.HasPolicy(sec, ptype, rule), nil
return model.HasPolicy(sec, ptype, rule)
}
// HasPolicy determines whether a model has the specified policy rule.
func (model Model) HasPolicy(sec string, ptype string, rule []string) bool {
func (model Model) HasPolicy(sec string, ptype string, rule []string) (bool, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
_, ok := model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)]
return ok
return ok, nil
}
// HasPolicies determines whether a model has any of the specified policies. If one is found we return true.
func (model Model) HasPolicies(sec string, ptype string, rules [][]string) bool {
func (model Model) HasPolicies(sec string, ptype string, rules [][]string) (bool, error) {
for i := 0; i < len(rules); i++ {
if model.HasPolicy(sec, ptype, rules[i]) {
return true
ok, err := model.HasPolicy(sec, ptype, rules[i])
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false
return false, nil
}
// AddPolicy adds a policy rule to the model.
func (model Model) AddPolicy(sec string, ptype string, rule []string) {
assertion := model[sec][ptype]
func (model Model) AddPolicy(sec string, ptype string, rule []string) error {
assertion, err := model.GetAssertion(sec, ptype)
if err != nil {
return err
}
assertion.Policy = append(assertion.Policy, rule)
assertion.PolicyMap[strings.Join(rule, DefaultSep)] = len(model[sec][ptype].Policy) - 1
@ -217,15 +247,21 @@ func (model Model) AddPolicy(sec string, ptype string, rule []string) {
assertion.PolicyMap[strings.Join(rule, DefaultSep)] = i
}
}
return nil
}
// AddPolicies adds policy rules to the model.
func (model Model) AddPolicies(sec string, ptype string, rules [][]string) {
_ = model.AddPoliciesWithAffected(sec, ptype, rules)
func (model Model) AddPolicies(sec string, ptype string, rules [][]string) error {
_, err := model.AddPoliciesWithAffected(sec, ptype, rules)
return err
}
// AddPoliciesWithAffected adds policy rules to the model, and returns affected rules.
func (model Model) AddPoliciesWithAffected(sec string, ptype string, rules [][]string) [][]string {
func (model Model) AddPoliciesWithAffected(sec string, ptype string, rules [][]string) ([][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
var affected [][]string
for _, rule := range rules {
hashKey := strings.Join(rule, DefaultSep)
@ -234,17 +270,24 @@ func (model Model) AddPoliciesWithAffected(sec string, ptype string, rules [][]s
continue
}
affected = append(affected, rule)
model.AddPolicy(sec, ptype, rule)
err = model.AddPolicy(sec, ptype, rule)
if err != nil {
return affected, err
}
}
return affected
return affected, err
}
// RemovePolicy removes a policy rule from the model.
// Deprecated: Using AddPoliciesWithAffected instead.
func (model Model) RemovePolicy(sec string, ptype string, rule []string) bool {
func (model Model) RemovePolicy(sec string, ptype string, rule []string) (bool, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
index, ok := model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)]
if !ok {
return false
return false, err
}
model[sec][ptype].Policy = append(model[sec][ptype].Policy[:index], model[sec][ptype].Policy[index+1:]...)
@ -253,26 +296,34 @@ func (model Model) RemovePolicy(sec string, ptype string, rule []string) bool {
model[sec][ptype].PolicyMap[strings.Join(model[sec][ptype].Policy[i], DefaultSep)] = i
}
return true
return true, err
}
// UpdatePolicy updates a policy rule from the model.
func (model Model) UpdatePolicy(sec string, ptype string, oldRule []string, newRule []string) bool {
func (model Model) UpdatePolicy(sec string, ptype string, oldRule []string, newRule []string) (bool, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
oldPolicy := strings.Join(oldRule, DefaultSep)
index, ok := model[sec][ptype].PolicyMap[oldPolicy]
if !ok {
return false
return false, nil
}
model[sec][ptype].Policy[index] = newRule
delete(model[sec][ptype].PolicyMap, oldPolicy)
model[sec][ptype].PolicyMap[strings.Join(newRule, DefaultSep)] = index
return true
return true, nil
}
// UpdatePolicies updates a policy rule from the model.
func (model Model) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) bool {
func (model Model) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
rollbackFlag := false
// index -> []{oldIndex, newIndex}
modifiedRuleIndex := make(map[int][]int)
@ -295,7 +346,7 @@ func (model Model) UpdatePolicies(sec string, ptype string, oldRules, newRules [
index, ok := model[sec][ptype].PolicyMap[oldPolicy]
if !ok {
rollbackFlag = true
return false
return false, nil
}
model[sec][ptype].Policy[index] = newRules[newIndex]
@ -305,17 +356,21 @@ func (model Model) UpdatePolicies(sec string, ptype string, oldRules, newRules [
newIndex++
}
return true
return true, nil
}
// RemovePolicies removes policy rules from the model.
func (model Model) RemovePolicies(sec string, ptype string, rules [][]string) bool {
affected := model.RemovePoliciesWithAffected(sec, ptype, rules)
return len(affected) != 0
func (model Model) RemovePolicies(sec string, ptype string, rules [][]string) (bool, error) {
affected, err := model.RemovePoliciesWithAffected(sec, ptype, rules)
return len(affected) != 0, err
}
// RemovePoliciesWithAffected removes policy rules from the model, and returns affected rules.
func (model Model) RemovePoliciesWithAffected(sec string, ptype string, rules [][]string) [][]string {
func (model Model) RemovePoliciesWithAffected(sec string, ptype string, rules [][]string) ([][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
var affected [][]string
for _, rule := range rules {
index, ok := model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)]
@ -330,11 +385,15 @@ func (model Model) RemovePoliciesWithAffected(sec string, ptype string, rules []
model[sec][ptype].PolicyMap[strings.Join(model[sec][ptype].Policy[i], DefaultSep)] = i
}
}
return affected
return affected, nil
}
// RemoveFilteredPolicy removes policy rules based on field filters from the model.
func (model Model) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, [][]string) {
func (model Model) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, [][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, nil, err
}
var tmp [][]string
var effects [][]string
res := false
@ -362,31 +421,40 @@ func (model Model) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int
res = true
}
return res, effects
return res, effects, nil
}
// GetValuesForFieldInPolicy gets all values for a field for all rules in a policy, duplicated values are removed.
func (model Model) GetValuesForFieldInPolicy(sec string, ptype string, fieldIndex int) []string {
func (model Model) GetValuesForFieldInPolicy(sec string, ptype string, fieldIndex int) ([]string, error) {
values := []string{}
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
for _, rule := range model[sec][ptype].Policy {
values = append(values, rule[fieldIndex])
}
util.ArrayRemoveDuplicates(&values)
return values
return values, nil
}
// GetValuesForFieldInPolicyAllTypes gets all values for a field for all rules in a policy of all ptypes, duplicated values are removed.
func (model Model) GetValuesForFieldInPolicyAllTypes(sec string, fieldIndex int) []string {
func (model Model) GetValuesForFieldInPolicyAllTypes(sec string, fieldIndex int) ([]string, error) {
values := []string{}
for ptype := range model[sec] {
values = append(values, model.GetValuesForFieldInPolicy(sec, ptype, fieldIndex)...)
v, err := model.GetValuesForFieldInPolicy(sec, ptype, fieldIndex)
if err != nil {
return nil, err
}
values = append(values, v...)
}
util.ArrayRemoveDuplicates(&values)
return values
return values, nil
}

View File

@ -199,20 +199,24 @@ func (e *Enforcer) GetNamedPermissionsForUser(ptype string, user string, domain
args[subIndex] = user
if len(domain) > 0 {
index, err := e.GetFieldIndex(ptype, constant.DomainIndex)
var index int
index, err = e.GetFieldIndex(ptype, constant.DomainIndex)
if err != nil {
return permission, err
}
args[index] = domain[0]
}
perm := e.GetFilteredNamedPolicy(ptype, 0, args...)
perm, err := e.GetFilteredNamedPolicy(ptype, 0, args...)
if err != nil {
return permission, err
}
permission = append(permission, perm...)
}
return permission, nil
}
// HasPermissionForUser determines whether a user has a permission.
func (e *Enforcer) HasPermissionForUser(user string, permission ...string) bool {
func (e *Enforcer) HasPermissionForUser(user string, permission ...string) (bool, error) {
return e.HasPolicy(util.JoinSlice(user, permission...))
}
@ -349,9 +353,18 @@ func (e *Enforcer) GetNamedImplicitPermissionsForUser(ptype string, user string,
// GetImplicitUsersForPermission("data1", "read") will get: ["alice", "bob"].
// Note: only users will be returned, roles (2nd arg in "g") will be excluded.
func (e *Enforcer) GetImplicitUsersForPermission(permission ...string) ([]string, error) {
pSubjects := e.GetAllSubjects()
gInherit := e.model.GetValuesForFieldInPolicyAllTypes("g", 1)
gSubjects := e.model.GetValuesForFieldInPolicyAllTypes("g", 0)
pSubjects, err := e.GetAllSubjects()
if err != nil {
return nil, err
}
gInherit, err := e.model.GetValuesForFieldInPolicyAllTypes("g", 1)
if err != nil {
return nil, err
}
gSubjects, err := e.model.GetValuesForFieldInPolicyAllTypes("g", 0)
if err != nil {
return nil, err
}
subjects := append(pSubjects, gSubjects...)
util.ArrayRemoveDuplicates(&subjects)
@ -504,7 +517,11 @@ func (e *Enforcer) GetImplicitUsersForResource(resource string) ([][]string, err
}
isRole := make(map[string]bool)
for _, role := range e.GetAllRoles() {
roles, err := e.GetAllRoles()
if err != nil {
return nil, err
}
for _, role := range roles {
isRole[role] = true
}
@ -550,8 +567,12 @@ func (e *Enforcer) GetImplicitUsersForResourceByDomain(resource string, domain s
isRole := make(map[string]bool)
for _, role := range e.GetAllRolesByDomain(domain) {
isRole[role] = true
if roles, err := e.GetAllRolesByDomain(domain); err != nil {
return nil, err
} else {
for _, role := range roles {
isRole[role] = true
}
}
for _, rule := range e.model["p"]["p"].Policy {

View File

@ -138,7 +138,7 @@ func (e *SyncedEnforcer) GetNamedPermissionsForUser(ptype string, user string, d
}
// HasPermissionForUser determines whether a user has a permission.
func (e *SyncedEnforcer) HasPermissionForUser(user string, permission ...string) bool {
func (e *SyncedEnforcer) HasPermissionForUser(user string, permission ...string) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasPermissionForUser(user, permission...)

View File

@ -76,14 +76,17 @@ func (e *Enforcer) DeleteRolesForUserInDomain(user string, domain string) (bool,
}
// GetAllUsersByDomain would get all users associated with the domain.
func (e *Enforcer) GetAllUsersByDomain(domain string) []string {
func (e *Enforcer) GetAllUsersByDomain(domain string) ([]string, error) {
m := make(map[string]struct{})
g := e.model["g"]["g"]
g, err := e.model.GetAssertion("g", "g")
if err != nil {
return []string{}, err
}
p := e.model["p"]["p"]
users := make([]string, 0)
index, err := e.GetFieldIndex("p", constant.DomainIndex)
if err != nil {
return []string{}
return []string{}, err
}
getUser := func(index int, policies [][]string, domain string, m map[string]struct{}) []string {
@ -102,12 +105,15 @@ func (e *Enforcer) GetAllUsersByDomain(domain string) []string {
users = append(users, getUser(2, g.Policy, domain, m)...)
users = append(users, getUser(index, p.Policy, domain, m)...)
return users
return users, nil
}
// DeleteAllUsersByDomain would delete all users associated with the domain.
func (e *Enforcer) DeleteAllUsersByDomain(domain string) (bool, error) {
g := e.model["g"]["g"]
g, err := e.model.GetAssertion("g", "g")
if err != nil {
return false, err
}
p := e.model["p"]["p"]
index, err := e.GetFieldIndex("p", constant.DomainIndex)
if err != nil {
@ -128,11 +134,11 @@ func (e *Enforcer) DeleteAllUsersByDomain(domain string) (bool, error) {
}
users := getUser(2, g.Policy, domain)
if _, err := e.RemoveGroupingPolicies(users); err != nil {
if _, err = e.RemoveGroupingPolicies(users); err != nil {
return false, err
}
users = getUser(index, p.Policy, domain)
if _, err := e.RemovePolicies(users); err != nil {
if _, err = e.RemovePolicies(users); err != nil {
return false, err
}
return true, nil
@ -163,8 +169,11 @@ func (e *Enforcer) GetAllDomains() ([]string, error) {
// GetAllRolesByDomain would get all roles associated with the domain.
// note: Not applicable to Domains with inheritance relationship (implicit roles)
func (e *Enforcer) GetAllRolesByDomain(domain string) []string {
g := e.model["g"]["g"]
func (e *Enforcer) GetAllRolesByDomain(domain string) ([]string, error) {
g, err := e.model.GetAssertion("g", "g")
if err != nil {
return []string{}, err
}
policies := g.Policy
roles := make([]string, 0)
existMap := make(map[string]bool) // remove duplicates
@ -179,5 +188,5 @@ func (e *Enforcer) GetAllRolesByDomain(domain string) []string {
}
}
return roles
return roles, nil
}

View File

@ -9,6 +9,8 @@ func Render(doc []byte) []byte {
renderer := NewRoffRenderer()
return blackfriday.Run(doc,
[]blackfriday.Option{blackfriday.WithRenderer(renderer),
blackfriday.WithExtensions(renderer.GetExtensions())}...)
[]blackfriday.Option{
blackfriday.WithRenderer(renderer),
blackfriday.WithExtensions(renderer.GetExtensions()),
}...)
}

View File

@ -1,6 +1,8 @@
package md2man
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
@ -20,34 +22,35 @@ type roffRenderer struct {
}
const (
titleHeader = ".TH "
topLevelHeader = "\n\n.SH "
secondLevelHdr = "\n.SH "
otherHeader = "\n.SS "
crTag = "\n"
emphTag = "\\fI"
emphCloseTag = "\\fP"
strongTag = "\\fB"
strongCloseTag = "\\fP"
breakTag = "\n.br\n"
paraTag = "\n.PP\n"
hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n"
linkTag = "\n\\[la]"
linkCloseTag = "\\[ra]"
codespanTag = "\\fB\\fC"
codespanCloseTag = "\\fR"
codeTag = "\n.PP\n.RS\n\n.nf\n"
codeCloseTag = "\n.fi\n.RE\n"
quoteTag = "\n.PP\n.RS\n"
quoteCloseTag = "\n.RE\n"
listTag = "\n.RS\n"
listCloseTag = "\n.RE\n"
dtTag = "\n.TP\n"
dd2Tag = "\n"
tableStart = "\n.TS\nallbox;\n"
tableEnd = ".TE\n"
tableCellStart = "T{\n"
tableCellEnd = "\nT}\n"
titleHeader = ".TH "
topLevelHeader = "\n\n.SH "
secondLevelHdr = "\n.SH "
otherHeader = "\n.SS "
crTag = "\n"
emphTag = "\\fI"
emphCloseTag = "\\fP"
strongTag = "\\fB"
strongCloseTag = "\\fP"
breakTag = "\n.br\n"
paraTag = "\n.PP\n"
hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n"
linkTag = "\n\\[la]"
linkCloseTag = "\\[ra]"
codespanTag = "\\fB"
codespanCloseTag = "\\fR"
codeTag = "\n.EX\n"
codeCloseTag = ".EE\n" // Do not prepend a newline character since code blocks, by definition, include a newline already (or at least as how blackfriday gives us on).
quoteTag = "\n.PP\n.RS\n"
quoteCloseTag = "\n.RE\n"
listTag = "\n.RS\n"
listCloseTag = "\n.RE\n"
dtTag = "\n.TP\n"
dd2Tag = "\n"
tableStart = "\n.TS\nallbox;\n"
tableEnd = ".TE\n"
tableCellStart = "T{\n"
tableCellEnd = "\nT}\n"
tablePreprocessor = `'\" t`
)
// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents
@ -74,6 +77,16 @@ func (r *roffRenderer) GetExtensions() blackfriday.Extensions {
// RenderHeader handles outputting the header at document start
func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {
// We need to walk the tree to check if there are any tables.
// If there are, we need to enable the roff table preprocessor.
ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
if node.Type == blackfriday.Table {
out(w, tablePreprocessor+"\n")
return blackfriday.Terminate
}
return blackfriday.GoToNext
})
// disable hyphenation
out(w, ".nh\n")
}
@ -86,8 +99,7 @@ func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {
// RenderNode is called for each node in a markdown document; based on the node
// type the equivalent roff output is sent to the writer
func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
var walkAction = blackfriday.GoToNext
walkAction := blackfriday.GoToNext
switch node.Type {
case blackfriday.Text:
@ -109,9 +121,16 @@ func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering
out(w, strongCloseTag)
}
case blackfriday.Link:
if !entering {
out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag)
// Don't render the link text for automatic links, because this
// will only duplicate the URL in the roff output.
// See https://daringfireball.net/projects/markdown/syntax#autolink
if !bytes.Equal(node.LinkData.Destination, node.FirstChild.Literal) {
out(w, string(node.FirstChild.Literal))
}
// Hyphens in a link must be escaped to avoid word-wrap in the rendered man page.
escapedLink := strings.ReplaceAll(string(node.LinkData.Destination), "-", "\\-")
out(w, linkTag+escapedLink+linkCloseTag)
walkAction = blackfriday.SkipChildren
case blackfriday.Image:
// ignore images
walkAction = blackfriday.SkipChildren
@ -160,6 +179,11 @@ func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering
r.handleTableCell(w, node, entering)
case blackfriday.HTMLSpan:
// ignore other HTML tags
case blackfriday.HTMLBlock:
if bytes.HasPrefix(node.Literal, []byte("<!--")) {
break // ignore comments, no warning
}
fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
default:
fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
}
@ -254,7 +278,7 @@ func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, ente
start = "\t"
}
if node.IsHeader {
start += codespanTag
start += strongTag
} else if nodeLiteralSize(node) > 30 {
start += tableCellStart
}
@ -262,7 +286,7 @@ func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, ente
} else {
var end string
if node.IsHeader {
end = codespanCloseTag
end = strongCloseTag
} else if nodeLiteralSize(node) > 30 {
end = tableCellEnd
}
@ -310,6 +334,28 @@ func out(w io.Writer, output string) {
}
func escapeSpecialChars(w io.Writer, text []byte) {
scanner := bufio.NewScanner(bytes.NewReader(text))
// count the number of lines in the text
// we need to know this to avoid adding a newline after the last line
n := bytes.Count(text, []byte{'\n'})
idx := 0
for scanner.Scan() {
dt := scanner.Bytes()
if idx < n {
idx++
dt = append(dt, '\n')
}
escapeSpecialCharsLine(w, dt)
}
if err := scanner.Err(); err != nil {
panic(err)
}
}
func escapeSpecialCharsLine(w io.Writer, text []byte) {
for i := 0; i < len(text); i++ {
// escape initial apostrophe or period
if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {

View File

@ -81,10 +81,19 @@ type Prober struct {
GotAudio, GotVideo bool
VideoStreamIdx, AudioStreamIdx int
PushedCount int
MaxProbePacketCount int
Streams []av.CodecData
CachedPkts []av.Packet
}
func NewProber(maxProbePacketCount int) *Prober {
prober := &Prober{
MaxProbePacketCount: maxProbePacketCount,
}
return prober
}
func (prober *Prober) CacheTag(_tag flvio.Tag, timestamp int32) {
pkt, _ := prober.TagToPacket(_tag, timestamp)
prober.CachedPkts = append(prober.CachedPkts, pkt)
@ -93,7 +102,11 @@ func (prober *Prober) CacheTag(_tag flvio.Tag, timestamp int32) {
func (prober *Prober) PushTag(tag flvio.Tag, timestamp int32) (err error) {
prober.PushedCount++
if prober.PushedCount > MaxProbePacketCount {
if prober.MaxProbePacketCount <= 0 {
prober.MaxProbePacketCount = MaxProbePacketCount
}
if prober.PushedCount > prober.MaxProbePacketCount {
err = fmt.Errorf("flv: max probe packet count reached")
return
}
@ -229,16 +242,21 @@ func (prober *Prober) PushTag(tag flvio.Tag, timestamp int32) (err error) {
}
func (prober *Prober) Probed() (ok bool) {
if prober.MaxProbePacketCount <= 0 {
prober.MaxProbePacketCount = MaxProbePacketCount
}
if prober.HasAudio || prober.HasVideo {
if prober.HasAudio == prober.GotAudio && prober.HasVideo == prober.GotVideo {
return true
}
} else {
if prober.PushedCount == MaxProbePacketCount {
return true
}
}
return
if prober.PushedCount == prober.MaxProbePacketCount {
return true
}
return false
}
func (prober *Prober) TagToPacket(tag flvio.Tag, timestamp int32) (pkt av.Packet, ok bool) {

View File

@ -65,10 +65,33 @@ type Server struct {
HandlePlay func(*Conn)
HandleConn func(*Conn)
MaxProbePacketCount int
SkipInvalidMessages bool
DebugChunks func(conn net.Conn) bool
listener net.Listener
doneChan chan struct{}
}
func (s *Server) HandleNetConn(netconn net.Conn) (err error) {
conn := NewConn(netconn)
conn.prober = flv.NewProber(s.MaxProbePacketCount)
conn.skipInvalidMessages = s.SkipInvalidMessages
if s.DebugChunks != nil {
conn.debugChunks = s.DebugChunks(netconn)
}
conn.isserver = true
err = s.handleConn(conn)
if Debug {
fmt.Println("rtmp: server: client closed err:", err)
}
conn.Close()
return
}
func (s *Server) handleConn(conn *Conn) (err error) {
if s.HandleConn != nil {
s.HandleConn(conn)
@ -171,15 +194,12 @@ func (s *Server) Serve(listener net.Listener) error {
fmt.Println("rtmp: server: accepted")
}
conn := NewConn(netconn)
conn.isserver = true
go func() {
err := s.handleConn(conn)
go func(conn net.Conn) {
err := s.HandleNetConn(conn)
if Debug {
fmt.Println("rtmp: server: client closed err:", err)
}
conn.Close()
}()
}(netconn)
}
}
@ -252,6 +272,10 @@ type Conn struct {
eventtype uint16
start time.Time
skipInvalidMessages bool
debugChunks bool
}
type txrxcount struct {
@ -279,6 +303,7 @@ func NewConn(netconn net.Conn) *Conn {
conn.readcsmap = make(map[uint32]*chunkStream)
conn.readMaxChunkSize = 128
conn.writeMaxChunkSize = 128
conn.readAckSize = 1048576
conn.txrxcount = &txrxcount{ReadWriter: netconn}
conn.bufr = bufio.NewReaderSize(conn.txrxcount, pio.RecommendBufioSize)
conn.bufw = bufio.NewWriterSize(conn.txrxcount, pio.RecommendBufioSize)
@ -295,12 +320,14 @@ type chunkStream struct {
gentimenow bool
timedelta uint32
hastimeext bool
timeext uint32
msgsid uint32
msgtypeid uint8
msgdatalen uint32
msgdataleft uint32
msghdrtype uint8
msgdata []byte
msgcount int
}
func (cs *chunkStream) Start() {
@ -379,6 +406,15 @@ func (conn *Conn) pollMsg() (err error) {
if err = conn.readChunk(); err != nil {
return
}
if conn.readAckSize != 0 && conn.ackn > conn.readAckSize {
if err = conn.writeAck(conn.ackn, false); err != nil {
return fmt.Errorf("writeACK: %w", err)
}
conn.flushWrite()
conn.ackn = 0
}
if conn.gotmsg {
return
}
@ -757,7 +793,7 @@ func (conn *Conn) writeConnect(path string) (err error) {
} else {
if conn.msgtypeid == msgtypeidWindowAckSize {
if len(conn.msgdata) == 4 {
conn.readAckSize = pio.U32BE(conn.msgdata)
conn.readAckSize = pio.U32BE(conn.msgdata) >> 1
}
//if err = self.writeWindowAckSize(0xffffffff); err != nil {
// return
@ -1004,14 +1040,10 @@ func (conn *Conn) WriteHeader(streams []av.CodecData) (err error) {
return
}
var metadata flvio.AMFMap = nil
var metadata flvio.AMFMap
//metadata = self.GetMetaData()
if metadata == nil {
if metadata, err = flv.NewMetadataByStreams(streams); err != nil {
return
}
if metadata, err = flv.NewMetadataByStreams(streams); err != nil {
return
}
// > onMetaData()
@ -1345,10 +1377,17 @@ func (conn *Conn) readChunk() (err error) {
csid = uint32(pio.U16BE(b)) + 64
}
newcs := false
cs := conn.readcsmap[csid]
if cs == nil {
cs = &chunkStream{}
conn.readcsmap[csid] = cs
newcs = true
}
if len(conn.readcsmap) > 16 {
err = fmt.Errorf("too many chunk stream ids")
return
}
var timestamp uint32
@ -1367,8 +1406,10 @@ func (conn *Conn) readChunk() (err error) {
//
// Figure 9 Chunk Message Header Type 0
if cs.msgdataleft != 0 {
err = fmt.Errorf("chunk msgdataleft=%d invalid", cs.msgdataleft)
return
if !conn.skipInvalidMessages {
err = fmt.Errorf("chunk msgdataleft=%d invalid", cs.msgdataleft)
return
}
}
h := b[:11]
if _, err = io.ReadFull(conn.bufr, h); err != nil {
@ -1387,6 +1428,7 @@ func (conn *Conn) readChunk() (err error) {
n += 4
timestamp = pio.U32BE(b)
cs.hastimeext = true
cs.timeext = timestamp
} else {
cs.hastimeext = false
}
@ -1403,10 +1445,16 @@ func (conn *Conn) readChunk() (err error) {
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// Figure 10 Chunk Message Header Type 1
if cs.msgdataleft != 0 {
err = fmt.Errorf("chunk msgdataleft=%d invalid", cs.msgdataleft)
if newcs {
err = fmt.Errorf("chunk message type 1 without previous chunk")
return
}
if cs.msgdataleft != 0 {
if !conn.skipInvalidMessages {
err = fmt.Errorf("chunk msgdataleft=%d invalid", cs.msgdataleft)
return
}
}
h := b[:7]
if _, err = io.ReadFull(conn.bufr, h); err != nil {
return
@ -1423,6 +1471,7 @@ func (conn *Conn) readChunk() (err error) {
n += 4
timestamp = pio.U32BE(b)
cs.hastimeext = true
cs.timeext = timestamp
} else {
cs.hastimeext = false
}
@ -1438,10 +1487,16 @@ func (conn *Conn) readChunk() (err error) {
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// Figure 11 Chunk Message Header Type 2
if cs.msgdataleft != 0 {
err = fmt.Errorf("chunk msgdataleft=%d invalid", cs.msgdataleft)
if newcs {
err = fmt.Errorf("chunk message type 2 without previous chunk")
return
}
if cs.msgdataleft != 0 {
if !conn.skipInvalidMessages {
err = fmt.Errorf("chunk msgdataleft=%d invalid", cs.msgdataleft)
return
}
}
h := b[:3]
if _, err = io.ReadFull(conn.bufr, h); err != nil {
return
@ -1456,6 +1511,7 @@ func (conn *Conn) readChunk() (err error) {
n += 4
timestamp = pio.U32BE(b)
cs.hastimeext = true
cs.timeext = timestamp
} else {
cs.hastimeext = false
}
@ -1464,6 +1520,11 @@ func (conn *Conn) readChunk() (err error) {
cs.Start()
case 3:
if newcs {
err = fmt.Errorf("chunk message type 3 without previous chunk")
return
}
if cs.msgdataleft == 0 {
switch cs.msghdrtype {
case 0:
@ -1474,6 +1535,7 @@ func (conn *Conn) readChunk() (err error) {
n += 4
timestamp = pio.U32BE(b)
cs.timenow = timestamp
cs.timeext = timestamp
}
case 1, 2:
if cs.hastimeext {
@ -1488,6 +1550,18 @@ func (conn *Conn) readChunk() (err error) {
cs.timenow += timestamp
}
cs.Start()
} else {
if cs.hastimeext {
var b []byte
if b, err = conn.bufr.Peek(4); err != nil {
return
}
if pio.U32BE(b) == cs.timeext {
if _, err = io.ReadFull(conn.bufr, b[:4]); err != nil {
return
}
}
}
}
default:
@ -1508,8 +1582,27 @@ func (conn *Conn) readChunk() (err error) {
cs.msgdataleft -= uint32(size)
if Debug {
fmt.Printf("rtmp: chunk msgsid=%d msgtypeid=%d msghdrtype=%d len=%d left=%d\n",
cs.msgsid, cs.msgtypeid, cs.msghdrtype, cs.msgdatalen, cs.msgdataleft)
fmt.Printf("rtmp: chunk msgsid=%d msgtypeid=%d msghdrtype=%d len=%d left=%d max=%d",
cs.msgsid, cs.msgtypeid, msghdrtype, cs.msgdatalen, cs.msgdataleft, conn.readMaxChunkSize)
}
if conn.debugChunks {
data := fmt.Sprintf("rtmp: chunk id=%d msgsid=%d msgtypeid=%d msghdrtype=%d timestamp=%d ext=%v len=%d left=%d max=%d",
csid, cs.msgsid, cs.msgtypeid, msghdrtype, cs.timenow, cs.hastimeext, cs.msgdatalen, cs.msgdataleft, conn.readMaxChunkSize)
if cs.msgtypeid != msgtypeidVideoMsg && cs.msgtypeid != msgtypeidAudioMsg {
if len(cs.msgdata) > 1024 {
data += " data=" + hex.EncodeToString(cs.msgdata[:1024]) + "... "
} else {
data += " data=" + hex.EncodeToString(cs.msgdata) + " "
}
} else {
data += " data= "
}
data += fmt.Sprintf("(%d bytes)", len(cs.msgdata))
fmt.Printf("%s\n", data)
}
if cs.msgdataleft == 0 {
@ -1521,20 +1614,27 @@ func (conn *Conn) readChunk() (err error) {
timestamp = cs.timenow
if cs.msgtypeid == msgtypeidVideoMsg || cs.msgtypeid == msgtypeidAudioMsg {
if !cs.gentimenow {
if cs.prevtimenow >= cs.timenow {
cs.tscount++
} else {
cs.tscount = 0
if cs.msgcount < 20 { // only consider the first video and audio messages
if !cs.gentimenow {
if cs.prevtimenow >= cs.timenow {
cs.tscount++
} else {
cs.tscount = 0
}
// if the previous timestamp is the same as the current for too often in a row, assume defect timestamps
if cs.tscount > 10 {
cs.gentimenow = true
}
cs.prevtimenow = cs.timenow
}
if cs.tscount > 3 {
cs.gentimenow = true
}
cs.msgcount++
}
if cs.gentimenow {
timestamp = uint32((time.Since(conn.start).Milliseconds() % 0xFFFFFFFF) & 0xFFFFFFFF)
timestamp = uint32(time.Since(conn.start).Milliseconds() % 0xFFFFFFFF)
}
}
@ -1542,19 +1642,11 @@ func (conn *Conn) readChunk() (err error) {
return fmt.Errorf("handleMsg: %w", err)
}
cs.prevtimenow = cs.timenow
cs.msgdata = nil
}
conn.ackn += uint32(n)
if conn.readAckSize != 0 && conn.ackn > conn.readAckSize {
if err = conn.writeAck(conn.ackn, false); err != nil {
return fmt.Errorf("writeACK: %w", err)
}
conn.flushWrite()
conn.ackn = 0
}
return
}
@ -1608,6 +1700,7 @@ func (conn *Conn) handleMsg(timestamp uint32, msgsid uint32, msgtypeid uint8, ms
switch msgtypeid {
case msgtypeidCommandMsgAMF0:
if _, err = conn.handleCommandMsgAMF0(msgdata); err != nil {
err = fmt.Errorf("AMF0: %w", err)
return
}
@ -1618,6 +1711,7 @@ func (conn *Conn) handleMsg(timestamp uint32, msgsid uint32, msgtypeid uint8, ms
}
// skip first byte
if _, err = conn.handleCommandMsgAMF0(msgdata[1:]); err != nil {
err = fmt.Errorf("AMF3: %w", err)
return
}
@ -1670,8 +1764,14 @@ func (conn *Conn) handleMsg(timestamp uint32, msgsid uint32, msgtypeid uint8, ms
if metaindex != -1 && metaindex < len(conn.datamsgvals) {
conn.metadata = conn.datamsgvals[metaindex].(flvio.AMFMap)
//fmt.Printf("onMetadata: %+v\n", self.metadata)
//fmt.Printf("videocodecid: %#08x (%f)\n", int64(self.metadata["videocodecid"].(float64)), self.metadata["videocodecid"].(float64))
//fmt.Printf("onMetadata: %+v\n", conn.metadata)
if _, hasVideo := conn.metadata["videocodecid"]; hasVideo {
conn.prober.HasVideo = true
}
if _, hasAudio := conn.metadata["audiocodecid"]; hasAudio {
conn.prober.HasAudio = true
}
}
case msgtypeidVideoMsg:
@ -1713,7 +1813,7 @@ func (conn *Conn) handleMsg(timestamp uint32, msgsid uint32, msgtypeid uint8, ms
if len(conn.msgdata) != 4 {
return fmt.Errorf("invalid packet of WindowAckSize")
}
conn.readAckSize = pio.U32BE(conn.msgdata)
conn.readAckSize = pio.U32BE(conn.msgdata) >> 1
default:
if Debug {
fmt.Printf("rtmp: unhandled msg: %d\n", msgtypeid)

View File

@ -269,7 +269,7 @@ func (c *Color) Printf(format string, a ...interface{}) (n int, err error) {
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, c.wrap(fmt.Sprint(a...)))
return fmt.Fprintln(w, c.wrap(sprintln(a...)))
}
// Println formats using the default formats for its operands and writes to
@ -278,7 +278,7 @@ func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Println(a ...interface{}) (n int, err error) {
return fmt.Fprintln(Output, c.wrap(fmt.Sprint(a...)))
return fmt.Fprintln(Output, c.wrap(sprintln(a...)))
}
// Sprint is just like Print, but returns a string instead of printing it.
@ -288,7 +288,7 @@ func (c *Color) Sprint(a ...interface{}) string {
// Sprintln is just like Println, but returns a string instead of printing it.
func (c *Color) Sprintln(a ...interface{}) string {
return fmt.Sprintln(c.Sprint(a...))
return c.wrap(sprintln(a...)) + "\n"
}
// Sprintf is just like Printf, but returns a string instead of printing it.
@ -370,7 +370,7 @@ func (c *Color) SprintfFunc() func(format string, a ...interface{}) string {
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintlnFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return fmt.Sprintln(c.Sprint(a...))
return c.wrap(sprintln(a...)) + "\n"
}
}
@ -648,3 +648,8 @@ func HiCyanString(format string, a ...interface{}) string { return colorString(f
func HiWhiteString(format string, a ...interface{}) string {
return colorString(format, FgHiWhite, a...)
}
// sprintln is a helper function to format a string with fmt.Sprintln and trim the trailing newline.
func sprintln(a ...interface{}) string {
return strings.TrimSuffix(fmt.Sprintln(a...), "\n")
}

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2020 Gabriel Vasile
Copyright (c) 2018 Gabriel Vasile
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -16,9 +16,6 @@
<a href="https://goreportcard.com/report/github.com/gabriel-vasile/mimetype">
<img alt="Go report card" src="https://goreportcard.com/badge/github.com/gabriel-vasile/mimetype">
</a>
<a href="https://codecov.io/gh/gabriel-vasile/mimetype">
<img alt="Code coverage" src="https://codecov.io/gh/gabriel-vasile/mimetype/branch/master/graph/badge.svg?token=qcfJF1kkl2"/>
</a>
<a href="LICENSE">
<img alt="License" src="https://img.shields.io/badge/License-MIT-green.svg">
</a>

View File

@ -3,6 +3,7 @@ package magic
import (
"bytes"
"encoding/binary"
"strconv"
)
var (
@ -74,51 +75,87 @@ func CRX(raw []byte, limit uint32) bool {
}
// Tar matches a (t)ape (ar)chive file.
// Tar files are divided into 512 bytes records. First record contains a 257
// bytes header padded with NUL.
func Tar(raw []byte, _ uint32) bool {
// The "magic" header field for files in in UStar (POSIX IEEE P1003.1) archives
// has the prefix "ustar". The values of the remaining bytes in this field vary
// by archiver implementation.
if len(raw) >= 512 && bytes.HasPrefix(raw[257:], []byte{0x75, 0x73, 0x74, 0x61, 0x72}) {
return true
}
const sizeRecord = 512
if len(raw) < 256 {
// The structure of a tar header:
// type TarHeader struct {
// Name [100]byte
// Mode [8]byte
// Uid [8]byte
// Gid [8]byte
// Size [12]byte
// Mtime [12]byte
// Chksum [8]byte
// Linkflag byte
// Linkname [100]byte
// Magic [8]byte
// Uname [32]byte
// Gname [32]byte
// Devmajor [8]byte
// Devminor [8]byte
// }
if len(raw) < sizeRecord {
return false
}
raw = raw[:sizeRecord]
// First 100 bytes of the header represent the file name.
// Check if file looks like Gentoo GLEP binary package.
if bytes.Contains(raw[:100], []byte("/gpkg-1\x00")) {
return false
}
// The older v7 format has no "magic" field, and therefore must be identified
// with heuristics based on legal ranges of values for other header fields:
// https://www.nationalarchives.gov.uk/PRONOM/Format/proFormatSearch.aspx?status=detailReport&id=385&strPageToDisplay=signatures
rules := []struct {
min, max uint8
i int
}{
{0x21, 0xEF, 0},
{0x30, 0x37, 105},
{0x20, 0x37, 106},
{0x00, 0x00, 107},
{0x30, 0x37, 113},
{0x20, 0x37, 114},
{0x00, 0x00, 115},
{0x30, 0x37, 121},
{0x20, 0x37, 122},
{0x00, 0x00, 123},
{0x30, 0x37, 134},
{0x30, 0x37, 146},
{0x30, 0x37, 153},
{0x00, 0x37, 154},
// Get the checksum recorded into the file.
recsum, err := tarParseOctal(raw[148:156])
if err != nil {
return false
}
for _, r := range rules {
if raw[r.i] < r.min || raw[r.i] > r.max {
return false
}
}
for _, i := range []uint8{135, 147, 155} {
if raw[i] != 0x00 && raw[i] != 0x20 {
return false
}
}
return true
sum1, sum2 := tarChksum(raw)
return recsum == sum1 || recsum == sum2
}
// tarParseOctal converts octal string to decimal int.
func tarParseOctal(b []byte) (int64, error) {
// Because unused fields are filled with NULs, we need to skip leading NULs.
// Fields may also be padded with spaces or NULs.
// So we remove leading and trailing NULs and spaces to be sure.
b = bytes.Trim(b, " \x00")
if len(b) == 0 {
return 0, nil
}
x, err := strconv.ParseUint(tarParseString(b), 8, 64)
if err != nil {
return 0, err
}
return int64(x), nil
}
// tarParseString converts a NUL ended bytes slice to a string.
func tarParseString(b []byte) string {
if i := bytes.IndexByte(b, 0); i >= 0 {
return string(b[:i])
}
return string(b)
}
// tarChksum computes the checksum for the header block b.
// The actual checksum is written to same b block after it has been calculated.
// Before calculation the bytes from b reserved for checksum have placeholder
// value of ASCII space 0x20.
// POSIX specifies a sum of the unsigned byte values, but the Sun tar used
// signed byte values. We compute and return both.
func tarChksum(b []byte) (unsigned, signed int64) {
for i, c := range b {
if 148 <= i && i < 156 {
c = ' ' // Treat the checksum field itself as all spaces.
}
unsigned += int64(c)
signed += int64(int8(c))
}
return unsigned, signed
}

View File

@ -153,8 +153,11 @@ func ftyp(sigs ...[]byte) Detector {
if len(raw) < 12 {
return false
}
if !bytes.Equal(raw[4:8], []byte("ftyp")) {
return false
}
for _, s := range sigs {
if bytes.Equal(raw[4:12], append([]byte("ftyp"), s...)) {
if bytes.Equal(raw[8:12], s) {
return true
}
}

View File

@ -1,7 +1,6 @@
package magic
import (
"bufio"
"bytes"
"strings"
"time"
@ -234,9 +233,10 @@ func GeoJSON(raw []byte, limit uint32) bool {
// types.
func NdJSON(raw []byte, limit uint32) bool {
lCount, hasObjOrArr := 0, false
sc := bufio.NewScanner(dropLastLine(raw, limit))
for sc.Scan() {
l := sc.Bytes()
raw = dropLastLine(raw, limit)
var l []byte
for len(raw) != 0 {
l, raw = scanLine(raw)
// Empty lines are allowed in NDJSON.
if l = trimRWS(trimLWS(l)); len(l) == 0 {
continue
@ -301,20 +301,15 @@ func Svg(raw []byte, limit uint32) bool {
}
// Srt matches a SubRip file.
func Srt(in []byte, _ uint32) bool {
s := bufio.NewScanner(bytes.NewReader(in))
if !s.Scan() {
return false
}
// First line must be 1.
if s.Text() != "1" {
return false
}
func Srt(raw []byte, _ uint32) bool {
line, raw := scanLine(raw)
if !s.Scan() {
// First line must be 1.
if string(line) != "1" {
return false
}
secondLine := s.Text()
line, raw = scanLine(raw)
secondLine := string(line)
// Timestamp format (e.g: 00:02:16,612 --> 00:02:19,376) limits secondLine
// length to exactly 29 characters.
if len(secondLine) != 29 {
@ -325,14 +320,12 @@ func Srt(in []byte, _ uint32) bool {
if strings.Contains(secondLine, ".") {
return false
}
// For Go <1.17, comma is not recognised as a decimal separator by `time.Parse`.
secondLine = strings.ReplaceAll(secondLine, ",", ".")
// Second line must be a time range.
ts := strings.Split(secondLine, " --> ")
if len(ts) != 2 {
return false
}
const layout = "15:04:05.000"
const layout = "15:04:05,000"
t0, err := time.Parse(layout, ts[0])
if err != nil {
return false
@ -345,8 +338,9 @@ func Srt(in []byte, _ uint32) bool {
return false
}
line, _ = scanLine(raw)
// A third line must exist and not be empty. This is the actual subtitle text.
return s.Scan() && len(s.Bytes()) != 0
return len(line) != 0
}
// Vtt matches a Web Video Text Tracks (WebVTT) file. See
@ -373,3 +367,15 @@ func Vtt(raw []byte, limit uint32) bool {
return bytes.Equal(raw, []byte{0xEF, 0xBB, 0xBF, 0x57, 0x45, 0x42, 0x56, 0x54, 0x54}) || // UTF-8 BOM and "WEBVTT"
bytes.Equal(raw, []byte{0x57, 0x45, 0x42, 0x56, 0x54, 0x54}) // "WEBVTT"
}
// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
return data[0 : len(data)-1]
}
return data
}
func scanLine(b []byte) (line, remainder []byte) {
line, remainder, _ = bytes.Cut(b, []byte("\n"))
return dropCR(line), remainder
}

View File

@ -18,7 +18,7 @@ func Tsv(raw []byte, limit uint32) bool {
}
func sv(in []byte, comma rune, limit uint32) bool {
r := csv.NewReader(dropLastLine(in, limit))
r := csv.NewReader(bytes.NewReader(dropLastLine(in, limit)))
r.Comma = comma
r.ReuseRecord = true
r.LazyQuotes = true
@ -44,20 +44,14 @@ func sv(in []byte, comma rune, limit uint32) bool {
// mimetype limits itself to ReadLimit bytes when performing a detection.
// This means, for file formats like CSV for NDJSON, the last line of the input
// can be an incomplete line.
func dropLastLine(b []byte, cutAt uint32) io.Reader {
if cutAt == 0 {
return bytes.NewReader(b)
func dropLastLine(b []byte, readLimit uint32) []byte {
if readLimit == 0 || uint32(len(b)) < readLimit {
return b
}
if uint32(len(b)) >= cutAt {
for i := cutAt - 1; i > 0; i-- {
if b[i] == '\n' {
return bytes.NewReader(b[:i])
}
for i := len(b) - 1; i > 0; i-- {
if b[i] == '\n' {
return b[:i]
}
// No newline was found between the 0 index and cutAt.
return bytes.NewReader(b[:cutAt])
}
return bytes.NewReader(b)
return b
}

View File

@ -7,14 +7,15 @@ package mimetype
import (
"io"
"io/ioutil"
"mime"
"os"
"sync/atomic"
)
var defaultLimit uint32 = 3072
// readLimit is the maximum number of bytes from the input used when detecting.
var readLimit uint32 = 3072
var readLimit uint32 = defaultLimit
// Detect returns the MIME type found from the provided byte slice.
//
@ -48,7 +49,7 @@ func DetectReader(r io.Reader) (*MIME, error) {
// Using atomic because readLimit can be written at the same time in other goroutine.
l := atomic.LoadUint32(&readLimit)
if l == 0 {
in, err = ioutil.ReadAll(r)
in, err = io.ReadAll(r)
if err != nil {
return errMIME, err
}
@ -103,6 +104,7 @@ func EqualsAny(s string, mimes ...string) bool {
// SetLimit sets the maximum number of bytes read from input when detecting the MIME type.
// Increasing the limit provides better detection for file formats which store
// their magical numbers towards the end of the file: docx, pptx, xlsx, etc.
// During detection data is read in a single block of size limit, i.e. it is not buffered.
// A limit of 0 means the whole input file will be used.
func SetLimit(limit uint32) {
// Using atomic because readLimit can be read at the same time in other goroutine.

View File

@ -1,7 +1,7 @@
Package validator
=================
<img align="right" src="logo.png">[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
![Project status](https://img.shields.io/badge/version-10.19.0-green.svg)
![Project status](https://img.shields.io/badge/version-10.20.0-green.svg)
[![Build Status](https://travis-ci.org/go-playground/validator.svg?branch=master)](https://travis-ci.org/go-playground/validator)
[![Coverage Status](https://coveralls.io/repos/go-playground/validator/badge.svg?branch=master&service=github)](https://coveralls.io/github/go-playground/validator?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/validator)](https://goreportcard.com/report/github.com/go-playground/validator)

View File

@ -64,8 +64,9 @@ var (
// defines a common or complex set of validation(s) to simplify
// adding validation to structs.
bakedInAliases = map[string]string{
"iscolor": "hexcolor|rgb|rgba|hsl|hsla",
"country_code": "iso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric",
"iscolor": "hexcolor|rgb|rgba|hsl|hsla",
"country_code": "iso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric",
"eu_country_code": "iso3166_1_alpha2_eu|iso3166_1_alpha3_eu|iso3166_1_alpha_numeric_eu",
}
// bakedInValidators is the default map of ValidationFunc
@ -133,6 +134,7 @@ var (
"urn_rfc2141": isUrnRFC2141, // RFC 2141
"file": isFile,
"filepath": isFilePath,
"base32": isBase32,
"base64": isBase64,
"base64url": isBase64URL,
"base64rawurl": isBase64RawURL,
@ -216,8 +218,11 @@ var (
"datetime": isDatetime,
"timezone": isTimeZone,
"iso3166_1_alpha2": isIso3166Alpha2,
"iso3166_1_alpha2_eu": isIso3166Alpha2EU,
"iso3166_1_alpha3": isIso3166Alpha3,
"iso3166_1_alpha3_eu": isIso3166Alpha3EU,
"iso3166_1_alpha_numeric": isIso3166AlphaNumeric,
"iso3166_1_alpha_numeric_eu": isIso3166AlphaNumericEU,
"iso3166_2": isIso31662,
"iso4217": isIso4217,
"iso4217_numeric": isIso4217Numeric,
@ -1399,6 +1404,11 @@ func isPostcodeByIso3166Alpha2Field(fl FieldLevel) bool {
return reg.MatchString(field.String())
}
// isBase32 is the validation function for validating if the current field's value is a valid base 32.
func isBase32(fl FieldLevel) bool {
return base32Regex.MatchString(fl.Field().String())
}
// isBase64 is the validation function for validating if the current field's value is a valid base 64.
func isBase64(fl FieldLevel) bool {
return base64Regex.MatchString(fl.Field().String())
@ -2762,12 +2772,24 @@ func isIso3166Alpha2(fl FieldLevel) bool {
return iso3166_1_alpha2[val]
}
// isIso3166Alpha2EU is the validation function for validating if the current field's value is a valid iso3166-1 alpha-2 European Union country code.
func isIso3166Alpha2EU(fl FieldLevel) bool {
val := fl.Field().String()
return iso3166_1_alpha2_eu[val]
}
// isIso3166Alpha3 is the validation function for validating if the current field's value is a valid iso3166-1 alpha-3 country code.
func isIso3166Alpha3(fl FieldLevel) bool {
val := fl.Field().String()
return iso3166_1_alpha3[val]
}
// isIso3166Alpha3EU is the validation function for validating if the current field's value is a valid iso3166-1 alpha-3 European Union country code.
func isIso3166Alpha3EU(fl FieldLevel) bool {
val := fl.Field().String()
return iso3166_1_alpha3_eu[val]
}
// isIso3166AlphaNumeric is the validation function for validating if the current field's value is a valid iso3166-1 alpha-numeric country code.
func isIso3166AlphaNumeric(fl FieldLevel) bool {
field := fl.Field()
@ -2790,6 +2812,28 @@ func isIso3166AlphaNumeric(fl FieldLevel) bool {
return iso3166_1_alpha_numeric[code]
}
// isIso3166AlphaNumericEU is the validation function for validating if the current field's value is a valid iso3166-1 alpha-numeric European Union country code.
func isIso3166AlphaNumericEU(fl FieldLevel) bool {
field := fl.Field()
var code int
switch field.Kind() {
case reflect.String:
i, err := strconv.Atoi(field.String())
if err != nil {
return false
}
code = i % 1000
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
code = int(field.Int() % 1000)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
code = int(field.Uint() % 1000)
default:
panic(fmt.Sprintf("Bad field type %T", field.Interface()))
}
return iso3166_1_alpha_numeric_eu[code]
}
// isIso31662 is the validation function for validating if the current field's value is a valid iso3166-2 code.
func isIso31662(fl FieldLevel) bool {
val := fl.Field().String()

View File

@ -54,6 +54,15 @@ var iso3166_1_alpha2 = map[string]bool{
"EH": true, "YE": true, "ZM": true, "ZW": true, "XK": true,
}
var iso3166_1_alpha2_eu = map[string]bool{
"AT": true, "BE": true, "BG": true, "HR": true, "CY": true,
"CZ": true, "DK": true, "EE": true, "FI": true, "FR": true,
"DE": true, "GR": true, "HU": true, "IE": true, "IT": true,
"LV": true, "LT": true, "LU": true, "MT": true, "NL": true,
"PL": true, "PT": true, "RO": true, "SK": true, "SI": true,
"ES": true, "SE": true,
}
var iso3166_1_alpha3 = map[string]bool{
// see: https://www.iso.org/iso-3166-country-codes.html
"AFG": true, "ALB": true, "DZA": true, "ASM": true, "AND": true,
@ -107,6 +116,15 @@ var iso3166_1_alpha3 = map[string]bool{
"VNM": true, "VGB": true, "VIR": true, "WLF": true, "ESH": true,
"YEM": true, "ZMB": true, "ZWE": true, "ALA": true, "UNK": true,
}
var iso3166_1_alpha3_eu = map[string]bool{
"AUT": true, "BEL": true, "BGR": true, "HRV": true, "CYP": true,
"CZE": true, "DNK": true, "EST": true, "FIN": true, "FRA": true,
"DEU": true, "GRC": true, "HUN": true, "IRL": true, "ITA": true,
"LVA": true, "LTU": true, "LUX": true, "MLT": true, "NLD": true,
"POL": true, "PRT": true, "ROU": true, "SVK": true, "SVN": true,
"ESP": true, "SWE": true,
}
var iso3166_1_alpha_numeric = map[int]bool{
// see: https://www.iso.org/iso-3166-country-codes.html
4: true, 8: true, 12: true, 16: true, 20: true,
@ -161,6 +179,15 @@ var iso3166_1_alpha_numeric = map[int]bool{
887: true, 894: true, 716: true, 248: true, 153: true,
}
var iso3166_1_alpha_numeric_eu = map[int]bool{
40: true, 56: true, 100: true, 191: true, 196: true,
200: true, 208: true, 233: true, 246: true, 250: true,
276: true, 300: true, 348: true, 372: true, 380: true,
428: true, 440: true, 442: true, 470: true, 528: true,
616: true, 620: true, 642: true, 703: true, 705: true,
724: true, 752: true,
}
var iso3166_2 = map[string]bool{
"AD-02": true, "AD-03": true, "AD-04": true, "AD-05": true, "AD-06": true,
"AD-07": true, "AD-08": true, "AE-AJ": true, "AE-AZ": true, "AE-DU": true,

View File

@ -916,6 +916,15 @@ according to the RFC 2141 spec.
Usage: urn_rfc2141
# Base32 String
This validates that a string value contains a valid bas324 value.
Although an empty string is valid base32 this will report an empty string
as an error, if you wish to accept an empty string as valid you can use
this with the omitempty tag.
Usage: base32
# Base64 String
This validates that a string value contains a valid base64 value.

View File

@ -17,6 +17,7 @@ const (
hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$"
emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
e164RegexString = "^\\+[1-9]?[0-9]{7,14}$"
base32RegexString = "^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=|[A-Z2-7]{8})$"
base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
base64URLRegexString = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{4})$"
base64RawURLRegexString = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2,4})$"
@ -31,7 +32,7 @@ const (
uUID4RFC4122RegexString = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
uUID5RFC4122RegexString = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
uUIDRFC4122RegexString = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
uLIDRegexString = "^[A-HJKMNP-TV-Z0-9]{26}$"
uLIDRegexString = "^(?i)[A-HJKMNP-TV-Z0-9]{26}$"
md4RegexString = "^[0-9a-f]{32}$"
md5RegexString = "^[0-9a-f]{32}$"
sha256RegexString = "^[0-9a-f]{64}$"
@ -89,6 +90,7 @@ var (
hslaRegex = regexp.MustCompile(hslaRegexString)
e164Regex = regexp.MustCompile(e164RegexString)
emailRegex = regexp.MustCompile(emailRegexString)
base32Regex = regexp.MustCompile(base32RegexString)
base64Regex = regexp.MustCompile(base64RegexString)
base64URLRegex = regexp.MustCompile(base64URLRegexString)
base64RawURLRegex = regexp.MustCompile(base64RawURLRegexString)

View File

@ -56,6 +56,9 @@ linters:
- cyclop
- containedctx
- revive
- nosnakecase
- exhaustruct
- depguard
issues:
exclude-rules:

View File

@ -30,7 +30,7 @@ golangci-lint: | $(BIN_DIR)
GOLANGCI_LINT_TMP_DIR=$$(mktemp -d); \
cd $$GOLANGCI_LINT_TMP_DIR; \
go mod init tmp; \
GOBIN=$(BIN_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.48.0; \
GOBIN=$(BIN_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2; \
rm -rf $$GOLANGCI_LINT_TMP_DIR; \
}

View File

@ -52,7 +52,7 @@ func (e *Encoder) EncodeContext(ctx context.Context, v interface{}, optFuncs ...
rctx.Option.Flag |= encoder.ContextOption
rctx.Option.Context = ctx
err := e.encodeWithOption(rctx, v, optFuncs...)
err := e.encodeWithOption(rctx, v, optFuncs...) //nolint: contextcheck
encoder.ReleaseRuntimeContext(rctx)
return err
@ -120,7 +120,7 @@ func marshalContext(ctx context.Context, v interface{}, optFuncs ...EncodeOption
optFunc(rctx.Option)
}
buf, err := encode(rctx, v)
buf, err := encode(rctx, v) //nolint: contextcheck
if err != nil {
encoder.ReleaseRuntimeContext(rctx)
return nil, err

View File

@ -85,6 +85,7 @@ func (d *ptrDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.P
}
c, err := d.dec.Decode(ctx, cursor, depth, newptr)
if err != nil {
*(*unsafe.Pointer)(p) = nil
return 0, err
}
cursor = c

View File

@ -147,7 +147,7 @@ func (d *unmarshalTextDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int
return nil, 0, fmt.Errorf("json: unmarshal text decoder does not support decode path")
}
func unquoteBytes(s []byte) (t []byte, ok bool) {
func unquoteBytes(s []byte) (t []byte, ok bool) { //nolint: nonamedreturns
length := len(s)
if length < 2 || s[0] != '"' || s[length-1] != '"' {
return

View File

@ -213,8 +213,8 @@ func compactString(dst, src []byte, cursor int64, escape bool) ([]byte, int64, e
dst = append(dst, src[start:cursor]...)
dst = append(dst, `\u202`...)
dst = append(dst, hex[src[cursor+2]&0xF])
cursor += 2
start = cursor + 3
cursor += 2
}
}
switch c {

View File

@ -480,7 +480,7 @@ func (c *Compiler) mapCode(typ *runtime.Type) (*MapCode, error) {
func (c *Compiler) listElemCode(typ *runtime.Type) (Code, error) {
switch {
case c.isPtrMarshalJSONType(typ):
case c.implementsMarshalJSONType(typ) || c.implementsMarshalJSONType(runtime.PtrTo(typ)):
return c.marshalJSONCode(typ)
case !typ.Implements(marshalTextType) && runtime.PtrTo(typ).Implements(marshalTextType):
return c.marshalTextCode(typ)

View File

@ -1,3 +1,27 @@
// This files's processing codes are inspired by https://github.com/segmentio/encoding.
// The license notation is as follows.
//
// # MIT License
//
// Copyright (c) 2019 Segment.io, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package encoder
import (

View File

@ -1,3 +1,27 @@
// This files's string processing codes are inspired by https://github.com/segmentio/encoding.
// The license notation is as follows.
//
// # MIT License
//
// Copyright (c) 2019 Segment.io, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package encoder
import (

View File

@ -252,7 +252,6 @@ func IfaceIndir(*Type) bool
//go:noescape
func RType2Type(t *Type) reflect.Type
//go:nolint structcheck
type emptyInterface struct {
_ *Type
ptr unsafe.Pointer

View File

@ -89,31 +89,31 @@ type UnmarshalerContext interface {
//
// Examples of struct field tags and their meanings:
//
// // Field appears in JSON as key "myName".
// Field int `json:"myName"`
// // Field appears in JSON as key "myName".
// Field int `json:"myName"`
//
// // Field appears in JSON as key "myName" and
// // the field is omitted from the object if its value is empty,
// // as defined above.
// Field int `json:"myName,omitempty"`
// // Field appears in JSON as key "myName" and
// // the field is omitted from the object if its value is empty,
// // as defined above.
// Field int `json:"myName,omitempty"`
//
// // Field appears in JSON as key "Field" (the default), but
// // the field is skipped if empty.
// // Note the leading comma.
// Field int `json:",omitempty"`
// // Field appears in JSON as key "Field" (the default), but
// // the field is skipped if empty.
// // Note the leading comma.
// Field int `json:",omitempty"`
//
// // Field is ignored by this package.
// Field int `json:"-"`
// // Field is ignored by this package.
// Field int `json:"-"`
//
// // Field appears in JSON as key "-".
// Field int `json:"-,"`
// // Field appears in JSON as key "-".
// Field int `json:"-,"`
//
// The "string" option signals that a field is stored as JSON inside a
// JSON-encoded string. It applies only to fields of string, floating point,
// integer, or boolean types. This extra level of encoding is sometimes used
// when communicating with JavaScript programs:
//
// Int64String int64 `json:",string"`
// Int64String int64 `json:",string"`
//
// The key name will be used if it's a non-empty string consisting of
// only Unicode letters, digits, and ASCII punctuation except quotation
@ -166,7 +166,6 @@ type UnmarshalerContext interface {
// JSON cannot represent cyclic data structures and Marshal does not
// handle them. Passing cyclic structures to Marshal will result in
// an infinite recursion.
//
func Marshal(v interface{}) ([]byte, error) {
return MarshalWithOption(v)
}
@ -264,14 +263,13 @@ func MarshalIndentWithOption(v interface{}, prefix, indent string, optFuncs ...E
//
// The JSON null value unmarshals into an interface, map, pointer, or slice
// by setting that Go value to nil. Because null is often used in JSON to mean
// ``not present,'' unmarshaling a JSON null into any other Go type has no effect
// “not present,” unmarshaling a JSON null into any other Go type has no effect
// on the value and produces no error.
//
// When unmarshaling quoted strings, invalid UTF-8 or
// invalid UTF-16 surrogate pairs are not treated as an error.
// Instead, they are replaced by the Unicode replacement
// character U+FFFD.
//
func Unmarshal(data []byte, v interface{}) error {
return unmarshal(data, v)
}
@ -299,7 +297,6 @@ func UnmarshalNoEscape(data []byte, v interface{}, optFuncs ...DecodeOptionFunc)
// Number, for JSON numbers
// string, for JSON string literals
// nil, for JSON null
//
type Token = json.Token
// A Number represents a JSON number literal.

View File

@ -1,205 +0,0 @@
// Copyright 2020 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package acme
import (
"context"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"fmt"
"io"
"math/big"
"net/http"
"time"
"go.uber.org/zap"
)
// RenewalInfo "is a new resource type introduced to ACME protocol.
// This new resource both allows clients to query the server for
// suggestions on when they should renew certificates, and allows
// clients to inform the server when they have completed renewal
// (or otherwise replaced the certificate to their satisfaction)."
//
// ACME Renewal Information (ARI):
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
//
// This is a DRAFT specification and the API is subject to change.
type RenewalInfo struct {
SuggestedWindow struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
} `json:"suggestedWindow"`
ExplanationURL string `json:"explanationURL"`
// This field is not part of the specified structure, but is
// important for proper conformance to the specification,
// so the Retry-After response header will be read and this
// field will be populated for ACME client consideration.
// Polling again for renewal info should not occur before
// this time.
RetryAfter time.Time `json:"-"`
}
// GetRenewalInfo returns the ACME Renewal Information (ARI) for the certificate represented by the
// "base64url-encoded [RFC4648] bytes of a DER-encoded CertID ASN.1 sequence [RFC6960]" without padding
// (call `CertIDSequence()` to get this value). It tacks on the Retry-After value if present.
func (c *Client) GetRenewalInfo(ctx context.Context, b64CertIDSeq string) (RenewalInfo, error) {
if err := c.provision(ctx); err != nil {
return RenewalInfo{}, err
}
endpoint := c.dir.RenewalInfo + b64CertIDSeq
var ari RenewalInfo
resp, err := c.httpReq(ctx, http.MethodGet, endpoint, nil, &ari)
if err != nil {
return RenewalInfo{}, err
}
ra, err := retryAfterTime(resp)
if err != nil && c.Logger != nil {
c.Logger.Error("setting Retry-After value", zap.Error(err))
}
ari.RetryAfter = ra
return ari, nil
}
// UpdateRenewalInfo notifies the ACME server that the certificate represented by b64CertIDSeq
// has been replaced. The b64CertIDSeq string can be obtained by calling `CertIDSequence()`.
func (c *Client) UpdateRenewalInfo(ctx context.Context, account Account, b64CertIDSeq string) error {
if err := c.provision(ctx); err != nil {
return err
}
payload := struct {
CertID string `json:"certID"`
Replaced bool `json:"replaced"`
}{
CertID: b64CertIDSeq,
Replaced: true,
}
resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, c.dir.RenewalInfo, payload, nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("updating renewal status: HTTP %d", resp.StatusCode)
}
return nil
}
// CertIDSequence returns the "base64url-encoded [RFC4648] bytes of a DER-encoded CertID ASN.1 sequence [RFC6960]"
// without padding for the given certificate chain. It is used primarily for requests to OCSP and ARI.
//
// The certificate chain must contain at least two elements: an end-entity certificate first, followed by an issuer
// certificate second. Of the end-entity certificate, only the SerialNumber field is required; and of the issuer
// certificate, only the RawSubjectPublicKeyInfo and RawSubject fields are required. If the issuer certificate is
// not provided, then it will be downloaded if the end-entity certificate contains the IssuingCertificateURL.
//
// As the return value may be used often during a certificate's lifetime, and in bulk with potentially tens of
// thousands of other certificates, it may be preferable to store or cache this value so that ASN.1 documents do
// not need to be repeatedly decoded and re-encoded.
func CertIDSequence(_ context.Context, certChain []*x509.Certificate, hash crypto.Hash, client *http.Client) (string, error) {
endEntityCert := certChain[0]
// if no chain was provided, we'll need to download the issuer cert
if len(certChain) == 1 {
if len(endEntityCert.IssuingCertificateURL) == 0 {
return "", fmt.Errorf("no URL to issuing certificate")
}
if client == nil {
client = http.DefaultClient
}
resp, err := client.Get(endEntityCert.IssuingCertificateURL[0])
if err != nil {
return "", fmt.Errorf("getting issuer certificate: %v", err)
}
defer resp.Body.Close()
issuerBytes, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
if err != nil {
return "", fmt.Errorf("reading issuer certificate: %v", err)
}
issuerCert, err := x509.ParseCertificate(issuerBytes)
if err != nil {
return "", fmt.Errorf("parsing issuer certificate: %v", err)
}
certChain = append(certChain, issuerCert)
}
issuerCert := certChain[1]
hashAlg, ok := hashOIDs[hash]
if !ok {
return "", x509.ErrUnsupportedAlgorithm
}
if !hash.Available() {
return "", x509.ErrUnsupportedAlgorithm
}
h := hash.New()
var publicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}
if _, err := asn1.Unmarshal(issuerCert.RawSubjectPublicKeyInfo, &publicKeyInfo); err != nil {
return "", err
}
h.Write(publicKeyInfo.PublicKey.RightAlign())
issuerKeyHash := h.Sum(nil)
h.Reset()
h.Write(issuerCert.RawSubject)
issuerNameHash := h.Sum(nil)
val, err := asn1.Marshal(certID{
HashAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: hashAlg,
},
NameHash: issuerNameHash,
IssuerKeyHash: issuerKeyHash,
SerialNumber: endEntityCert.SerialNumber,
})
if err != nil {
return "", err
}
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(val), nil
}
type certID struct {
HashAlgorithm pkix.AlgorithmIdentifier
NameHash []byte
IssuerKeyHash []byte
SerialNumber *big.Int
}
var hashOIDs = map[crypto.Hash]asn1.ObjectIdentifier{
crypto.SHA1: asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26}),
crypto.SHA256: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 1}),
crypto.SHA384: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 2}),
crypto.SHA512: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 3}),
}

149
vendor/github.com/mholt/acmez/csr.go generated vendored
View File

@ -1,149 +0,0 @@
// Copyright 2020 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package acmez
import (
"crypto/x509"
"encoding/asn1"
"errors"
"github.com/mholt/acmez/acme"
"golang.org/x/crypto/cryptobyte"
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
)
var (
oidExtensionSubjectAltName = []int{2, 5, 29, 17}
oidPermanentIdentifier = []int{1, 3, 6, 1, 5, 5, 7, 8, 3}
oidHardwareModuleName = []int{1, 3, 6, 1, 5, 5, 7, 8, 4}
)
// RFC 5280 - https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6
//
// OtherName ::= SEQUENCE {
// type-id OBJECT IDENTIFIER,
// value [0] EXPLICIT ANY DEFINED BY type-id }
type otherName struct {
TypeID asn1.ObjectIdentifier
Value asn1.RawValue
}
// permanentIdentifier is defined in RFC 4043 as an optional feature that can be
// used by a CA to indicate that two or more certificates relate to the same
// entity.
//
// The OID defined for this SAN is "1.3.6.1.5.5.7.8.3".
//
// See https://www.rfc-editor.org/rfc/rfc4043
//
// PermanentIdentifier ::= SEQUENCE {
// identifierValue UTF8String OPTIONAL,
// assigner OBJECT IDENTIFIER OPTIONAL
// }
type permanentIdentifier struct {
IdentifierValue string `asn1:"utf8,optional"`
Assigner asn1.ObjectIdentifier `asn1:"optional"`
}
// hardwareModuleName is defined in RFC 4108 as an optional feature that can be
// used to identify a hardware module.
//
// The OID defined for this SAN is "1.3.6.1.5.5.7.8.4".
//
// See https://www.rfc-editor.org/rfc/rfc4108#section-5
//
// HardwareModuleName ::= SEQUENCE {
// hwType OBJECT IDENTIFIER,
// hwSerialNum OCTET STRING
// }
type hardwareModuleName struct {
Type asn1.ObjectIdentifier
SerialNumber []byte `asn1:"tag:4"`
}
func forEachSAN(der cryptobyte.String, callback func(tag int, data []byte) error) error {
if !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) {
return errors.New("invalid subject alternative name extension")
}
for !der.Empty() {
var san cryptobyte.String
var tag cryptobyte_asn1.Tag
if !der.ReadAnyASN1Element(&san, &tag) {
return errors.New("invalid subject alternative name extension")
}
if err := callback(int(tag^0x80), san); err != nil {
return err
}
}
return nil
}
// createIdentifiersUsingCSR extracts the list of ACME identifiers from the
// given Certificate Signing Request.
func createIdentifiersUsingCSR(csr *x509.CertificateRequest) ([]acme.Identifier, error) {
var ids []acme.Identifier
for _, name := range csr.DNSNames {
ids = append(ids, acme.Identifier{
Type: "dns", // RFC 8555 §9.7.7
Value: name,
})
}
for _, ip := range csr.IPAddresses {
ids = append(ids, acme.Identifier{
Type: "ip", // RFC 8738
Value: ip.String(),
})
}
// Extract permanent identifiers and hardware module values.
// This block will ignore errors.
for _, ext := range csr.Extensions {
if ext.Id.Equal(oidExtensionSubjectAltName) {
err := forEachSAN(ext.Value, func(tag int, data []byte) error {
var on otherName
if rest, err := asn1.UnmarshalWithParams(data, &on, "tag:0"); err != nil || len(rest) > 0 {
return nil
}
switch {
case on.TypeID.Equal(oidPermanentIdentifier):
var pi permanentIdentifier
if _, err := asn1.Unmarshal(on.Value.Bytes, &pi); err == nil {
ids = append(ids, acme.Identifier{
Type: "permanent-identifier", // draft-acme-device-attest-00 §3
Value: pi.IdentifierValue,
})
}
case on.TypeID.Equal(oidHardwareModuleName):
var hmn hardwareModuleName
if _, err := asn1.Unmarshal(on.Value.Bytes, &hmn); err == nil {
ids = append(ids, acme.Identifier{
Type: "hardware-module", // draft-acme-device-attest-00 §4
Value: string(hmn.SerialNumber),
})
}
}
return nil
})
if err != nil {
return nil, err
}
break
}
}
return ids, nil
}

Some files were not shown because too many files have changed in this diff Show More