Move to coreos/go-oidc/v3.
Move from gopkg.in/coreos/go-oidc.v2 module to github.com/coreos/go-oidc/v3, which is the current supported version of this module. This fixes #167. Signed-off-by: James Peach <jpeach@cloudflare.com>
This commit is contained in:
parent
5c6207debc
commit
0ef161e7d9
5
go.mod
5
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
|
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
|
||||||
github.com/coredns/coredns v1.8.7
|
github.com/coredns/coredns v1.8.7
|
||||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
||||||
|
github.com/coreos/go-oidc/v3 v3.1.0
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||||
|
@ -33,7 +34,6 @@ require (
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
|
||||||
github.com/prometheus/client_golang v1.11.0
|
github.com/prometheus/client_golang v1.11.0
|
||||||
github.com/prometheus/client_model v0.2.0
|
github.com/prometheus/client_model v0.2.0
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
github.com/prometheus/common v0.32.1 // indirect
|
||||||
|
@ -51,9 +51,8 @@ require (
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||||
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb // indirect
|
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb // indirect
|
||||||
google.golang.org/grpc v1.43.0 // indirect
|
google.golang.org/grpc v1.43.0 // indirect
|
||||||
gopkg.in/coreos/go-oidc.v2 v2.1.0
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible
|
zombiezen.com/go/capnproto2 v2.18.0+incompatible
|
||||||
|
|
11
go.sum
11
go.sum
|
@ -133,6 +133,8 @@ github.com/coredns/coredns v1.8.7 h1:wVMjAnyFnY7Mc18AFO+9qbGD6ODPtdVUIlzoWrHr3hk
|
||||||
github.com/coredns/coredns v1.8.7/go.mod h1:bFmbgEfeRz5aizL2VsQ5LRlsvJuXWkgG/MWG9zxqjVM=
|
github.com/coredns/coredns v1.8.7/go.mod h1:bFmbgEfeRz5aizL2VsQ5LRlsvJuXWkgG/MWG9zxqjVM=
|
||||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmqg700pbXhzvmoaXKyfxWRkjRym7Q=
|
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmqg700pbXhzvmoaXKyfxWRkjRym7Q=
|
||||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.1.0 h1:6avEvcdvTa1qYsOZ6I5PRkSYHzpTNWgKYmaJfaYbrRw=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
@ -474,8 +476,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
|
||||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
@ -674,6 +674,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
@ -1052,15 +1053,13 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/coreos/go-oidc.v2 v2.1.0 h1:E8PjVFdj/SLDKB0hvb70KTbMbYVHjqztiQdSkIg8E+I=
|
|
||||||
gopkg.in/coreos/go-oidc.v2 v2.1.0/go.mod h1:fYaTe2FS96wZZwR17YTDHwG+Mw6fmyqJNxN2eNCGPCI=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||||
gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A=
|
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||||
gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
"gopkg.in/coreos/go-oidc.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
// +build !golint
|
|
||||||
|
|
||||||
// Don't lint this file. We don't want to have to add a comment to each constant.
|
|
||||||
|
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
const (
|
|
||||||
// JOSE asymmetric signing algorithm values as defined by RFC 7518
|
// JOSE asymmetric signing algorithm values as defined by RFC 7518
|
||||||
//
|
//
|
||||||
// see: https://tools.ietf.org/html/rfc7518#section-3.1
|
// see: https://tools.ietf.org/html/rfc7518#section-3.1
|
||||||
|
const (
|
||||||
RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
|
RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
|
||||||
RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
|
RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
|
||||||
RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
|
RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
|
|
@ -9,56 +9,44 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pquerna/cachecontrol"
|
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// keysExpiryDelta is the allowed clock skew between a client and the OpenID Connect
|
|
||||||
// server.
|
|
||||||
//
|
|
||||||
// When keys expire, they are valid for this amount of time after.
|
|
||||||
//
|
|
||||||
// If the keys have not expired, and an ID Token claims it was signed by a key not in
|
|
||||||
// the cache, if and only if the keys expire in this amount of time, the keys will be
|
|
||||||
// updated.
|
|
||||||
const keysExpiryDelta = 30 * time.Second
|
|
||||||
|
|
||||||
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
|
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
|
||||||
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
|
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
|
||||||
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
|
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
|
||||||
// exposed for providers that don't support discovery or to prevent round trips to the
|
// exposed for providers that don't support discovery or to prevent round trips to the
|
||||||
// discovery URL.
|
// discovery URL.
|
||||||
//
|
//
|
||||||
// The returned KeySet is a long lived verifier that caches keys based on cache-control
|
// The returned KeySet is a long lived verifier that caches keys based on any
|
||||||
// headers. Reuse a common remote key set instead of creating new ones as needed.
|
// keys change. Reuse a common remote key set instead of creating new ones as needed.
|
||||||
//
|
func NewRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet {
|
||||||
// The behavior of the returned KeySet is undefined once the context is canceled.
|
|
||||||
func NewRemoteKeySet(ctx context.Context, jwksURL string) KeySet {
|
|
||||||
return newRemoteKeySet(ctx, jwksURL, time.Now)
|
return newRemoteKeySet(ctx, jwksURL, time.Now)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
|
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *RemoteKeySet {
|
||||||
if now == nil {
|
if now == nil {
|
||||||
now = time.Now
|
now = time.Now
|
||||||
}
|
}
|
||||||
return &remoteKeySet{jwksURL: jwksURL, ctx: ctx, now: now}
|
return &RemoteKeySet{jwksURL: jwksURL, ctx: cloneContext(ctx), now: now}
|
||||||
}
|
}
|
||||||
|
|
||||||
type remoteKeySet struct {
|
// RemoteKeySet is a KeySet implementation that validates JSON web tokens against
|
||||||
|
// a jwks_uri endpoint.
|
||||||
|
type RemoteKeySet struct {
|
||||||
jwksURL string
|
jwksURL string
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
|
|
||||||
// guard all other fields
|
// guard all other fields
|
||||||
mu sync.Mutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
// inflight suppresses parallel execution of updateKeys and allows
|
// inflight suppresses parallel execution of updateKeys and allows
|
||||||
// multiple goroutines to wait for its result.
|
// multiple goroutines to wait for its result.
|
||||||
inflight *inflight
|
inflight *inflight
|
||||||
|
|
||||||
// A set of cached keys and their expiry.
|
// A set of cached keys.
|
||||||
cachedKeys []jose.JSONWebKey
|
cachedKeys []jose.JSONWebKey
|
||||||
expiry time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// inflight is used to wait on some in-flight request from multiple goroutines.
|
// inflight is used to wait on some in-flight request from multiple goroutines.
|
||||||
|
@ -93,7 +81,12 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) {
|
||||||
return i.keys, i.err
|
return i.keys, i.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
|
// VerifySignature validates a payload against a signature from the jwks_uri.
|
||||||
|
//
|
||||||
|
// Users MUST NOT call this method directly and should use an IDTokenVerifier
|
||||||
|
// instead. This method skips critical validations such as 'alg' values and is
|
||||||
|
// only exported to implement the KeySet interface.
|
||||||
|
func (r *RemoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
|
||||||
jws, err := jose.ParseSigned(jwt)
|
jws, err := jose.ParseSigned(jwt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
||||||
|
@ -101,7 +94,7 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte,
|
||||||
return r.verify(ctx, jws)
|
return r.verify(ctx, jws)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
func (r *RemoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
||||||
// We don't support JWTs signed with multiple signatures.
|
// We don't support JWTs signed with multiple signatures.
|
||||||
keyID := ""
|
keyID := ""
|
||||||
for _, sig := range jws.Signatures {
|
for _, sig := range jws.Signatures {
|
||||||
|
@ -109,9 +102,7 @@ func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) (
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
keys, expiry := r.keysFromCache()
|
keys := r.keysFromCache()
|
||||||
|
|
||||||
// Don't check expiry yet. This optimizes for when the provider is unavailable.
|
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if keyID == "" || key.KeyID == keyID {
|
if keyID == "" || key.KeyID == keyID {
|
||||||
if payload, err := jws.Verify(&key); err == nil {
|
if payload, err := jws.Verify(&key); err == nil {
|
||||||
|
@ -120,11 +111,10 @@ func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.now().Add(keysExpiryDelta).After(expiry) {
|
// If the kid doesn't match, check for new keys from the remote. This is the
|
||||||
// Keys haven't expired, don't refresh.
|
// strategy recommended by the spec.
|
||||||
return nil, errors.New("failed to verify id token signature")
|
//
|
||||||
}
|
// https://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys
|
||||||
|
|
||||||
keys, err := r.keysFromRemote(ctx)
|
keys, err := r.keysFromRemote(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetching keys %v", err)
|
return nil, fmt.Errorf("fetching keys %v", err)
|
||||||
|
@ -140,15 +130,15 @@ func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) (
|
||||||
return nil, errors.New("failed to verify id token signature")
|
return nil, errors.New("failed to verify id token signature")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey, expiry time.Time) {
|
func (r *RemoteKeySet) keysFromCache() (keys []jose.JSONWebKey) {
|
||||||
r.mu.Lock()
|
r.mu.RLock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.RUnlock()
|
||||||
return r.cachedKeys, r.expiry
|
return r.cachedKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
// keysFromRemote syncs the key set from the remote set, records the values in the
|
// keysFromRemote syncs the key set from the remote set, records the values in the
|
||||||
// cache, and returns the key set.
|
// cache, and returns the key set.
|
||||||
func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
|
func (r *RemoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
|
||||||
// Need to lock to inspect the inflight request field.
|
// Need to lock to inspect the inflight request field.
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
// If there's not a current inflight request, create one.
|
// If there's not a current inflight request, create one.
|
||||||
|
@ -160,7 +150,7 @@ func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e
|
||||||
// once the goroutine is done.
|
// once the goroutine is done.
|
||||||
go func() {
|
go func() {
|
||||||
// Sync keys and finish inflight when that's done.
|
// Sync keys and finish inflight when that's done.
|
||||||
keys, expiry, err := r.updateKeys()
|
keys, err := r.updateKeys()
|
||||||
|
|
||||||
r.inflight.done(keys, err)
|
r.inflight.done(keys, err)
|
||||||
|
|
||||||
|
@ -171,7 +161,6 @@ func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
r.cachedKeys = keys
|
r.cachedKeys = keys
|
||||||
r.expiry = expiry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free inflight so a different request can run.
|
// Free inflight so a different request can run.
|
||||||
|
@ -189,40 +178,31 @@ func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) updateKeys() ([]jose.JSONWebKey, time.Time, error) {
|
func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) {
|
||||||
req, err := http.NewRequest("GET", r.jwksURL, nil)
|
req, err := http.NewRequest("GET", r.jwksURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, time.Time{}, fmt.Errorf("oidc: can't create request: %v", err)
|
return nil, fmt.Errorf("oidc: can't create request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := doRequest(r.ctx, req)
|
resp, err := doRequest(r.ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, time.Time{}, fmt.Errorf("oidc: get keys failed %v", err)
|
return nil, fmt.Errorf("oidc: get keys failed %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, time.Time{}, fmt.Errorf("unable to read response body: %v", err)
|
return nil, fmt.Errorf("unable to read response body: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, time.Time{}, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
|
return nil, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keySet jose.JSONWebKeySet
|
var keySet jose.JSONWebKeySet
|
||||||
err = unmarshalResp(resp, body, &keySet)
|
err = unmarshalResp(resp, body, &keySet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, time.Time{}, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)
|
return nil, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)
|
||||||
}
|
}
|
||||||
|
return keySet.Keys, nil
|
||||||
// If the server doesn't provide cache control headers, assume the
|
|
||||||
// keys expire immediately.
|
|
||||||
expiry := r.now()
|
|
||||||
|
|
||||||
_, e, err := cachecontrol.CachableResponse(req, resp, cachecontrol.Options{})
|
|
||||||
if err == nil && e.After(expiry) {
|
|
||||||
expiry = e
|
|
||||||
}
|
|
||||||
return keySet.Keys, expiry, nil
|
|
||||||
}
|
}
|
121
vendor/gopkg.in/coreos/go-oidc.v2/oidc.go → vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go
generated
vendored
121
vendor/gopkg.in/coreos/go-oidc.v2/oidc.go → vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go
generated
vendored
|
@ -17,7 +17,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -40,6 +39,10 @@ var (
|
||||||
errInvalidAtHash = errors.New("access token hash does not match value in ID token")
|
errInvalidAtHash = errors.New("access token hash does not match value in ID token")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
var issuerURLKey contextKey
|
||||||
|
|
||||||
// ClientContext returns a new Context that carries the provided HTTP client.
|
// ClientContext returns a new Context that carries the provided HTTP client.
|
||||||
//
|
//
|
||||||
// This method sets the same context key used by the golang.org/x/oauth2 package,
|
// This method sets the same context key used by the golang.org/x/oauth2 package,
|
||||||
|
@ -55,6 +58,36 @@ func ClientContext(ctx context.Context, client *http.Client) context.Context {
|
||||||
return context.WithValue(ctx, oauth2.HTTPClient, client)
|
return context.WithValue(ctx, oauth2.HTTPClient, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cloneContext copies a context's bag-of-values into a new context that isn't
|
||||||
|
// associated with its cancellation. This is used to initialize remote keys sets
|
||||||
|
// which run in the background and aren't associated with the initial context.
|
||||||
|
func cloneContext(ctx context.Context) context.Context {
|
||||||
|
cp := context.Background()
|
||||||
|
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
|
||||||
|
cp = ClientContext(cp, c)
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsecureIssuerURLContext allows discovery to work when the issuer_url reported
|
||||||
|
// by upstream is mismatched with the discovery URL. This is meant for integration
|
||||||
|
// with off-spec providers such as Azure.
|
||||||
|
//
|
||||||
|
// discoveryBaseURL := "https://login.microsoftonline.com/organizations/v2.0"
|
||||||
|
// issuerURL := "https://login.microsoftonline.com/my-tenantid/v2.0"
|
||||||
|
//
|
||||||
|
// ctx := oidc.InsecureIssuerURLContext(parentContext, issuerURL)
|
||||||
|
//
|
||||||
|
// // Provider will be discovered with the discoveryBaseURL, but use issuerURL
|
||||||
|
// // for future issuer validation.
|
||||||
|
// provider, err := oidc.NewProvider(ctx, discoveryBaseURL)
|
||||||
|
//
|
||||||
|
// This is insecure because validating the correct issuer is critical for multi-tenant
|
||||||
|
// proivders. Any overrides here MUST be carefully reviewed.
|
||||||
|
func InsecureIssuerURLContext(ctx context.Context, issuerURL string) context.Context {
|
||||||
|
return context.WithValue(ctx, issuerURLKey, issuerURL)
|
||||||
|
}
|
||||||
|
|
||||||
func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||||
client := http.DefaultClient
|
client := http.DefaultClient
|
||||||
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
|
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
|
||||||
|
@ -69,6 +102,7 @@ type Provider struct {
|
||||||
authURL string
|
authURL string
|
||||||
tokenURL string
|
tokenURL string
|
||||||
userInfoURL string
|
userInfoURL string
|
||||||
|
algorithms []string
|
||||||
|
|
||||||
// Raw claims returned by the server.
|
// Raw claims returned by the server.
|
||||||
rawClaims []byte
|
rawClaims []byte
|
||||||
|
@ -76,17 +110,28 @@ type Provider struct {
|
||||||
remoteKeySet KeySet
|
remoteKeySet KeySet
|
||||||
}
|
}
|
||||||
|
|
||||||
type cachedKeys struct {
|
|
||||||
keys []jose.JSONWebKey
|
|
||||||
expiry time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type providerJSON struct {
|
type providerJSON struct {
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
AuthURL string `json:"authorization_endpoint"`
|
AuthURL string `json:"authorization_endpoint"`
|
||||||
TokenURL string `json:"token_endpoint"`
|
TokenURL string `json:"token_endpoint"`
|
||||||
JWKSURL string `json:"jwks_uri"`
|
JWKSURL string `json:"jwks_uri"`
|
||||||
UserInfoURL string `json:"userinfo_endpoint"`
|
UserInfoURL string `json:"userinfo_endpoint"`
|
||||||
|
Algorithms []string `json:"id_token_signing_alg_values_supported"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedAlgorithms is a list of algorithms explicitly supported by this
|
||||||
|
// package. If a provider supports other algorithms, such as HS256 or none,
|
||||||
|
// those values won't be passed to the IDTokenVerifier.
|
||||||
|
var supportedAlgorithms = map[string]bool{
|
||||||
|
RS256: true,
|
||||||
|
RS384: true,
|
||||||
|
RS512: true,
|
||||||
|
ES256: true,
|
||||||
|
ES384: true,
|
||||||
|
ES512: true,
|
||||||
|
PS256: true,
|
||||||
|
PS384: true,
|
||||||
|
PS512: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProvider uses the OpenID Connect discovery mechanism to construct a Provider.
|
// NewProvider uses the OpenID Connect discovery mechanism to construct a Provider.
|
||||||
|
@ -120,16 +165,27 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
|
||||||
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
|
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Issuer != issuer {
|
issuerURL, skipIssuerValidation := ctx.Value(issuerURLKey).(string)
|
||||||
|
if !skipIssuerValidation {
|
||||||
|
issuerURL = issuer
|
||||||
|
}
|
||||||
|
if p.Issuer != issuerURL && !skipIssuerValidation {
|
||||||
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
|
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
|
||||||
}
|
}
|
||||||
|
var algs []string
|
||||||
|
for _, a := range p.Algorithms {
|
||||||
|
if supportedAlgorithms[a] {
|
||||||
|
algs = append(algs, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
return &Provider{
|
return &Provider{
|
||||||
issuer: p.Issuer,
|
issuer: issuerURL,
|
||||||
authURL: p.AuthURL,
|
authURL: p.AuthURL,
|
||||||
tokenURL: p.TokenURL,
|
tokenURL: p.TokenURL,
|
||||||
userInfoURL: p.UserInfoURL,
|
userInfoURL: p.UserInfoURL,
|
||||||
|
algorithms: algs,
|
||||||
rawClaims: body,
|
rawClaims: body,
|
||||||
remoteKeySet: NewRemoteKeySet(ctx, p.JWKSURL),
|
remoteKeySet: NewRemoteKeySet(cloneContext(ctx), p.JWKSURL),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +224,16 @@ type UserInfo struct {
|
||||||
claims []byte
|
claims []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type userInfoRaw struct {
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
// Handle providers that return email_verified as a string
|
||||||
|
// https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁 and
|
||||||
|
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
|
||||||
|
EmailVerified stringAsBool `json:"email_verified"`
|
||||||
|
}
|
||||||
|
|
||||||
// Claims unmarshals the raw JSON object claims into the provided object.
|
// Claims unmarshals the raw JSON object claims into the provided object.
|
||||||
func (u *UserInfo) Claims(v interface{}) error {
|
func (u *UserInfo) Claims(v interface{}) error {
|
||||||
if u.claims == nil {
|
if u.claims == nil {
|
||||||
|
@ -206,12 +272,27 @@ func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource)
|
||||||
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
var userInfo UserInfo
|
ct := resp.Header.Get("Content-Type")
|
||||||
|
mediaType, _, parseErr := mime.ParseMediaType(ct)
|
||||||
|
if parseErr == nil && mediaType == "application/jwt" {
|
||||||
|
payload, err := p.remoteKeySet.VerifySignature(ctx, string(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc: invalid userinfo jwt signature %v", err)
|
||||||
|
}
|
||||||
|
body = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo userInfoRaw
|
||||||
if err := json.Unmarshal(body, &userInfo); err != nil {
|
if err := json.Unmarshal(body, &userInfo); err != nil {
|
||||||
return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err)
|
return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err)
|
||||||
}
|
}
|
||||||
userInfo.claims = body
|
return &UserInfo{
|
||||||
return &userInfo, nil
|
Subject: userInfo.Subject,
|
||||||
|
Profile: userInfo.Profile,
|
||||||
|
Email: userInfo.Email,
|
||||||
|
EmailVerified: bool(userInfo.EmailVerified),
|
||||||
|
claims: body,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDToken is an OpenID Connect extension that provides a predictable representation
|
// IDToken is an OpenID Connect extension that provides a predictable representation
|
||||||
|
@ -333,6 +414,20 @@ type claimSource struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type stringAsBool bool
|
||||||
|
|
||||||
|
func (sb *stringAsBool) UnmarshalJSON(b []byte) error {
|
||||||
|
switch string(b) {
|
||||||
|
case "true", `"true"`:
|
||||||
|
*sb = true
|
||||||
|
case "false", `"false"`:
|
||||||
|
*sb = false
|
||||||
|
default:
|
||||||
|
return errors.New("invalid value for boolean")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type audience []string
|
type audience []string
|
||||||
|
|
||||||
func (a *audience) UnmarshalJSON(b []byte) error {
|
func (a *audience) UnmarshalJSON(b []byte) error {
|
||||||
|
@ -345,7 +440,7 @@ func (a *audience) UnmarshalJSON(b []byte) error {
|
||||||
if err := json.Unmarshal(b, &auds); err != nil {
|
if err := json.Unmarshal(b, &auds); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*a = audience(auds)
|
*a = auds
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,9 @@ type Config struct {
|
||||||
ClientID string
|
ClientID string
|
||||||
// If specified, only this set of algorithms may be used to sign the JWT.
|
// If specified, only this set of algorithms may be used to sign the JWT.
|
||||||
//
|
//
|
||||||
// Since many providers only support RS256, SupportedSigningAlgs defaults to this value.
|
// If the IDTokenVerifier is created from a provider with (*Provider).Verifier, this
|
||||||
|
// defaults to the set of algorithms the provider supports. Otherwise this values
|
||||||
|
// defaults to RS256.
|
||||||
SupportedSigningAlgs []string
|
SupportedSigningAlgs []string
|
||||||
|
|
||||||
// If true, no ClientID check performed. Must be true if ClientID field is empty.
|
// If true, no ClientID check performed. Must be true if ClientID field is empty.
|
||||||
|
@ -105,6 +107,13 @@ type Config struct {
|
||||||
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
|
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
|
||||||
// undefined once the Provider's context is canceled.
|
// undefined once the Provider's context is canceled.
|
||||||
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
|
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
|
||||||
|
if len(config.SupportedSigningAlgs) == 0 && len(p.algorithms) > 0 {
|
||||||
|
// Make a copy so we don't modify the config values.
|
||||||
|
cp := &Config{}
|
||||||
|
*cp = *config
|
||||||
|
cp.SupportedSigningAlgs = p.algorithms
|
||||||
|
config = cp
|
||||||
|
}
|
||||||
return NewVerifier(p.issuer, p.remoteKeySet, config)
|
return NewVerifier(p.issuer, p.remoteKeySet, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,21 +171,7 @@ func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src
|
||||||
return token.claims, nil
|
return token.claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseClaim(raw []byte, name string, v interface{}) error {
|
// Verify parses a raw ID Token, verifies it's been signed by the provider, performs
|
||||||
var parsed map[string]json.RawMessage
|
|
||||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
val, ok := parsed[name]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("claim doesn't exist: %s", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Unmarshal([]byte(val), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify parses a raw ID Token, verifies it's been signed by the provider, preforms
|
|
||||||
// any additional checks depending on the Config, and returns the payload.
|
// any additional checks depending on the Config, and returns the payload.
|
||||||
//
|
//
|
||||||
// Verify does NOT do nonce validation, which is the callers responsibility.
|
// Verify does NOT do nonce validation, which is the callers responsibility.
|
|
@ -1,10 +0,0 @@
|
||||||
language: go
|
|
||||||
|
|
||||||
install:
|
|
||||||
- go get -d -v ./...
|
|
||||||
- go get -u github.com/stretchr/testify/require
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.7
|
|
||||||
- 1.8
|
|
||||||
- tip
|
|
|
@ -1,202 +0,0 @@
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
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.
|
|
|
@ -1,107 +0,0 @@
|
||||||
# cachecontrol: HTTP Caching Parser and Interpretation
|
|
||||||
|
|
||||||
[![GoDoc](https://godoc.org/github.com/pquerna/cachecontrol?status.svg)](https://godoc.org/github.com/pquerna/cachecontrol)[![Build Status](https://travis-ci.org/pquerna/cachecontrol.svg?branch=master)](https://travis-ci.org/pquerna/cachecontrol)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
`cachecontrol` implements [RFC 7234](http://tools.ietf.org/html/rfc7234) __Hypertext Transfer Protocol (HTTP/1.1): Caching__. It does this by parsing the `Cache-Control` and other headers, providing information about requests and responses -- but `cachecontrol` does not implement an actual cache backend, just the control plane to make decisions about if a particular response is cachable.
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
`cachecontrol.CachableResponse` returns an array of [reasons](https://godoc.org/github.com/pquerna/cachecontrol/cacheobject#Reason) why a response should not be cached and when it expires. In the case that `len(reasons) == 0`, the response is cachable according to the RFC. However, some people want non-compliant caches for various business use cases, so each reason is specifically named, so if your cache wants to cache `POST` requests, it can easily do that, but still be RFC compliant in other situations.
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
|
|
||||||
## Can you cache Example.com?
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pquerna/cachecontrol"
|
|
||||||
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
req, _ := http.NewRequest("GET", "http://www.example.com/", nil)
|
|
||||||
|
|
||||||
res, _ := http.DefaultClient.Do(req)
|
|
||||||
_, _ = ioutil.ReadAll(res.Body)
|
|
||||||
|
|
||||||
reasons, expires, _ := cachecontrol.CachableResponse(req, res, cachecontrol.Options{})
|
|
||||||
|
|
||||||
fmt.Println("Reasons to not cache: ", reasons)
|
|
||||||
fmt.Println("Expiration: ", expires.String())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Can I use this in a high performance caching server?
|
|
||||||
|
|
||||||
`cachecontrol` is divided into two packages: `cachecontrol` with a high level API, and a lower level `cacheobject` package. Use [Object](https://godoc.org/github.com/pquerna/cachecontrol/cacheobject#Object) in a high performance use case where you have previously parsed headers containing dates or would like to avoid memory allocations.
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pquerna/cachecontrol/cacheobject"
|
|
||||||
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
req, _ := http.NewRequest("GET", "http://www.example.com/", nil)
|
|
||||||
|
|
||||||
res, _ := http.DefaultClient.Do(req)
|
|
||||||
_, _ = ioutil.ReadAll(res.Body)
|
|
||||||
|
|
||||||
reqDir, _ := cacheobject.ParseRequestCacheControl(req.Header.Get("Cache-Control"))
|
|
||||||
|
|
||||||
resDir, _ := cacheobject.ParseResponseCacheControl(res.Header.Get("Cache-Control"))
|
|
||||||
expiresHeader, _ := http.ParseTime(res.Header.Get("Expires"))
|
|
||||||
dateHeader, _ := http.ParseTime(res.Header.Get("Date"))
|
|
||||||
lastModifiedHeader, _ := http.ParseTime(res.Header.Get("Last-Modified"))
|
|
||||||
|
|
||||||
obj := cacheobject.Object{
|
|
||||||
RespDirectives: resDir,
|
|
||||||
RespHeaders: res.Header,
|
|
||||||
RespStatusCode: res.StatusCode,
|
|
||||||
RespExpiresHeader: expiresHeader,
|
|
||||||
RespDateHeader: dateHeader,
|
|
||||||
RespLastModifiedHeader: lastModifiedHeader,
|
|
||||||
|
|
||||||
ReqDirectives: reqDir,
|
|
||||||
ReqHeaders: req.Header,
|
|
||||||
ReqMethod: req.Method,
|
|
||||||
|
|
||||||
NowUTC: time.Now().UTC(),
|
|
||||||
}
|
|
||||||
rv := cacheobject.ObjectResults{}
|
|
||||||
|
|
||||||
cacheobject.CachableObject(&obj, &rv)
|
|
||||||
cacheobject.ExpirationObject(&obj, &rv)
|
|
||||||
|
|
||||||
fmt.Println("Errors: ", rv.OutErr)
|
|
||||||
fmt.Println("Reasons to not cache: ", rv.OutReasons)
|
|
||||||
fmt.Println("Warning headers to add: ", rv.OutWarnings)
|
|
||||||
fmt.Println("Expiration: ", rv.OutExpirationTime.String())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Improvements, bugs, adding features, and taking cachecontrol new directions!
|
|
||||||
|
|
||||||
Please [open issues in Github](https://github.com/pquerna/cachecontrol/issues) for ideas, bugs, and general thoughts. Pull requests are of course preferred :)
|
|
||||||
|
|
||||||
# Credits
|
|
||||||
|
|
||||||
`cachecontrol` has recieved significant contributions from:
|
|
||||||
|
|
||||||
* [Paul Querna](https://github.com/pquerna)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
`cachecontrol` is licensed under the [Apache License, Version 2.0](./LICENSE)
|
|
|
@ -1,48 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2015 Paul Querna
|
|
||||||
*
|
|
||||||
* 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 cachecontrol
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pquerna/cachecontrol/cacheobject"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
// Set to True for a prviate cache, which is not shared amoung users (eg, in a browser)
|
|
||||||
// Set to False for a "shared" cache, which is more common in a server context.
|
|
||||||
PrivateCache bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given an HTTP Request, the future Status Code, and an ResponseWriter,
|
|
||||||
// determine the possible reasons a response SHOULD NOT be cached.
|
|
||||||
func CachableResponseWriter(req *http.Request,
|
|
||||||
statusCode int,
|
|
||||||
resp http.ResponseWriter,
|
|
||||||
opts Options) ([]cacheobject.Reason, time.Time, error) {
|
|
||||||
return cacheobject.UsingRequestResponse(req, statusCode, resp.Header(), opts.PrivateCache)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given an HTTP Request and Response, determine the possible reasons a response SHOULD NOT
|
|
||||||
// be cached.
|
|
||||||
func CachableResponse(req *http.Request,
|
|
||||||
resp *http.Response,
|
|
||||||
opts Options) ([]cacheobject.Reason, time.Time, error) {
|
|
||||||
return cacheobject.UsingRequestResponse(req, resp.StatusCode, resp.Header, opts.PrivateCache)
|
|
||||||
}
|
|
|
@ -1,546 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2015 Paul Querna
|
|
||||||
*
|
|
||||||
* 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 cacheobject
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"net/textproto"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO(pquerna): add extensions from here: http://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrQuoteMismatch = errors.New("Missing closing quote")
|
|
||||||
ErrMaxAgeDeltaSeconds = errors.New("Failed to parse delta-seconds in `max-age`")
|
|
||||||
ErrSMaxAgeDeltaSeconds = errors.New("Failed to parse delta-seconds in `s-maxage`")
|
|
||||||
ErrMaxStaleDeltaSeconds = errors.New("Failed to parse delta-seconds in `min-fresh`")
|
|
||||||
ErrMinFreshDeltaSeconds = errors.New("Failed to parse delta-seconds in `min-fresh`")
|
|
||||||
ErrNoCacheNoArgs = errors.New("Unexpected argument to `no-cache`")
|
|
||||||
ErrNoStoreNoArgs = errors.New("Unexpected argument to `no-store`")
|
|
||||||
ErrNoTransformNoArgs = errors.New("Unexpected argument to `no-transform`")
|
|
||||||
ErrOnlyIfCachedNoArgs = errors.New("Unexpected argument to `only-if-cached`")
|
|
||||||
ErrMustRevalidateNoArgs = errors.New("Unexpected argument to `must-revalidate`")
|
|
||||||
ErrPublicNoArgs = errors.New("Unexpected argument to `public`")
|
|
||||||
ErrProxyRevalidateNoArgs = errors.New("Unexpected argument to `proxy-revalidate`")
|
|
||||||
// Experimental
|
|
||||||
ErrImmutableNoArgs = errors.New("Unexpected argument to `immutable`")
|
|
||||||
ErrStaleIfErrorDeltaSeconds = errors.New("Failed to parse delta-seconds in `stale-if-error`")
|
|
||||||
ErrStaleWhileRevalidateDeltaSeconds = errors.New("Failed to parse delta-seconds in `stale-while-revalidate`")
|
|
||||||
)
|
|
||||||
|
|
||||||
func whitespace(b byte) bool {
|
|
||||||
if b == '\t' || b == ' ' {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func parse(value string, cd cacheDirective) error {
|
|
||||||
var err error = nil
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
for i < len(value) && err == nil {
|
|
||||||
// eat leading whitespace or commas
|
|
||||||
if whitespace(value[i]) || value[i] == ',' {
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
j := i + 1
|
|
||||||
|
|
||||||
for j < len(value) {
|
|
||||||
if !isToken(value[j]) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
|
|
||||||
token := strings.ToLower(value[i:j])
|
|
||||||
tokenHasFields := hasFieldNames(token)
|
|
||||||
/*
|
|
||||||
println("GOT TOKEN:")
|
|
||||||
println(" i -> ", i)
|
|
||||||
println(" j -> ", j)
|
|
||||||
println(" token -> ", token)
|
|
||||||
*/
|
|
||||||
|
|
||||||
if j+1 < len(value) && value[j] == '=' {
|
|
||||||
k := j + 1
|
|
||||||
// minimum size two bytes of "", but we let httpUnquote handle it.
|
|
||||||
if k < len(value) && value[k] == '"' {
|
|
||||||
eaten, result := httpUnquote(value[k:])
|
|
||||||
if eaten == -1 {
|
|
||||||
return ErrQuoteMismatch
|
|
||||||
}
|
|
||||||
i = k + eaten
|
|
||||||
|
|
||||||
err = cd.addPair(token, result)
|
|
||||||
} else {
|
|
||||||
z := k
|
|
||||||
for z < len(value) {
|
|
||||||
if tokenHasFields {
|
|
||||||
if whitespace(value[z]) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if whitespace(value[z]) || value[z] == ',' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
z++
|
|
||||||
}
|
|
||||||
i = z
|
|
||||||
|
|
||||||
result := value[k:z]
|
|
||||||
if result != "" && result[len(result)-1] == ',' {
|
|
||||||
result = result[:len(result)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cd.addPair(token, result)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if token != "," {
|
|
||||||
err = cd.addToken(token)
|
|
||||||
}
|
|
||||||
i = j
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeltaSeconds specifies a non-negative integer, representing
|
|
||||||
// time in seconds: http://tools.ietf.org/html/rfc7234#section-1.2.1
|
|
||||||
//
|
|
||||||
// When set to -1, this means unset.
|
|
||||||
//
|
|
||||||
type DeltaSeconds int32
|
|
||||||
|
|
||||||
// Parser for delta-seconds, a uint31, more or less:
|
|
||||||
// http://tools.ietf.org/html/rfc7234#section-1.2.1
|
|
||||||
func parseDeltaSeconds(v string) (DeltaSeconds, error) {
|
|
||||||
n, err := strconv.ParseUint(v, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
if numError, ok := err.(*strconv.NumError); ok {
|
|
||||||
if numError.Err == strconv.ErrRange {
|
|
||||||
return DeltaSeconds(math.MaxInt32), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DeltaSeconds(-1), err
|
|
||||||
} else {
|
|
||||||
if n > math.MaxInt32 {
|
|
||||||
return DeltaSeconds(math.MaxInt32), nil
|
|
||||||
} else {
|
|
||||||
return DeltaSeconds(n), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields present in a header.
|
|
||||||
type FieldNames map[string]bool
|
|
||||||
|
|
||||||
// internal interface for shared methods of RequestCacheDirectives and ResponseCacheDirectives
|
|
||||||
type cacheDirective interface {
|
|
||||||
addToken(s string) error
|
|
||||||
addPair(s string, v string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOW LEVEL API: Repersentation of possible request directives in a `Cache-Control` header: http://tools.ietf.org/html/rfc7234#section-5.2.1
|
|
||||||
//
|
|
||||||
// Note: Many fields will be `nil` in practice.
|
|
||||||
//
|
|
||||||
type RequestCacheDirectives struct {
|
|
||||||
|
|
||||||
// max-age(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.1
|
|
||||||
//
|
|
||||||
// The "max-age" request directive indicates that the client is
|
|
||||||
// unwilling to accept a response whose age is greater than the
|
|
||||||
// specified number of seconds. Unless the max-stale request directive
|
|
||||||
// is also present, the client is not willing to accept a stale
|
|
||||||
// response.
|
|
||||||
MaxAge DeltaSeconds
|
|
||||||
|
|
||||||
// max-stale(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.2
|
|
||||||
//
|
|
||||||
// The "max-stale" request directive indicates that the client is
|
|
||||||
// willing to accept a response that has exceeded its freshness
|
|
||||||
// lifetime. If max-stale is assigned a value, then the client is
|
|
||||||
// willing to accept a response that has exceeded its freshness lifetime
|
|
||||||
// by no more than the specified number of seconds. If no value is
|
|
||||||
// assigned to max-stale, then the client is willing to accept a stale
|
|
||||||
// response of any age.
|
|
||||||
MaxStale DeltaSeconds
|
|
||||||
|
|
||||||
// min-fresh(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.3
|
|
||||||
//
|
|
||||||
// The "min-fresh" request directive indicates that the client is
|
|
||||||
// willing to accept a response whose freshness lifetime is no less than
|
|
||||||
// its current age plus the specified time in seconds. That is, the
|
|
||||||
// client wants a response that will still be fresh for at least the
|
|
||||||
// specified number of seconds.
|
|
||||||
MinFresh DeltaSeconds
|
|
||||||
|
|
||||||
// no-cache(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.4
|
|
||||||
//
|
|
||||||
// The "no-cache" request directive indicates that a cache MUST NOT use
|
|
||||||
// a stored response to satisfy the request without successful
|
|
||||||
// validation on the origin server.
|
|
||||||
NoCache bool
|
|
||||||
|
|
||||||
// no-store(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.5
|
|
||||||
//
|
|
||||||
// The "no-store" request directive indicates that a cache MUST NOT
|
|
||||||
// store any part of either this request or any response to it. This
|
|
||||||
// directive applies to both private and shared caches.
|
|
||||||
NoStore bool
|
|
||||||
|
|
||||||
// no-transform(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.6
|
|
||||||
//
|
|
||||||
// The "no-transform" request directive indicates that an intermediary
|
|
||||||
// (whether or not it implements a cache) MUST NOT transform the
|
|
||||||
// payload, as defined in Section 5.7.2 of RFC7230.
|
|
||||||
NoTransform bool
|
|
||||||
|
|
||||||
// only-if-cached(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.7
|
|
||||||
//
|
|
||||||
// The "only-if-cached" request directive indicates that the client only
|
|
||||||
// wishes to obtain a stored response.
|
|
||||||
OnlyIfCached bool
|
|
||||||
|
|
||||||
// Extensions: http://tools.ietf.org/html/rfc7234#section-5.2.3
|
|
||||||
//
|
|
||||||
// The Cache-Control header field can be extended through the use of one
|
|
||||||
// or more cache-extension tokens, each with an optional value. A cache
|
|
||||||
// MUST ignore unrecognized cache directives.
|
|
||||||
Extensions []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *RequestCacheDirectives) addToken(token string) error {
|
|
||||||
var err error = nil
|
|
||||||
|
|
||||||
switch token {
|
|
||||||
case "max-age":
|
|
||||||
err = ErrMaxAgeDeltaSeconds
|
|
||||||
case "max-stale":
|
|
||||||
err = ErrMaxStaleDeltaSeconds
|
|
||||||
case "min-fresh":
|
|
||||||
err = ErrMinFreshDeltaSeconds
|
|
||||||
case "no-cache":
|
|
||||||
cd.NoCache = true
|
|
||||||
case "no-store":
|
|
||||||
cd.NoStore = true
|
|
||||||
case "no-transform":
|
|
||||||
cd.NoTransform = true
|
|
||||||
case "only-if-cached":
|
|
||||||
cd.OnlyIfCached = true
|
|
||||||
default:
|
|
||||||
cd.Extensions = append(cd.Extensions, token)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *RequestCacheDirectives) addPair(token string, v string) error {
|
|
||||||
var err error = nil
|
|
||||||
|
|
||||||
switch token {
|
|
||||||
case "max-age":
|
|
||||||
cd.MaxAge, err = parseDeltaSeconds(v)
|
|
||||||
if err != nil {
|
|
||||||
err = ErrMaxAgeDeltaSeconds
|
|
||||||
}
|
|
||||||
case "max-stale":
|
|
||||||
cd.MaxStale, err = parseDeltaSeconds(v)
|
|
||||||
if err != nil {
|
|
||||||
err = ErrMaxStaleDeltaSeconds
|
|
||||||
}
|
|
||||||
case "min-fresh":
|
|
||||||
cd.MinFresh, err = parseDeltaSeconds(v)
|
|
||||||
if err != nil {
|
|
||||||
err = ErrMinFreshDeltaSeconds
|
|
||||||
}
|
|
||||||
case "no-cache":
|
|
||||||
err = ErrNoCacheNoArgs
|
|
||||||
case "no-store":
|
|
||||||
err = ErrNoStoreNoArgs
|
|
||||||
case "no-transform":
|
|
||||||
err = ErrNoTransformNoArgs
|
|
||||||
case "only-if-cached":
|
|
||||||
err = ErrOnlyIfCachedNoArgs
|
|
||||||
default:
|
|
||||||
// TODO(pquerna): this sucks, making user re-parse
|
|
||||||
cd.Extensions = append(cd.Extensions, token+"="+v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOW LEVEL API: Parses a Cache Control Header from a Request into a set of directives.
|
|
||||||
func ParseRequestCacheControl(value string) (*RequestCacheDirectives, error) {
|
|
||||||
cd := &RequestCacheDirectives{
|
|
||||||
MaxAge: -1,
|
|
||||||
MaxStale: -1,
|
|
||||||
MinFresh: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := parse(value, cd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOW LEVEL API: Repersentation of possible response directives in a `Cache-Control` header: http://tools.ietf.org/html/rfc7234#section-5.2.2
|
|
||||||
//
|
|
||||||
// Note: Many fields will be `nil` in practice.
|
|
||||||
//
|
|
||||||
type ResponseCacheDirectives struct {
|
|
||||||
|
|
||||||
// must-revalidate(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.1
|
|
||||||
//
|
|
||||||
// The "must-revalidate" response directive indicates that once it has
|
|
||||||
// become stale, a cache MUST NOT use the response to satisfy subsequent
|
|
||||||
// requests without successful validation on the origin server.
|
|
||||||
MustRevalidate bool
|
|
||||||
|
|
||||||
// no-cache(FieldName): http://tools.ietf.org/html/rfc7234#section-5.2.2.2
|
|
||||||
//
|
|
||||||
// The "no-cache" response directive indicates that the response MUST
|
|
||||||
// NOT be used to satisfy a subsequent request without successful
|
|
||||||
// validation on the origin server.
|
|
||||||
//
|
|
||||||
// If the no-cache response directive specifies one or more field-names,
|
|
||||||
// then a cache MAY use the response to satisfy a subsequent request,
|
|
||||||
// subject to any other restrictions on caching. However, any header
|
|
||||||
// fields in the response that have the field-name(s) listed MUST NOT be
|
|
||||||
// sent in the response to a subsequent request without successful
|
|
||||||
// revalidation with the origin server.
|
|
||||||
NoCache FieldNames
|
|
||||||
|
|
||||||
// no-cache(cast-to-bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.2
|
|
||||||
//
|
|
||||||
// While the RFC defines optional field-names on a no-cache directive,
|
|
||||||
// many applications only want to know if any no-cache directives were
|
|
||||||
// present at all.
|
|
||||||
NoCachePresent bool
|
|
||||||
|
|
||||||
// no-store(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.3
|
|
||||||
//
|
|
||||||
// The "no-store" request directive indicates that a cache MUST NOT
|
|
||||||
// store any part of either this request or any response to it. This
|
|
||||||
// directive applies to both private and shared caches.
|
|
||||||
NoStore bool
|
|
||||||
|
|
||||||
// no-transform(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.4
|
|
||||||
//
|
|
||||||
// The "no-transform" response directive indicates that an intermediary
|
|
||||||
// (regardless of whether it implements a cache) MUST NOT transform the
|
|
||||||
// payload, as defined in Section 5.7.2 of RFC7230.
|
|
||||||
NoTransform bool
|
|
||||||
|
|
||||||
// public(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.5
|
|
||||||
//
|
|
||||||
// The "public" response directive indicates that any cache MAY store
|
|
||||||
// the response, even if the response would normally be non-cacheable or
|
|
||||||
// cacheable only within a private cache.
|
|
||||||
Public bool
|
|
||||||
|
|
||||||
// private(FieldName): http://tools.ietf.org/html/rfc7234#section-5.2.2.6
|
|
||||||
//
|
|
||||||
// The "private" response directive indicates that the response message
|
|
||||||
// is intended for a single user and MUST NOT be stored by a shared
|
|
||||||
// cache. A private cache MAY store the response and reuse it for later
|
|
||||||
// requests, even if the response would normally be non-cacheable.
|
|
||||||
//
|
|
||||||
// If the private response directive specifies one or more field-names,
|
|
||||||
// this requirement is limited to the field-values associated with the
|
|
||||||
// listed response header fields. That is, a shared cache MUST NOT
|
|
||||||
// store the specified field-names(s), whereas it MAY store the
|
|
||||||
// remainder of the response message.
|
|
||||||
Private FieldNames
|
|
||||||
|
|
||||||
// private(cast-to-bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.6
|
|
||||||
//
|
|
||||||
// While the RFC defines optional field-names on a private directive,
|
|
||||||
// many applications only want to know if any private directives were
|
|
||||||
// present at all.
|
|
||||||
PrivatePresent bool
|
|
||||||
|
|
||||||
// proxy-revalidate(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.7
|
|
||||||
//
|
|
||||||
// The "proxy-revalidate" response directive has the same meaning as the
|
|
||||||
// must-revalidate response directive, except that it does not apply to
|
|
||||||
// private caches.
|
|
||||||
ProxyRevalidate bool
|
|
||||||
|
|
||||||
// max-age(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.2.8
|
|
||||||
//
|
|
||||||
// The "max-age" response directive indicates that the response is to be
|
|
||||||
// considered stale after its age is greater than the specified number
|
|
||||||
// of seconds.
|
|
||||||
MaxAge DeltaSeconds
|
|
||||||
|
|
||||||
// s-maxage(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.2.9
|
|
||||||
//
|
|
||||||
// The "s-maxage" response directive indicates that, in shared caches,
|
|
||||||
// the maximum age specified by this directive overrides the maximum age
|
|
||||||
// specified by either the max-age directive or the Expires header
|
|
||||||
// field. The s-maxage directive also implies the semantics of the
|
|
||||||
// proxy-revalidate response directive.
|
|
||||||
SMaxAge DeltaSeconds
|
|
||||||
|
|
||||||
////
|
|
||||||
// Experimental features
|
|
||||||
// - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Extension_Cache-Control_directives
|
|
||||||
// - https://www.fastly.com/blog/stale-while-revalidate-stale-if-error-available-today
|
|
||||||
////
|
|
||||||
|
|
||||||
// immutable(cast-to-bool): experimental feature
|
|
||||||
Immutable bool
|
|
||||||
|
|
||||||
// stale-if-error(delta seconds): experimental feature
|
|
||||||
StaleIfError DeltaSeconds
|
|
||||||
|
|
||||||
// stale-while-revalidate(delta seconds): experimental feature
|
|
||||||
StaleWhileRevalidate DeltaSeconds
|
|
||||||
|
|
||||||
// Extensions: http://tools.ietf.org/html/rfc7234#section-5.2.3
|
|
||||||
//
|
|
||||||
// The Cache-Control header field can be extended through the use of one
|
|
||||||
// or more cache-extension tokens, each with an optional value. A cache
|
|
||||||
// MUST ignore unrecognized cache directives.
|
|
||||||
Extensions []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOW LEVEL API: Parses a Cache Control Header from a Response into a set of directives.
|
|
||||||
func ParseResponseCacheControl(value string) (*ResponseCacheDirectives, error) {
|
|
||||||
cd := &ResponseCacheDirectives{
|
|
||||||
MaxAge: -1,
|
|
||||||
SMaxAge: -1,
|
|
||||||
// Exerimantal stale timeouts
|
|
||||||
StaleIfError: -1,
|
|
||||||
StaleWhileRevalidate: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := parse(value, cd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *ResponseCacheDirectives) addToken(token string) error {
|
|
||||||
var err error = nil
|
|
||||||
switch token {
|
|
||||||
case "must-revalidate":
|
|
||||||
cd.MustRevalidate = true
|
|
||||||
case "no-cache":
|
|
||||||
cd.NoCachePresent = true
|
|
||||||
case "no-store":
|
|
||||||
cd.NoStore = true
|
|
||||||
case "no-transform":
|
|
||||||
cd.NoTransform = true
|
|
||||||
case "public":
|
|
||||||
cd.Public = true
|
|
||||||
case "private":
|
|
||||||
cd.PrivatePresent = true
|
|
||||||
case "proxy-revalidate":
|
|
||||||
cd.ProxyRevalidate = true
|
|
||||||
case "max-age":
|
|
||||||
err = ErrMaxAgeDeltaSeconds
|
|
||||||
case "s-maxage":
|
|
||||||
err = ErrSMaxAgeDeltaSeconds
|
|
||||||
// Experimental
|
|
||||||
case "immutable":
|
|
||||||
cd.Immutable = true
|
|
||||||
case "stale-if-error":
|
|
||||||
err = ErrMaxAgeDeltaSeconds
|
|
||||||
case "stale-while-revalidate":
|
|
||||||
err = ErrMaxAgeDeltaSeconds
|
|
||||||
default:
|
|
||||||
cd.Extensions = append(cd.Extensions, token)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasFieldNames(token string) bool {
|
|
||||||
switch token {
|
|
||||||
case "no-cache":
|
|
||||||
return true
|
|
||||||
case "private":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *ResponseCacheDirectives) addPair(token string, v string) error {
|
|
||||||
var err error = nil
|
|
||||||
|
|
||||||
switch token {
|
|
||||||
case "must-revalidate":
|
|
||||||
err = ErrMustRevalidateNoArgs
|
|
||||||
case "no-cache":
|
|
||||||
cd.NoCachePresent = true
|
|
||||||
tokens := strings.Split(v, ",")
|
|
||||||
if cd.NoCache == nil {
|
|
||||||
cd.NoCache = make(FieldNames)
|
|
||||||
}
|
|
||||||
for _, t := range tokens {
|
|
||||||
k := http.CanonicalHeaderKey(textproto.TrimString(t))
|
|
||||||
cd.NoCache[k] = true
|
|
||||||
}
|
|
||||||
case "no-store":
|
|
||||||
err = ErrNoStoreNoArgs
|
|
||||||
case "no-transform":
|
|
||||||
err = ErrNoTransformNoArgs
|
|
||||||
case "public":
|
|
||||||
err = ErrPublicNoArgs
|
|
||||||
case "private":
|
|
||||||
cd.PrivatePresent = true
|
|
||||||
tokens := strings.Split(v, ",")
|
|
||||||
if cd.Private == nil {
|
|
||||||
cd.Private = make(FieldNames)
|
|
||||||
}
|
|
||||||
for _, t := range tokens {
|
|
||||||
k := http.CanonicalHeaderKey(textproto.TrimString(t))
|
|
||||||
cd.Private[k] = true
|
|
||||||
}
|
|
||||||
case "proxy-revalidate":
|
|
||||||
err = ErrProxyRevalidateNoArgs
|
|
||||||
case "max-age":
|
|
||||||
cd.MaxAge, err = parseDeltaSeconds(v)
|
|
||||||
case "s-maxage":
|
|
||||||
cd.SMaxAge, err = parseDeltaSeconds(v)
|
|
||||||
// Experimental
|
|
||||||
case "immutable":
|
|
||||||
err = ErrImmutableNoArgs
|
|
||||||
case "stale-if-error":
|
|
||||||
cd.StaleIfError, err = parseDeltaSeconds(v)
|
|
||||||
case "stale-while-revalidate":
|
|
||||||
cd.StaleWhileRevalidate, err = parseDeltaSeconds(v)
|
|
||||||
default:
|
|
||||||
// TODO(pquerna): this sucks, making user re-parse, and its technically not 'quoted' like the original,
|
|
||||||
// but this is still easier, just a SplitN on "="
|
|
||||||
cd.Extensions = append(cd.Extensions, token+"="+v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
// Copyright 2009 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cacheobject
|
|
||||||
|
|
||||||
// This file deals with lexical matters of HTTP
|
|
||||||
|
|
||||||
func isSeparator(c byte) bool {
|
|
||||||
switch c {
|
|
||||||
case '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t':
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCtl(c byte) bool { return (0 <= c && c <= 31) || c == 127 }
|
|
||||||
|
|
||||||
func isChar(c byte) bool { return 0 <= c && c <= 127 }
|
|
||||||
|
|
||||||
func isAnyText(c byte) bool { return !isCtl(c) }
|
|
||||||
|
|
||||||
func isQdText(c byte) bool { return isAnyText(c) && c != '"' }
|
|
||||||
|
|
||||||
func isToken(c byte) bool { return isChar(c) && !isCtl(c) && !isSeparator(c) }
|
|
||||||
|
|
||||||
// Valid escaped sequences are not specified in RFC 2616, so for now, we assume
|
|
||||||
// that they coincide with the common sense ones used by GO. Malformed
|
|
||||||
// characters should probably not be treated as errors by a robust (forgiving)
|
|
||||||
// parser, so we replace them with the '?' character.
|
|
||||||
func httpUnquotePair(b byte) byte {
|
|
||||||
// skip the first byte, which should always be '\'
|
|
||||||
switch b {
|
|
||||||
case 'a':
|
|
||||||
return '\a'
|
|
||||||
case 'b':
|
|
||||||
return '\b'
|
|
||||||
case 'f':
|
|
||||||
return '\f'
|
|
||||||
case 'n':
|
|
||||||
return '\n'
|
|
||||||
case 'r':
|
|
||||||
return '\r'
|
|
||||||
case 't':
|
|
||||||
return '\t'
|
|
||||||
case 'v':
|
|
||||||
return '\v'
|
|
||||||
case '\\':
|
|
||||||
return '\\'
|
|
||||||
case '\'':
|
|
||||||
return '\''
|
|
||||||
case '"':
|
|
||||||
return '"'
|
|
||||||
}
|
|
||||||
return '?'
|
|
||||||
}
|
|
||||||
|
|
||||||
// raw must begin with a valid quoted string. Only the first quoted string is
|
|
||||||
// parsed and is unquoted in result. eaten is the number of bytes parsed, or -1
|
|
||||||
// upon failure.
|
|
||||||
func httpUnquote(raw string) (eaten int, result string) {
|
|
||||||
buf := make([]byte, len(raw))
|
|
||||||
if raw[0] != '"' {
|
|
||||||
return -1, ""
|
|
||||||
}
|
|
||||||
eaten = 1
|
|
||||||
j := 0 // # of bytes written in buf
|
|
||||||
for i := 1; i < len(raw); i++ {
|
|
||||||
switch b := raw[i]; b {
|
|
||||||
case '"':
|
|
||||||
eaten++
|
|
||||||
buf = buf[0:j]
|
|
||||||
return i + 1, string(buf)
|
|
||||||
case '\\':
|
|
||||||
if len(raw) < i+2 {
|
|
||||||
return -1, ""
|
|
||||||
}
|
|
||||||
buf[j] = httpUnquotePair(raw[i+1])
|
|
||||||
eaten += 2
|
|
||||||
j++
|
|
||||||
i++
|
|
||||||
default:
|
|
||||||
if isQdText(b) {
|
|
||||||
buf[j] = b
|
|
||||||
} else {
|
|
||||||
buf[j] = '?'
|
|
||||||
}
|
|
||||||
eaten++
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1, ""
|
|
||||||
}
|
|
|
@ -1,387 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2015 Paul Querna
|
|
||||||
*
|
|
||||||
* 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 cacheobject
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LOW LEVEL API: Repersents a potentially cachable HTTP object.
|
|
||||||
//
|
|
||||||
// This struct is designed to be serialized efficiently, so in a high
|
|
||||||
// performance caching server, things like Date-Strings don't need to be
|
|
||||||
// parsed for every use of a cached object.
|
|
||||||
type Object struct {
|
|
||||||
CacheIsPrivate bool
|
|
||||||
|
|
||||||
RespDirectives *ResponseCacheDirectives
|
|
||||||
RespHeaders http.Header
|
|
||||||
RespStatusCode int
|
|
||||||
RespExpiresHeader time.Time
|
|
||||||
RespDateHeader time.Time
|
|
||||||
RespLastModifiedHeader time.Time
|
|
||||||
|
|
||||||
ReqDirectives *RequestCacheDirectives
|
|
||||||
ReqHeaders http.Header
|
|
||||||
ReqMethod string
|
|
||||||
|
|
||||||
NowUTC time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOW LEVEL API: Repersents the results of examinig an Object with
|
|
||||||
// CachableObject and ExpirationObject.
|
|
||||||
//
|
|
||||||
// TODO(pquerna): decide if this is a good idea or bad
|
|
||||||
type ObjectResults struct {
|
|
||||||
OutReasons []Reason
|
|
||||||
OutWarnings []Warning
|
|
||||||
OutExpirationTime time.Time
|
|
||||||
OutErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOW LEVEL API: Check if a object is cachable.
|
|
||||||
func CachableObject(obj *Object, rv *ObjectResults) {
|
|
||||||
rv.OutReasons = nil
|
|
||||||
rv.OutWarnings = nil
|
|
||||||
rv.OutErr = nil
|
|
||||||
|
|
||||||
switch obj.ReqMethod {
|
|
||||||
case "GET":
|
|
||||||
break
|
|
||||||
case "HEAD":
|
|
||||||
break
|
|
||||||
case "POST":
|
|
||||||
/**
|
|
||||||
POST: http://tools.ietf.org/html/rfc7231#section-4.3.3
|
|
||||||
|
|
||||||
Responses to POST requests are only cacheable when they include
|
|
||||||
explicit freshness information (see Section 4.2.1 of [RFC7234]).
|
|
||||||
However, POST caching is not widely implemented. For cases where an
|
|
||||||
origin server wishes the client to be able to cache the result of a
|
|
||||||
POST in a way that can be reused by a later GET, the origin server
|
|
||||||
MAY send a 200 (OK) response containing the result and a
|
|
||||||
Content-Location header field that has the same value as the POST's
|
|
||||||
effective request URI (Section 3.1.4.2).
|
|
||||||
*/
|
|
||||||
if !hasFreshness(obj.ReqDirectives, obj.RespDirectives, obj.RespHeaders, obj.RespExpiresHeader, obj.CacheIsPrivate) {
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodPOST)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "PUT":
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodPUT)
|
|
||||||
|
|
||||||
case "DELETE":
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodDELETE)
|
|
||||||
|
|
||||||
case "CONNECT":
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodCONNECT)
|
|
||||||
|
|
||||||
case "OPTIONS":
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodOPTIONS)
|
|
||||||
|
|
||||||
case "TRACE":
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodTRACE)
|
|
||||||
|
|
||||||
// HTTP Extension Methods: http://www.iana.org/assignments/http-methods/http-methods.xhtml
|
|
||||||
//
|
|
||||||
// To my knowledge, none of them are cachable. Please open a ticket if this is not the case!
|
|
||||||
//
|
|
||||||
default:
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodUnkown)
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj.ReqDirectives.NoStore {
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestNoStore)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storing Responses to Authenticated Requests: http://tools.ietf.org/html/rfc7234#section-3.2
|
|
||||||
authz := obj.ReqHeaders.Get("Authorization")
|
|
||||||
if authz != "" {
|
|
||||||
if obj.RespDirectives.MustRevalidate ||
|
|
||||||
obj.RespDirectives.Public ||
|
|
||||||
obj.RespDirectives.SMaxAge != -1 {
|
|
||||||
// Expires of some kind present, this is potentially OK.
|
|
||||||
} else {
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestAuthorizationHeader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj.RespDirectives.PrivatePresent && !obj.CacheIsPrivate {
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonResponsePrivate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj.RespDirectives.NoStore {
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonResponseNoStore)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
the response either:
|
|
||||||
|
|
||||||
* contains an Expires header field (see Section 5.3), or
|
|
||||||
|
|
||||||
* contains a max-age response directive (see Section 5.2.2.8), or
|
|
||||||
|
|
||||||
* contains a s-maxage response directive (see Section 5.2.2.9)
|
|
||||||
and the cache is shared, or
|
|
||||||
|
|
||||||
* contains a Cache Control Extension (see Section 5.2.3) that
|
|
||||||
allows it to be cached, or
|
|
||||||
|
|
||||||
* has a status code that is defined as cacheable by default (see
|
|
||||||
Section 4.2.2), or
|
|
||||||
|
|
||||||
* contains a public response directive (see Section 5.2.2.5).
|
|
||||||
*/
|
|
||||||
|
|
||||||
expires := obj.RespHeaders.Get("Expires") != ""
|
|
||||||
statusCachable := cachableStatusCode(obj.RespStatusCode)
|
|
||||||
|
|
||||||
if expires ||
|
|
||||||
obj.RespDirectives.MaxAge != -1 ||
|
|
||||||
(obj.RespDirectives.SMaxAge != -1 && !obj.CacheIsPrivate) ||
|
|
||||||
statusCachable ||
|
|
||||||
obj.RespDirectives.Public {
|
|
||||||
/* cachable by default, at least one of the above conditions was true */
|
|
||||||
} else {
|
|
||||||
rv.OutReasons = append(rv.OutReasons, ReasonResponseUncachableByDefault)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var twentyFourHours = time.Duration(24 * time.Hour)
|
|
||||||
|
|
||||||
const debug = false
|
|
||||||
|
|
||||||
// LOW LEVEL API: Update an objects expiration time.
|
|
||||||
func ExpirationObject(obj *Object, rv *ObjectResults) {
|
|
||||||
/**
|
|
||||||
* Okay, lets calculate Freshness/Expiration now. woo:
|
|
||||||
* http://tools.ietf.org/html/rfc7234#section-4.2
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
o If the cache is shared and the s-maxage response directive
|
|
||||||
(Section 5.2.2.9) is present, use its value, or
|
|
||||||
|
|
||||||
o If the max-age response directive (Section 5.2.2.8) is present,
|
|
||||||
use its value, or
|
|
||||||
|
|
||||||
o If the Expires response header field (Section 5.3) is present, use
|
|
||||||
its value minus the value of the Date response header field, or
|
|
||||||
|
|
||||||
o Otherwise, no explicit expiration time is present in the response.
|
|
||||||
A heuristic freshness lifetime might be applicable; see
|
|
||||||
Section 4.2.2.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var expiresTime time.Time
|
|
||||||
|
|
||||||
if obj.RespDirectives.SMaxAge != -1 && !obj.CacheIsPrivate {
|
|
||||||
expiresTime = obj.NowUTC.Add(time.Second * time.Duration(obj.RespDirectives.SMaxAge))
|
|
||||||
} else if obj.RespDirectives.MaxAge != -1 {
|
|
||||||
expiresTime = obj.NowUTC.UTC().Add(time.Second * time.Duration(obj.RespDirectives.MaxAge))
|
|
||||||
} else if !obj.RespExpiresHeader.IsZero() {
|
|
||||||
serverDate := obj.RespDateHeader
|
|
||||||
if serverDate.IsZero() {
|
|
||||||
// common enough case when a Date: header has not yet been added to an
|
|
||||||
// active response.
|
|
||||||
serverDate = obj.NowUTC
|
|
||||||
}
|
|
||||||
expiresTime = obj.NowUTC.Add(obj.RespExpiresHeader.Sub(serverDate))
|
|
||||||
} else if !obj.RespLastModifiedHeader.IsZero() {
|
|
||||||
// heuristic freshness lifetime
|
|
||||||
rv.OutWarnings = append(rv.OutWarnings, WarningHeuristicExpiration)
|
|
||||||
|
|
||||||
// http://httpd.apache.org/docs/2.4/mod/mod_cache.html#cachelastmodifiedfactor
|
|
||||||
// CacheMaxExpire defaults to 24 hours
|
|
||||||
// CacheLastModifiedFactor: is 0.1
|
|
||||||
//
|
|
||||||
// expiry-period = MIN(time-since-last-modified-date * factor, 24 hours)
|
|
||||||
//
|
|
||||||
// obj.NowUTC
|
|
||||||
|
|
||||||
since := obj.RespLastModifiedHeader.Sub(obj.NowUTC)
|
|
||||||
since = time.Duration(float64(since) * -0.1)
|
|
||||||
|
|
||||||
if since > twentyFourHours {
|
|
||||||
expiresTime = obj.NowUTC.Add(twentyFourHours)
|
|
||||||
} else {
|
|
||||||
expiresTime = obj.NowUTC.Add(since)
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
println("Now UTC: ", obj.NowUTC.String())
|
|
||||||
println("Last-Modified: ", obj.RespLastModifiedHeader.String())
|
|
||||||
println("Since: ", since.String())
|
|
||||||
println("TwentyFourHours: ", twentyFourHours.String())
|
|
||||||
println("Expiration: ", expiresTime.String())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO(pquerna): what should the default behavoir be for expiration time?
|
|
||||||
}
|
|
||||||
|
|
||||||
rv.OutExpirationTime = expiresTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate cachability based on an HTTP request, and parts of the response.
|
|
||||||
func UsingRequestResponse(req *http.Request,
|
|
||||||
statusCode int,
|
|
||||||
respHeaders http.Header,
|
|
||||||
privateCache bool) ([]Reason, time.Time, error) {
|
|
||||||
reasons, time, _, _, err := UsingRequestResponseWithObject(req, statusCode, respHeaders, privateCache)
|
|
||||||
return reasons, time, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate cachability based on an HTTP request, and parts of the response.
|
|
||||||
// Returns the parsed Object as well.
|
|
||||||
func UsingRequestResponseWithObject(req *http.Request,
|
|
||||||
statusCode int,
|
|
||||||
respHeaders http.Header,
|
|
||||||
privateCache bool) ([]Reason, time.Time, []Warning, *Object, error) {
|
|
||||||
var reqHeaders http.Header
|
|
||||||
var reqMethod string
|
|
||||||
|
|
||||||
var reqDir *RequestCacheDirectives = nil
|
|
||||||
respDir, err := ParseResponseCacheControl(respHeaders.Get("Cache-Control"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, time.Time{}, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if req != nil {
|
|
||||||
reqDir, err = ParseRequestCacheControl(req.Header.Get("Cache-Control"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, time.Time{}, nil, nil, err
|
|
||||||
}
|
|
||||||
reqHeaders = req.Header
|
|
||||||
reqMethod = req.Method
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiresHeader time.Time
|
|
||||||
var dateHeader time.Time
|
|
||||||
var lastModifiedHeader time.Time
|
|
||||||
|
|
||||||
if respHeaders.Get("Expires") != "" {
|
|
||||||
expiresHeader, err = http.ParseTime(respHeaders.Get("Expires"))
|
|
||||||
if err != nil {
|
|
||||||
// sometimes servers will return `Expires: 0` or `Expires: -1` to
|
|
||||||
// indicate expired content
|
|
||||||
expiresHeader = time.Time{}
|
|
||||||
}
|
|
||||||
expiresHeader = expiresHeader.UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
if respHeaders.Get("Date") != "" {
|
|
||||||
dateHeader, err = http.ParseTime(respHeaders.Get("Date"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, time.Time{}, nil, nil, err
|
|
||||||
}
|
|
||||||
dateHeader = dateHeader.UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
if respHeaders.Get("Last-Modified") != "" {
|
|
||||||
lastModifiedHeader, err = http.ParseTime(respHeaders.Get("Last-Modified"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, time.Time{}, nil, nil, err
|
|
||||||
}
|
|
||||||
lastModifiedHeader = lastModifiedHeader.UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
obj := Object{
|
|
||||||
CacheIsPrivate: privateCache,
|
|
||||||
|
|
||||||
RespDirectives: respDir,
|
|
||||||
RespHeaders: respHeaders,
|
|
||||||
RespStatusCode: statusCode,
|
|
||||||
RespExpiresHeader: expiresHeader,
|
|
||||||
RespDateHeader: dateHeader,
|
|
||||||
RespLastModifiedHeader: lastModifiedHeader,
|
|
||||||
|
|
||||||
ReqDirectives: reqDir,
|
|
||||||
ReqHeaders: reqHeaders,
|
|
||||||
ReqMethod: reqMethod,
|
|
||||||
|
|
||||||
NowUTC: time.Now().UTC(),
|
|
||||||
}
|
|
||||||
rv := ObjectResults{}
|
|
||||||
|
|
||||||
CachableObject(&obj, &rv)
|
|
||||||
if rv.OutErr != nil {
|
|
||||||
return nil, time.Time{}, nil, nil, rv.OutErr
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpirationObject(&obj, &rv)
|
|
||||||
if rv.OutErr != nil {
|
|
||||||
return nil, time.Time{}, nil, nil, rv.OutErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv.OutReasons, rv.OutExpirationTime, rv.OutWarnings, &obj, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate if a freshness directive is present: http://tools.ietf.org/html/rfc7234#section-4.2.1
|
|
||||||
func hasFreshness(reqDir *RequestCacheDirectives, respDir *ResponseCacheDirectives, respHeaders http.Header, respExpires time.Time, privateCache bool) bool {
|
|
||||||
if !privateCache && respDir.SMaxAge != -1 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if respDir.MaxAge != -1 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !respExpires.IsZero() || respHeaders.Get("Expires") != "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func cachableStatusCode(statusCode int) bool {
|
|
||||||
/*
|
|
||||||
Responses with status codes that are defined as cacheable by default
|
|
||||||
(e.g., 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 in
|
|
||||||
this specification) can be reused by a cache with heuristic
|
|
||||||
expiration unless otherwise indicated by the method definition or
|
|
||||||
explicit cache controls [RFC7234]; all other status codes are not
|
|
||||||
cacheable by default.
|
|
||||||
*/
|
|
||||||
switch statusCode {
|
|
||||||
case 200:
|
|
||||||
return true
|
|
||||||
case 203:
|
|
||||||
return true
|
|
||||||
case 204:
|
|
||||||
return true
|
|
||||||
case 206:
|
|
||||||
return true
|
|
||||||
case 300:
|
|
||||||
return true
|
|
||||||
case 301:
|
|
||||||
return true
|
|
||||||
case 404:
|
|
||||||
return true
|
|
||||||
case 405:
|
|
||||||
return true
|
|
||||||
case 410:
|
|
||||||
return true
|
|
||||||
case 414:
|
|
||||||
return true
|
|
||||||
case 501:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2015 Paul Querna
|
|
||||||
*
|
|
||||||
* 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 cacheobject
|
|
||||||
|
|
||||||
// Repersents a potential Reason to not cache an object.
|
|
||||||
//
|
|
||||||
// Applications may wish to ignore specific reasons, which will make them non-RFC
|
|
||||||
// compliant, but this type gives them specific cases they can choose to ignore,
|
|
||||||
// making them compliant in as many cases as they can.
|
|
||||||
type Reason int
|
|
||||||
|
|
||||||
const (
|
|
||||||
|
|
||||||
// The request method was POST and an Expiration header was not supplied.
|
|
||||||
ReasonRequestMethodPOST Reason = iota
|
|
||||||
|
|
||||||
// The request method was PUT and PUTs are not cachable.
|
|
||||||
ReasonRequestMethodPUT
|
|
||||||
|
|
||||||
// The request method was DELETE and DELETEs are not cachable.
|
|
||||||
ReasonRequestMethodDELETE
|
|
||||||
|
|
||||||
// The request method was CONNECT and CONNECTs are not cachable.
|
|
||||||
ReasonRequestMethodCONNECT
|
|
||||||
|
|
||||||
// The request method was OPTIONS and OPTIONS are not cachable.
|
|
||||||
ReasonRequestMethodOPTIONS
|
|
||||||
|
|
||||||
// The request method was TRACE and TRACEs are not cachable.
|
|
||||||
ReasonRequestMethodTRACE
|
|
||||||
|
|
||||||
// The request method was not recognized by cachecontrol, and should not be cached.
|
|
||||||
ReasonRequestMethodUnkown
|
|
||||||
|
|
||||||
// The request included an Cache-Control: no-store header
|
|
||||||
ReasonRequestNoStore
|
|
||||||
|
|
||||||
// The request included an Authorization header without an explicit Public or Expiration time: http://tools.ietf.org/html/rfc7234#section-3.2
|
|
||||||
ReasonRequestAuthorizationHeader
|
|
||||||
|
|
||||||
// The response included an Cache-Control: no-store header
|
|
||||||
ReasonResponseNoStore
|
|
||||||
|
|
||||||
// The response included an Cache-Control: private header and this is not a Private cache
|
|
||||||
ReasonResponsePrivate
|
|
||||||
|
|
||||||
// The response failed to meet at least one of the conditions specified in RFC 7234 section 3: http://tools.ietf.org/html/rfc7234#section-3
|
|
||||||
ReasonResponseUncachableByDefault
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r Reason) String() string {
|
|
||||||
switch r {
|
|
||||||
case ReasonRequestMethodPOST:
|
|
||||||
return "ReasonRequestMethodPOST"
|
|
||||||
case ReasonRequestMethodPUT:
|
|
||||||
return "ReasonRequestMethodPUT"
|
|
||||||
case ReasonRequestMethodDELETE:
|
|
||||||
return "ReasonRequestMethodDELETE"
|
|
||||||
case ReasonRequestMethodCONNECT:
|
|
||||||
return "ReasonRequestMethodCONNECT"
|
|
||||||
case ReasonRequestMethodOPTIONS:
|
|
||||||
return "ReasonRequestMethodOPTIONS"
|
|
||||||
case ReasonRequestMethodTRACE:
|
|
||||||
return "ReasonRequestMethodTRACE"
|
|
||||||
case ReasonRequestMethodUnkown:
|
|
||||||
return "ReasonRequestMethodUnkown"
|
|
||||||
case ReasonRequestNoStore:
|
|
||||||
return "ReasonRequestNoStore"
|
|
||||||
case ReasonRequestAuthorizationHeader:
|
|
||||||
return "ReasonRequestAuthorizationHeader"
|
|
||||||
case ReasonResponseNoStore:
|
|
||||||
return "ReasonResponseNoStore"
|
|
||||||
case ReasonResponsePrivate:
|
|
||||||
return "ReasonResponsePrivate"
|
|
||||||
case ReasonResponseUncachableByDefault:
|
|
||||||
return "ReasonResponseUncachableByDefault"
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(r)
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2015 Paul Querna
|
|
||||||
*
|
|
||||||
* 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 cacheobject
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Repersents an HTTP Warning: http://tools.ietf.org/html/rfc7234#section-5.5
|
|
||||||
type Warning int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Response is Stale
|
|
||||||
// A cache SHOULD generate this whenever the sent response is stale.
|
|
||||||
WarningResponseIsStale Warning = 110
|
|
||||||
|
|
||||||
// Revalidation Failed
|
|
||||||
// A cache SHOULD generate this when sending a stale
|
|
||||||
// response because an attempt to validate the response failed, due to an
|
|
||||||
// inability to reach the server.
|
|
||||||
WarningRevalidationFailed Warning = 111
|
|
||||||
|
|
||||||
// Disconnected Operation
|
|
||||||
// A cache SHOULD generate this if it is intentionally disconnected from
|
|
||||||
// the rest of the network for a period of time.
|
|
||||||
WarningDisconnectedOperation Warning = 112
|
|
||||||
|
|
||||||
// Heuristic Expiration
|
|
||||||
//
|
|
||||||
// A cache SHOULD generate this if it heuristically chose a freshness
|
|
||||||
// lifetime greater than 24 hours and the response's age is greater than
|
|
||||||
// 24 hours.
|
|
||||||
WarningHeuristicExpiration Warning = 113
|
|
||||||
|
|
||||||
// Miscellaneous Warning
|
|
||||||
//
|
|
||||||
// The warning text can include arbitrary information to be presented to
|
|
||||||
// a human user or logged. A system receiving this warning MUST NOT
|
|
||||||
// take any automated action, besides presenting the warning to the
|
|
||||||
// user.
|
|
||||||
WarningMiscellaneousWarning Warning = 199
|
|
||||||
|
|
||||||
// Transformation Applied
|
|
||||||
//
|
|
||||||
// This Warning code MUST be added by a proxy if it applies any
|
|
||||||
// transformation to the representation, such as changing the
|
|
||||||
// content-coding, media-type, or modifying the representation data,
|
|
||||||
// unless this Warning code already appears in the response.
|
|
||||||
WarningTransformationApplied Warning = 214
|
|
||||||
|
|
||||||
// Miscellaneous Persistent Warning
|
|
||||||
//
|
|
||||||
// The warning text can include arbitrary information to be presented to
|
|
||||||
// a human user or logged. A system receiving this warning MUST NOT
|
|
||||||
// take any automated action.
|
|
||||||
WarningMiscellaneousPersistentWarning Warning = 299
|
|
||||||
)
|
|
||||||
|
|
||||||
func (w Warning) HeaderString(agent string, date time.Time) string {
|
|
||||||
if agent == "" {
|
|
||||||
agent = "-"
|
|
||||||
} else {
|
|
||||||
// TODO(pquerna): this doesn't escape agent if it contains bad things.
|
|
||||||
agent = `"` + agent + `"`
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(`%d %s "%s" %s`, w, agent, w.String(), date.Format(http.TimeFormat))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w Warning) String() string {
|
|
||||||
switch w {
|
|
||||||
case WarningResponseIsStale:
|
|
||||||
return "Response is Stale"
|
|
||||||
case WarningRevalidationFailed:
|
|
||||||
return "Revalidation Failed"
|
|
||||||
case WarningDisconnectedOperation:
|
|
||||||
return "Disconnected Operation"
|
|
||||||
case WarningHeuristicExpiration:
|
|
||||||
return "Heuristic Expiration"
|
|
||||||
case WarningMiscellaneousWarning:
|
|
||||||
// TODO(pquerna): ideally had a better way to override this one code.
|
|
||||||
return "Miscellaneous Warning"
|
|
||||||
case WarningTransformationApplied:
|
|
||||||
return "Transformation Applied"
|
|
||||||
case WarningMiscellaneousPersistentWarning:
|
|
||||||
// TODO(pquerna): same as WarningMiscellaneousWarning
|
|
||||||
return "Miscellaneous Persistent Warning"
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(w)
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2015 Paul Querna
|
|
||||||
*
|
|
||||||
* 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 cachecontrol implements the logic for HTTP Caching
|
|
||||||
//
|
|
||||||
// Deciding if an HTTP Response can be cached is often harder
|
|
||||||
// and more bug prone than an actual cache storage backend.
|
|
||||||
// cachecontrol provides a simple interface to determine if
|
|
||||||
// request and response pairs are cachable as defined under
|
|
||||||
// RFC 7234 http://tools.ietf.org/html/rfc7234
|
|
||||||
package cachecontrol
|
|
|
@ -1,2 +0,0 @@
|
||||||
/bin
|
|
||||||
/gopath
|
|
|
@ -1,16 +0,0 @@
|
||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- "1.9"
|
|
||||||
- "1.10"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- go get -v -t github.com/coreos/go-oidc/...
|
|
||||||
- go get golang.org/x/tools/cmd/cover
|
|
||||||
- go get github.com/golang/lint/golint
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./test
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
|
@ -1,71 +0,0 @@
|
||||||
# How to Contribute
|
|
||||||
|
|
||||||
CoreOS projects are [Apache 2.0 licensed](LICENSE) and accept contributions via
|
|
||||||
GitHub pull requests. This document outlines some of the conventions on
|
|
||||||
development workflow, commit message formatting, contact points and other
|
|
||||||
resources to make it easier to get your contribution accepted.
|
|
||||||
|
|
||||||
# Certificate of Origin
|
|
||||||
|
|
||||||
By contributing to this project you agree to the Developer Certificate of
|
|
||||||
Origin (DCO). This document was created by the Linux Kernel community and is a
|
|
||||||
simple statement that you, as a contributor, have the legal right to make the
|
|
||||||
contribution. See the [DCO](DCO) file for details.
|
|
||||||
|
|
||||||
# Email and Chat
|
|
||||||
|
|
||||||
The project currently uses the general CoreOS email list and IRC channel:
|
|
||||||
- Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev)
|
|
||||||
- IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org
|
|
||||||
|
|
||||||
Please avoid emailing maintainers found in the MAINTAINERS file directly. They
|
|
||||||
are very busy and read the mailing lists.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
- Fork the repository on GitHub
|
|
||||||
- Read the [README](README.md) for build and test instructions
|
|
||||||
- Play with the project, submit bugs, submit patches!
|
|
||||||
|
|
||||||
## Contribution Flow
|
|
||||||
|
|
||||||
This is a rough outline of what a contributor's workflow looks like:
|
|
||||||
|
|
||||||
- Create a topic branch from where you want to base your work (usually master).
|
|
||||||
- Make commits of logical units.
|
|
||||||
- Make sure your commit messages are in the proper format (see below).
|
|
||||||
- Push your changes to a topic branch in your fork of the repository.
|
|
||||||
- Make sure the tests pass, and add any new tests as appropriate.
|
|
||||||
- Submit a pull request to the original repository.
|
|
||||||
|
|
||||||
Thanks for your contributions!
|
|
||||||
|
|
||||||
### Format of the Commit Message
|
|
||||||
|
|
||||||
We follow a rough convention for commit messages that is designed to answer two
|
|
||||||
questions: what changed and why. The subject line should feature the what and
|
|
||||||
the body of the commit should describe the why.
|
|
||||||
|
|
||||||
```
|
|
||||||
scripts: add the test-cluster command
|
|
||||||
|
|
||||||
this uses tmux to setup a test cluster that you can easily kill and
|
|
||||||
start for debugging.
|
|
||||||
|
|
||||||
Fixes #38
|
|
||||||
```
|
|
||||||
|
|
||||||
The format can be described more formally as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
<subsystem>: <what changed>
|
|
||||||
<BLANK LINE>
|
|
||||||
<why this change was made>
|
|
||||||
<BLANK LINE>
|
|
||||||
<footer>
|
|
||||||
```
|
|
||||||
|
|
||||||
The first line is the subject and should be no longer than 70 characters, the
|
|
||||||
second line is always blank, and other lines should be wrapped at 80 characters.
|
|
||||||
This allows the message to be easier to read on GitHub as well as in various
|
|
||||||
git tools.
|
|
|
@ -1,36 +0,0 @@
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
660 York Street, Suite 102,
|
|
||||||
San Francisco, CA 94110 USA
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
|
@ -1,3 +0,0 @@
|
||||||
Eric Chiang <ericchiang@google.com> (@ericchiang)
|
|
||||||
Mike Danese <mikedanese@google.com> (@mikedanese)
|
|
||||||
Rithu Leena John <rjohn@redhat.com> (@rithujohn191)
|
|
|
@ -1,72 +0,0 @@
|
||||||
# go-oidc
|
|
||||||
|
|
||||||
[![GoDoc](https://godoc.org/github.com/coreos/go-oidc?status.svg)](https://godoc.org/github.com/coreos/go-oidc)
|
|
||||||
[![Build Status](https://travis-ci.org/coreos/go-oidc.png?branch=master)](https://travis-ci.org/coreos/go-oidc)
|
|
||||||
|
|
||||||
## OpenID Connect support for Go
|
|
||||||
|
|
||||||
This package enables OpenID Connect support for the [golang.org/x/oauth2](https://godoc.org/golang.org/x/oauth2) package.
|
|
||||||
|
|
||||||
```go
|
|
||||||
provider, err := oidc.NewProvider(ctx, "https://accounts.google.com")
|
|
||||||
if err != nil {
|
|
||||||
// handle error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure an OpenID Connect aware OAuth2 client.
|
|
||||||
oauth2Config := oauth2.Config{
|
|
||||||
ClientID: clientID,
|
|
||||||
ClientSecret: clientSecret,
|
|
||||||
RedirectURL: redirectURL,
|
|
||||||
|
|
||||||
// Discovery returns the OAuth2 endpoints.
|
|
||||||
Endpoint: provider.Endpoint(),
|
|
||||||
|
|
||||||
// "openid" is a required scope for OpenID Connect flows.
|
|
||||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
OAuth2 redirects are unchanged.
|
|
||||||
|
|
||||||
```go
|
|
||||||
func handleRedirect(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The on responses, the provider can be used to verify ID Tokens.
|
|
||||||
|
|
||||||
```go
|
|
||||||
var verifier = provider.Verifier(&oidc.Config{ClientID: clientID})
|
|
||||||
|
|
||||||
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Verify state and errors.
|
|
||||||
|
|
||||||
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
|
|
||||||
if err != nil {
|
|
||||||
// handle error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the ID Token from OAuth2 token.
|
|
||||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
|
||||||
if !ok {
|
|
||||||
// handle missing token
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and verify ID Token payload.
|
|
||||||
idToken, err := verifier.Verify(ctx, rawIDToken)
|
|
||||||
if err != nil {
|
|
||||||
// handle error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract custom claims
|
|
||||||
var claims struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Verified bool `json:"email_verified"`
|
|
||||||
}
|
|
||||||
if err := idToken.Claims(&claims); err != nil {
|
|
||||||
// handle error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,61 +0,0 @@
|
||||||
## CoreOS Community Code of Conduct
|
|
||||||
|
|
||||||
### Contributor Code of Conduct
|
|
||||||
|
|
||||||
As contributors and maintainers of this project, and in the interest of
|
|
||||||
fostering an open and welcoming community, we pledge to respect all people who
|
|
||||||
contribute through reporting issues, posting feature requests, updating
|
|
||||||
documentation, submitting pull requests or patches, and other activities.
|
|
||||||
|
|
||||||
We are committed to making participation in this project a harassment-free
|
|
||||||
experience for everyone, regardless of level of experience, gender, gender
|
|
||||||
identity and expression, sexual orientation, disability, personal appearance,
|
|
||||||
body size, race, ethnicity, age, religion, or nationality.
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery
|
|
||||||
* Personal attacks
|
|
||||||
* Trolling or insulting/derogatory comments
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
|
|
||||||
* Other unethical or unprofessional conduct.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
|
|
||||||
project maintainers commit themselves to fairly and consistently applying these
|
|
||||||
principles to every aspect of managing this project. Project maintainers who do
|
|
||||||
not follow or enforce the Code of Conduct may be permanently removed from the
|
|
||||||
project team.
|
|
||||||
|
|
||||||
This code of conduct applies both within project spaces and in public spaces
|
|
||||||
when an individual is representing the project or its community.
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported by contacting a project maintainer, Brandon Philips
|
|
||||||
<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the Contributor Covenant
|
|
||||||
(http://contributor-covenant.org), version 1.2.0, available at
|
|
||||||
http://contributor-covenant.org/version/1/2/0/
|
|
||||||
|
|
||||||
### CoreOS Events Code of Conduct
|
|
||||||
|
|
||||||
CoreOS events are working conferences intended for professional networking and
|
|
||||||
collaboration in the CoreOS community. Attendees are expected to behave
|
|
||||||
according to professional standards and in accordance with their employer’s
|
|
||||||
policies on appropriate workplace behavior.
|
|
||||||
|
|
||||||
While at CoreOS events or related social networking opportunities, attendees
|
|
||||||
should not engage in discriminatory or offensive speech or actions including
|
|
||||||
but not limited to gender, sexuality, race, age, disability, or religion.
|
|
||||||
Speakers should be especially aware of these concerns.
|
|
||||||
|
|
||||||
CoreOS does not condone any statements by speakers contrary to these standards.
|
|
||||||
CoreOS reserves the right to deny entrance and/or eject from an event (without
|
|
||||||
refund) any individual found to be engaging in discriminatory or offensive
|
|
||||||
speech or actions.
|
|
||||||
|
|
||||||
Please bring any concerns to the immediate attention of designated on-site
|
|
||||||
staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
|
|
|
@ -1,16 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Filter out any files with a !golint build tag.
|
|
||||||
LINTABLE=$( go list -tags=golint -f '
|
|
||||||
{{- range $i, $file := .GoFiles -}}
|
|
||||||
{{ $file }} {{ end }}
|
|
||||||
{{ range $i, $file := .TestGoFiles -}}
|
|
||||||
{{ $file }} {{ end }}' github.com/coreos/go-oidc )
|
|
||||||
|
|
||||||
go test -v -i -race github.com/coreos/go-oidc/...
|
|
||||||
go test -v -race github.com/coreos/go-oidc/...
|
|
||||||
golint -set_exit_status $LINTABLE
|
|
||||||
go vet github.com/coreos/go-oidc/...
|
|
||||||
go build -v ./example/...
|
|
|
@ -5,3 +5,4 @@
|
||||||
*.pem
|
*.pem
|
||||||
*.cov
|
*.cov
|
||||||
jose-util/jose-util
|
jose-util/jose-util
|
||||||
|
jose-util.t.err
|
|
@ -26,6 +26,8 @@ before_install:
|
||||||
- go get github.com/wadey/gocovmerge
|
- go get github.com/wadey/gocovmerge
|
||||||
- go get github.com/mattn/goveralls
|
- go get github.com/mattn/goveralls
|
||||||
- go get github.com/stretchr/testify/assert
|
- go get github.com/stretchr/testify/assert
|
||||||
|
- go get github.com/stretchr/testify/require
|
||||||
|
- go get github.com/google/go-cmp/cmp
|
||||||
- go get golang.org/x/tools/cmd/cover || true
|
- go get golang.org/x/tools/cmd/cover || true
|
||||||
- go get code.google.com/p/go.tools/cmd/cover || true
|
- go get code.google.com/p/go.tools/cmd/cover || true
|
||||||
- pip install cram --user
|
- pip install cram --user
|
||||||
|
@ -35,10 +37,9 @@ script:
|
||||||
- go test ./cipher -v -covermode=count -coverprofile=cipher/profile.cov
|
- go test ./cipher -v -covermode=count -coverprofile=cipher/profile.cov
|
||||||
- go test ./jwt -v -covermode=count -coverprofile=jwt/profile.cov
|
- go test ./jwt -v -covermode=count -coverprofile=jwt/profile.cov
|
||||||
- go test ./json -v # no coverage for forked encoding/json package
|
- go test ./json -v # no coverage for forked encoding/json package
|
||||||
- cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t
|
- cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t # cram tests jose-util
|
||||||
- cd ..
|
- cd ..
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- gocovmerge *.cov */*.cov > merged.coverprofile
|
- gocovmerge *.cov */*.cov > merged.coverprofile
|
||||||
- $HOME/gopath/bin/goveralls -coverprofile merged.coverprofile -service=travis-ci
|
- $HOME/gopath/bin/goveralls -coverprofile merged.coverprofile -service=travis-ci
|
||||||
|
|
||||||
|
|
|
@ -23,13 +23,12 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"regexp"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2/json"
|
"gopkg.in/square/go-jose.v2/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
var stripWhitespaceRegex = regexp.MustCompile("\\s")
|
|
||||||
|
|
||||||
// Helper function to serialize known-good objects.
|
// Helper function to serialize known-good objects.
|
||||||
// Precondition: value is not a nil pointer.
|
// Precondition: value is not a nil pointer.
|
||||||
func mustSerializeJSON(value interface{}) []byte {
|
func mustSerializeJSON(value interface{}) []byte {
|
||||||
|
@ -56,7 +55,14 @@ func mustSerializeJSON(value interface{}) []byte {
|
||||||
|
|
||||||
// Strip all newlines and whitespace
|
// Strip all newlines and whitespace
|
||||||
func stripWhitespace(data string) string {
|
func stripWhitespace(data string) string {
|
||||||
return stripWhitespaceRegex.ReplaceAllString(data, "")
|
buf := strings.Builder{}
|
||||||
|
buf.Grow(len(data))
|
||||||
|
for _, r := range data {
|
||||||
|
if !unicode.IsSpace(r) {
|
||||||
|
buf.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform compression based on algorithm
|
// Perform compression based on algorithm
|
||||||
|
|
|
@ -17,15 +17,20 @@
|
||||||
package jose
|
package jose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -58,15 +63,30 @@ type rawJSONWebKey struct {
|
||||||
Qi *byteBuffer `json:"qi,omitempty"`
|
Qi *byteBuffer `json:"qi,omitempty"`
|
||||||
// Certificates
|
// Certificates
|
||||||
X5c []string `json:"x5c,omitempty"`
|
X5c []string `json:"x5c,omitempty"`
|
||||||
|
X5u *url.URL `json:"x5u,omitempty"`
|
||||||
|
X5tSHA1 string `json:"x5t,omitempty"`
|
||||||
|
X5tSHA256 string `json:"x5t#S256,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONWebKey represents a public or private key in JWK format.
|
// JSONWebKey represents a public or private key in JWK format.
|
||||||
type JSONWebKey struct {
|
type JSONWebKey struct {
|
||||||
|
// Cryptographic key, can be a symmetric or asymmetric key.
|
||||||
Key interface{}
|
Key interface{}
|
||||||
Certificates []*x509.Certificate
|
// Key identifier, parsed from `kid` header.
|
||||||
KeyID string
|
KeyID string
|
||||||
|
// Key algorithm, parsed from `alg` header.
|
||||||
Algorithm string
|
Algorithm string
|
||||||
|
// Key use, parsed from `use` header.
|
||||||
Use string
|
Use string
|
||||||
|
|
||||||
|
// X.509 certificate chain, parsed from `x5c` header.
|
||||||
|
Certificates []*x509.Certificate
|
||||||
|
// X.509 certificate URL, parsed from `x5u` header.
|
||||||
|
CertificatesURL *url.URL
|
||||||
|
// X.509 certificate thumbprint (SHA-1), parsed from `x5t` header.
|
||||||
|
CertificateThumbprintSHA1 []byte
|
||||||
|
// X.509 certificate thumbprint (SHA-256), parsed from `x5t#S256` header.
|
||||||
|
CertificateThumbprintSHA256 []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON serializes the given key to its JSON representation.
|
// MarshalJSON serializes the given key to its JSON representation.
|
||||||
|
@ -105,6 +125,39 @@ func (k JSONWebKey) MarshalJSON() ([]byte, error) {
|
||||||
raw.X5c = append(raw.X5c, base64.StdEncoding.EncodeToString(cert.Raw))
|
raw.X5c = append(raw.X5c, base64.StdEncoding.EncodeToString(cert.Raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x5tSHA1Len := len(k.CertificateThumbprintSHA1)
|
||||||
|
x5tSHA256Len := len(k.CertificateThumbprintSHA256)
|
||||||
|
if x5tSHA1Len > 0 {
|
||||||
|
if x5tSHA1Len != sha1.Size {
|
||||||
|
return nil, fmt.Errorf("square/go-jose: invalid SHA-1 thumbprint (must be %d bytes, not %d)", sha1.Size, x5tSHA1Len)
|
||||||
|
}
|
||||||
|
raw.X5tSHA1 = base64.RawURLEncoding.EncodeToString(k.CertificateThumbprintSHA1)
|
||||||
|
}
|
||||||
|
if x5tSHA256Len > 0 {
|
||||||
|
if x5tSHA256Len != sha256.Size {
|
||||||
|
return nil, fmt.Errorf("square/go-jose: invalid SHA-256 thumbprint (must be %d bytes, not %d)", sha256.Size, x5tSHA256Len)
|
||||||
|
}
|
||||||
|
raw.X5tSHA256 = base64.RawURLEncoding.EncodeToString(k.CertificateThumbprintSHA256)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cert chain is attached (as opposed to being behind a URL), check the
|
||||||
|
// keys thumbprints to make sure they match what is expected. This is to
|
||||||
|
// ensure we don't accidentally produce a JWK with semantically inconsistent
|
||||||
|
// data in the headers.
|
||||||
|
if len(k.Certificates) > 0 {
|
||||||
|
expectedSHA1 := sha1.Sum(k.Certificates[0].Raw)
|
||||||
|
expectedSHA256 := sha256.Sum256(k.Certificates[0].Raw)
|
||||||
|
|
||||||
|
if len(k.CertificateThumbprintSHA1) > 0 && !bytes.Equal(k.CertificateThumbprintSHA1, expectedSHA1[:]) {
|
||||||
|
return nil, errors.New("square/go-jose: invalid SHA-1 thumbprint, does not match cert chain")
|
||||||
|
}
|
||||||
|
if len(k.CertificateThumbprintSHA256) > 0 && !bytes.Equal(k.CertificateThumbprintSHA256, expectedSHA256[:]) {
|
||||||
|
return nil, errors.New("square/go-jose: invalid or SHA-256 thumbprint, does not match cert chain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raw.X5u = k.CertificatesURL
|
||||||
|
|
||||||
return json.Marshal(raw)
|
return json.Marshal(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,28 +169,61 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
certs, err := parseCertificateChain(raw.X5c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("square/go-jose: failed to unmarshal x5c field: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
var key interface{}
|
var key interface{}
|
||||||
|
var certPub interface{}
|
||||||
|
var keyPub interface{}
|
||||||
|
|
||||||
|
if len(certs) > 0 {
|
||||||
|
// We need to check that leaf public key matches the key embedded in this
|
||||||
|
// JWK, as required by the standard (see RFC 7517, Section 4.7). Otherwise
|
||||||
|
// the JWK parsed could be semantically invalid. Technically, should also
|
||||||
|
// check key usage fields and other extensions on the cert here, but the
|
||||||
|
// standard doesn't exactly explain how they're supposed to map from the
|
||||||
|
// JWK representation to the X.509 extensions.
|
||||||
|
certPub = certs[0].PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
switch raw.Kty {
|
switch raw.Kty {
|
||||||
case "EC":
|
case "EC":
|
||||||
if raw.D != nil {
|
if raw.D != nil {
|
||||||
key, err = raw.ecPrivateKey()
|
key, err = raw.ecPrivateKey()
|
||||||
|
if err == nil {
|
||||||
|
keyPub = key.(*ecdsa.PrivateKey).Public()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
key, err = raw.ecPublicKey()
|
key, err = raw.ecPublicKey()
|
||||||
|
keyPub = key
|
||||||
}
|
}
|
||||||
case "RSA":
|
case "RSA":
|
||||||
if raw.D != nil {
|
if raw.D != nil {
|
||||||
key, err = raw.rsaPrivateKey()
|
key, err = raw.rsaPrivateKey()
|
||||||
|
if err == nil {
|
||||||
|
keyPub = key.(*rsa.PrivateKey).Public()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
key, err = raw.rsaPublicKey()
|
key, err = raw.rsaPublicKey()
|
||||||
|
keyPub = key
|
||||||
}
|
}
|
||||||
case "oct":
|
case "oct":
|
||||||
|
if certPub != nil {
|
||||||
|
return errors.New("square/go-jose: invalid JWK, found 'oct' (symmetric) key with cert chain")
|
||||||
|
}
|
||||||
key, err = raw.symmetricKey()
|
key, err = raw.symmetricKey()
|
||||||
case "OKP":
|
case "OKP":
|
||||||
if raw.Crv == "Ed25519" && raw.X != nil {
|
if raw.Crv == "Ed25519" && raw.X != nil {
|
||||||
if raw.D != nil {
|
if raw.D != nil {
|
||||||
key, err = raw.edPrivateKey()
|
key, err = raw.edPrivateKey()
|
||||||
|
if err == nil {
|
||||||
|
keyPub = key.(ed25519.PrivateKey).Public()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
key, err = raw.edPublicKey()
|
key, err = raw.edPublicKey()
|
||||||
|
keyPub = key
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = fmt.Errorf("square/go-jose: unknown curve %s'", raw.Crv)
|
err = fmt.Errorf("square/go-jose: unknown curve %s'", raw.Crv)
|
||||||
|
@ -146,12 +232,78 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
|
||||||
err = fmt.Errorf("square/go-jose: unknown json web key type '%s'", raw.Kty)
|
err = fmt.Errorf("square/go-jose: unknown json web key type '%s'", raw.Kty)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
*k = JSONWebKey{Key: key, KeyID: raw.Kid, Algorithm: raw.Alg, Use: raw.Use}
|
|
||||||
|
|
||||||
k.Certificates, err = parseCertificateChain(raw.X5c)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal x5c field: %s", err)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if certPub != nil && keyPub != nil {
|
||||||
|
if !reflect.DeepEqual(certPub, keyPub) {
|
||||||
|
return errors.New("square/go-jose: invalid JWK, public keys in key and x5c fields to not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*k = JSONWebKey{Key: key, KeyID: raw.Kid, Algorithm: raw.Alg, Use: raw.Use, Certificates: certs}
|
||||||
|
|
||||||
|
k.CertificatesURL = raw.X5u
|
||||||
|
|
||||||
|
// x5t parameters are base64url-encoded SHA thumbprints
|
||||||
|
// See RFC 7517, Section 4.8, https://tools.ietf.org/html/rfc7517#section-4.8
|
||||||
|
x5tSHA1bytes, err := base64.RawURLEncoding.DecodeString(raw.X5tSHA1)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("square/go-jose: invalid JWK, x5t header has invalid encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 7517, Section 4.8 is ambiguous as to whether the digest output should be byte or hex,
|
||||||
|
// for this reason, after base64 decoding, if the size is sha1.Size it's likely that the value is a byte encoded
|
||||||
|
// checksum so we skip this. Otherwise if the checksum was hex encoded we expect a 40 byte sized array so we'll
|
||||||
|
// try to hex decode it. When Marshalling this value we'll always use a base64 encoded version of byte format checksum.
|
||||||
|
if len(x5tSHA1bytes) == 2*sha1.Size {
|
||||||
|
hx, err := hex.DecodeString(string(x5tSHA1bytes))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("square/go-jose: invalid JWK, unable to hex decode x5t: %v", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
x5tSHA1bytes = hx
|
||||||
|
}
|
||||||
|
|
||||||
|
k.CertificateThumbprintSHA1 = x5tSHA1bytes
|
||||||
|
|
||||||
|
x5tSHA256bytes, err := base64.RawURLEncoding.DecodeString(raw.X5tSHA256)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("square/go-jose: invalid JWK, x5t#S256 header has invalid encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(x5tSHA256bytes) == 2*sha256.Size {
|
||||||
|
hx256, err := hex.DecodeString(string(x5tSHA256bytes))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("square/go-jose: invalid JWK, unable to hex decode x5t#S256: %v", err)
|
||||||
|
}
|
||||||
|
x5tSHA256bytes = hx256
|
||||||
|
}
|
||||||
|
|
||||||
|
k.CertificateThumbprintSHA256 = x5tSHA256bytes
|
||||||
|
|
||||||
|
x5tSHA1Len := len(k.CertificateThumbprintSHA1)
|
||||||
|
x5tSHA256Len := len(k.CertificateThumbprintSHA256)
|
||||||
|
if x5tSHA1Len > 0 && x5tSHA1Len != sha1.Size {
|
||||||
|
return errors.New("square/go-jose: invalid JWK, x5t header is of incorrect size")
|
||||||
|
}
|
||||||
|
if x5tSHA256Len > 0 && x5tSHA256Len != sha256.Size {
|
||||||
|
return errors.New("square/go-jose: invalid JWK, x5t#S256 header is of incorrect size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If certificate chain *and* thumbprints are set, verify correctness.
|
||||||
|
if len(k.Certificates) > 0 {
|
||||||
|
leaf := k.Certificates[0]
|
||||||
|
sha1sum := sha1.Sum(leaf.Raw)
|
||||||
|
sha256sum := sha256.Sum256(leaf.Raw)
|
||||||
|
|
||||||
|
if len(k.CertificateThumbprintSHA1) > 0 && !bytes.Equal(sha1sum[:], k.CertificateThumbprintSHA1) {
|
||||||
|
return errors.New("square/go-jose: invalid JWK, x5c thumbprint does not match x5t value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(k.CertificateThumbprintSHA256) > 0 && !bytes.Equal(sha256sum[:], k.CertificateThumbprintSHA256) {
|
||||||
|
return errors.New("square/go-jose: invalid JWK, x5c thumbprint does not match x5t#S256 value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,14 +102,14 @@ func (sig Signature) mergedHeaders() rawHeader {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute data to be signed
|
// Compute data to be signed
|
||||||
func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature) []byte {
|
func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature) ([]byte, error) {
|
||||||
var authData bytes.Buffer
|
var authData bytes.Buffer
|
||||||
|
|
||||||
protectedHeader := new(rawHeader)
|
protectedHeader := new(rawHeader)
|
||||||
|
|
||||||
if signature.original != nil && signature.original.Protected != nil {
|
if signature.original != nil && signature.original.Protected != nil {
|
||||||
if err := json.Unmarshal(signature.original.Protected.bytes(), protectedHeader); err != nil {
|
if err := json.Unmarshal(signature.original.Protected.bytes(), protectedHeader); err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
authData.WriteString(signature.original.Protected.base64())
|
authData.WriteString(signature.original.Protected.base64())
|
||||||
} else if signature.protected != nil {
|
} else if signature.protected != nil {
|
||||||
|
@ -134,7 +134,7 @@ func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature
|
||||||
authData.Write(payload)
|
authData.Write(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
return authData.Bytes()
|
return authData.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSignedFull parses a message in full format.
|
// parseSignedFull parses a message in full format.
|
||||||
|
|
|
@ -370,7 +370,11 @@ func (obj JSONWebSignature) DetachedVerify(payload []byte, verificationKey inter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input := obj.computeAuthData(payload, &signature)
|
input, err := obj.computeAuthData(payload, &signature)
|
||||||
|
if err != nil {
|
||||||
|
return ErrCryptoFailure
|
||||||
|
}
|
||||||
|
|
||||||
alg := headers.getSignatureAlgorithm()
|
alg := headers.getSignatureAlgorithm()
|
||||||
err = verifier.verifyPayload(input, signature.Signature, alg)
|
err = verifier.verifyPayload(input, signature.Signature, alg)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -421,7 +425,11 @@ outer:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input := obj.computeAuthData(payload, &signature)
|
input, err := obj.computeAuthData(payload, &signature)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
alg := headers.getSignatureAlgorithm()
|
alg := headers.getSignatureAlgorithm()
|
||||||
err = verifier.verifyPayload(input, signature.Signature, alg)
|
err = verifier.verifyPayload(input, signature.Signature, alg)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -65,6 +65,9 @@ github.com/coredns/coredns/request
|
||||||
# github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
# github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
||||||
## explicit
|
## explicit
|
||||||
github.com/coreos/go-oidc/jose
|
github.com/coreos/go-oidc/jose
|
||||||
|
# github.com/coreos/go-oidc/v3 v3.1.0
|
||||||
|
## explicit; go 1.14
|
||||||
|
github.com/coreos/go-oidc/v3/oidc
|
||||||
# github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
# github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
## explicit
|
## explicit
|
||||||
github.com/coreos/go-systemd/daemon
|
github.com/coreos/go-systemd/daemon
|
||||||
|
@ -290,10 +293,6 @@ github.com/pkg/errors
|
||||||
# github.com/pmezard/go-difflib v1.0.0
|
# github.com/pmezard/go-difflib v1.0.0
|
||||||
## explicit
|
## explicit
|
||||||
github.com/pmezard/go-difflib/difflib
|
github.com/pmezard/go-difflib/difflib
|
||||||
# github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35
|
|
||||||
## explicit
|
|
||||||
github.com/pquerna/cachecontrol
|
|
||||||
github.com/pquerna/cachecontrol/cacheobject
|
|
||||||
# github.com/prometheus/client_golang v1.11.0
|
# github.com/prometheus/client_golang v1.11.0
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/prometheus/client_golang/prometheus
|
github.com/prometheus/client_golang/prometheus
|
||||||
|
@ -530,13 +529,10 @@ google.golang.org/protobuf/types/descriptorpb
|
||||||
google.golang.org/protobuf/types/known/anypb
|
google.golang.org/protobuf/types/known/anypb
|
||||||
google.golang.org/protobuf/types/known/durationpb
|
google.golang.org/protobuf/types/known/durationpb
|
||||||
google.golang.org/protobuf/types/known/timestamppb
|
google.golang.org/protobuf/types/known/timestamppb
|
||||||
# gopkg.in/coreos/go-oidc.v2 v2.1.0
|
|
||||||
## explicit
|
|
||||||
gopkg.in/coreos/go-oidc.v2
|
|
||||||
# gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
# gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
## explicit
|
## explicit
|
||||||
gopkg.in/natefinch/lumberjack.v2
|
gopkg.in/natefinch/lumberjack.v2
|
||||||
# gopkg.in/square/go-jose.v2 v2.4.0
|
# gopkg.in/square/go-jose.v2 v2.5.1
|
||||||
## explicit
|
## explicit
|
||||||
gopkg.in/square/go-jose.v2
|
gopkg.in/square/go-jose.v2
|
||||||
gopkg.in/square/go-jose.v2/cipher
|
gopkg.in/square/go-jose.v2/cipher
|
||||||
|
|
Loading…
Reference in New Issue