Upgrade dependencies

This commit is contained in:
Ingo Oppermann 2024-05-28 14:26:54 +02:00
parent e271a0257b
commit 8ae7ba8dfe
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
554 changed files with 53675 additions and 22835 deletions

62
go.mod
View File

@ -1,35 +1,35 @@
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/atrox/haikunatorgo/v2 v2.0.1
github.com/caddyserver/certmagic v0.20.0
github.com/caddyserver/certmagic v0.21.2
github.com/datarhei/gosrt v0.6.0
github.com/datarhei/joy4 v0.0.0-20240515122635-392a77dff12d
github.com/go-playground/validator/v10 v10.19.0
github.com/datarhei/joy4 v0.0.0-20240528121836-da80d79b6435
github.com/go-playground/validator/v10 v10.20.0
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/invopop/jsonschema v0.4.0
github.com/joho/godotenv v1.5.1
github.com/labstack/echo-jwt v0.0.0-20221127215225-c84d41a71003
github.com/labstack/echo/v4 v4.11.4
github.com/labstack/echo/v4 v4.12.0
github.com/lithammer/shortuuid/v4 v4.0.0
github.com/mattn/go-isatty v0.0.20
github.com/minio/minio-go/v7 v7.0.69
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.uber.org/zap v1.27.0
golang.org/x/mod v0.17.0
@ -40,11 +40,12 @@ require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // 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/gabriel-vasile/mimetype v1.4.3 // 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
@ -53,57 +54,54 @@ require (
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/iancoleman/orderedmap v0.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
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/miekg/dns v1.1.58 // 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/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
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.52.3 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/prometheus/common v0.53.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.2.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.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/urfave/cli/v2 v2.27.1 // 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.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/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/crypto v0.23.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

126
go.sum
View File

@ -1,11 +1,11 @@
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/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/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
@ -20,16 +20,18 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
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/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/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/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-20240515122635-392a77dff12d h1:pb2UfphyP0/aCBMRdearSQUuCCQZrzMh3R/BAff9/TY=
github.com/datarhei/joy4 v0.0.0-20240515122635-392a77dff12d/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=
@ -37,8 +39,8 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
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/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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@ -58,10 +60,12 @@ 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/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.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
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=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
@ -72,7 +76,6 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -89,8 +92,6 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@ -103,8 +104,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-jwt v0.0.0-20221127215225-c84d41a71003 h1:FyalHKl9hnJvhNbrABJXXjC2hG7gvIF0ioW9i0xHNQU=
github.com/labstack/echo-jwt v0.0.0-20221127215225-c84d41a71003/go.mod h1:ovRFgyKvi73jQIFCWz9ByQwzhIyohkzY0MFAlPGyr8Q=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@ -114,8 +115,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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -123,23 +124,16 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
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=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJnn2g=
github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -148,14 +142,14 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prep/average v0.0.0-20200506183628-d26c465f48c3 h1:Y7qCvg282QmlyrVQuL2fgGwebuw7zvfnRym09r+dUGc=
github.com/prep/average v0.0.0-20200506183628-d26c465f48c3/go.mod h1:0ZE5gcyWKS151WBDIpmLshHY0l+3edpuKnBUWVVbWKk=
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.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
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=
@ -166,14 +160,14 @@ 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/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us=
github.com/sosodev/duration v1.2.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -192,19 +186,19 @@ github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0J
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
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/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/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=
@ -212,8 +206,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=
@ -228,12 +222,12 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
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.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.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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -244,18 +238,18 @@ 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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.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.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.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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

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

@ -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,6 +320,7 @@ type chunkStream struct {
gentimenow bool
timedelta uint32
hastimeext bool
timeext uint32
msgsid uint32
msgtypeid uint8
msgdatalen uint32
@ -380,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
}
@ -758,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
@ -1005,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()
@ -1346,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
@ -1368,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 {
@ -1388,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
}
@ -1404,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
@ -1424,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
}
@ -1439,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
@ -1457,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
}
@ -1465,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:
@ -1475,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 {
@ -1489,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:
@ -1509,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 {
@ -1549,18 +1641,12 @@ func (conn *Conn) readChunk() (err error) {
if err = conn.handleMsg(timestamp, cs.msgsid, cs.msgtypeid, cs.msgdata); err != nil {
return fmt.Errorf("handleMsg: %w", err)
}
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
}
@ -1614,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
}
@ -1624,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
}
@ -1676,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:
@ -1719,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

@ -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)

32
vendor/github.com/goccy/go-json/.codecov.yml generated vendored Normal file
View File

@ -0,0 +1,32 @@
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: 70%
threshold: 2%
patch: off
changes: no
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "header,diff"
behavior: default
require_changes: no
ignore:
- internal/encoder/vm_color
- internal/encoder/vm_color_indent

2
vendor/github.com/goccy/go-json/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
cover.html
cover.out

86
vendor/github.com/goccy/go-json/.golangci.yml generated vendored Normal file
View File

@ -0,0 +1,86 @@
run:
skip-files:
- encode_optype.go
- ".*_test\\.go$"
linters-settings:
govet:
enable-all: true
disable:
- shadow
linters:
enable-all: true
disable:
- dogsled
- dupl
- exhaustive
- exhaustivestruct
- errorlint
- forbidigo
- funlen
- gci
- gochecknoglobals
- gochecknoinits
- gocognit
- gocritic
- gocyclo
- godot
- godox
- goerr113
- gofumpt
- gomnd
- gosec
- ifshort
- lll
- makezero
- nakedret
- nestif
- nlreturn
- paralleltest
- testpackage
- thelper
- wrapcheck
- interfacer
- lll
- nakedret
- nestif
- nlreturn
- testpackage
- wsl
- varnamelen
- nilnil
- ireturn
- govet
- forcetypeassert
- cyclop
- containedctx
- revive
- nosnakecase
- exhaustruct
- depguard
issues:
exclude-rules:
# not needed
- path: /*.go
text: "ST1003: should not use underscores in package names"
linters:
- stylecheck
- path: /*.go
text: "don't use an underscore in package name"
linters:
- golint
- path: rtype.go
linters:
- golint
- stylecheck
- path: error.go
linters:
- staticcheck
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0

425
vendor/github.com/goccy/go-json/CHANGELOG.md generated vendored Normal file
View File

@ -0,0 +1,425 @@
# v0.10.2 - 2023/03/20
### New features
* Support DebugDOT option for debugging encoder ( #440 )
### Fix bugs
* Fix combination of embedding structure and omitempty option ( #442 )
# v0.10.1 - 2023/03/13
### Fix bugs
* Fix checkptr error for array decoder ( #415 )
* Fix added buffer size check when decoding key ( #430 )
* Fix handling of anonymous fields other than struct ( #431 )
* Fix to not optimize when lower conversion can't handle byte-by-byte ( #432 )
* Fix a problem that MarshalIndent does not work when UnorderedMap is specified ( #435 )
* Fix mapDecoder.DecodeStream() for empty objects containing whitespace ( #425 )
* Fix an issue that could not set the correct NextField for fields in the embedded structure ( #438 )
# v0.10.0 - 2022/11/29
### New features
* Support JSON Path ( #250 )
### Fix bugs
* Fix marshaler for map's key ( #409 )
# v0.9.11 - 2022/08/18
### Fix bugs
* Fix unexpected behavior when buffer ends with backslash ( #383 )
* Fix stream decoding of escaped character ( #387 )
# v0.9.10 - 2022/07/15
### Fix bugs
* Fix boundary exception of type caching ( #382 )
# v0.9.9 - 2022/07/15
### Fix bugs
* Fix encoding of directed interface with typed nil ( #377 )
* Fix embedded primitive type encoding using alias ( #378 )
* Fix slice/array type encoding with types implementing MarshalJSON ( #379 )
* Fix unicode decoding when the expected buffer state is not met after reading ( #380 )
# v0.9.8 - 2022/06/30
### Fix bugs
* Fix decoding of surrogate-pair ( #365 )
* Fix handling of embedded primitive type ( #366 )
* Add validation of escape sequence for decoder ( #367 )
* Fix stream tokenizing respecting UseNumber ( #369 )
* Fix encoding when struct pointer type that implements Marshal JSON is embedded ( #375 )
### Improve performance
* Improve performance of linkRecursiveCode ( #368 )
# v0.9.7 - 2022/04/22
### Fix bugs
#### Encoder
* Add filtering process for encoding on slow path ( #355 )
* Fix encoding of interface{} with pointer type ( #363 )
#### Decoder
* Fix map key decoder that implements UnmarshalJSON ( #353 )
* Fix decoding of []uint8 type ( #361 )
### New features
* Add DebugWith option for encoder ( #356 )
# v0.9.6 - 2022/03/22
### Fix bugs
* Correct the handling of the minimum value of int type for decoder ( #344 )
* Fix bugs of stream decoder's bufferSize ( #349 )
* Add a guard to use typeptr more safely ( #351 )
### Improve decoder performance
* Improve escapeString's performance ( #345 )
### Others
* Update go version for CI ( #347 )
# v0.9.5 - 2022/03/04
### Fix bugs
* Fix panic when decoding time.Time with context ( #328 )
* Fix reading the next character in buffer to nul consideration ( #338 )
* Fix incorrect handling on skipValue ( #341 )
### Improve decoder performance
* Improve performance when a payload contains escape sequence ( #334 )
# v0.9.4 - 2022/01/21
* Fix IsNilForMarshaler for string type with omitempty ( #323 )
* Fix the case where the embedded field is at the end ( #326 )
# v0.9.3 - 2022/01/14
* Fix logic of removing struct field for decoder ( #322 )
# v0.9.2 - 2022/01/14
* Add invalid decoder to delay type error judgment at decode ( #321 )
# v0.9.1 - 2022/01/11
* Fix encoding of MarshalText/MarshalJSON operation with head offset ( #319 )
# v0.9.0 - 2022/01/05
### New feature
* Supports dynamic filtering of struct fields ( #314 )
### Improve encoding performance
* Improve map encoding performance ( #310 )
* Optimize encoding path for escaped string ( #311 )
* Add encoding option for performance ( #312 )
### Fix bugs
* Fix panic at encoding map value on 1.18 ( #310 )
* Fix MarshalIndent for interface type ( #317 )
# v0.8.1 - 2021/12/05
* Fix operation conversion from PtrHead to Head in Recursive type ( #305 )
# v0.8.0 - 2021/12/02
* Fix embedded field conflict behavior ( #300 )
* Refactor compiler for encoder ( #301 #302 )
# v0.7.10 - 2021/10/16
* Fix conversion from pointer to uint64 ( #294 )
# v0.7.9 - 2021/09/28
* Fix encoding of nil value about interface type that has method ( #291 )
# v0.7.8 - 2021/09/01
* Fix mapassign_faststr for indirect struct type ( #283 )
* Fix encoding of not empty interface type ( #284 )
* Fix encoding of empty struct interface type ( #286 )
# v0.7.7 - 2021/08/25
* Fix invalid utf8 on stream decoder ( #279 )
* Fix buffer length bug on string stream decoder ( #280 )
Thank you @orisano !!
# v0.7.6 - 2021/08/13
* Fix nil slice assignment ( #276 )
* Improve error message ( #277 )
# v0.7.5 - 2021/08/12
* Fix encoding of embedded struct with tags ( #265 )
* Fix encoding of embedded struct that isn't first field ( #272 )
* Fix decoding of binary type with escaped char ( #273 )
# v0.7.4 - 2021/07/06
* Fix encoding of indirect layout structure ( #264 )
# v0.7.3 - 2021/06/29
* Fix encoding of pointer type in empty interface ( #262 )
# v0.7.2 - 2021/06/26
### Fix decoder
* Add decoder for func type to fix decoding of nil function value ( #257 )
* Fix stream decoding of []byte type ( #258 )
### Performance
* Improve decoding performance of map[string]interface{} type ( use `mapassign_faststr` ) ( #256 )
* Improve encoding performance of empty interface type ( remove recursive calling of `vm.Run` ) ( #259 )
### Benchmark
* Add bytedance/sonic as benchmark target ( #254 )
# v0.7.1 - 2021/06/18
### Fix decoder
* Fix error when unmarshal empty array ( #253 )
# v0.7.0 - 2021/06/12
### Support context for MarshalJSON and UnmarshalJSON ( #248 )
* json.MarshalContext(context.Context, interface{}, ...json.EncodeOption) ([]byte, error)
* json.NewEncoder(io.Writer).EncodeContext(context.Context, interface{}, ...json.EncodeOption) error
* json.UnmarshalContext(context.Context, []byte, interface{}, ...json.DecodeOption) error
* json.NewDecoder(io.Reader).DecodeContext(context.Context, interface{}) error
```go
type MarshalerContext interface {
MarshalJSON(context.Context) ([]byte, error)
}
type UnmarshalerContext interface {
UnmarshalJSON(context.Context, []byte) error
}
```
### Add DecodeFieldPriorityFirstWin option ( #242 )
In the default behavior, go-json, like encoding/json, will reflect the result of the last evaluation when a field with the same name exists. I've added new options to allow you to change this behavior. `json.DecodeFieldPriorityFirstWin` option reflects the result of the first evaluation if a field with the same name exists. This behavior has a performance advantage as it allows the subsequent strings to be skipped if all fields have been evaluated.
### Fix encoder
* Fix indent number contains recursive type ( #249 )
* Fix encoding of using empty interface as map key ( #244 )
### Fix decoder
* Fix decoding fields containing escaped characters ( #237 )
### Refactor
* Move some tests to subdirectory ( #243 )
* Refactor package layout for decoder ( #238 )
# v0.6.1 - 2021/06/02
### Fix encoder
* Fix value of totalLength for encoding ( #236 )
# v0.6.0 - 2021/06/01
### Support Colorize option for encoding (#233)
```go
b, err := json.MarshalWithOption(v, json.Colorize(json.DefaultColorScheme))
if err != nil {
...
}
fmt.Println(string(b)) // print colored json
```
### Refactor
* Fix opcode layout - Adjust memory layout of the opcode to 128 bytes in a 64-bit environment ( #230 )
* Refactor encode option ( #231 )
* Refactor escape string ( #232 )
# v0.5.1 - 2021/5/20
### Optimization
* Add type addrShift to enable bigger encoder/decoder cache ( #213 )
### Fix decoder
* Keep original reference of slice element ( #229 )
### Refactor
* Refactor Debug mode for encoding ( #226 )
* Generate VM sources for encoding ( #227 )
* Refactor validator for null/true/false for decoding ( #221 )
# v0.5.0 - 2021/5/9
### Supports using omitempty and string tags at the same time ( #216 )
### Fix decoder
* Fix stream decoder for unicode char ( #215 )
* Fix decoding of slice element ( #219 )
* Fix calculating of buffer length for stream decoder ( #220 )
### Refactor
* replace skipWhiteSpace goto by loop ( #212 )
# v0.4.14 - 2021/5/4
### Benchmark
* Add valyala/fastjson to benchmark ( #193 )
* Add benchmark task for CI ( #211 )
### Fix decoder
* Fix decoding of slice with unmarshal json type ( #198 )
* Fix decoding of null value for interface type that does not implement Unmarshaler ( #205 )
* Fix decoding of null value to []byte by json.Unmarshal ( #206 )
* Fix decoding of backslash char at the end of string ( #207 )
* Fix stream decoder for null/true/false value ( #208 )
* Fix stream decoder for slow reader ( #211 )
### Performance
* If cap of slice is enough, reuse slice data for compatibility with encoding/json ( #200 )
# v0.4.13 - 2021/4/20
### Fix json.Compact and json.Indent
* Support validation the input buffer for json.Compact and json.Indent ( #189 )
* Optimize json.Compact and json.Indent ( improve memory footprint ) ( #190 )
# v0.4.12 - 2021/4/15
### Fix encoder
* Fix unnecessary indent for empty slice type ( #181 )
* Fix encoding of omitempty feature for the slice or interface type ( #183 )
* Fix encoding custom types zero values with omitempty when marshaller exists ( #187 )
### Fix decoder
* Fix decoder for invalid top level value ( #184 )
* Fix decoder for invalid number value ( #185 )
# v0.4.11 - 2021/4/3
* Improve decoder performance for interface type
# v0.4.10 - 2021/4/2
### Fix encoder
* Fixed a bug when encoding slice and map containing recursive structures
* Fixed a logic to determine if indirect reference
# v0.4.9 - 2021/3/29
### Add debug mode
If you use `json.MarshalWithOption(v, json.Debug())` and `panic` occurred in `go-json`, produces debug information to console.
### Support a new feature to compatible with encoding/json
- invalid UTF-8 is coerced to valid UTF-8 ( without performance down )
### Fix encoder
- Fixed handling of MarshalJSON of function type
### Fix decoding of slice of pointer type
If there is a pointer value, go-json will use it. (This behavior is necessary to achieve the ability to prioritize pre-filled values). However, since slices are reused internally, there was a bug that referred to the previous pointer value. Therefore, it is not necessary to refer to the pointer value in advance for the slice element, so we explicitly initialize slice element by `nil`.
# v0.4.8 - 2021/3/21
### Reduce memory usage at compile time
* go-json have used about 2GB of memory at compile time, but now it can compile with about less than 550MB.
### Fix any encoder's bug
* Add many test cases for encoder
* Fix composite type ( slice/array/map )
* Fix pointer types
* Fix encoding of MarshalJSON or MarshalText or json.Number type
### Refactor encoder
* Change package layout for reducing memory usage at compile
* Remove anonymous and only operation
* Remove root property from encodeCompileContext and opcode
### Fix CI
* Add Go 1.16
* Remove Go 1.13
* Fix `make cover` task
### Number/Delim/Token/RawMessage use the types defined in encoding/json by type alias
# v0.4.7 - 2021/02/22
### Fix decoder
* Fix decoding of deep recursive structure
* Fix decoding of embedded unexported pointer field
* Fix invalid test case
* Fix decoding of invalid value
* Fix decoding of prefilled value
* Fix not being able to return UnmarshalTypeError when it should be returned
* Fix decoding of null value
* Fix decoding of type of null string
* Use pre allocated pointer if exists it at decoding
### Reduce memory usage at compile
* Integrate int/int8/int16/int32/int64 and uint/uint8/uint16/uint32/uint64 operation to reduce memory usage at compile
### Remove unnecessary optype

View File

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

39
vendor/github.com/goccy/go-json/Makefile generated vendored Normal file
View File

@ -0,0 +1,39 @@
PKG := github.com/goccy/go-json
BIN_DIR := $(CURDIR)/bin
PKGS := $(shell go list ./... | grep -v internal/cmd|grep -v test)
COVER_PKGS := $(foreach pkg,$(PKGS),$(subst $(PKG),.,$(pkg)))
COMMA := ,
EMPTY :=
SPACE := $(EMPTY) $(EMPTY)
COVERPKG_OPT := $(subst $(SPACE),$(COMMA),$(COVER_PKGS))
$(BIN_DIR):
@mkdir -p $(BIN_DIR)
.PHONY: cover
cover:
go test -coverpkg=$(COVERPKG_OPT) -coverprofile=cover.out ./...
.PHONY: cover-html
cover-html: cover
go tool cover -html=cover.out
.PHONY: lint
lint: golangci-lint
$(BIN_DIR)/golangci-lint run
golangci-lint: | $(BIN_DIR)
@{ \
set -e; \
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.54.2; \
rm -rf $$GOLANGCI_LINT_TMP_DIR; \
}
.PHONY: generate
generate:
go generate ./internal/...

529
vendor/github.com/goccy/go-json/README.md generated vendored Normal file
View File

@ -0,0 +1,529 @@
# go-json
![Go](https://github.com/goccy/go-json/workflows/Go/badge.svg)
[![GoDoc](https://godoc.org/github.com/goccy/go-json?status.svg)](https://pkg.go.dev/github.com/goccy/go-json?tab=doc)
[![codecov](https://codecov.io/gh/goccy/go-json/branch/master/graph/badge.svg)](https://codecov.io/gh/goccy/go-json)
Fast JSON encoder/decoder compatible with encoding/json for Go
<img width="400px" src="https://user-images.githubusercontent.com/209884/92572337-42b42900-f2bf-11ea-973a-c74a359553a5.png"></img>
# Roadmap
```
* version ( expected release date )
* v0.9.0
|
| while maintaining compatibility with encoding/json, we will add convenient APIs
|
v
* v1.0.0
```
We are accepting requests for features that will be implemented between v0.9.0 and v.1.0.0.
If you have the API you need, please submit your issue [here](https://github.com/goccy/go-json/issues).
# Features
- Drop-in replacement of `encoding/json`
- Fast ( See [Benchmark section](https://github.com/goccy/go-json#benchmarks) )
- Flexible customization with options
- Coloring the encoded string
- Can propagate context.Context to `MarshalJSON` or `UnmarshalJSON`
- Can dynamically filter the fields of the structure type-safely
# Installation
```
go get github.com/goccy/go-json
```
# How to use
Replace import statement from `encoding/json` to `github.com/goccy/go-json`
```
-import "encoding/json"
+import "github.com/goccy/go-json"
```
# JSON library comparison
| name | encoder | decoder | compatible with `encoding/json` |
| :----: | :------: | :-----: | :-----------------------------: |
| encoding/json | yes | yes | N/A |
| [json-iterator/go](https://github.com/json-iterator/go) | yes | yes | partial |
| [easyjson](https://github.com/mailru/easyjson) | yes | yes | no |
| [gojay](https://github.com/francoispqt/gojay) | yes | yes | no |
| [segmentio/encoding/json](https://github.com/segmentio/encoding/tree/master/json) | yes | yes | partial |
| [jettison](https://github.com/wI2L/jettison) | yes | no | no |
| [simdjson-go](https://github.com/minio/simdjson-go) | no | yes | no |
| goccy/go-json | yes | yes | yes |
- `json-iterator/go` isn't compatible with `encoding/json` in many ways (e.g. https://github.com/json-iterator/go/issues/229 ), but it hasn't been supported for a long time.
- `segmentio/encoding/json` is well supported for encoders, but some are not supported for decoder APIs such as `Token` ( streaming decode )
## Other libraries
- [jingo](https://github.com/bet365/jingo)
I tried the benchmark but it didn't work.
Also, it seems to panic when it receives an unexpected value because there is no error handling...
- [ffjson](https://github.com/pquerna/ffjson)
Benchmarking gave very slow results.
It seems that it is assumed that the user will use the buffer pool properly.
Also, development seems to have already stopped
# Benchmarks
```
$ cd benchmarks
$ go test -bench .
```
## Encode
<img width="700px" src="https://user-images.githubusercontent.com/209884/107126758-0845cb00-68f5-11eb-8db7-086fcf9bcfaa.png"></img>
<img width="700px" src="https://user-images.githubusercontent.com/209884/107126757-07ad3480-68f5-11eb-87aa-858cc5eacfcb.png"></img>
## Decode
<img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979944-bd1d6d80-7002-11eb-944b-9d17b6674e3f.png">
<img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979931-b989e680-7002-11eb-87a0-66fc22d90dd4.png">
<img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979940-bc84d700-7002-11eb-9647-869bbc25c9d9.png">
# Fuzzing
[go-json-fuzz](https://github.com/goccy/go-json-fuzz) is the repository for fuzzing tests.
If you run the test in this repository and find a bug, please commit to corpus to go-json-fuzz and report the issue to [go-json](https://github.com/goccy/go-json/issues).
# How it works
`go-json` is very fast in both encoding and decoding compared to other libraries.
It's easier to implement by using automatic code generation for performance or by using a dedicated interface, but `go-json` dares to stick to compatibility with `encoding/json` and is the simple interface. Despite this, we are developing with the aim of being the fastest library.
Here, we explain the various speed-up techniques implemented by `go-json`.
## Basic technique
The techniques listed here are the ones used by most of the libraries listed above.
### Buffer reuse
Since the only value required for the result of `json.Marshal(interface{}) ([]byte, error)` is `[]byte`, the only value that must be allocated during encoding is the return value `[]byte` .
Also, as the number of allocations increases, the performance will be affected, so the number of allocations should be kept as low as possible when creating `[]byte`.
Therefore, there is a technique to reduce the number of times a new buffer must be allocated by reusing the buffer used for the previous encoding by using `sync.Pool`.
Finally, you allocate a buffer that is as long as the resulting buffer and copy the contents into it, you only need to allocate the buffer once in theory.
```go
type buffer struct {
data []byte
}
var bufPool = sync.Pool{
New: func() interface{} {
return &buffer{data: make([]byte, 0, 1024)}
},
}
buf := bufPool.Get().(*buffer)
data := encode(buf.data) // reuse buf.data
newBuf := make([]byte, len(data))
copy(newBuf, buf)
buf.data = data
bufPool.Put(buf)
```
### Elimination of reflection
As you know, the reflection operation is very slow.
Therefore, using the fact that the address position where the type information is stored is fixed for each binary ( we call this `typeptr` ),
we can use the address in the type information to call a pre-built optimized process.
For example, you can get the address to the type information from `interface{}` as follows and you can use that information to call a process that does not have reflection.
To process without reflection, pass a pointer (`unsafe.Pointer`) to the value is stored.
```go
type emptyInterface struct {
typ unsafe.Pointer
ptr unsafe.Pointer
}
var typeToEncoder = map[uintptr]func(unsafe.Pointer)([]byte, error){}
func Marshal(v interface{}) ([]byte, error) {
iface := (*emptyInterface)(unsafe.Pointer(&v)
typeptr := uintptr(iface.typ)
if enc, exists := typeToEncoder[typeptr]; exists {
return enc(iface.ptr)
}
...
}
```
※ In reality, `typeToEncoder` can be referenced by multiple goroutines, so exclusive control is required.
## Unique speed-up technique
## Encoder
### Do not escape arguments of `Marshal`
`json.Marshal` and `json.Unmarshal` receive `interface{}` value and they perform type determination dynamically to process.
In normal case, you need to use the `reflect` library to determine the type dynamically, but since `reflect.Type` is defined as `interface`, when you call the method of `reflect.Type`, The reflect's argument is escaped.
Therefore, the arguments for `Marshal` and `Unmarshal` are always escaped to the heap.
However, `go-json` can use the feature of `reflect.Type` while avoiding escaping.
`reflect.Type` is defined as `interface`, but in reality `reflect.Type` is implemented only by the structure `rtype` defined in the `reflect` package.
For this reason, to date `reflect.Type` is the same as `*reflect.rtype`.
Therefore, by directly handling `*reflect.rtype`, which is an implementation of `reflect.Type`, it is possible to avoid escaping because it changes from `interface` to using `struct`.
The technique for working with `*reflect.rtype` directly from `go-json` is implemented at [rtype.go](https://github.com/goccy/go-json/blob/master/internal/runtime/rtype.go)
Also, the same technique is cut out as a library ( https://github.com/goccy/go-reflect )
Initially this feature was the default behavior of `go-json`.
But after careful testing, I found that I passed a large value to `json.Marshal()` and if the argument could not be assigned to the stack, it could not be properly escaped to the heap (a bug in the Go compiler).
Therefore, this feature will be provided as an **optional** until this issue is resolved.
To use it, add `NoEscape` like `MarshalNoEscape()`
### Encoding using opcode sequence
I explained that you can use `typeptr` to call a pre-built process from type information.
In other libraries, this dedicated process is processed by making it an function calling like anonymous function, but function calls are inherently slow processes and should be avoided as much as possible.
Therefore, `go-json` adopted the Instruction-based execution processing system, which is also used to implement virtual machines for programming language.
If it is the first type to encode, create the opcode ( instruction ) sequence required for encoding.
From the second time onward, use `typeptr` to get the cached pre-built opcode sequence and encode it based on it. An example of the opcode sequence is shown below.
```go
json.Marshal(struct{
X int `json:"x"`
Y string `json:"y"`
}{X: 1, Y: "hello"})
```
When encoding a structure like the one above, create a sequence of opcodes like this:
```
- opStructFieldHead ( `{` )
- opStructFieldInt ( `"x": 1,` )
- opStructFieldString ( `"y": "hello"` )
- opStructEnd ( `}` )
- opEnd
```
※ When processing each operation, write the letters on the right.
In addition, each opcode is managed by the following structure (
Pseudo code ).
```go
type opType int
const (
opStructFieldHead opType = iota
opStructFieldInt
opStructFieldStirng
opStructEnd
opEnd
)
type opcode struct {
op opType
key []byte
next *opcode
}
```
The process of encoding using the opcode sequence is roughly implemented as follows.
```go
func encode(code *opcode, b []byte, p unsafe.Pointer) ([]byte, error) {
for {
switch code.op {
case opStructFieldHead:
b = append(b, '{')
code = code.next
case opStructFieldInt:
b = append(b, code.key...)
b = appendInt((*int)(unsafe.Pointer(uintptr(p)+code.offset)))
code = code.next
case opStructFieldString:
b = append(b, code.key...)
b = appendString((*string)(unsafe.Pointer(uintptr(p)+code.offset)))
code = code.next
case opStructEnd:
b = append(b, '}')
code = code.next
case opEnd:
goto END
}
}
END:
return b, nil
}
```
In this way, the huge `switch-case` is used to encode by manipulating the linked list opcodes to avoid unnecessary function calls.
### Opcode sequence optimization
One of the advantages of encoding using the opcode sequence is the ease of optimization.
The opcode sequence mentioned above is actually converted into the following optimized operations and used.
```
- opStructFieldHeadInt ( `{"x": 1,` )
- opStructEndString ( `"y": "hello"}` )
- opEnd
```
It has been reduced from 5 opcodes to 3 opcodes !
Reducing the number of opcodees means reducing the number of branches with `switch-case`.
In other words, the closer the number of operations is to 1, the faster the processing can be performed.
In `go-json`, optimization to reduce the number of opcodes itself like the above and it speeds up by preparing opcodes with optimized paths.
### Change recursive call from CALL to JMP
Recursive processing is required during encoding if the type is defined recursively as follows:
```go
type T struct {
X int
U *U
}
type U struct {
T *T
}
b, err := json.Marshal(&T{
X: 1,
U: &U{
T: &T{
X: 2,
},
},
})
fmt.Println(string(b)) // {"X":1,"U":{"T":{"X":2,"U":null}}}
```
In `go-json`, recursive processing is processed by the operation type of ` opStructFieldRecursive`.
In this operation, after acquiring the opcode sequence used for recursive processing, the function is **not** called recursively as it is, but the necessary values are saved by itself and implemented by moving to the next operation.
The technique of implementing recursive processing with the `JMP` operation while avoiding the `CALL` operation is a famous technique for implementing a high-speed virtual machine.
For more details, please refer to [the article](https://engineering.mercari.com/blog/entry/1599563768-081104c850) ( but Japanese only ).
### Dispatch by typeptr from map to slice
When retrieving the data cached from the type information by `typeptr`, we usually use map.
Map requires exclusive control, so use `sync.Map` for a naive implementation.
However, this is slow, so it's a good idea to use the `atomic` package for exclusive control as implemented by `segmentio/encoding/json` ( https://github.com/segmentio/encoding/blob/master/json/codec.go#L41-L55 ).
This implementation slows down the set instead of speeding up the get, but it works well because of the nature of the library, it encodes much more for the same type.
However, as a result of profiling, I noticed that `runtime.mapaccess2` accounts for a significant percentage of the execution time. So I thought if I could change the lookup from map to slice.
There is an API named `typelinks` defined in the `runtime` package that the `reflect` package uses internally.
This allows you to get all the type information defined in the binary at runtime.
The fact that all type information can be acquired means that by constructing slices in advance with the acquired total number of type information, it is possible to look up with the value of `typeptr` without worrying about out-of-range access.
However, if there is too much type information, it will use a lot of memory, so by default we will only use this optimization if the slice size fits within **2Mib** .
If this approach is not available, it will fall back to the `atomic` based process described above.
If you want to know more, please refer to the implementation [here](https://github.com/goccy/go-json/blob/master/internal/runtime/type.go#L36-L100)
## Decoder
### Dispatch by typeptr from map to slice
Like the encoder, the decoder also uses typeptr to call the dedicated process.
### Faster termination character inspection using NUL character
In order to decode, you have to traverse the input buffer character by position.
At that time, if you check whether the buffer has reached the end, it will be very slow.
`buf` : `[]byte` type variable. holds the string passed to the decoder
`cursor` : `int64` type variable. holds the current read position
```go
buflen := len(buf)
for ; cursor < buflen; cursor++ { // compare cursor and buflen at all times, it is so slow.
switch buf[cursor] {
case ' ', '\n', '\r', '\t':
}
}
```
Therefore, by adding the `NUL` (`\000`) character to the end of the read buffer as shown below, it is possible to check the termination character at the same time as other characters.
```go
for {
switch buf[cursor] {
case ' ', '\n', '\r', '\t':
case '\000':
return nil
}
cursor++
}
```
### Use Boundary Check Elimination
Due to the `NUL` character optimization, the Go compiler does a boundary check every time, even though `buf[cursor]` does not cause out-of-range access.
Therefore, `go-json` eliminates boundary check by fetching characters for hotspot by pointer operation. For example, the following code.
```go
func char(ptr unsafe.Pointer, offset int64) byte {
return *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(offset)))
}
p := (*sliceHeader)(&unsafe.Pointer(buf)).data
for {
switch char(p, cursor) {
case ' ', '\n', '\r', '\t':
case '\000':
return nil
}
cursor++
}
```
### Checking the existence of fields of struct using Bitmaps
I found by the profiling result, in the struct decode, lookup process for field was taking a long time.
For example, consider decoding a string like `{"a":1,"b":2,"c":3}` into the following structure:
```go
type T struct {
A int `json:"a"`
B int `json:"b"`
C int `json:"c"`
}
```
At this time, it was found that it takes a lot of time to acquire the decoding process corresponding to the field from the field name as shown below during the decoding process.
```go
fieldName := decodeKey(buf, cursor) // "a" or "b" or "c"
decoder, exists := fieldToDecoderMap[fieldName] // so slow
if exists {
decoder(buf, cursor)
} else {
skipValue(buf, cursor)
}
```
To improve this process, `json-iterator/go` is optimized so that it can be branched by switch-case when the number of fields in the structure is 10 or less (switch-case is faster than map). However, there is a risk of hash collision because the value hashed by the FNV algorithm is used for conditional branching. Also, `gojay` processes this part at high speed by letting the library user yourself write `switch-case`.
`go-json` considers and implements a new approach that is different from these. I call this **bitmap field optimization**.
The range of values per character can be represented by `[256]byte`. Also, if the number of fields in the structure is 8 or less, `int8` type can represent the state of each field.
In other words, it has the following structure.
- Base ( 8bit ): `00000000`
- Key "a": `00000001` ( assign key "a" to the first bit )
- Key "b": `00000010` ( assign key "b" to the second bit )
- Key "c": `00000100` ( assign key "c" to the third bit )
Bitmap structure is the following
```
| key index(0) |
------------------------
0 | 00000000 |
1 | 00000000 |
~~ | |
97 (a) | 00000001 |
98 (b) | 00000010 |
99 (c) | 00000100 |
~~ | |
255 | 00000000 |
```
You can think of this as a Bitmap with a height of `256` and a width of the maximum string length in the field name.
In other words, it can be represented by the following type .
```go
[maxFieldKeyLength][256]int8
```
When decoding a field character, check whether the corresponding character exists by referring to the pre-built bitmap like the following.
```go
var curBit int8 = math.MaxInt8 // 11111111
c := char(buf, cursor)
bit := bitmap[keyIdx][c]
curBit &= bit
if curBit == 0 {
// not found field
}
```
If `curBit` is not `0` until the end of the field string, then the string is
You may have hit one of the fields.
But the possibility is that if the decoded string is shorter than the field string, you will get a false hit.
- input: `{"a":1}`
```go
type T struct {
X int `json:"abc"`
}
```
※ Since `a` is shorter than `abc`, it can decode to the end of the field character without `curBit` being 0.
Rest assured. In this case, it doesn't matter because you can tell if you hit by comparing the string length of `a` with the string length of `abc`.
Finally, calculate the position of the bit where `1` is set and get the corresponding value, and you're done.
Using this technique, field lookups are possible with only bitwise operations and access to slices.
`go-json` uses a similar technique for fields with 9 or more and 16 or less fields. At this time, Bitmap is constructed as `[maxKeyLen][256]int16` type.
Currently, this optimization is not performed when the maximum length of the field name is long (specifically, 64 bytes or more) in addition to the limitation of the number of fields from the viewpoint of saving memory usage.
### Others
I have done a lot of other optimizations. I will find time to write about them. If you have any questions about what's written here or other optimizations, please visit the `#go-json` channel on `gophers.slack.com` .
## Reference
Regarding the story of go-json, there are the following articles in Japanese only.
- https://speakerdeck.com/goccy/zui-su-falsejsonraiburariwoqiu-mete
- https://engineering.mercari.com/blog/entry/1599563768-081104c850/
# Looking for Sponsors
I'm looking for sponsors this library. This library is being developed as a personal project in my spare time. If you want a quick response or problem resolution when using this library in your project, please register as a [sponsor](https://github.com/sponsors/goccy). I will cooperate as much as possible. Of course, this library is developed as an MIT license, so you can use it freely for free.
# License
MIT

68
vendor/github.com/goccy/go-json/color.go generated vendored Normal file
View File

@ -0,0 +1,68 @@
package json
import (
"fmt"
"github.com/goccy/go-json/internal/encoder"
)
type (
ColorFormat = encoder.ColorFormat
ColorScheme = encoder.ColorScheme
)
const escape = "\x1b"
type colorAttr int
//nolint:deadcode,varcheck
const (
fgBlackColor colorAttr = iota + 30
fgRedColor
fgGreenColor
fgYellowColor
fgBlueColor
fgMagentaColor
fgCyanColor
fgWhiteColor
)
//nolint:deadcode,varcheck
const (
fgHiBlackColor colorAttr = iota + 90
fgHiRedColor
fgHiGreenColor
fgHiYellowColor
fgHiBlueColor
fgHiMagentaColor
fgHiCyanColor
fgHiWhiteColor
)
func createColorFormat(attr colorAttr) ColorFormat {
return ColorFormat{
Header: wrapColor(attr),
Footer: resetColor(),
}
}
func wrapColor(attr colorAttr) string {
return fmt.Sprintf("%s[%dm", escape, attr)
}
func resetColor() string {
return wrapColor(colorAttr(0))
}
var (
DefaultColorScheme = &ColorScheme{
Int: createColorFormat(fgHiMagentaColor),
Uint: createColorFormat(fgHiMagentaColor),
Float: createColorFormat(fgHiMagentaColor),
Bool: createColorFormat(fgHiYellowColor),
String: createColorFormat(fgHiGreenColor),
Binary: createColorFormat(fgHiRedColor),
ObjectKey: createColorFormat(fgHiCyanColor),
Null: createColorFormat(fgBlueColor),
}
)

263
vendor/github.com/goccy/go-json/decode.go generated vendored Normal file
View File

@ -0,0 +1,263 @@
package json
import (
"context"
"fmt"
"io"
"reflect"
"unsafe"
"github.com/goccy/go-json/internal/decoder"
"github.com/goccy/go-json/internal/errors"
"github.com/goccy/go-json/internal/runtime"
)
type Decoder struct {
s *decoder.Stream
}
const (
nul = '\000'
)
type emptyInterface struct {
typ *runtime.Type
ptr unsafe.Pointer
}
func unmarshal(data []byte, v interface{}, optFuncs ...DecodeOptionFunc) error {
src := make([]byte, len(data)+1) // append nul byte to the end
copy(src, data)
header := (*emptyInterface)(unsafe.Pointer(&v))
if err := validateType(header.typ, uintptr(header.ptr)); err != nil {
return err
}
dec, err := decoder.CompileToGetDecoder(header.typ)
if err != nil {
return err
}
ctx := decoder.TakeRuntimeContext()
ctx.Buf = src
ctx.Option.Flags = 0
for _, optFunc := range optFuncs {
optFunc(ctx.Option)
}
cursor, err := dec.Decode(ctx, 0, 0, header.ptr)
if err != nil {
decoder.ReleaseRuntimeContext(ctx)
return err
}
decoder.ReleaseRuntimeContext(ctx)
return validateEndBuf(src, cursor)
}
func unmarshalContext(ctx context.Context, data []byte, v interface{}, optFuncs ...DecodeOptionFunc) error {
src := make([]byte, len(data)+1) // append nul byte to the end
copy(src, data)
header := (*emptyInterface)(unsafe.Pointer(&v))
if err := validateType(header.typ, uintptr(header.ptr)); err != nil {
return err
}
dec, err := decoder.CompileToGetDecoder(header.typ)
if err != nil {
return err
}
rctx := decoder.TakeRuntimeContext()
rctx.Buf = src
rctx.Option.Flags = 0
rctx.Option.Flags |= decoder.ContextOption
rctx.Option.Context = ctx
for _, optFunc := range optFuncs {
optFunc(rctx.Option)
}
cursor, err := dec.Decode(rctx, 0, 0, header.ptr)
if err != nil {
decoder.ReleaseRuntimeContext(rctx)
return err
}
decoder.ReleaseRuntimeContext(rctx)
return validateEndBuf(src, cursor)
}
var (
pathDecoder = decoder.NewPathDecoder()
)
func extractFromPath(path *Path, data []byte, optFuncs ...DecodeOptionFunc) ([][]byte, error) {
if path.path.RootSelectorOnly {
return [][]byte{data}, nil
}
src := make([]byte, len(data)+1) // append nul byte to the end
copy(src, data)
ctx := decoder.TakeRuntimeContext()
ctx.Buf = src
ctx.Option.Flags = 0
ctx.Option.Flags |= decoder.PathOption
ctx.Option.Path = path.path
for _, optFunc := range optFuncs {
optFunc(ctx.Option)
}
paths, cursor, err := pathDecoder.DecodePath(ctx, 0, 0)
if err != nil {
decoder.ReleaseRuntimeContext(ctx)
return nil, err
}
decoder.ReleaseRuntimeContext(ctx)
if err := validateEndBuf(src, cursor); err != nil {
return nil, err
}
return paths, nil
}
func unmarshalNoEscape(data []byte, v interface{}, optFuncs ...DecodeOptionFunc) error {
src := make([]byte, len(data)+1) // append nul byte to the end
copy(src, data)
header := (*emptyInterface)(unsafe.Pointer(&v))
if err := validateType(header.typ, uintptr(header.ptr)); err != nil {
return err
}
dec, err := decoder.CompileToGetDecoder(header.typ)
if err != nil {
return err
}
ctx := decoder.TakeRuntimeContext()
ctx.Buf = src
ctx.Option.Flags = 0
for _, optFunc := range optFuncs {
optFunc(ctx.Option)
}
cursor, err := dec.Decode(ctx, 0, 0, noescape(header.ptr))
if err != nil {
decoder.ReleaseRuntimeContext(ctx)
return err
}
decoder.ReleaseRuntimeContext(ctx)
return validateEndBuf(src, cursor)
}
func validateEndBuf(src []byte, cursor int64) error {
for {
switch src[cursor] {
case ' ', '\t', '\n', '\r':
cursor++
continue
case nul:
return nil
}
return errors.ErrSyntax(
fmt.Sprintf("invalid character '%c' after top-level value", src[cursor]),
cursor+1,
)
}
}
//nolint:staticcheck
//go:nosplit
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}
func validateType(typ *runtime.Type, p uintptr) error {
if typ == nil || typ.Kind() != reflect.Ptr || p == 0 {
return &InvalidUnmarshalError{Type: runtime.RType2Type(typ)}
}
return nil
}
// NewDecoder returns a new decoder that reads from r.
//
// The decoder introduces its own buffering and may
// read data from r beyond the JSON values requested.
func NewDecoder(r io.Reader) *Decoder {
s := decoder.NewStream(r)
return &Decoder{
s: s,
}
}
// Buffered returns a reader of the data remaining in the Decoder's
// buffer. The reader is valid until the next call to Decode.
func (d *Decoder) Buffered() io.Reader {
return d.s.Buffered()
}
// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
//
// See the documentation for Unmarshal for details about
// the conversion of JSON into a Go value.
func (d *Decoder) Decode(v interface{}) error {
return d.DecodeWithOption(v)
}
// DecodeContext reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v with context.Context.
func (d *Decoder) DecodeContext(ctx context.Context, v interface{}) error {
d.s.Option.Flags |= decoder.ContextOption
d.s.Option.Context = ctx
return d.DecodeWithOption(v)
}
func (d *Decoder) DecodeWithOption(v interface{}, optFuncs ...DecodeOptionFunc) error {
header := (*emptyInterface)(unsafe.Pointer(&v))
typ := header.typ
ptr := uintptr(header.ptr)
typeptr := uintptr(unsafe.Pointer(typ))
// noescape trick for header.typ ( reflect.*rtype )
copiedType := *(**runtime.Type)(unsafe.Pointer(&typeptr))
if err := validateType(copiedType, ptr); err != nil {
return err
}
dec, err := decoder.CompileToGetDecoder(typ)
if err != nil {
return err
}
if err := d.s.PrepareForDecode(); err != nil {
return err
}
s := d.s
for _, optFunc := range optFuncs {
optFunc(s.Option)
}
if err := dec.DecodeStream(s, 0, header.ptr); err != nil {
return err
}
s.Reset()
return nil
}
func (d *Decoder) More() bool {
return d.s.More()
}
func (d *Decoder) Token() (Token, error) {
return d.s.Token()
}
// DisallowUnknownFields causes the Decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
func (d *Decoder) DisallowUnknownFields() {
d.s.DisallowUnknownFields = true
}
func (d *Decoder) InputOffset() int64 {
return d.s.TotalOffset()
}
// UseNumber causes the Decoder to unmarshal a number into an interface{} as a
// Number instead of as a float64.
func (d *Decoder) UseNumber() {
d.s.UseNumber = true
}

13
vendor/github.com/goccy/go-json/docker-compose.yml generated vendored Normal file
View File

@ -0,0 +1,13 @@
version: '2'
services:
go-json:
image: golang:1.18
volumes:
- '.:/go/src/go-json'
deploy:
resources:
limits:
memory: 620M
working_dir: /go/src/go-json
command: |
sh -c "go test -c . && ls go-json.test"

326
vendor/github.com/goccy/go-json/encode.go generated vendored Normal file
View File

@ -0,0 +1,326 @@
package json
import (
"context"
"io"
"os"
"unsafe"
"github.com/goccy/go-json/internal/encoder"
"github.com/goccy/go-json/internal/encoder/vm"
"github.com/goccy/go-json/internal/encoder/vm_color"
"github.com/goccy/go-json/internal/encoder/vm_color_indent"
"github.com/goccy/go-json/internal/encoder/vm_indent"
)
// An Encoder writes JSON values to an output stream.
type Encoder struct {
w io.Writer
enabledIndent bool
enabledHTMLEscape bool
prefix string
indentStr string
}
// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w: w, enabledHTMLEscape: true}
}
// Encode writes the JSON encoding of v to the stream, followed by a newline character.
//
// See the documentation for Marshal for details about the conversion of Go values to JSON.
func (e *Encoder) Encode(v interface{}) error {
return e.EncodeWithOption(v)
}
// EncodeWithOption call Encode with EncodeOption.
func (e *Encoder) EncodeWithOption(v interface{}, optFuncs ...EncodeOptionFunc) error {
ctx := encoder.TakeRuntimeContext()
ctx.Option.Flag = 0
err := e.encodeWithOption(ctx, v, optFuncs...)
encoder.ReleaseRuntimeContext(ctx)
return err
}
// EncodeContext call Encode with context.Context and EncodeOption.
func (e *Encoder) EncodeContext(ctx context.Context, v interface{}, optFuncs ...EncodeOptionFunc) error {
rctx := encoder.TakeRuntimeContext()
rctx.Option.Flag = 0
rctx.Option.Flag |= encoder.ContextOption
rctx.Option.Context = ctx
err := e.encodeWithOption(rctx, v, optFuncs...) //nolint: contextcheck
encoder.ReleaseRuntimeContext(rctx)
return err
}
func (e *Encoder) encodeWithOption(ctx *encoder.RuntimeContext, v interface{}, optFuncs ...EncodeOptionFunc) error {
if e.enabledHTMLEscape {
ctx.Option.Flag |= encoder.HTMLEscapeOption
}
ctx.Option.Flag |= encoder.NormalizeUTF8Option
ctx.Option.DebugOut = os.Stdout
for _, optFunc := range optFuncs {
optFunc(ctx.Option)
}
var (
buf []byte
err error
)
if e.enabledIndent {
buf, err = encodeIndent(ctx, v, e.prefix, e.indentStr)
} else {
buf, err = encode(ctx, v)
}
if err != nil {
return err
}
if e.enabledIndent {
buf = buf[:len(buf)-2]
} else {
buf = buf[:len(buf)-1]
}
buf = append(buf, '\n')
if _, err := e.w.Write(buf); err != nil {
return err
}
return nil
}
// SetEscapeHTML specifies whether problematic HTML characters should be escaped inside JSON quoted strings.
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e to avoid certain safety problems that can arise when embedding JSON in HTML.
//
// In non-HTML settings where the escaping interferes with the readability of the output, SetEscapeHTML(false) disables this behavior.
func (e *Encoder) SetEscapeHTML(on bool) {
e.enabledHTMLEscape = on
}
// SetIndent instructs the encoder to format each subsequent encoded value as if indented by the package-level function Indent(dst, src, prefix, indent).
// Calling SetIndent("", "") disables indentation.
func (e *Encoder) SetIndent(prefix, indent string) {
if prefix == "" && indent == "" {
e.enabledIndent = false
return
}
e.prefix = prefix
e.indentStr = indent
e.enabledIndent = true
}
func marshalContext(ctx context.Context, v interface{}, optFuncs ...EncodeOptionFunc) ([]byte, error) {
rctx := encoder.TakeRuntimeContext()
rctx.Option.Flag = 0
rctx.Option.Flag = encoder.HTMLEscapeOption | encoder.NormalizeUTF8Option | encoder.ContextOption
rctx.Option.Context = ctx
for _, optFunc := range optFuncs {
optFunc(rctx.Option)
}
buf, err := encode(rctx, v) //nolint: contextcheck
if err != nil {
encoder.ReleaseRuntimeContext(rctx)
return nil, err
}
// this line exists to escape call of `runtime.makeslicecopy` .
// if use `make([]byte, len(buf)-1)` and `copy(copied, buf)`,
// dst buffer size and src buffer size are differrent.
// in this case, compiler uses `runtime.makeslicecopy`, but it is slow.
buf = buf[:len(buf)-1]
copied := make([]byte, len(buf))
copy(copied, buf)
encoder.ReleaseRuntimeContext(rctx)
return copied, nil
}
func marshal(v interface{}, optFuncs ...EncodeOptionFunc) ([]byte, error) {
ctx := encoder.TakeRuntimeContext()
ctx.Option.Flag = 0
ctx.Option.Flag |= (encoder.HTMLEscapeOption | encoder.NormalizeUTF8Option)
for _, optFunc := range optFuncs {
optFunc(ctx.Option)
}
buf, err := encode(ctx, v)
if err != nil {
encoder.ReleaseRuntimeContext(ctx)
return nil, err
}
// this line exists to escape call of `runtime.makeslicecopy` .
// if use `make([]byte, len(buf)-1)` and `copy(copied, buf)`,
// dst buffer size and src buffer size are differrent.
// in this case, compiler uses `runtime.makeslicecopy`, but it is slow.
buf = buf[:len(buf)-1]
copied := make([]byte, len(buf))
copy(copied, buf)
encoder.ReleaseRuntimeContext(ctx)
return copied, nil
}
func marshalNoEscape(v interface{}) ([]byte, error) {
ctx := encoder.TakeRuntimeContext()
ctx.Option.Flag = 0
ctx.Option.Flag |= (encoder.HTMLEscapeOption | encoder.NormalizeUTF8Option)
buf, err := encodeNoEscape(ctx, v)
if err != nil {
encoder.ReleaseRuntimeContext(ctx)
return nil, err
}
// this line exists to escape call of `runtime.makeslicecopy` .
// if use `make([]byte, len(buf)-1)` and `copy(copied, buf)`,
// dst buffer size and src buffer size are differrent.
// in this case, compiler uses `runtime.makeslicecopy`, but it is slow.
buf = buf[:len(buf)-1]
copied := make([]byte, len(buf))
copy(copied, buf)
encoder.ReleaseRuntimeContext(ctx)
return copied, nil
}
func marshalIndent(v interface{}, prefix, indent string, optFuncs ...EncodeOptionFunc) ([]byte, error) {
ctx := encoder.TakeRuntimeContext()
ctx.Option.Flag = 0
ctx.Option.Flag |= (encoder.HTMLEscapeOption | encoder.NormalizeUTF8Option | encoder.IndentOption)
for _, optFunc := range optFuncs {
optFunc(ctx.Option)
}
buf, err := encodeIndent(ctx, v, prefix, indent)
if err != nil {
encoder.ReleaseRuntimeContext(ctx)
return nil, err
}
buf = buf[:len(buf)-2]
copied := make([]byte, len(buf))
copy(copied, buf)
encoder.ReleaseRuntimeContext(ctx)
return copied, nil
}
func encode(ctx *encoder.RuntimeContext, v interface{}) ([]byte, error) {
b := ctx.Buf[:0]
if v == nil {
b = encoder.AppendNull(ctx, b)
b = encoder.AppendComma(ctx, b)
return b, nil
}
header := (*emptyInterface)(unsafe.Pointer(&v))
typ := header.typ
typeptr := uintptr(unsafe.Pointer(typ))
codeSet, err := encoder.CompileToGetCodeSet(ctx, typeptr)
if err != nil {
return nil, err
}
p := uintptr(header.ptr)
ctx.Init(p, codeSet.CodeLength)
ctx.KeepRefs = append(ctx.KeepRefs, header.ptr)
buf, err := encodeRunCode(ctx, b, codeSet)
if err != nil {
return nil, err
}
ctx.Buf = buf
return buf, nil
}
func encodeNoEscape(ctx *encoder.RuntimeContext, v interface{}) ([]byte, error) {
b := ctx.Buf[:0]
if v == nil {
b = encoder.AppendNull(ctx, b)
b = encoder.AppendComma(ctx, b)
return b, nil
}
header := (*emptyInterface)(unsafe.Pointer(&v))
typ := header.typ
typeptr := uintptr(unsafe.Pointer(typ))
codeSet, err := encoder.CompileToGetCodeSet(ctx, typeptr)
if err != nil {
return nil, err
}
p := uintptr(header.ptr)
ctx.Init(p, codeSet.CodeLength)
buf, err := encodeRunCode(ctx, b, codeSet)
if err != nil {
return nil, err
}
ctx.Buf = buf
return buf, nil
}
func encodeIndent(ctx *encoder.RuntimeContext, v interface{}, prefix, indent string) ([]byte, error) {
b := ctx.Buf[:0]
if v == nil {
b = encoder.AppendNull(ctx, b)
b = encoder.AppendCommaIndent(ctx, b)
return b, nil
}
header := (*emptyInterface)(unsafe.Pointer(&v))
typ := header.typ
typeptr := uintptr(unsafe.Pointer(typ))
codeSet, err := encoder.CompileToGetCodeSet(ctx, typeptr)
if err != nil {
return nil, err
}
p := uintptr(header.ptr)
ctx.Init(p, codeSet.CodeLength)
buf, err := encodeRunIndentCode(ctx, b, codeSet, prefix, indent)
ctx.KeepRefs = append(ctx.KeepRefs, header.ptr)
if err != nil {
return nil, err
}
ctx.Buf = buf
return buf, nil
}
func encodeRunCode(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]byte, error) {
if (ctx.Option.Flag & encoder.DebugOption) != 0 {
if (ctx.Option.Flag & encoder.ColorizeOption) != 0 {
return vm_color.DebugRun(ctx, b, codeSet)
}
return vm.DebugRun(ctx, b, codeSet)
}
if (ctx.Option.Flag & encoder.ColorizeOption) != 0 {
return vm_color.Run(ctx, b, codeSet)
}
return vm.Run(ctx, b, codeSet)
}
func encodeRunIndentCode(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet, prefix, indent string) ([]byte, error) {
ctx.Prefix = []byte(prefix)
ctx.IndentStr = []byte(indent)
if (ctx.Option.Flag & encoder.DebugOption) != 0 {
if (ctx.Option.Flag & encoder.ColorizeOption) != 0 {
return vm_color_indent.DebugRun(ctx, b, codeSet)
}
return vm_indent.DebugRun(ctx, b, codeSet)
}
if (ctx.Option.Flag & encoder.ColorizeOption) != 0 {
return vm_color_indent.Run(ctx, b, codeSet)
}
return vm_indent.Run(ctx, b, codeSet)
}

41
vendor/github.com/goccy/go-json/error.go generated vendored Normal file
View File

@ -0,0 +1,41 @@
package json
import (
"github.com/goccy/go-json/internal/errors"
)
// Before Go 1.2, an InvalidUTF8Error was returned by Marshal when
// attempting to encode a string value with invalid UTF-8 sequences.
// As of Go 1.2, Marshal instead coerces the string to valid UTF-8 by
// replacing invalid bytes with the Unicode replacement rune U+FFFD.
//
// Deprecated: No longer used; kept for compatibility.
type InvalidUTF8Error = errors.InvalidUTF8Error
// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal.
// (The argument to Unmarshal must be a non-nil pointer.)
type InvalidUnmarshalError = errors.InvalidUnmarshalError
// A MarshalerError represents an error from calling a MarshalJSON or MarshalText method.
type MarshalerError = errors.MarshalerError
// A SyntaxError is a description of a JSON syntax error.
type SyntaxError = errors.SyntaxError
// An UnmarshalFieldError describes a JSON object key that
// led to an unexported (and therefore unwritable) struct field.
//
// Deprecated: No longer used; kept for compatibility.
type UnmarshalFieldError = errors.UnmarshalFieldError
// An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type.
type UnmarshalTypeError = errors.UnmarshalTypeError
// An UnsupportedTypeError is returned by Marshal when attempting
// to encode an unsupported value type.
type UnsupportedTypeError = errors.UnsupportedTypeError
type UnsupportedValueError = errors.UnsupportedValueError
type PathError = errors.PathError

View File

@ -0,0 +1,41 @@
package decoder
import (
"unsafe"
"github.com/goccy/go-json/internal/runtime"
)
type anonymousFieldDecoder struct {
structType *runtime.Type
offset uintptr
dec Decoder
}
func newAnonymousFieldDecoder(structType *runtime.Type, offset uintptr, dec Decoder) *anonymousFieldDecoder {
return &anonymousFieldDecoder{
structType: structType,
offset: offset,
dec: dec,
}
}
func (d *anonymousFieldDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
if *(*unsafe.Pointer)(p) == nil {
*(*unsafe.Pointer)(p) = unsafe_New(d.structType)
}
p = *(*unsafe.Pointer)(p)
return d.dec.DecodeStream(s, depth, unsafe.Pointer(uintptr(p)+d.offset))
}
func (d *anonymousFieldDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
if *(*unsafe.Pointer)(p) == nil {
*(*unsafe.Pointer)(p) = unsafe_New(d.structType)
}
p = *(*unsafe.Pointer)(p)
return d.dec.Decode(ctx, cursor, depth, unsafe.Pointer(uintptr(p)+d.offset))
}
func (d *anonymousFieldDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return d.dec.DecodePath(ctx, cursor, depth)
}

View File

@ -0,0 +1,176 @@
package decoder
import (
"fmt"
"unsafe"
"github.com/goccy/go-json/internal/errors"
"github.com/goccy/go-json/internal/runtime"
)
type arrayDecoder struct {
elemType *runtime.Type
size uintptr
valueDecoder Decoder
alen int
structName string
fieldName string
zeroValue unsafe.Pointer
}
func newArrayDecoder(dec Decoder, elemType *runtime.Type, alen int, structName, fieldName string) *arrayDecoder {
// workaround to avoid checkptr errors. cannot use `*(*unsafe.Pointer)(unsafe_New(elemType))` directly.
zeroValuePtr := unsafe_New(elemType)
zeroValue := **(**unsafe.Pointer)(unsafe.Pointer(&zeroValuePtr))
return &arrayDecoder{
valueDecoder: dec,
elemType: elemType,
size: elemType.Size(),
alen: alen,
structName: structName,
fieldName: fieldName,
zeroValue: zeroValue,
}
}
func (d *arrayDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
depth++
if depth > maxDecodeNestingDepth {
return errors.ErrExceededMaxDepth(s.char(), s.cursor)
}
for {
switch s.char() {
case ' ', '\n', '\t', '\r':
case 'n':
if err := nullBytes(s); err != nil {
return err
}
return nil
case '[':
idx := 0
s.cursor++
if s.skipWhiteSpace() == ']' {
for idx < d.alen {
*(*unsafe.Pointer)(unsafe.Pointer(uintptr(p) + uintptr(idx)*d.size)) = d.zeroValue
idx++
}
s.cursor++
return nil
}
for {
if idx < d.alen {
if err := d.valueDecoder.DecodeStream(s, depth, unsafe.Pointer(uintptr(p)+uintptr(idx)*d.size)); err != nil {
return err
}
} else {
if err := s.skipValue(depth); err != nil {
return err
}
}
idx++
switch s.skipWhiteSpace() {
case ']':
for idx < d.alen {
*(*unsafe.Pointer)(unsafe.Pointer(uintptr(p) + uintptr(idx)*d.size)) = d.zeroValue
idx++
}
s.cursor++
return nil
case ',':
s.cursor++
continue
case nul:
if s.read() {
s.cursor++
continue
}
goto ERROR
default:
goto ERROR
}
}
case nul:
if s.read() {
continue
}
goto ERROR
default:
goto ERROR
}
s.cursor++
}
ERROR:
return errors.ErrUnexpectedEndOfJSON("array", s.totalOffset())
}
func (d *arrayDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
buf := ctx.Buf
depth++
if depth > maxDecodeNestingDepth {
return 0, errors.ErrExceededMaxDepth(buf[cursor], cursor)
}
for {
switch buf[cursor] {
case ' ', '\n', '\t', '\r':
cursor++
continue
case 'n':
if err := validateNull(buf, cursor); err != nil {
return 0, err
}
cursor += 4
return cursor, nil
case '[':
idx := 0
cursor++
cursor = skipWhiteSpace(buf, cursor)
if buf[cursor] == ']' {
for idx < d.alen {
*(*unsafe.Pointer)(unsafe.Pointer(uintptr(p) + uintptr(idx)*d.size)) = d.zeroValue
idx++
}
cursor++
return cursor, nil
}
for {
if idx < d.alen {
c, err := d.valueDecoder.Decode(ctx, cursor, depth, unsafe.Pointer(uintptr(p)+uintptr(idx)*d.size))
if err != nil {
return 0, err
}
cursor = c
} else {
c, err := skipValue(buf, cursor, depth)
if err != nil {
return 0, err
}
cursor = c
}
idx++
cursor = skipWhiteSpace(buf, cursor)
switch buf[cursor] {
case ']':
for idx < d.alen {
*(*unsafe.Pointer)(unsafe.Pointer(uintptr(p) + uintptr(idx)*d.size)) = d.zeroValue
idx++
}
cursor++
return cursor, nil
case ',':
cursor++
continue
default:
return 0, errors.ErrInvalidCharacter(buf[cursor], "array", cursor)
}
}
default:
return 0, errors.ErrUnexpectedEndOfJSON("array", cursor)
}
}
}
func (d *arrayDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return nil, 0, fmt.Errorf("json: array decoder does not support decode path")
}

View File

@ -0,0 +1,438 @@
package decoder
import (
"fmt"
"reflect"
"strconv"
)
var (
nilValue = reflect.ValueOf(nil)
)
func AssignValue(src, dst reflect.Value) error {
if dst.Type().Kind() != reflect.Ptr {
return fmt.Errorf("invalid dst type. required pointer type: %T", dst.Type())
}
casted, err := castValue(dst.Elem().Type(), src)
if err != nil {
return err
}
dst.Elem().Set(casted)
return nil
}
func castValue(t reflect.Type, v reflect.Value) (reflect.Value, error) {
switch t.Kind() {
case reflect.Int:
vv, err := castInt(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(int(vv.Int())), nil
case reflect.Int8:
vv, err := castInt(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(int8(vv.Int())), nil
case reflect.Int16:
vv, err := castInt(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(int16(vv.Int())), nil
case reflect.Int32:
vv, err := castInt(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(int32(vv.Int())), nil
case reflect.Int64:
return castInt(v)
case reflect.Uint:
vv, err := castUint(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(uint(vv.Uint())), nil
case reflect.Uint8:
vv, err := castUint(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(uint8(vv.Uint())), nil
case reflect.Uint16:
vv, err := castUint(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(uint16(vv.Uint())), nil
case reflect.Uint32:
vv, err := castUint(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(uint32(vv.Uint())), nil
case reflect.Uint64:
return castUint(v)
case reflect.Uintptr:
vv, err := castUint(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(uintptr(vv.Uint())), nil
case reflect.String:
return castString(v)
case reflect.Bool:
return castBool(v)
case reflect.Float32:
vv, err := castFloat(v)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(float32(vv.Float())), nil
case reflect.Float64:
return castFloat(v)
case reflect.Array:
return castArray(t, v)
case reflect.Slice:
return castSlice(t, v)
case reflect.Map:
return castMap(t, v)
case reflect.Struct:
return castStruct(t, v)
}
return v, nil
}
func castInt(v reflect.Value) (reflect.Value, error) {
switch v.Type().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v, nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return reflect.ValueOf(int64(v.Uint())), nil
case reflect.String:
i64, err := strconv.ParseInt(v.String(), 10, 64)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(i64), nil
case reflect.Bool:
if v.Bool() {
return reflect.ValueOf(int64(1)), nil
}
return reflect.ValueOf(int64(0)), nil
case reflect.Float32, reflect.Float64:
return reflect.ValueOf(int64(v.Float())), nil
case reflect.Array:
if v.Len() > 0 {
return castInt(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to int64 from empty array")
case reflect.Slice:
if v.Len() > 0 {
return castInt(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to int64 from empty slice")
case reflect.Interface:
return castInt(reflect.ValueOf(v.Interface()))
case reflect.Map:
return nilValue, fmt.Errorf("failed to cast to int64 from map")
case reflect.Struct:
return nilValue, fmt.Errorf("failed to cast to int64 from struct")
case reflect.Ptr:
return castInt(v.Elem())
}
return nilValue, fmt.Errorf("failed to cast to int64 from %s", v.Type().Kind())
}
func castUint(v reflect.Value) (reflect.Value, error) {
switch v.Type().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return reflect.ValueOf(uint64(v.Int())), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v, nil
case reflect.String:
u64, err := strconv.ParseUint(v.String(), 10, 64)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(u64), nil
case reflect.Bool:
if v.Bool() {
return reflect.ValueOf(uint64(1)), nil
}
return reflect.ValueOf(uint64(0)), nil
case reflect.Float32, reflect.Float64:
return reflect.ValueOf(uint64(v.Float())), nil
case reflect.Array:
if v.Len() > 0 {
return castUint(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to uint64 from empty array")
case reflect.Slice:
if v.Len() > 0 {
return castUint(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to uint64 from empty slice")
case reflect.Interface:
return castUint(reflect.ValueOf(v.Interface()))
case reflect.Map:
return nilValue, fmt.Errorf("failed to cast to uint64 from map")
case reflect.Struct:
return nilValue, fmt.Errorf("failed to cast to uint64 from struct")
case reflect.Ptr:
return castUint(v.Elem())
}
return nilValue, fmt.Errorf("failed to cast to uint64 from %s", v.Type().Kind())
}
func castString(v reflect.Value) (reflect.Value, error) {
switch v.Type().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return reflect.ValueOf(fmt.Sprint(v.Int())), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return reflect.ValueOf(fmt.Sprint(v.Uint())), nil
case reflect.String:
return v, nil
case reflect.Bool:
if v.Bool() {
return reflect.ValueOf("true"), nil
}
return reflect.ValueOf("false"), nil
case reflect.Float32, reflect.Float64:
return reflect.ValueOf(fmt.Sprint(v.Float())), nil
case reflect.Array:
if v.Len() > 0 {
return castString(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to string from empty array")
case reflect.Slice:
if v.Len() > 0 {
return castString(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to string from empty slice")
case reflect.Interface:
return castString(reflect.ValueOf(v.Interface()))
case reflect.Map:
return nilValue, fmt.Errorf("failed to cast to string from map")
case reflect.Struct:
return nilValue, fmt.Errorf("failed to cast to string from struct")
case reflect.Ptr:
return castString(v.Elem())
}
return nilValue, fmt.Errorf("failed to cast to string from %s", v.Type().Kind())
}
func castBool(v reflect.Value) (reflect.Value, error) {
switch v.Type().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
switch v.Int() {
case 0:
return reflect.ValueOf(false), nil
case 1:
return reflect.ValueOf(true), nil
}
return nilValue, fmt.Errorf("failed to cast to bool from %d", v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
switch v.Uint() {
case 0:
return reflect.ValueOf(false), nil
case 1:
return reflect.ValueOf(true), nil
}
return nilValue, fmt.Errorf("failed to cast to bool from %d", v.Uint())
case reflect.String:
b, err := strconv.ParseBool(v.String())
if err != nil {
return nilValue, err
}
return reflect.ValueOf(b), nil
case reflect.Bool:
return v, nil
case reflect.Float32, reflect.Float64:
switch v.Float() {
case 0:
return reflect.ValueOf(false), nil
case 1:
return reflect.ValueOf(true), nil
}
return nilValue, fmt.Errorf("failed to cast to bool from %f", v.Float())
case reflect.Array:
if v.Len() > 0 {
return castBool(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to string from empty array")
case reflect.Slice:
if v.Len() > 0 {
return castBool(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to string from empty slice")
case reflect.Interface:
return castBool(reflect.ValueOf(v.Interface()))
case reflect.Map:
return nilValue, fmt.Errorf("failed to cast to string from map")
case reflect.Struct:
return nilValue, fmt.Errorf("failed to cast to string from struct")
case reflect.Ptr:
return castBool(v.Elem())
}
return nilValue, fmt.Errorf("failed to cast to bool from %s", v.Type().Kind())
}
func castFloat(v reflect.Value) (reflect.Value, error) {
switch v.Type().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return reflect.ValueOf(float64(v.Int())), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return reflect.ValueOf(float64(v.Uint())), nil
case reflect.String:
f64, err := strconv.ParseFloat(v.String(), 64)
if err != nil {
return nilValue, err
}
return reflect.ValueOf(f64), nil
case reflect.Bool:
if v.Bool() {
return reflect.ValueOf(float64(1)), nil
}
return reflect.ValueOf(float64(0)), nil
case reflect.Float32, reflect.Float64:
return v, nil
case reflect.Array:
if v.Len() > 0 {
return castFloat(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to float64 from empty array")
case reflect.Slice:
if v.Len() > 0 {
return castFloat(v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to float64 from empty slice")
case reflect.Interface:
return castFloat(reflect.ValueOf(v.Interface()))
case reflect.Map:
return nilValue, fmt.Errorf("failed to cast to float64 from map")
case reflect.Struct:
return nilValue, fmt.Errorf("failed to cast to float64 from struct")
case reflect.Ptr:
return castFloat(v.Elem())
}
return nilValue, fmt.Errorf("failed to cast to float64 from %s", v.Type().Kind())
}
func castArray(t reflect.Type, v reflect.Value) (reflect.Value, error) {
kind := v.Type().Kind()
if kind == reflect.Interface {
return castArray(t, reflect.ValueOf(v.Interface()))
}
if kind != reflect.Slice && kind != reflect.Array {
return nilValue, fmt.Errorf("failed to cast to array from %s", kind)
}
if t.Elem() == v.Type().Elem() {
return v, nil
}
if t.Len() != v.Len() {
return nilValue, fmt.Errorf("failed to cast [%d]array from slice of %d length", t.Len(), v.Len())
}
ret := reflect.New(t).Elem()
for i := 0; i < v.Len(); i++ {
vv, err := castValue(t.Elem(), v.Index(i))
if err != nil {
return nilValue, err
}
ret.Index(i).Set(vv)
}
return ret, nil
}
func castSlice(t reflect.Type, v reflect.Value) (reflect.Value, error) {
kind := v.Type().Kind()
if kind == reflect.Interface {
return castSlice(t, reflect.ValueOf(v.Interface()))
}
if kind != reflect.Slice && kind != reflect.Array {
return nilValue, fmt.Errorf("failed to cast to slice from %s", kind)
}
if t.Elem() == v.Type().Elem() {
return v, nil
}
ret := reflect.MakeSlice(t, v.Len(), v.Len())
for i := 0; i < v.Len(); i++ {
vv, err := castValue(t.Elem(), v.Index(i))
if err != nil {
return nilValue, err
}
ret.Index(i).Set(vv)
}
return ret, nil
}
func castMap(t reflect.Type, v reflect.Value) (reflect.Value, error) {
ret := reflect.MakeMap(t)
switch v.Type().Kind() {
case reflect.Map:
iter := v.MapRange()
for iter.Next() {
key, err := castValue(t.Key(), iter.Key())
if err != nil {
return nilValue, err
}
value, err := castValue(t.Elem(), iter.Value())
if err != nil {
return nilValue, err
}
ret.SetMapIndex(key, value)
}
return ret, nil
case reflect.Interface:
return castMap(t, reflect.ValueOf(v.Interface()))
case reflect.Slice:
if v.Len() > 0 {
return castMap(t, v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to map from empty slice")
}
return nilValue, fmt.Errorf("failed to cast to map from %s", v.Type().Kind())
}
func castStruct(t reflect.Type, v reflect.Value) (reflect.Value, error) {
ret := reflect.New(t).Elem()
switch v.Type().Kind() {
case reflect.Map:
iter := v.MapRange()
for iter.Next() {
key := iter.Key()
k, err := castString(key)
if err != nil {
return nilValue, err
}
fieldName := k.String()
field, ok := t.FieldByName(fieldName)
if ok {
value, err := castValue(field.Type, iter.Value())
if err != nil {
return nilValue, err
}
ret.FieldByName(fieldName).Set(value)
}
}
return ret, nil
case reflect.Struct:
for i := 0; i < v.Type().NumField(); i++ {
name := v.Type().Field(i).Name
ret.FieldByName(name).Set(v.FieldByName(name))
}
return ret, nil
case reflect.Interface:
return castStruct(t, reflect.ValueOf(v.Interface()))
case reflect.Slice:
if v.Len() > 0 {
return castStruct(t, v.Index(0))
}
return nilValue, fmt.Errorf("failed to cast to struct from empty slice")
default:
return nilValue, fmt.Errorf("failed to cast to struct from %s", v.Type().Kind())
}
}

View File

@ -0,0 +1,83 @@
package decoder
import (
"fmt"
"unsafe"
"github.com/goccy/go-json/internal/errors"
)
type boolDecoder struct {
structName string
fieldName string
}
func newBoolDecoder(structName, fieldName string) *boolDecoder {
return &boolDecoder{structName: structName, fieldName: fieldName}
}
func (d *boolDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
c := s.skipWhiteSpace()
for {
switch c {
case 't':
if err := trueBytes(s); err != nil {
return err
}
**(**bool)(unsafe.Pointer(&p)) = true
return nil
case 'f':
if err := falseBytes(s); err != nil {
return err
}
**(**bool)(unsafe.Pointer(&p)) = false
return nil
case 'n':
if err := nullBytes(s); err != nil {
return err
}
return nil
case nul:
if s.read() {
c = s.char()
continue
}
goto ERROR
}
break
}
ERROR:
return errors.ErrUnexpectedEndOfJSON("bool", s.totalOffset())
}
func (d *boolDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
buf := ctx.Buf
cursor = skipWhiteSpace(buf, cursor)
switch buf[cursor] {
case 't':
if err := validateTrue(buf, cursor); err != nil {
return 0, err
}
cursor += 4
**(**bool)(unsafe.Pointer(&p)) = true
return cursor, nil
case 'f':
if err := validateFalse(buf, cursor); err != nil {
return 0, err
}
cursor += 5
**(**bool)(unsafe.Pointer(&p)) = false
return cursor, nil
case 'n':
if err := validateNull(buf, cursor); err != nil {
return 0, err
}
cursor += 4
return cursor, nil
}
return 0, errors.ErrUnexpectedEndOfJSON("bool", cursor)
}
func (d *boolDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return nil, 0, fmt.Errorf("json: bool decoder does not support decode path")
}

View File

@ -0,0 +1,118 @@
package decoder
import (
"encoding/base64"
"fmt"
"unsafe"
"github.com/goccy/go-json/internal/errors"
"github.com/goccy/go-json/internal/runtime"
)
type bytesDecoder struct {
typ *runtime.Type
sliceDecoder Decoder
stringDecoder *stringDecoder
structName string
fieldName string
}
func byteUnmarshalerSliceDecoder(typ *runtime.Type, structName string, fieldName string) Decoder {
var unmarshalDecoder Decoder
switch {
case runtime.PtrTo(typ).Implements(unmarshalJSONType):
unmarshalDecoder = newUnmarshalJSONDecoder(runtime.PtrTo(typ), structName, fieldName)
case runtime.PtrTo(typ).Implements(unmarshalTextType):
unmarshalDecoder = newUnmarshalTextDecoder(runtime.PtrTo(typ), structName, fieldName)
default:
unmarshalDecoder, _ = compileUint8(typ, structName, fieldName)
}
return newSliceDecoder(unmarshalDecoder, typ, 1, structName, fieldName)
}
func newBytesDecoder(typ *runtime.Type, structName string, fieldName string) *bytesDecoder {
return &bytesDecoder{
typ: typ,
sliceDecoder: byteUnmarshalerSliceDecoder(typ, structName, fieldName),
stringDecoder: newStringDecoder(structName, fieldName),
structName: structName,
fieldName: fieldName,
}
}
func (d *bytesDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
bytes, err := d.decodeStreamBinary(s, depth, p)
if err != nil {
return err
}
if bytes == nil {
s.reset()
return nil
}
decodedLen := base64.StdEncoding.DecodedLen(len(bytes))
buf := make([]byte, decodedLen)
n, err := base64.StdEncoding.Decode(buf, bytes)
if err != nil {
return err
}
*(*[]byte)(p) = buf[:n]
s.reset()
return nil
}
func (d *bytesDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
bytes, c, err := d.decodeBinary(ctx, cursor, depth, p)
if err != nil {
return 0, err
}
if bytes == nil {
return c, nil
}
cursor = c
decodedLen := base64.StdEncoding.DecodedLen(len(bytes))
b := make([]byte, decodedLen)
n, err := base64.StdEncoding.Decode(b, bytes)
if err != nil {
return 0, err
}
*(*[]byte)(p) = b[:n]
return cursor, nil
}
func (d *bytesDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return nil, 0, fmt.Errorf("json: []byte decoder does not support decode path")
}
func (d *bytesDecoder) decodeStreamBinary(s *Stream, depth int64, p unsafe.Pointer) ([]byte, error) {
c := s.skipWhiteSpace()
if c == '[' {
if d.sliceDecoder == nil {
return nil, &errors.UnmarshalTypeError{
Type: runtime.RType2Type(d.typ),
Offset: s.totalOffset(),
}
}
err := d.sliceDecoder.DecodeStream(s, depth, p)
return nil, err
}
return d.stringDecoder.decodeStreamByte(s)
}
func (d *bytesDecoder) decodeBinary(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) ([]byte, int64, error) {
buf := ctx.Buf
cursor = skipWhiteSpace(buf, cursor)
if buf[cursor] == '[' {
if d.sliceDecoder == nil {
return nil, 0, &errors.UnmarshalTypeError{
Type: runtime.RType2Type(d.typ),
Offset: cursor,
}
}
c, err := d.sliceDecoder.Decode(ctx, cursor, depth, p)
if err != nil {
return nil, 0, err
}
return nil, c, nil
}
return d.stringDecoder.decodeByte(buf, cursor)
}

View File

@ -0,0 +1,487 @@
package decoder
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"sync/atomic"
"unicode"
"unsafe"
"github.com/goccy/go-json/internal/runtime"
)
var (
jsonNumberType = reflect.TypeOf(json.Number(""))
typeAddr *runtime.TypeAddr
cachedDecoderMap unsafe.Pointer // map[uintptr]decoder
cachedDecoder []Decoder
)
func init() {
typeAddr = runtime.AnalyzeTypeAddr()
if typeAddr == nil {
typeAddr = &runtime.TypeAddr{}
}
cachedDecoder = make([]Decoder, typeAddr.AddrRange>>typeAddr.AddrShift+1)
}
func loadDecoderMap() map[uintptr]Decoder {
p := atomic.LoadPointer(&cachedDecoderMap)
return *(*map[uintptr]Decoder)(unsafe.Pointer(&p))
}
func storeDecoder(typ uintptr, dec Decoder, m map[uintptr]Decoder) {
newDecoderMap := make(map[uintptr]Decoder, len(m)+1)
newDecoderMap[typ] = dec
for k, v := range m {
newDecoderMap[k] = v
}
atomic.StorePointer(&cachedDecoderMap, *(*unsafe.Pointer)(unsafe.Pointer(&newDecoderMap)))
}
func compileToGetDecoderSlowPath(typeptr uintptr, typ *runtime.Type) (Decoder, error) {
decoderMap := loadDecoderMap()
if dec, exists := decoderMap[typeptr]; exists {
return dec, nil
}
dec, err := compileHead(typ, map[uintptr]Decoder{})
if err != nil {
return nil, err
}
storeDecoder(typeptr, dec, decoderMap)
return dec, nil
}
func compileHead(typ *runtime.Type, structTypeToDecoder map[uintptr]Decoder) (Decoder, error) {
switch {
case implementsUnmarshalJSONType(runtime.PtrTo(typ)):
return newUnmarshalJSONDecoder(runtime.PtrTo(typ), "", ""), nil
case runtime.PtrTo(typ).Implements(unmarshalTextType):
return newUnmarshalTextDecoder(runtime.PtrTo(typ), "", ""), nil
}
return compile(typ.Elem(), "", "", structTypeToDecoder)
}
func compile(typ *runtime.Type, structName, fieldName string, structTypeToDecoder map[uintptr]Decoder) (Decoder, error) {
switch {
case implementsUnmarshalJSONType(runtime.PtrTo(typ)):
return newUnmarshalJSONDecoder(runtime.PtrTo(typ), structName, fieldName), nil
case runtime.PtrTo(typ).Implements(unmarshalTextType):
return newUnmarshalTextDecoder(runtime.PtrTo(typ), structName, fieldName), nil
}
switch typ.Kind() {
case reflect.Ptr:
return compilePtr(typ, structName, fieldName, structTypeToDecoder)
case reflect.Struct:
return compileStruct(typ, structName, fieldName, structTypeToDecoder)
case reflect.Slice:
elem := typ.Elem()
if elem.Kind() == reflect.Uint8 {
return compileBytes(elem, structName, fieldName)
}
return compileSlice(typ, structName, fieldName, structTypeToDecoder)
case reflect.Array:
return compileArray(typ, structName, fieldName, structTypeToDecoder)
case reflect.Map:
return compileMap(typ, structName, fieldName, structTypeToDecoder)
case reflect.Interface:
return compileInterface(typ, structName, fieldName)
case reflect.Uintptr:
return compileUint(typ, structName, fieldName)
case reflect.Int:
return compileInt(typ, structName, fieldName)
case reflect.Int8:
return compileInt8(typ, structName, fieldName)
case reflect.Int16:
return compileInt16(typ, structName, fieldName)
case reflect.Int32:
return compileInt32(typ, structName, fieldName)
case reflect.Int64:
return compileInt64(typ, structName, fieldName)
case reflect.Uint:
return compileUint(typ, structName, fieldName)
case reflect.Uint8:
return compileUint8(typ, structName, fieldName)
case reflect.Uint16:
return compileUint16(typ, structName, fieldName)
case reflect.Uint32:
return compileUint32(typ, structName, fieldName)
case reflect.Uint64:
return compileUint64(typ, structName, fieldName)
case reflect.String:
return compileString(typ, structName, fieldName)
case reflect.Bool:
return compileBool(structName, fieldName)
case reflect.Float32:
return compileFloat32(structName, fieldName)
case reflect.Float64:
return compileFloat64(structName, fieldName)
case reflect.Func:
return compileFunc(typ, structName, fieldName)
}
return newInvalidDecoder(typ, structName, fieldName), nil
}
func isStringTagSupportedType(typ *runtime.Type) bool {
switch {
case implementsUnmarshalJSONType(runtime.PtrTo(typ)):
return false
case runtime.PtrTo(typ).Implements(unmarshalTextType):
return false
}
switch typ.Kind() {
case reflect.Map:
return false
case reflect.Slice:
return false
case reflect.Array:
return false
case reflect.Struct:
return false
case reflect.Interface:
return false
}
return true
}
func compileMapKey(typ *runtime.Type, structName, fieldName string, structTypeToDecoder map[uintptr]Decoder) (Decoder, error) {
if runtime.PtrTo(typ).Implements(unmarshalTextType) {
return newUnmarshalTextDecoder(runtime.PtrTo(typ), structName, fieldName), nil
}
if typ.Kind() == reflect.String {
return newStringDecoder(structName, fieldName), nil
}
dec, err := compile(typ, structName, fieldName, structTypeToDecoder)
if err != nil {
return nil, err
}
for {
switch t := dec.(type) {
case *stringDecoder, *interfaceDecoder:
return dec, nil
case *boolDecoder, *intDecoder, *uintDecoder, *numberDecoder:
return newWrappedStringDecoder(typ, dec, structName, fieldName), nil
case *ptrDecoder:
dec = t.dec
default:
return newInvalidDecoder(typ, structName, fieldName), nil
}
}
}
func compilePtr(typ *runtime.Type, structName, fieldName string, structTypeToDecoder map[uintptr]Decoder) (Decoder, error) {
dec, err := compile(typ.Elem(), structName, fieldName, structTypeToDecoder)
if err != nil {
return nil, err
}
return newPtrDecoder(dec, typ.Elem(), structName, fieldName), nil
}
func compileInt(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newIntDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v int64) {
*(*int)(p) = int(v)
}), nil
}
func compileInt8(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newIntDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v int64) {
*(*int8)(p) = int8(v)
}), nil
}
func compileInt16(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newIntDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v int64) {
*(*int16)(p) = int16(v)
}), nil
}
func compileInt32(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newIntDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v int64) {
*(*int32)(p) = int32(v)
}), nil
}
func compileInt64(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newIntDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v int64) {
*(*int64)(p) = v
}), nil
}
func compileUint(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newUintDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v uint64) {
*(*uint)(p) = uint(v)
}), nil
}
func compileUint8(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newUintDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v uint64) {
*(*uint8)(p) = uint8(v)
}), nil
}
func compileUint16(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newUintDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v uint64) {
*(*uint16)(p) = uint16(v)
}), nil
}
func compileUint32(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newUintDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v uint64) {
*(*uint32)(p) = uint32(v)
}), nil
}
func compileUint64(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newUintDecoder(typ, structName, fieldName, func(p unsafe.Pointer, v uint64) {
*(*uint64)(p) = v
}), nil
}
func compileFloat32(structName, fieldName string) (Decoder, error) {
return newFloatDecoder(structName, fieldName, func(p unsafe.Pointer, v float64) {
*(*float32)(p) = float32(v)
}), nil
}
func compileFloat64(structName, fieldName string) (Decoder, error) {
return newFloatDecoder(structName, fieldName, func(p unsafe.Pointer, v float64) {
*(*float64)(p) = v
}), nil
}
func compileString(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
if typ == runtime.Type2RType(jsonNumberType) {
return newNumberDecoder(structName, fieldName, func(p unsafe.Pointer, v json.Number) {
*(*json.Number)(p) = v
}), nil
}
return newStringDecoder(structName, fieldName), nil
}
func compileBool(structName, fieldName string) (Decoder, error) {
return newBoolDecoder(structName, fieldName), nil
}
func compileBytes(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newBytesDecoder(typ, structName, fieldName), nil
}
func compileSlice(typ *runtime.Type, structName, fieldName string, structTypeToDecoder map[uintptr]Decoder) (Decoder, error) {
elem := typ.Elem()
decoder, err := compile(elem, structName, fieldName, structTypeToDecoder)
if err != nil {
return nil, err
}
return newSliceDecoder(decoder, elem, elem.Size(), structName, fieldName), nil
}
func compileArray(typ *runtime.Type, structName, fieldName string, structTypeToDecoder map[uintptr]Decoder) (Decoder, error) {
elem := typ.Elem()
decoder, err := compile(elem, structName, fieldName, structTypeToDecoder)
if err != nil {
return nil, err
}
return newArrayDecoder(decoder, elem, typ.Len(), structName, fieldName), nil
}
func compileMap(typ *runtime.Type, structName, fieldName string, structTypeToDecoder map[uintptr]Decoder) (Decoder, error) {
keyDec, err := compileMapKey(typ.Key(), structName, fieldName, structTypeToDecoder)
if err != nil {
return nil, err
}
valueDec, err := compile(typ.Elem(), structName, fieldName, structTypeToDecoder)
if err != nil {
return nil, err
}
return newMapDecoder(typ, typ.Key(), keyDec, typ.Elem(), valueDec, structName, fieldName), nil
}
func compileInterface(typ *runtime.Type, structName, fieldName string) (Decoder, error) {
return newInterfaceDecoder(typ, structName, fieldName), nil
}
func compileFunc(typ *runtime.Type, strutName, fieldName string) (Decoder, error) {
return newFuncDecoder(typ, strutName, fieldName), nil
}
func typeToStructTags(typ *runtime.Type) runtime.StructTags {
tags := runtime.StructTags{}
fieldNum := typ.NumField()
for i := 0; i < fieldNum; i++ {
field := typ.Field(i)
if runtime.IsIgnoredStructField(field) {
continue
}
tags = append(tags, runtime.StructTagFromField(field))
}
return tags
}
func compileStruct(typ *runtime.Type, structName, fieldName string, structTypeToDecoder map[uintptr]Decoder) (Decoder, error) {
fieldNum := typ.NumField()
fieldMap := map[string]*structFieldSet{}
typeptr := uintptr(unsafe.Pointer(typ))
if dec, exists := structTypeToDecoder[typeptr]; exists {
return dec, nil
}
structDec := newStructDecoder(structName, fieldName, fieldMap)
structTypeToDecoder[typeptr] = structDec
structName = typ.Name()
tags := typeToStructTags(typ)
allFields := []*structFieldSet{}
for i := 0; i < fieldNum; i++ {
field := typ.Field(i)
if runtime.IsIgnoredStructField(field) {
continue
}
isUnexportedField := unicode.IsLower([]rune(field.Name)[0])
tag := runtime.StructTagFromField(field)
dec, err := compile(runtime.Type2RType(field.Type), structName, field.Name, structTypeToDecoder)
if err != nil {
return nil, err
}
if field.Anonymous && !tag.IsTaggedKey {
if stDec, ok := dec.(*structDecoder); ok {
if runtime.Type2RType(field.Type) == typ {
// recursive definition
continue
}
for k, v := range stDec.fieldMap {
if tags.ExistsKey(k) {
continue
}
fieldSet := &structFieldSet{
dec: v.dec,
offset: field.Offset + v.offset,
isTaggedKey: v.isTaggedKey,
key: k,
keyLen: int64(len(k)),
}
allFields = append(allFields, fieldSet)
}
} else if pdec, ok := dec.(*ptrDecoder); ok {
contentDec := pdec.contentDecoder()
if pdec.typ == typ {
// recursive definition
continue
}
var fieldSetErr error
if isUnexportedField {
fieldSetErr = fmt.Errorf(
"json: cannot set embedded pointer to unexported struct: %v",
field.Type.Elem(),
)
}
if dec, ok := contentDec.(*structDecoder); ok {
for k, v := range dec.fieldMap {
if tags.ExistsKey(k) {
continue
}
fieldSet := &structFieldSet{
dec: newAnonymousFieldDecoder(pdec.typ, v.offset, v.dec),
offset: field.Offset,
isTaggedKey: v.isTaggedKey,
key: k,
keyLen: int64(len(k)),
err: fieldSetErr,
}
allFields = append(allFields, fieldSet)
}
} else {
fieldSet := &structFieldSet{
dec: pdec,
offset: field.Offset,
isTaggedKey: tag.IsTaggedKey,
key: field.Name,
keyLen: int64(len(field.Name)),
}
allFields = append(allFields, fieldSet)
}
} else {
fieldSet := &structFieldSet{
dec: dec,
offset: field.Offset,
isTaggedKey: tag.IsTaggedKey,
key: field.Name,
keyLen: int64(len(field.Name)),
}
allFields = append(allFields, fieldSet)
}
} else {
if tag.IsString && isStringTagSupportedType(runtime.Type2RType(field.Type)) {
dec = newWrappedStringDecoder(runtime.Type2RType(field.Type), dec, structName, field.Name)
}
var key string
if tag.Key != "" {
key = tag.Key
} else {
key = field.Name
}
fieldSet := &structFieldSet{
dec: dec,
offset: field.Offset,
isTaggedKey: tag.IsTaggedKey,
key: key,
keyLen: int64(len(key)),
}
allFields = append(allFields, fieldSet)
}
}
for _, set := range filterDuplicatedFields(allFields) {
fieldMap[set.key] = set
lower := strings.ToLower(set.key)
if _, exists := fieldMap[lower]; !exists {
// first win
fieldMap[lower] = set
}
}
delete(structTypeToDecoder, typeptr)
structDec.tryOptimize()
return structDec, nil
}
func filterDuplicatedFields(allFields []*structFieldSet) []*structFieldSet {
fieldMap := map[string][]*structFieldSet{}
for _, field := range allFields {
fieldMap[field.key] = append(fieldMap[field.key], field)
}
duplicatedFieldMap := map[string]struct{}{}
for k, sets := range fieldMap {
sets = filterFieldSets(sets)
if len(sets) != 1 {
duplicatedFieldMap[k] = struct{}{}
}
}
filtered := make([]*structFieldSet, 0, len(allFields))
for _, field := range allFields {
if _, exists := duplicatedFieldMap[field.key]; exists {
continue
}
filtered = append(filtered, field)
}
return filtered
}
func filterFieldSets(sets []*structFieldSet) []*structFieldSet {
if len(sets) == 1 {
return sets
}
filtered := make([]*structFieldSet, 0, len(sets))
for _, set := range sets {
if set.isTaggedKey {
filtered = append(filtered, set)
}
}
return filtered
}
func implementsUnmarshalJSONType(typ *runtime.Type) bool {
return typ.Implements(unmarshalJSONType) || typ.Implements(unmarshalJSONContextType)
}

View File

@ -0,0 +1,29 @@
//go:build !race
// +build !race
package decoder
import (
"unsafe"
"github.com/goccy/go-json/internal/runtime"
)
func CompileToGetDecoder(typ *runtime.Type) (Decoder, error) {
typeptr := uintptr(unsafe.Pointer(typ))
if typeptr > typeAddr.MaxTypeAddr {
return compileToGetDecoderSlowPath(typeptr, typ)
}
index := (typeptr - typeAddr.BaseTypeAddr) >> typeAddr.AddrShift
if dec := cachedDecoder[index]; dec != nil {
return dec, nil
}
dec, err := compileHead(typ, map[uintptr]Decoder{})
if err != nil {
return nil, err
}
cachedDecoder[index] = dec
return dec, nil
}

View File

@ -0,0 +1,37 @@
//go:build race
// +build race
package decoder
import (
"sync"
"unsafe"
"github.com/goccy/go-json/internal/runtime"
)
var decMu sync.RWMutex
func CompileToGetDecoder(typ *runtime.Type) (Decoder, error) {
typeptr := uintptr(unsafe.Pointer(typ))
if typeptr > typeAddr.MaxTypeAddr {
return compileToGetDecoderSlowPath(typeptr, typ)
}
index := (typeptr - typeAddr.BaseTypeAddr) >> typeAddr.AddrShift
decMu.RLock()
if dec := cachedDecoder[index]; dec != nil {
decMu.RUnlock()
return dec, nil
}
decMu.RUnlock()
dec, err := compileHead(typ, map[uintptr]Decoder{})
if err != nil {
return nil, err
}
decMu.Lock()
cachedDecoder[index] = dec
decMu.Unlock()
return dec, nil
}

View File

@ -0,0 +1,254 @@
package decoder
import (
"sync"
"unsafe"
"github.com/goccy/go-json/internal/errors"
)
type RuntimeContext struct {
Buf []byte
Option *Option
}
var (
runtimeContextPool = sync.Pool{
New: func() interface{} {
return &RuntimeContext{
Option: &Option{},
}
},
}
)
func TakeRuntimeContext() *RuntimeContext {
return runtimeContextPool.Get().(*RuntimeContext)
}
func ReleaseRuntimeContext(ctx *RuntimeContext) {
runtimeContextPool.Put(ctx)
}
var (
isWhiteSpace = [256]bool{}
)
func init() {
isWhiteSpace[' '] = true
isWhiteSpace['\n'] = true
isWhiteSpace['\t'] = true
isWhiteSpace['\r'] = true
}
func char(ptr unsafe.Pointer, offset int64) byte {
return *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(offset)))
}
func skipWhiteSpace(buf []byte, cursor int64) int64 {
for isWhiteSpace[buf[cursor]] {
cursor++
}
return cursor
}
func skipObject(buf []byte, cursor, depth int64) (int64, error) {
braceCount := 1
for {
switch buf[cursor] {
case '{':
braceCount++
depth++
if depth > maxDecodeNestingDepth {
return 0, errors.ErrExceededMaxDepth(buf[cursor], cursor)
}
case '}':
depth--
braceCount--
if braceCount == 0 {
return cursor + 1, nil
}
case '[':
depth++
if depth > maxDecodeNestingDepth {
return 0, errors.ErrExceededMaxDepth(buf[cursor], cursor)
}
case ']':
depth--
case '"':
for {
cursor++
switch buf[cursor] {
case '\\':
cursor++
if buf[cursor] == nul {
return 0, errors.ErrUnexpectedEndOfJSON("string of object", cursor)
}
case '"':
goto SWITCH_OUT
case nul:
return 0, errors.ErrUnexpectedEndOfJSON("string of object", cursor)
}
}
case nul:
return 0, errors.ErrUnexpectedEndOfJSON("object of object", cursor)
}
SWITCH_OUT:
cursor++
}
}
func skipArray(buf []byte, cursor, depth int64) (int64, error) {
bracketCount := 1
for {
switch buf[cursor] {
case '[':
bracketCount++
depth++
if depth > maxDecodeNestingDepth {
return 0, errors.ErrExceededMaxDepth(buf[cursor], cursor)
}
case ']':
bracketCount--
depth--
if bracketCount == 0 {
return cursor + 1, nil
}
case '{':
depth++
if depth > maxDecodeNestingDepth {
return 0, errors.ErrExceededMaxDepth(buf[cursor], cursor)
}
case '}':
depth--
case '"':
for {
cursor++
switch buf[cursor] {
case '\\':
cursor++
if buf[cursor] == nul {
return 0, errors.ErrUnexpectedEndOfJSON("string of object", cursor)
}
case '"':
goto SWITCH_OUT
case nul:
return 0, errors.ErrUnexpectedEndOfJSON("string of object", cursor)
}
}
case nul:
return 0, errors.ErrUnexpectedEndOfJSON("array of object", cursor)
}
SWITCH_OUT:
cursor++
}
}
func skipValue(buf []byte, cursor, depth int64) (int64, error) {
for {
switch buf[cursor] {
case ' ', '\t', '\n', '\r':
cursor++
continue
case '{':
return skipObject(buf, cursor+1, depth+1)
case '[':
return skipArray(buf, cursor+1, depth+1)
case '"':
for {
cursor++
switch buf[cursor] {
case '\\':
cursor++
if buf[cursor] == nul {
return 0, errors.ErrUnexpectedEndOfJSON("string of object", cursor)
}
case '"':
return cursor + 1, nil
case nul:
return 0, errors.ErrUnexpectedEndOfJSON("string of object", cursor)
}
}
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
for {
cursor++
if floatTable[buf[cursor]] {
continue
}
break
}
return cursor, nil
case 't':
if err := validateTrue(buf, cursor); err != nil {
return 0, err
}
cursor += 4
return cursor, nil
case 'f':
if err := validateFalse(buf, cursor); err != nil {
return 0, err
}
cursor += 5
return cursor, nil
case 'n':
if err := validateNull(buf, cursor); err != nil {
return 0, err
}
cursor += 4
return cursor, nil
default:
return cursor, errors.ErrUnexpectedEndOfJSON("null", cursor)
}
}
}
func validateTrue(buf []byte, cursor int64) error {
if cursor+3 >= int64(len(buf)) {
return errors.ErrUnexpectedEndOfJSON("true", cursor)
}
if buf[cursor+1] != 'r' {
return errors.ErrInvalidCharacter(buf[cursor+1], "true", cursor)
}
if buf[cursor+2] != 'u' {
return errors.ErrInvalidCharacter(buf[cursor+2], "true", cursor)
}
if buf[cursor+3] != 'e' {
return errors.ErrInvalidCharacter(buf[cursor+3], "true", cursor)
}
return nil
}
func validateFalse(buf []byte, cursor int64) error {
if cursor+4 >= int64(len(buf)) {
return errors.ErrUnexpectedEndOfJSON("false", cursor)
}
if buf[cursor+1] != 'a' {
return errors.ErrInvalidCharacter(buf[cursor+1], "false", cursor)
}
if buf[cursor+2] != 'l' {
return errors.ErrInvalidCharacter(buf[cursor+2], "false", cursor)
}
if buf[cursor+3] != 's' {
return errors.ErrInvalidCharacter(buf[cursor+3], "false", cursor)
}
if buf[cursor+4] != 'e' {
return errors.ErrInvalidCharacter(buf[cursor+4], "false", cursor)
}
return nil
}
func validateNull(buf []byte, cursor int64) error {
if cursor+3 >= int64(len(buf)) {
return errors.ErrUnexpectedEndOfJSON("null", cursor)
}
if buf[cursor+1] != 'u' {
return errors.ErrInvalidCharacter(buf[cursor+1], "null", cursor)
}
if buf[cursor+2] != 'l' {
return errors.ErrInvalidCharacter(buf[cursor+2], "null", cursor)
}
if buf[cursor+3] != 'l' {
return errors.ErrInvalidCharacter(buf[cursor+3], "null", cursor)
}
return nil
}

View File

@ -0,0 +1,170 @@
package decoder
import (
"strconv"
"unsafe"
"github.com/goccy/go-json/internal/errors"
)
type floatDecoder struct {
op func(unsafe.Pointer, float64)
structName string
fieldName string
}
func newFloatDecoder(structName, fieldName string, op func(unsafe.Pointer, float64)) *floatDecoder {
return &floatDecoder{op: op, structName: structName, fieldName: fieldName}
}
var (
floatTable = [256]bool{
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
'.': true,
'e': true,
'E': true,
'+': true,
'-': true,
}
validEndNumberChar = [256]bool{
nul: true,
' ': true,
'\t': true,
'\r': true,
'\n': true,
',': true,
':': true,
'}': true,
']': true,
}
)
func floatBytes(s *Stream) []byte {
start := s.cursor
for {
s.cursor++
if floatTable[s.char()] {
continue
} else if s.char() == nul {
if s.read() {
s.cursor-- // for retry current character
continue
}
}
break
}
return s.buf[start:s.cursor]
}
func (d *floatDecoder) decodeStreamByte(s *Stream) ([]byte, error) {
for {
switch s.char() {
case ' ', '\n', '\t', '\r':
s.cursor++
continue
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return floatBytes(s), nil
case 'n':
if err := nullBytes(s); err != nil {
return nil, err
}
return nil, nil
case nul:
if s.read() {
continue
}
goto ERROR
default:
goto ERROR
}
}
ERROR:
return nil, errors.ErrUnexpectedEndOfJSON("float", s.totalOffset())
}
func (d *floatDecoder) decodeByte(buf []byte, cursor int64) ([]byte, int64, error) {
for {
switch buf[cursor] {
case ' ', '\n', '\t', '\r':
cursor++
continue
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
start := cursor
cursor++
for floatTable[buf[cursor]] {
cursor++
}
num := buf[start:cursor]
return num, cursor, nil
case 'n':
if err := validateNull(buf, cursor); err != nil {
return nil, 0, err
}
cursor += 4
return nil, cursor, nil
default:
return nil, 0, errors.ErrUnexpectedEndOfJSON("float", cursor)
}
}
}
func (d *floatDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
bytes, err := d.decodeStreamByte(s)
if err != nil {
return err
}
if bytes == nil {
return nil
}
str := *(*string)(unsafe.Pointer(&bytes))
f64, err := strconv.ParseFloat(str, 64)
if err != nil {
return errors.ErrSyntax(err.Error(), s.totalOffset())
}
d.op(p, f64)
return nil
}
func (d *floatDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
buf := ctx.Buf
bytes, c, err := d.decodeByte(buf, cursor)
if err != nil {
return 0, err
}
if bytes == nil {
return c, nil
}
cursor = c
if !validEndNumberChar[buf[cursor]] {
return 0, errors.ErrUnexpectedEndOfJSON("float", cursor)
}
s := *(*string)(unsafe.Pointer(&bytes))
f64, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, errors.ErrSyntax(err.Error(), cursor)
}
d.op(p, f64)
return cursor, nil
}
func (d *floatDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
buf := ctx.Buf
bytes, c, err := d.decodeByte(buf, cursor)
if err != nil {
return nil, 0, err
}
if bytes == nil {
return [][]byte{nullbytes}, c, nil
}
return [][]byte{bytes}, c, nil
}

View File

@ -0,0 +1,146 @@
package decoder
import (
"bytes"
"fmt"
"unsafe"
"github.com/goccy/go-json/internal/errors"
"github.com/goccy/go-json/internal/runtime"
)
type funcDecoder struct {
typ *runtime.Type
structName string
fieldName string
}
func newFuncDecoder(typ *runtime.Type, structName, fieldName string) *funcDecoder {
fnDecoder := &funcDecoder{typ, structName, fieldName}
return fnDecoder
}
func (d *funcDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
s.skipWhiteSpace()
start := s.cursor
if err := s.skipValue(depth); err != nil {
return err
}
src := s.buf[start:s.cursor]
if len(src) > 0 {
switch src[0] {
case '"':
return &errors.UnmarshalTypeError{
Value: "string",
Type: runtime.RType2Type(d.typ),
Offset: s.totalOffset(),
}
case '[':
return &errors.UnmarshalTypeError{
Value: "array",
Type: runtime.RType2Type(d.typ),
Offset: s.totalOffset(),
}
case '{':
return &errors.UnmarshalTypeError{
Value: "object",
Type: runtime.RType2Type(d.typ),
Offset: s.totalOffset(),
}
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return &errors.UnmarshalTypeError{
Value: "number",
Type: runtime.RType2Type(d.typ),
Offset: s.totalOffset(),
}
case 'n':
if err := nullBytes(s); err != nil {
return err
}
*(*unsafe.Pointer)(p) = nil
return nil
case 't':
if err := trueBytes(s); err == nil {
return &errors.UnmarshalTypeError{
Value: "boolean",
Type: runtime.RType2Type(d.typ),
Offset: s.totalOffset(),
}
}
case 'f':
if err := falseBytes(s); err == nil {
return &errors.UnmarshalTypeError{
Value: "boolean",
Type: runtime.RType2Type(d.typ),
Offset: s.totalOffset(),
}
}
}
}
return errors.ErrInvalidBeginningOfValue(s.buf[s.cursor], s.totalOffset())
}
func (d *funcDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
buf := ctx.Buf
cursor = skipWhiteSpace(buf, cursor)
start := cursor
end, err := skipValue(buf, cursor, depth)
if err != nil {
return 0, err
}
src := buf[start:end]
if len(src) > 0 {
switch src[0] {
case '"':
return 0, &errors.UnmarshalTypeError{
Value: "string",
Type: runtime.RType2Type(d.typ),
Offset: start,
}
case '[':
return 0, &errors.UnmarshalTypeError{
Value: "array",
Type: runtime.RType2Type(d.typ),
Offset: start,
}
case '{':
return 0, &errors.UnmarshalTypeError{
Value: "object",
Type: runtime.RType2Type(d.typ),
Offset: start,
}
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return 0, &errors.UnmarshalTypeError{
Value: "number",
Type: runtime.RType2Type(d.typ),
Offset: start,
}
case 'n':
if bytes.Equal(src, nullbytes) {
*(*unsafe.Pointer)(p) = nil
return end, nil
}
case 't':
if err := validateTrue(buf, start); err == nil {
return 0, &errors.UnmarshalTypeError{
Value: "boolean",
Type: runtime.RType2Type(d.typ),
Offset: start,
}
}
case 'f':
if err := validateFalse(buf, start); err == nil {
return 0, &errors.UnmarshalTypeError{
Value: "boolean",
Type: runtime.RType2Type(d.typ),
Offset: start,
}
}
}
}
return cursor, errors.ErrInvalidBeginningOfValue(buf[cursor], cursor)
}
func (d *funcDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return nil, 0, fmt.Errorf("json: func decoder does not support decode path")
}

246
vendor/github.com/goccy/go-json/internal/decoder/int.go generated vendored Normal file
View File

@ -0,0 +1,246 @@
package decoder
import (
"fmt"
"reflect"
"unsafe"
"github.com/goccy/go-json/internal/errors"
"github.com/goccy/go-json/internal/runtime"
)
type intDecoder struct {
typ *runtime.Type
kind reflect.Kind
op func(unsafe.Pointer, int64)
structName string
fieldName string
}
func newIntDecoder(typ *runtime.Type, structName, fieldName string, op func(unsafe.Pointer, int64)) *intDecoder {
return &intDecoder{
typ: typ,
kind: typ.Kind(),
op: op,
structName: structName,
fieldName: fieldName,
}
}
func (d *intDecoder) typeError(buf []byte, offset int64) *errors.UnmarshalTypeError {
return &errors.UnmarshalTypeError{
Value: fmt.Sprintf("number %s", string(buf)),
Type: runtime.RType2Type(d.typ),
Struct: d.structName,
Field: d.fieldName,
Offset: offset,
}
}
var (
pow10i64 = [...]int64{
1e00, 1e01, 1e02, 1e03, 1e04, 1e05, 1e06, 1e07, 1e08, 1e09,
1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18,
}
pow10i64Len = len(pow10i64)
)
func (d *intDecoder) parseInt(b []byte) (int64, error) {
isNegative := false
if b[0] == '-' {
b = b[1:]
isNegative = true
}
maxDigit := len(b)
if maxDigit > pow10i64Len {
return 0, fmt.Errorf("invalid length of number")
}
sum := int64(0)
for i := 0; i < maxDigit; i++ {
c := int64(b[i]) - 48
digitValue := pow10i64[maxDigit-i-1]
sum += c * digitValue
}
if isNegative {
return -1 * sum, nil
}
return sum, nil
}
var (
numTable = [256]bool{
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
}
)
var (
numZeroBuf = []byte{'0'}
)
func (d *intDecoder) decodeStreamByte(s *Stream) ([]byte, error) {
for {
switch s.char() {
case ' ', '\n', '\t', '\r':
s.cursor++
continue
case '-':
start := s.cursor
for {
s.cursor++
if numTable[s.char()] {
continue
} else if s.char() == nul {
if s.read() {
s.cursor-- // for retry current character
continue
}
}
break
}
num := s.buf[start:s.cursor]
if len(num) < 2 {
goto ERROR
}
return num, nil
case '0':
s.cursor++
return numZeroBuf, nil
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
start := s.cursor
for {
s.cursor++
if numTable[s.char()] {
continue
} else if s.char() == nul {
if s.read() {
s.cursor-- // for retry current character
continue
}
}
break
}
num := s.buf[start:s.cursor]
return num, nil
case 'n':
if err := nullBytes(s); err != nil {
return nil, err
}
return nil, nil
case nul:
if s.read() {
continue
}
goto ERROR
default:
return nil, d.typeError([]byte{s.char()}, s.totalOffset())
}
}
ERROR:
return nil, errors.ErrUnexpectedEndOfJSON("number(integer)", s.totalOffset())
}
func (d *intDecoder) decodeByte(buf []byte, cursor int64) ([]byte, int64, error) {
b := (*sliceHeader)(unsafe.Pointer(&buf)).data
for {
switch char(b, cursor) {
case ' ', '\n', '\t', '\r':
cursor++
continue
case '0':
cursor++
return numZeroBuf, cursor, nil
case '-', '1', '2', '3', '4', '5', '6', '7', '8', '9':
start := cursor
cursor++
for numTable[char(b, cursor)] {
cursor++
}
num := buf[start:cursor]
return num, cursor, nil
case 'n':
if err := validateNull(buf, cursor); err != nil {
return nil, 0, err
}
cursor += 4
return nil, cursor, nil
default:
return nil, 0, d.typeError([]byte{char(b, cursor)}, cursor)
}
}
}
func (d *intDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
bytes, err := d.decodeStreamByte(s)
if err != nil {
return err
}
if bytes == nil {
return nil
}
i64, err := d.parseInt(bytes)
if err != nil {
return d.typeError(bytes, s.totalOffset())
}
switch d.kind {
case reflect.Int8:
if i64 < -1*(1<<7) || (1<<7) <= i64 {
return d.typeError(bytes, s.totalOffset())
}
case reflect.Int16:
if i64 < -1*(1<<15) || (1<<15) <= i64 {
return d.typeError(bytes, s.totalOffset())
}
case reflect.Int32:
if i64 < -1*(1<<31) || (1<<31) <= i64 {
return d.typeError(bytes, s.totalOffset())
}
}
d.op(p, i64)
s.reset()
return nil
}
func (d *intDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
bytes, c, err := d.decodeByte(ctx.Buf, cursor)
if err != nil {
return 0, err
}
if bytes == nil {
return c, nil
}
cursor = c
i64, err := d.parseInt(bytes)
if err != nil {
return 0, d.typeError(bytes, cursor)
}
switch d.kind {
case reflect.Int8:
if i64 < -1*(1<<7) || (1<<7) <= i64 {
return 0, d.typeError(bytes, cursor)
}
case reflect.Int16:
if i64 < -1*(1<<15) || (1<<15) <= i64 {
return 0, d.typeError(bytes, cursor)
}
case reflect.Int32:
if i64 < -1*(1<<31) || (1<<31) <= i64 {
return 0, d.typeError(bytes, cursor)
}
}
d.op(p, i64)
return cursor, nil
}
func (d *intDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return nil, 0, fmt.Errorf("json: int decoder does not support decode path")
}

View File

@ -0,0 +1,528 @@
package decoder
import (
"bytes"
"encoding"
"encoding/json"
"reflect"
"unsafe"
"github.com/goccy/go-json/internal/errors"
"github.com/goccy/go-json/internal/runtime"
)
type interfaceDecoder struct {
typ *runtime.Type
structName string
fieldName string
sliceDecoder *sliceDecoder
mapDecoder *mapDecoder
floatDecoder *floatDecoder
numberDecoder *numberDecoder
stringDecoder *stringDecoder
}
func newEmptyInterfaceDecoder(structName, fieldName string) *interfaceDecoder {
ifaceDecoder := &interfaceDecoder{
typ: emptyInterfaceType,
structName: structName,
fieldName: fieldName,
floatDecoder: newFloatDecoder(structName, fieldName, func(p unsafe.Pointer, v float64) {
*(*interface{})(p) = v
}),
numberDecoder: newNumberDecoder(structName, fieldName, func(p unsafe.Pointer, v json.Number) {
*(*interface{})(p) = v
}),
stringDecoder: newStringDecoder(structName, fieldName),
}
ifaceDecoder.sliceDecoder = newSliceDecoder(
ifaceDecoder,
emptyInterfaceType,
emptyInterfaceType.Size(),
structName, fieldName,
)
ifaceDecoder.mapDecoder = newMapDecoder(
interfaceMapType,
stringType,
ifaceDecoder.stringDecoder,
interfaceMapType.Elem(),
ifaceDecoder,
structName,
fieldName,
)
return ifaceDecoder
}
func newInterfaceDecoder(typ *runtime.Type, structName, fieldName string) *interfaceDecoder {
emptyIfaceDecoder := newEmptyInterfaceDecoder(structName, fieldName)
stringDecoder := newStringDecoder(structName, fieldName)
return &interfaceDecoder{
typ: typ,
structName: structName,
fieldName: fieldName,
sliceDecoder: newSliceDecoder(
emptyIfaceDecoder,
emptyInterfaceType,
emptyInterfaceType.Size(),
structName, fieldName,
),
mapDecoder: newMapDecoder(
interfaceMapType,
stringType,
stringDecoder,
interfaceMapType.Elem(),
emptyIfaceDecoder,
structName,
fieldName,
),
floatDecoder: newFloatDecoder(structName, fieldName, func(p unsafe.Pointer, v float64) {
*(*interface{})(p) = v
}),
numberDecoder: newNumberDecoder(structName, fieldName, func(p unsafe.Pointer, v json.Number) {
*(*interface{})(p) = v
}),
stringDecoder: stringDecoder,
}
}
func (d *interfaceDecoder) numDecoder(s *Stream) Decoder {
if s.UseNumber {
return d.numberDecoder
}
return d.floatDecoder
}
var (
emptyInterfaceType = runtime.Type2RType(reflect.TypeOf((*interface{})(nil)).Elem())
EmptyInterfaceType = emptyInterfaceType
interfaceMapType = runtime.Type2RType(
reflect.TypeOf((*map[string]interface{})(nil)).Elem(),
)
stringType = runtime.Type2RType(
reflect.TypeOf(""),
)
)
func decodeStreamUnmarshaler(s *Stream, depth int64, unmarshaler json.Unmarshaler) error {
start := s.cursor
if err := s.skipValue(depth); err != nil {
return err
}
src := s.buf[start:s.cursor]
dst := make([]byte, len(src))
copy(dst, src)
if err := unmarshaler.UnmarshalJSON(dst); err != nil {
return err
}
return nil
}
func decodeStreamUnmarshalerContext(s *Stream, depth int64, unmarshaler unmarshalerContext) error {
start := s.cursor
if err := s.skipValue(depth); err != nil {
return err
}
src := s.buf[start:s.cursor]
dst := make([]byte, len(src))
copy(dst, src)
if err := unmarshaler.UnmarshalJSON(s.Option.Context, dst); err != nil {
return err
}
return nil
}
func decodeUnmarshaler(buf []byte, cursor, depth int64, unmarshaler json.Unmarshaler) (int64, error) {
cursor = skipWhiteSpace(buf, cursor)
start := cursor
end, err := skipValue(buf, cursor, depth)
if err != nil {
return 0, err
}
src := buf[start:end]
dst := make([]byte, len(src))
copy(dst, src)
if err := unmarshaler.UnmarshalJSON(dst); err != nil {
return 0, err
}
return end, nil
}
func decodeUnmarshalerContext(ctx *RuntimeContext, buf []byte, cursor, depth int64, unmarshaler unmarshalerContext) (int64, error) {
cursor = skipWhiteSpace(buf, cursor)
start := cursor
end, err := skipValue(buf, cursor, depth)
if err != nil {
return 0, err
}
src := buf[start:end]
dst := make([]byte, len(src))
copy(dst, src)
if err := unmarshaler.UnmarshalJSON(ctx.Option.Context, dst); err != nil {
return 0, err
}
return end, nil
}
func decodeStreamTextUnmarshaler(s *Stream, depth int64, unmarshaler encoding.TextUnmarshaler, p unsafe.Pointer) error {
start := s.cursor
if err := s.skipValue(depth); err != nil {
return err
}
src := s.buf[start:s.cursor]
if bytes.Equal(src, nullbytes) {
*(*unsafe.Pointer)(p) = nil
return nil
}
dst := make([]byte, len(src))
copy(dst, src)
if err := unmarshaler.UnmarshalText(dst); err != nil {
return err
}
return nil
}
func decodeTextUnmarshaler(buf []byte, cursor, depth int64, unmarshaler encoding.TextUnmarshaler, p unsafe.Pointer) (int64, error) {
cursor = skipWhiteSpace(buf, cursor)
start := cursor
end, err := skipValue(buf, cursor, depth)
if err != nil {
return 0, err
}
src := buf[start:end]
if bytes.Equal(src, nullbytes) {
*(*unsafe.Pointer)(p) = nil
return end, nil
}
if s, ok := unquoteBytes(src); ok {
src = s
}
if err := unmarshaler.UnmarshalText(src); err != nil {
return 0, err
}
return end, nil
}
func (d *interfaceDecoder) decodeStreamEmptyInterface(s *Stream, depth int64, p unsafe.Pointer) error {
c := s.skipWhiteSpace()
for {
switch c {
case '{':
var v map[string]interface{}
ptr := unsafe.Pointer(&v)
if err := d.mapDecoder.DecodeStream(s, depth, ptr); err != nil {
return err
}
*(*interface{})(p) = v
return nil
case '[':
var v []interface{}
ptr := unsafe.Pointer(&v)
if err := d.sliceDecoder.DecodeStream(s, depth, ptr); err != nil {
return err
}
*(*interface{})(p) = v
return nil
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return d.numDecoder(s).DecodeStream(s, depth, p)
case '"':
s.cursor++
start := s.cursor
for {
switch s.char() {
case '\\':
if _, err := decodeEscapeString(s, nil); err != nil {
return err
}
case '"':
literal := s.buf[start:s.cursor]
s.cursor++
*(*interface{})(p) = string(literal)
return nil
case nul:
if s.read() {
continue
}
return errors.ErrUnexpectedEndOfJSON("string", s.totalOffset())
}
s.cursor++
}
case 't':
if err := trueBytes(s); err != nil {
return err
}
**(**interface{})(unsafe.Pointer(&p)) = true
return nil
case 'f':
if err := falseBytes(s); err != nil {
return err
}
**(**interface{})(unsafe.Pointer(&p)) = false
return nil
case 'n':
if err := nullBytes(s); err != nil {
return err
}
*(*interface{})(p) = nil
return nil
case nul:
if s.read() {
c = s.char()
continue
}
}
break
}
return errors.ErrInvalidBeginningOfValue(c, s.totalOffset())
}
type emptyInterface struct {
typ *runtime.Type
ptr unsafe.Pointer
}
func (d *interfaceDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
runtimeInterfaceValue := *(*interface{})(unsafe.Pointer(&emptyInterface{
typ: d.typ,
ptr: p,
}))
rv := reflect.ValueOf(runtimeInterfaceValue)
if rv.NumMethod() > 0 && rv.CanInterface() {
if u, ok := rv.Interface().(unmarshalerContext); ok {
return decodeStreamUnmarshalerContext(s, depth, u)
}
if u, ok := rv.Interface().(json.Unmarshaler); ok {
return decodeStreamUnmarshaler(s, depth, u)
}
if u, ok := rv.Interface().(encoding.TextUnmarshaler); ok {
return decodeStreamTextUnmarshaler(s, depth, u, p)
}
if s.skipWhiteSpace() == 'n' {
if err := nullBytes(s); err != nil {
return err
}
*(*interface{})(p) = nil
return nil
}
return d.errUnmarshalType(rv.Type(), s.totalOffset())
}
iface := rv.Interface()
ifaceHeader := (*emptyInterface)(unsafe.Pointer(&iface))
typ := ifaceHeader.typ
if ifaceHeader.ptr == nil || d.typ == typ || typ == nil {
// concrete type is empty interface
return d.decodeStreamEmptyInterface(s, depth, p)
}
if typ.Kind() == reflect.Ptr && typ.Elem() == d.typ || typ.Kind() != reflect.Ptr {
return d.decodeStreamEmptyInterface(s, depth, p)
}
if s.skipWhiteSpace() == 'n' {
if err := nullBytes(s); err != nil {
return err
}
*(*interface{})(p) = nil
return nil
}
decoder, err := CompileToGetDecoder(typ)
if err != nil {
return err
}
return decoder.DecodeStream(s, depth, ifaceHeader.ptr)
}
func (d *interfaceDecoder) errUnmarshalType(typ reflect.Type, offset int64) *errors.UnmarshalTypeError {
return &errors.UnmarshalTypeError{
Value: typ.String(),
Type: typ,
Offset: offset,
Struct: d.structName,
Field: d.fieldName,
}
}
func (d *interfaceDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
buf := ctx.Buf
runtimeInterfaceValue := *(*interface{})(unsafe.Pointer(&emptyInterface{
typ: d.typ,
ptr: p,
}))
rv := reflect.ValueOf(runtimeInterfaceValue)
if rv.NumMethod() > 0 && rv.CanInterface() {
if u, ok := rv.Interface().(unmarshalerContext); ok {
return decodeUnmarshalerContext(ctx, buf, cursor, depth, u)
}
if u, ok := rv.Interface().(json.Unmarshaler); ok {
return decodeUnmarshaler(buf, cursor, depth, u)
}
if u, ok := rv.Interface().(encoding.TextUnmarshaler); ok {
return decodeTextUnmarshaler(buf, cursor, depth, u, p)
}
cursor = skipWhiteSpace(buf, cursor)
if buf[cursor] == 'n' {
if err := validateNull(buf, cursor); err != nil {
return 0, err
}
cursor += 4
**(**interface{})(unsafe.Pointer(&p)) = nil
return cursor, nil
}
return 0, d.errUnmarshalType(rv.Type(), cursor)
}
iface := rv.Interface()
ifaceHeader := (*emptyInterface)(unsafe.Pointer(&iface))
typ := ifaceHeader.typ
if ifaceHeader.ptr == nil || d.typ == typ || typ == nil {
// concrete type is empty interface
return d.decodeEmptyInterface(ctx, cursor, depth, p)
}
if typ.Kind() == reflect.Ptr && typ.Elem() == d.typ || typ.Kind() != reflect.Ptr {
return d.decodeEmptyInterface(ctx, cursor, depth, p)
}
cursor = skipWhiteSpace(buf, cursor)
if buf[cursor] == 'n' {
if err := validateNull(buf, cursor); err != nil {
return 0, err
}
cursor += 4
**(**interface{})(unsafe.Pointer(&p)) = nil
return cursor, nil
}
decoder, err := CompileToGetDecoder(typ)
if err != nil {
return 0, err
}
return decoder.Decode(ctx, cursor, depth, ifaceHeader.ptr)
}
func (d *interfaceDecoder) decodeEmptyInterface(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
buf := ctx.Buf
cursor = skipWhiteSpace(buf, cursor)
switch buf[cursor] {
case '{':
var v map[string]interface{}
ptr := unsafe.Pointer(&v)
cursor, err := d.mapDecoder.Decode(ctx, cursor, depth, ptr)
if err != nil {
return 0, err
}
**(**interface{})(unsafe.Pointer(&p)) = v
return cursor, nil
case '[':
var v []interface{}
ptr := unsafe.Pointer(&v)
cursor, err := d.sliceDecoder.Decode(ctx, cursor, depth, ptr)
if err != nil {
return 0, err
}
**(**interface{})(unsafe.Pointer(&p)) = v
return cursor, nil
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return d.floatDecoder.Decode(ctx, cursor, depth, p)
case '"':
var v string
ptr := unsafe.Pointer(&v)
cursor, err := d.stringDecoder.Decode(ctx, cursor, depth, ptr)
if err != nil {
return 0, err
}
**(**interface{})(unsafe.Pointer(&p)) = v
return cursor, nil
case 't':
if err := validateTrue(buf, cursor); err != nil {
return 0, err
}
cursor += 4
**(**interface{})(unsafe.Pointer(&p)) = true
return cursor, nil
case 'f':
if err := validateFalse(buf, cursor); err != nil {
return 0, err
}
cursor += 5
**(**interface{})(unsafe.Pointer(&p)) = false
return cursor, nil
case 'n':
if err := validateNull(buf, cursor); err != nil {
return 0, err
}
cursor += 4
**(**interface{})(unsafe.Pointer(&p)) = nil
return cursor, nil
}
return cursor, errors.ErrInvalidBeginningOfValue(buf[cursor], cursor)
}
func NewPathDecoder() Decoder {
ifaceDecoder := &interfaceDecoder{
typ: emptyInterfaceType,
structName: "",
fieldName: "",
floatDecoder: newFloatDecoder("", "", func(p unsafe.Pointer, v float64) {
*(*interface{})(p) = v
}),
numberDecoder: newNumberDecoder("", "", func(p unsafe.Pointer, v json.Number) {
*(*interface{})(p) = v
}),
stringDecoder: newStringDecoder("", ""),
}
ifaceDecoder.sliceDecoder = newSliceDecoder(
ifaceDecoder,
emptyInterfaceType,
emptyInterfaceType.Size(),
"", "",
)
ifaceDecoder.mapDecoder = newMapDecoder(
interfaceMapType,
stringType,
ifaceDecoder.stringDecoder,
interfaceMapType.Elem(),
ifaceDecoder,
"", "",
)
return ifaceDecoder
}
var (
truebytes = []byte("true")
falsebytes = []byte("false")
)
func (d *interfaceDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
buf := ctx.Buf
cursor = skipWhiteSpace(buf, cursor)
switch buf[cursor] {
case '{':
return d.mapDecoder.DecodePath(ctx, cursor, depth)
case '[':
return d.sliceDecoder.DecodePath(ctx, cursor, depth)
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return d.floatDecoder.DecodePath(ctx, cursor, depth)
case '"':
return d.stringDecoder.DecodePath(ctx, cursor, depth)
case 't':
if err := validateTrue(buf, cursor); err != nil {
return nil, 0, err
}
cursor += 4
return [][]byte{truebytes}, cursor, nil
case 'f':
if err := validateFalse(buf, cursor); err != nil {
return nil, 0, err
}
cursor += 5
return [][]byte{falsebytes}, cursor, nil
case 'n':
if err := validateNull(buf, cursor); err != nil {
return nil, 0, err
}
cursor += 4
return [][]byte{nullbytes}, cursor, nil
}
return nil, cursor, errors.ErrInvalidBeginningOfValue(buf[cursor], cursor)
}

View File

@ -0,0 +1,55 @@
package decoder
import (
"reflect"
"unsafe"
"github.com/goccy/go-json/internal/errors"
"github.com/goccy/go-json/internal/runtime"
)
type invalidDecoder struct {
typ *runtime.Type
kind reflect.Kind
structName string
fieldName string
}
func newInvalidDecoder(typ *runtime.Type, structName, fieldName string) *invalidDecoder {
return &invalidDecoder{
typ: typ,
kind: typ.Kind(),
structName: structName,
fieldName: fieldName,
}
}
func (d *invalidDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
return &errors.UnmarshalTypeError{
Value: "object",
Type: runtime.RType2Type(d.typ),
Offset: s.totalOffset(),
Struct: d.structName,
Field: d.fieldName,
}
}
func (d *invalidDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
return 0, &errors.UnmarshalTypeError{
Value: "object",
Type: runtime.RType2Type(d.typ),
Offset: cursor,
Struct: d.structName,
Field: d.fieldName,
}
}
func (d *invalidDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return nil, 0, &errors.UnmarshalTypeError{
Value: "object",
Type: runtime.RType2Type(d.typ),
Offset: cursor,
Struct: d.structName,
Field: d.fieldName,
}
}

280
vendor/github.com/goccy/go-json/internal/decoder/map.go generated vendored Normal file
View File

@ -0,0 +1,280 @@
package decoder
import (
"reflect"
"unsafe"
"github.com/goccy/go-json/internal/errors"
"github.com/goccy/go-json/internal/runtime"
)
type mapDecoder struct {
mapType *runtime.Type
keyType *runtime.Type
valueType *runtime.Type
canUseAssignFaststrType bool
keyDecoder Decoder
valueDecoder Decoder
structName string
fieldName string
}
func newMapDecoder(mapType *runtime.Type, keyType *runtime.Type, keyDec Decoder, valueType *runtime.Type, valueDec Decoder, structName, fieldName string) *mapDecoder {
return &mapDecoder{
mapType: mapType,
keyDecoder: keyDec,
keyType: keyType,
canUseAssignFaststrType: canUseAssignFaststrType(keyType, valueType),
valueType: valueType,
valueDecoder: valueDec,
structName: structName,
fieldName: fieldName,
}
}
const (
mapMaxElemSize = 128
)
// See detail: https://github.com/goccy/go-json/pull/283
func canUseAssignFaststrType(key *runtime.Type, value *runtime.Type) bool {
indirectElem := value.Size() > mapMaxElemSize
if indirectElem {
return false
}
return key.Kind() == reflect.String
}
//go:linkname makemap reflect.makemap
func makemap(*runtime.Type, int) unsafe.Pointer
//nolint:golint
//go:linkname mapassign_faststr runtime.mapassign_faststr
//go:noescape
func mapassign_faststr(t *runtime.Type, m unsafe.Pointer, s string) unsafe.Pointer
//go:linkname mapassign reflect.mapassign
//go:noescape
func mapassign(t *runtime.Type, m unsafe.Pointer, k, v unsafe.Pointer)
func (d *mapDecoder) mapassign(t *runtime.Type, m, k, v unsafe.Pointer) {
if d.canUseAssignFaststrType {
mapV := mapassign_faststr(t, m, *(*string)(k))
typedmemmove(d.valueType, mapV, v)
} else {
mapassign(t, m, k, v)
}
}
func (d *mapDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
depth++
if depth > maxDecodeNestingDepth {
return errors.ErrExceededMaxDepth(s.char(), s.cursor)
}
switch s.skipWhiteSpace() {
case 'n':
if err := nullBytes(s); err != nil {
return err
}
**(**unsafe.Pointer)(unsafe.Pointer(&p)) = nil
return nil
case '{':
default:
return errors.ErrExpected("{ character for map value", s.totalOffset())
}
mapValue := *(*unsafe.Pointer)(p)
if mapValue == nil {
mapValue = makemap(d.mapType, 0)
}
s.cursor++
if s.skipWhiteSpace() == '}' {
*(*unsafe.Pointer)(p) = mapValue
s.cursor++
return nil
}
for {
k := unsafe_New(d.keyType)
if err := d.keyDecoder.DecodeStream(s, depth, k); err != nil {
return err
}
s.skipWhiteSpace()
if !s.equalChar(':') {
return errors.ErrExpected("colon after object key", s.totalOffset())
}
s.cursor++
v := unsafe_New(d.valueType)
if err := d.valueDecoder.DecodeStream(s, depth, v); err != nil {
return err
}
d.mapassign(d.mapType, mapValue, k, v)
s.skipWhiteSpace()
if s.equalChar('}') {
**(**unsafe.Pointer)(unsafe.Pointer(&p)) = mapValue
s.cursor++
return nil
}
if !s.equalChar(',') {
return errors.ErrExpected("comma after object value", s.totalOffset())
}
s.cursor++
}
}
func (d *mapDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
buf := ctx.Buf
depth++
if depth > maxDecodeNestingDepth {
return 0, errors.ErrExceededMaxDepth(buf[cursor], cursor)
}
cursor = skipWhiteSpace(buf, cursor)
buflen := int64(len(buf))
if buflen < 2 {
return 0, errors.ErrExpected("{} for map", cursor)
}
switch buf[cursor] {
case 'n':
if err := validateNull(buf, cursor); err != nil {
return 0, err
}
cursor += 4
**(**unsafe.Pointer)(unsafe.Pointer(&p)) = nil
return cursor, nil
case '{':
default:
return 0, errors.ErrExpected("{ character for map value", cursor)
}
cursor++
cursor = skipWhiteSpace(buf, cursor)
mapValue := *(*unsafe.Pointer)(p)
if mapValue == nil {
mapValue = makemap(d.mapType, 0)
}
if buf[cursor] == '}' {
**(**unsafe.Pointer)(unsafe.Pointer(&p)) = mapValue
cursor++
return cursor, nil
}
for {
k := unsafe_New(d.keyType)
keyCursor, err := d.keyDecoder.Decode(ctx, cursor, depth, k)
if err != nil {
return 0, err
}
cursor = skipWhiteSpace(buf, keyCursor)
if buf[cursor] != ':' {
return 0, errors.ErrExpected("colon after object key", cursor)
}
cursor++
v := unsafe_New(d.valueType)
valueCursor, err := d.valueDecoder.Decode(ctx, cursor, depth, v)
if err != nil {
return 0, err
}
d.mapassign(d.mapType, mapValue, k, v)
cursor = skipWhiteSpace(buf, valueCursor)
if buf[cursor] == '}' {
**(**unsafe.Pointer)(unsafe.Pointer(&p)) = mapValue
cursor++
return cursor, nil
}
if buf[cursor] != ',' {
return 0, errors.ErrExpected("comma after object value", cursor)
}
cursor++
}
}
func (d *mapDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
buf := ctx.Buf
depth++
if depth > maxDecodeNestingDepth {
return nil, 0, errors.ErrExceededMaxDepth(buf[cursor], cursor)
}
cursor = skipWhiteSpace(buf, cursor)
buflen := int64(len(buf))
if buflen < 2 {
return nil, 0, errors.ErrExpected("{} for map", cursor)
}
switch buf[cursor] {
case 'n':
if err := validateNull(buf, cursor); err != nil {
return nil, 0, err
}
cursor += 4
return [][]byte{nullbytes}, cursor, nil
case '{':
default:
return nil, 0, errors.ErrExpected("{ character for map value", cursor)
}
cursor++
cursor = skipWhiteSpace(buf, cursor)
if buf[cursor] == '}' {
cursor++
return nil, cursor, nil
}
keyDecoder, ok := d.keyDecoder.(*stringDecoder)
if !ok {
return nil, 0, &errors.UnmarshalTypeError{
Value: "string",
Type: reflect.TypeOf(""),
Offset: cursor,
Struct: d.structName,
Field: d.fieldName,
}
}
ret := [][]byte{}
for {
key, keyCursor, err := keyDecoder.decodeByte(buf, cursor)
if err != nil {
return nil, 0, err
}
cursor = skipWhiteSpace(buf, keyCursor)
if buf[cursor] != ':' {
return nil, 0, errors.ErrExpected("colon after object key", cursor)
}
cursor++
child, found, err := ctx.Option.Path.Field(string(key))
if err != nil {
return nil, 0, err
}
if found {
if child != nil {
oldPath := ctx.Option.Path.node
ctx.Option.Path.node = child
paths, c, err := d.valueDecoder.DecodePath(ctx, cursor, depth)
if err != nil {
return nil, 0, err
}
ctx.Option.Path.node = oldPath
ret = append(ret, paths...)
cursor = c
} else {
start := cursor
end, err := skipValue(buf, cursor, depth)
if err != nil {
return nil, 0, err
}
ret = append(ret, buf[start:end])
cursor = end
}
} else {
c, err := skipValue(buf, cursor, depth)
if err != nil {
return nil, 0, err
}
cursor = c
}
cursor = skipWhiteSpace(buf, cursor)
if buf[cursor] == '}' {
cursor++
return ret, cursor, nil
}
if buf[cursor] != ',' {
return nil, 0, errors.ErrExpected("comma after object value", cursor)
}
cursor++
}
}

View File

@ -0,0 +1,123 @@
package decoder
import (
"encoding/json"
"strconv"
"unsafe"
"github.com/goccy/go-json/internal/errors"
)
type numberDecoder struct {
stringDecoder *stringDecoder
op func(unsafe.Pointer, json.Number)
structName string
fieldName string
}
func newNumberDecoder(structName, fieldName string, op func(unsafe.Pointer, json.Number)) *numberDecoder {
return &numberDecoder{
stringDecoder: newStringDecoder(structName, fieldName),
op: op,
structName: structName,
fieldName: fieldName,
}
}
func (d *numberDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) error {
bytes, err := d.decodeStreamByte(s)
if err != nil {
return err
}
if _, err := strconv.ParseFloat(*(*string)(unsafe.Pointer(&bytes)), 64); err != nil {
return errors.ErrSyntax(err.Error(), s.totalOffset())
}
d.op(p, json.Number(string(bytes)))
s.reset()
return nil
}
func (d *numberDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe.Pointer) (int64, error) {
bytes, c, err := d.decodeByte(ctx.Buf, cursor)
if err != nil {
return 0, err
}
if _, err := strconv.ParseFloat(*(*string)(unsafe.Pointer(&bytes)), 64); err != nil {
return 0, errors.ErrSyntax(err.Error(), c)
}
cursor = c
s := *(*string)(unsafe.Pointer(&bytes))
d.op(p, json.Number(s))
return cursor, nil
}
func (d *numberDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
bytes, c, err := d.decodeByte(ctx.Buf, cursor)
if err != nil {
return nil, 0, err
}
if bytes == nil {
return [][]byte{nullbytes}, c, nil
}
return [][]byte{bytes}, c, nil
}
func (d *numberDecoder) decodeStreamByte(s *Stream) ([]byte, error) {
start := s.cursor
for {
switch s.char() {
case ' ', '\n', '\t', '\r':
s.cursor++
continue
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return floatBytes(s), nil
case 'n':
if err := nullBytes(s); err != nil {
return nil, err
}
return nil, nil
case '"':
return d.stringDecoder.decodeStreamByte(s)
case nul:
if s.read() {
continue
}
goto ERROR
default:
goto ERROR
}
}
ERROR:
if s.cursor == start {
return nil, errors.ErrInvalidBeginningOfValue(s.char(), s.totalOffset())
}
return nil, errors.ErrUnexpectedEndOfJSON("json.Number", s.totalOffset())
}
func (d *numberDecoder) decodeByte(buf []byte, cursor int64) ([]byte, int64, error) {
for {
switch buf[cursor] {
case ' ', '\n', '\t', '\r':
cursor++
continue
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
start := cursor
cursor++
for floatTable[buf[cursor]] {
cursor++
}
num := buf[start:cursor]
return num, cursor, nil
case 'n':
if err := validateNull(buf, cursor); err != nil {
return nil, 0, err
}
cursor += 4
return nil, cursor, nil
case '"':
return d.stringDecoder.decodeByte(buf, cursor)
default:
return nil, 0, errors.ErrUnexpectedEndOfJSON("json.Number", cursor)
}
}
}

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