AUTH-1972: Deletes token lock file if backoff retry attempts exceeded and intercepts signals until lock is released
This commit is contained in:
parent
3c93d9b300
commit
9adbab96af
|
@ -73,15 +73,9 @@
|
||||||
version = "v1.2.0"
|
version = "v1.2.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:6a503e232df389d94ebb97dfb22d4ae463b6e2f351660613e11d9e42f57ab6df"
|
digest = "1:6f70106e7bc1c803e8a0a4519e09c12d154771acfa2559206e97b033bbd1dd38"
|
||||||
name = "github.com/coreos/go-oidc"
|
name = "github.com/coreos/go-oidc"
|
||||||
packages = [
|
packages = ["jose"]
|
||||||
"http",
|
|
||||||
"jose",
|
|
||||||
"key",
|
|
||||||
"oauth2",
|
|
||||||
"oidc",
|
|
||||||
]
|
|
||||||
pruneopts = "UT"
|
pruneopts = "UT"
|
||||||
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
||||||
|
|
||||||
|
@ -93,18 +87,6 @@
|
||||||
revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076"
|
revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076"
|
||||||
version = "v19"
|
version = "v19"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:6fda0d7f5e52b081e075775b1ecebf1ea0c923e7be33604ed0225ae078e701b5"
|
|
||||||
name = "github.com/coreos/pkg"
|
|
||||||
packages = [
|
|
||||||
"health",
|
|
||||||
"httputil",
|
|
||||||
"timeutil",
|
|
||||||
]
|
|
||||||
pruneopts = "UT"
|
|
||||||
revision = "97fdf19511ea361ae1c100dd393cc47f8dcfa1e1"
|
|
||||||
version = "v4"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||||
name = "github.com/davecgh/go-spew"
|
name = "github.com/davecgh/go-spew"
|
||||||
|
@ -212,14 +194,6 @@
|
||||||
pruneopts = "UT"
|
pruneopts = "UT"
|
||||||
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
|
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:75ab90ae3f5d876167e60f493beadfe66f0ed861a710f283fb06c86437a09538"
|
|
||||||
name = "github.com/jonboulle/clockwork"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = "UT"
|
|
||||||
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
|
|
||||||
version = "v0.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
|
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
|
||||||
name = "github.com/konsorten/go-windows-terminal-sequences"
|
name = "github.com/konsorten/go-windows-terminal-sequences"
|
||||||
|
@ -375,9 +349,12 @@
|
||||||
version = "v1.4.2"
|
version = "v1.4.2"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:f85e109eda8f6080877185d1c39e98dd8795e1780c08beca28304b87fd855a1c"
|
digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6"
|
||||||
name = "github.com/stretchr/testify"
|
name = "github.com/stretchr/testify"
|
||||||
packages = ["assert"]
|
packages = [
|
||||||
|
"assert",
|
||||||
|
"require",
|
||||||
|
]
|
||||||
pruneopts = "UT"
|
pruneopts = "UT"
|
||||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||||
version = "v1.2.1"
|
version = "v1.2.1"
|
||||||
|
@ -577,7 +554,6 @@
|
||||||
"github.com/coredns/coredns/plugin/pkg/rcode",
|
"github.com/coredns/coredns/plugin/pkg/rcode",
|
||||||
"github.com/coredns/coredns/request",
|
"github.com/coredns/coredns/request",
|
||||||
"github.com/coreos/go-oidc/jose",
|
"github.com/coreos/go-oidc/jose",
|
||||||
"github.com/coreos/go-oidc/oidc",
|
|
||||||
"github.com/coreos/go-systemd/daemon",
|
"github.com/coreos/go-systemd/daemon",
|
||||||
"github.com/elgs/gosqljson",
|
"github.com/elgs/gosqljson",
|
||||||
"github.com/equinox-io/equinox",
|
"github.com/equinox-io/equinox",
|
||||||
|
@ -597,6 +573,7 @@
|
||||||
"github.com/rifflock/lfshook",
|
"github.com/rifflock/lfshook",
|
||||||
"github.com/sirupsen/logrus",
|
"github.com/sirupsen/logrus",
|
||||||
"github.com/stretchr/testify/assert",
|
"github.com/stretchr/testify/assert",
|
||||||
|
"github.com/stretchr/testify/require",
|
||||||
"golang.org/x/crypto/nacl/box",
|
"golang.org/x/crypto/nacl/box",
|
||||||
"golang.org/x/crypto/ssh",
|
"golang.org/x/crypto/ssh",
|
||||||
"golang.org/x/crypto/ssh/terminal",
|
"golang.org/x/crypto/ssh/terminal",
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||||
|
@ -24,6 +26,27 @@ var logger = log.CreateLogger()
|
||||||
type lock struct {
|
type lock struct {
|
||||||
lockFilePath string
|
lockFilePath string
|
||||||
backoff *origin.BackoffHandler
|
backoff *origin.BackoffHandler
|
||||||
|
sigHandler *signalHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
type signalHandler struct {
|
||||||
|
sigChannel chan os.Signal
|
||||||
|
signals []os.Signal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *signalHandler) register(handler func()){
|
||||||
|
s.sigChannel = make(chan os.Signal, 1)
|
||||||
|
signal.Notify(s.sigChannel, s.signals...)
|
||||||
|
go func(s *signalHandler) {
|
||||||
|
for range s.sigChannel {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *signalHandler) deregister() {
|
||||||
|
signal.Stop(s.sigChannel)
|
||||||
|
close(s.sigChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func errDeleteTokenFailed(lockFilePath string) error {
|
func errDeleteTokenFailed(lockFilePath string) error {
|
||||||
|
@ -36,10 +59,19 @@ func newLock(path string) *lock {
|
||||||
return &lock{
|
return &lock{
|
||||||
lockFilePath: lockPath,
|
lockFilePath: lockPath,
|
||||||
backoff: &origin.BackoffHandler{MaxRetries: 7},
|
backoff: &origin.BackoffHandler{MaxRetries: 7},
|
||||||
|
sigHandler: &signalHandler{
|
||||||
|
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lock) Acquire() error {
|
func (l *lock) Acquire() error {
|
||||||
|
// Intercept SIGINT and SIGTERM to release lock before exiting
|
||||||
|
l.sigHandler.register(func() {
|
||||||
|
l.deleteLockFile()
|
||||||
|
os.Exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
// Check for a path.lock file
|
// Check for a path.lock file
|
||||||
// if the lock file exists; start polling
|
// if the lock file exists; start polling
|
||||||
// if not, create the lock file and go through the normal flow.
|
// if not, create the lock file and go through the normal flow.
|
||||||
|
@ -47,8 +79,10 @@ func (l *lock) Acquire() error {
|
||||||
for isTokenLocked(l.lockFilePath) {
|
for isTokenLocked(l.lockFilePath) {
|
||||||
if l.backoff.Backoff(context.Background()) {
|
if l.backoff.Backoff(context.Background()) {
|
||||||
continue
|
continue
|
||||||
} else {
|
}
|
||||||
return errDeleteTokenFailed(l.lockFilePath)
|
|
||||||
|
if err := l.deleteLockFile(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,13 +94,18 @@ func (l *lock) Acquire() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lock) Release() error {
|
func (l *lock) deleteLockFile() error {
|
||||||
if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) {
|
||||||
return errDeleteTokenFailed(l.lockFilePath)
|
return errDeleteTokenFailed(l.lockFilePath)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *lock) Release() error {
|
||||||
|
defer l.sigHandler.deregister()
|
||||||
|
return l.deleteLockFile()
|
||||||
|
}
|
||||||
|
|
||||||
// isTokenLocked checks to see if there is another process attempting to get the token already
|
// isTokenLocked checks to see if there is another process attempting to get the token already
|
||||||
func isTokenLocked(lockFilePath string) bool {
|
func isTokenLocked(lockFilePath string) bool {
|
||||||
exists, err := config.FileExists(lockFilePath)
|
exists, err := config.FileExists(lockFilePath)
|
||||||
|
@ -84,12 +123,13 @@ func FetchToken(appURL *url.URL) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
lock := newLock(path)
|
fileLock := newLock(path)
|
||||||
err = lock.Acquire()
|
|
||||||
|
err = fileLock.Acquire()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer lock.Release()
|
defer fileLock.Release()
|
||||||
|
|
||||||
// check to see if another process has gotten a token while we waited for the lock
|
// check to see if another process has gotten a token while we waited for the lock
|
||||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSignalHandler(t *testing.T) {
|
||||||
|
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
|
||||||
|
handlerRan := false
|
||||||
|
done := make(chan struct{})
|
||||||
|
timer := time.NewTimer(time.Second)
|
||||||
|
sigHandler.register(func(){
|
||||||
|
handlerRan = true
|
||||||
|
done <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
p, err := os.FindProcess(os.Getpid())
|
||||||
|
require.Nil(t, err)
|
||||||
|
p.Signal(syscall.SIGUSR1)
|
||||||
|
|
||||||
|
// Blocks for up to one second to make sure the handler callback runs before the assert.
|
||||||
|
select {
|
||||||
|
case <- done:
|
||||||
|
assert.True(t, handlerRan)
|
||||||
|
case <- timer.C:
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
sigHandler.deregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignalHandlerClose(t *testing.T) {
|
||||||
|
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
|
||||||
|
done := make(chan struct{})
|
||||||
|
timer := time.NewTimer(time.Second)
|
||||||
|
sigHandler.register(func(){done <- struct{}{}})
|
||||||
|
sigHandler.deregister()
|
||||||
|
|
||||||
|
p, err := os.FindProcess(os.Getpid())
|
||||||
|
require.Nil(t, err)
|
||||||
|
p.Signal(syscall.SIGUSR1)
|
||||||
|
select {
|
||||||
|
case <- done:
|
||||||
|
t.Fail()
|
||||||
|
case <- timer.C:
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
package http
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
Do(*http.Request) (*http.Response, error)
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
// Package http is DEPRECATED. Use net/http instead.
|
|
||||||
package http
|
|
|
@ -1,161 +0,0 @@
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WriteError(w http.ResponseWriter, code int, msg string) {
|
|
||||||
e := struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}{
|
|
||||||
Error: msg,
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(e)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("go-oidc: failed to marshal %#v: %v", e, err)
|
|
||||||
code = http.StatusInternalServerError
|
|
||||||
b = []byte(`{"error":"server_error"}`)
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
w.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BasicAuth parses a username and password from the request's
|
|
||||||
// Authorization header. This was pulled from golang master:
|
|
||||||
// https://codereview.appspot.com/76540043
|
|
||||||
func BasicAuth(r *http.Request) (username, password string, ok bool) {
|
|
||||||
auth := r.Header.Get("Authorization")
|
|
||||||
if auth == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(auth, "Basic ") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic "))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cs := string(c)
|
|
||||||
s := strings.IndexByte(cs, ':')
|
|
||||||
if s < 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return cs[:s], cs[s+1:], true
|
|
||||||
}
|
|
||||||
|
|
||||||
func cacheControlMaxAge(hdr string) (time.Duration, bool, error) {
|
|
||||||
for _, field := range strings.Split(hdr, ",") {
|
|
||||||
parts := strings.SplitN(strings.TrimSpace(field), "=", 2)
|
|
||||||
k := strings.ToLower(strings.TrimSpace(parts[0]))
|
|
||||||
if k != "max-age" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return 0, false, errors.New("max-age has no value")
|
|
||||||
}
|
|
||||||
|
|
||||||
v := strings.TrimSpace(parts[1])
|
|
||||||
if v == "" {
|
|
||||||
return 0, false, errors.New("max-age has empty value")
|
|
||||||
}
|
|
||||||
|
|
||||||
age, err := strconv.Atoi(v)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if age <= 0 {
|
|
||||||
return 0, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Duration(age) * time.Second, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func expires(date, expires string) (time.Duration, bool, error) {
|
|
||||||
if date == "" || expires == "" {
|
|
||||||
return 0, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var te time.Time
|
|
||||||
var err error
|
|
||||||
if expires == "0" {
|
|
||||||
return 0, false, nil
|
|
||||||
}
|
|
||||||
te, err = time.Parse(time.RFC1123, expires)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
td, err := time.Parse(time.RFC1123, date)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ttl := te.Sub(td)
|
|
||||||
|
|
||||||
// headers indicate data already expired, caller should not
|
|
||||||
// have to care about this case
|
|
||||||
if ttl <= 0 {
|
|
||||||
return 0, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ttl, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cacheable(hdr http.Header) (time.Duration, bool, error) {
|
|
||||||
ttl, ok, err := cacheControlMaxAge(hdr.Get("Cache-Control"))
|
|
||||||
if err != nil || ok {
|
|
||||||
return ttl, ok, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return expires(hdr.Get("Date"), hdr.Get("Expires"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeQuery appends additional query values to an existing URL.
|
|
||||||
func MergeQuery(u url.URL, q url.Values) url.URL {
|
|
||||||
uv := u.Query()
|
|
||||||
for k, vs := range q {
|
|
||||||
for _, v := range vs {
|
|
||||||
uv.Add(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u.RawQuery = uv.Encode()
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewResourceLocation appends a resource id to the end of the requested URL path.
|
|
||||||
func NewResourceLocation(reqURL *url.URL, id string) string {
|
|
||||||
var u url.URL
|
|
||||||
u = *reqURL
|
|
||||||
u.Path = path.Join(u.Path, id)
|
|
||||||
u.RawQuery = ""
|
|
||||||
u.Fragment = ""
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyRequest returns a clone of the provided *http.Request.
|
|
||||||
// The returned object is a shallow copy of the struct and a
|
|
||||||
// deep copy of its Header field.
|
|
||||||
func CopyRequest(r *http.Request) *http.Request {
|
|
||||||
r2 := *r
|
|
||||||
r2.Header = make(http.Header)
|
|
||||||
for k, s := range r.Header {
|
|
||||||
r2.Header[k] = s
|
|
||||||
}
|
|
||||||
return &r2
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseNonEmptyURL checks that a string is a parsable URL which is also not empty
|
|
||||||
// since `url.Parse("")` does not return an error. Must contian a scheme and a host.
|
|
||||||
func ParseNonEmptyURL(u string) (*url.URL, error) {
|
|
||||||
if u == "" {
|
|
||||||
return nil, errors.New("url is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
ur, err := url.Parse(u)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ur.Scheme == "" {
|
|
||||||
return nil, errors.New("url scheme is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ur.Host == "" {
|
|
||||||
return nil, errors.New("url host is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ur, nil
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
// Package key is DEPRECATED. Use github.com/coreos/go-oidc instead.
|
|
||||||
package key
|
|
|
@ -1,153 +0,0 @@
|
||||||
package key
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewPublicKey(jwk jose.JWK) *PublicKey {
|
|
||||||
return &PublicKey{jwk: jwk}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PublicKey struct {
|
|
||||||
jwk jose.JWK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *PublicKey) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(&k.jwk)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *PublicKey) UnmarshalJSON(data []byte) error {
|
|
||||||
var jwk jose.JWK
|
|
||||||
if err := json.Unmarshal(data, &jwk); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
k.jwk = jwk
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *PublicKey) ID() string {
|
|
||||||
return k.jwk.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *PublicKey) Verifier() (jose.Verifier, error) {
|
|
||||||
return jose.NewVerifierRSA(k.jwk)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrivateKey struct {
|
|
||||||
KeyID string
|
|
||||||
PrivateKey *rsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *PrivateKey) ID() string {
|
|
||||||
return k.KeyID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *PrivateKey) Signer() jose.Signer {
|
|
||||||
return jose.NewSignerRSA(k.ID(), *k.PrivateKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *PrivateKey) JWK() jose.JWK {
|
|
||||||
return jose.JWK{
|
|
||||||
ID: k.KeyID,
|
|
||||||
Type: "RSA",
|
|
||||||
Alg: "RS256",
|
|
||||||
Use: "sig",
|
|
||||||
Exponent: k.PrivateKey.PublicKey.E,
|
|
||||||
Modulus: k.PrivateKey.PublicKey.N,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeySet interface {
|
|
||||||
ExpiresAt() time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type PublicKeySet struct {
|
|
||||||
keys []PublicKey
|
|
||||||
index map[string]*PublicKey
|
|
||||||
expiresAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPublicKeySet(jwks []jose.JWK, exp time.Time) *PublicKeySet {
|
|
||||||
keys := make([]PublicKey, len(jwks))
|
|
||||||
index := make(map[string]*PublicKey)
|
|
||||||
for i, jwk := range jwks {
|
|
||||||
keys[i] = *NewPublicKey(jwk)
|
|
||||||
index[keys[i].ID()] = &keys[i]
|
|
||||||
}
|
|
||||||
return &PublicKeySet{
|
|
||||||
keys: keys,
|
|
||||||
index: index,
|
|
||||||
expiresAt: exp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PublicKeySet) ExpiresAt() time.Time {
|
|
||||||
return s.expiresAt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PublicKeySet) Keys() []PublicKey {
|
|
||||||
return s.keys
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PublicKeySet) Key(id string) *PublicKey {
|
|
||||||
return s.index[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrivateKeySet struct {
|
|
||||||
keys []*PrivateKey
|
|
||||||
ActiveKeyID string
|
|
||||||
expiresAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPrivateKeySet(keys []*PrivateKey, exp time.Time) *PrivateKeySet {
|
|
||||||
return &PrivateKeySet{
|
|
||||||
keys: keys,
|
|
||||||
ActiveKeyID: keys[0].ID(),
|
|
||||||
expiresAt: exp.UTC(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrivateKeySet) Keys() []*PrivateKey {
|
|
||||||
return s.keys
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrivateKeySet) ExpiresAt() time.Time {
|
|
||||||
return s.expiresAt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrivateKeySet) Active() *PrivateKey {
|
|
||||||
for i, k := range s.keys {
|
|
||||||
if k.ID() == s.ActiveKeyID {
|
|
||||||
return s.keys[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type GeneratePrivateKeyFunc func() (*PrivateKey, error)
|
|
||||||
|
|
||||||
func GeneratePrivateKey() (*PrivateKey, error) {
|
|
||||||
pk, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keyID := make([]byte, 20)
|
|
||||||
if _, err := io.ReadFull(rand.Reader, keyID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
k := PrivateKey{
|
|
||||||
KeyID: hex.EncodeToString(keyID),
|
|
||||||
PrivateKey: pk,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &k, nil
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
package key
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jonboulle/clockwork"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
"github.com/coreos/pkg/health"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PrivateKeyManager interface {
|
|
||||||
ExpiresAt() time.Time
|
|
||||||
Signer() (jose.Signer, error)
|
|
||||||
JWKs() ([]jose.JWK, error)
|
|
||||||
PublicKeys() ([]PublicKey, error)
|
|
||||||
|
|
||||||
WritableKeySetRepo
|
|
||||||
health.Checkable
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPrivateKeyManager() PrivateKeyManager {
|
|
||||||
return &privateKeyManager{
|
|
||||||
clock: clockwork.NewRealClock(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type privateKeyManager struct {
|
|
||||||
keySet *PrivateKeySet
|
|
||||||
clock clockwork.Clock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *privateKeyManager) ExpiresAt() time.Time {
|
|
||||||
if m.keySet == nil {
|
|
||||||
return m.clock.Now().UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.keySet.ExpiresAt()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *privateKeyManager) Signer() (jose.Signer, error) {
|
|
||||||
if err := m.Healthy(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.keySet.Active().Signer(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *privateKeyManager) JWKs() ([]jose.JWK, error) {
|
|
||||||
if err := m.Healthy(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := m.keySet.Keys()
|
|
||||||
jwks := make([]jose.JWK, len(keys))
|
|
||||||
for i, k := range keys {
|
|
||||||
jwks[i] = k.JWK()
|
|
||||||
}
|
|
||||||
return jwks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *privateKeyManager) PublicKeys() ([]PublicKey, error) {
|
|
||||||
jwks, err := m.JWKs()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keys := make([]PublicKey, len(jwks))
|
|
||||||
for i, jwk := range jwks {
|
|
||||||
keys[i] = *NewPublicKey(jwk)
|
|
||||||
}
|
|
||||||
return keys, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *privateKeyManager) Healthy() error {
|
|
||||||
if m.keySet == nil {
|
|
||||||
return errors.New("private key manager uninitialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.keySet.Keys()) == 0 {
|
|
||||||
return errors.New("private key manager zero keys")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.keySet.ExpiresAt().Before(m.clock.Now().UTC()) {
|
|
||||||
return errors.New("private key manager keys expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *privateKeyManager) Set(keySet KeySet) error {
|
|
||||||
privKeySet, ok := keySet.(*PrivateKeySet)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("unable to cast to PrivateKeySet")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.keySet = privKeySet
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package key
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrorNoKeys = errors.New("no keys found")
|
|
||||||
|
|
||||||
type WritableKeySetRepo interface {
|
|
||||||
Set(KeySet) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReadableKeySetRepo interface {
|
|
||||||
Get() (KeySet, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrivateKeySetRepo interface {
|
|
||||||
WritableKeySetRepo
|
|
||||||
ReadableKeySetRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPrivateKeySetRepo() PrivateKeySetRepo {
|
|
||||||
return &memPrivateKeySetRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type memPrivateKeySetRepo struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
pks PrivateKeySet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *memPrivateKeySetRepo) Set(ks KeySet) error {
|
|
||||||
pks, ok := ks.(*PrivateKeySet)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("unable to cast to PrivateKeySet")
|
|
||||||
} else if pks == nil {
|
|
||||||
return errors.New("nil KeySet")
|
|
||||||
}
|
|
||||||
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
r.pks = *pks
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *memPrivateKeySetRepo) Get() (KeySet, error) {
|
|
||||||
r.mu.RLock()
|
|
||||||
defer r.mu.RUnlock()
|
|
||||||
|
|
||||||
if r.pks.keys == nil {
|
|
||||||
return nil, ErrorNoKeys
|
|
||||||
}
|
|
||||||
return KeySet(&r.pks), nil
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
package key
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
ptime "github.com/coreos/pkg/timeutil"
|
|
||||||
"github.com/jonboulle/clockwork"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrorPrivateKeysExpired = errors.New("private keys have expired")
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewPrivateKeyRotator(repo PrivateKeySetRepo, ttl time.Duration) *PrivateKeyRotator {
|
|
||||||
return &PrivateKeyRotator{
|
|
||||||
repo: repo,
|
|
||||||
ttl: ttl,
|
|
||||||
|
|
||||||
keep: 2,
|
|
||||||
generateKey: GeneratePrivateKey,
|
|
||||||
clock: clockwork.NewRealClock(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrivateKeyRotator struct {
|
|
||||||
repo PrivateKeySetRepo
|
|
||||||
generateKey GeneratePrivateKeyFunc
|
|
||||||
clock clockwork.Clock
|
|
||||||
keep int
|
|
||||||
ttl time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PrivateKeyRotator) expiresAt() time.Time {
|
|
||||||
return r.clock.Now().UTC().Add(r.ttl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PrivateKeyRotator) Healthy() error {
|
|
||||||
pks, err := r.privateKeySet()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.clock.Now().After(pks.ExpiresAt()) {
|
|
||||||
return ErrorPrivateKeysExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PrivateKeyRotator) privateKeySet() (*PrivateKeySet, error) {
|
|
||||||
ks, err := r.repo.Get()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pks, ok := ks.(*PrivateKeySet)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("unable to cast to PrivateKeySet")
|
|
||||||
}
|
|
||||||
return pks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PrivateKeyRotator) nextRotation() (time.Duration, error) {
|
|
||||||
pks, err := r.privateKeySet()
|
|
||||||
if err == ErrorNoKeys {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := r.clock.Now()
|
|
||||||
|
|
||||||
// Ideally, we want to rotate after half the TTL has elapsed.
|
|
||||||
idealRotationTime := pks.ExpiresAt().Add(-r.ttl / 2)
|
|
||||||
|
|
||||||
// If we are past the ideal rotation time, rotate immediatly.
|
|
||||||
return max(0, idealRotationTime.Sub(now)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func max(a, b time.Duration) time.Duration {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PrivateKeyRotator) Run() chan struct{} {
|
|
||||||
attempt := func() {
|
|
||||||
k, err := r.generateKey()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("go-oidc: failed generating signing key: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exp := r.expiresAt()
|
|
||||||
if err := rotatePrivateKeys(r.repo, k, r.keep, exp); err != nil {
|
|
||||||
log.Printf("go-oidc: key rotation failed: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
var nextRotation time.Duration
|
|
||||||
var sleep time.Duration
|
|
||||||
var err error
|
|
||||||
for {
|
|
||||||
if nextRotation, err = r.nextRotation(); err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sleep = ptime.ExpBackoff(sleep, time.Minute)
|
|
||||||
log.Printf("go-oidc: error getting nextRotation, retrying in %v: %v", sleep, err)
|
|
||||||
time.Sleep(sleep)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-r.clock.After(nextRotation):
|
|
||||||
attempt()
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return stop
|
|
||||||
}
|
|
||||||
|
|
||||||
func rotatePrivateKeys(repo PrivateKeySetRepo, k *PrivateKey, keep int, exp time.Time) error {
|
|
||||||
ks, err := repo.Get()
|
|
||||||
if err != nil && err != ErrorNoKeys {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys []*PrivateKey
|
|
||||||
if ks != nil {
|
|
||||||
pks, ok := ks.(*PrivateKeySet)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("unable to cast to PrivateKeySet")
|
|
||||||
}
|
|
||||||
keys = pks.Keys()
|
|
||||||
}
|
|
||||||
|
|
||||||
keys = append([]*PrivateKey{k}, keys...)
|
|
||||||
if l := len(keys); l > keep {
|
|
||||||
keys = keys[0:keep]
|
|
||||||
}
|
|
||||||
|
|
||||||
nks := PrivateKeySet{
|
|
||||||
keys: keys,
|
|
||||||
ActiveKeyID: k.ID(),
|
|
||||||
expiresAt: exp,
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo.Set(KeySet(&nks))
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
package key
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jonboulle/clockwork"
|
|
||||||
|
|
||||||
"github.com/coreos/pkg/timeutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewKeySetSyncer(r ReadableKeySetRepo, w WritableKeySetRepo) *KeySetSyncer {
|
|
||||||
return &KeySetSyncer{
|
|
||||||
readable: r,
|
|
||||||
writable: w,
|
|
||||||
clock: clockwork.NewRealClock(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeySetSyncer struct {
|
|
||||||
readable ReadableKeySetRepo
|
|
||||||
writable WritableKeySetRepo
|
|
||||||
clock clockwork.Clock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *KeySetSyncer) Run() chan struct{} {
|
|
||||||
stop := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
var failing bool
|
|
||||||
var next time.Duration
|
|
||||||
for {
|
|
||||||
exp, err := syncKeySet(s.readable, s.writable, s.clock)
|
|
||||||
if err != nil || exp == 0 {
|
|
||||||
if !failing {
|
|
||||||
failing = true
|
|
||||||
next = time.Second
|
|
||||||
} else {
|
|
||||||
next = timeutil.ExpBackoff(next, time.Minute)
|
|
||||||
}
|
|
||||||
if exp == 0 {
|
|
||||||
log.Printf("Synced to already expired key set, retrying in %v: %v", next, err)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
log.Printf("Failed syncing key set, retrying in %v: %v", next, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
failing = false
|
|
||||||
next = exp / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-s.clock.After(next):
|
|
||||||
continue
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return stop
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sync(r ReadableKeySetRepo, w WritableKeySetRepo) (time.Duration, error) {
|
|
||||||
return syncKeySet(r, w, clockwork.NewRealClock())
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncKeySet copies the keyset from r to the KeySet at w and returns the duration in which the KeySet will expire.
|
|
||||||
// If keyset has already expired, returns a zero duration.
|
|
||||||
func syncKeySet(r ReadableKeySetRepo, w WritableKeySetRepo, clock clockwork.Clock) (exp time.Duration, err error) {
|
|
||||||
var ks KeySet
|
|
||||||
ks, err = r.Get()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ks == nil {
|
|
||||||
err = errors.New("no source KeySet")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = w.Set(ks); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
now := clock.Now()
|
|
||||||
if ks.ExpiresAt().After(now) {
|
|
||||||
exp = ks.ExpiresAt().Sub(now)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
// Package oauth2 is DEPRECATED. Use golang.org/x/oauth instead.
|
|
||||||
package oauth2
|
|
|
@ -1,29 +0,0 @@
|
||||||
package oauth2
|
|
||||||
|
|
||||||
const (
|
|
||||||
ErrorAccessDenied = "access_denied"
|
|
||||||
ErrorInvalidClient = "invalid_client"
|
|
||||||
ErrorInvalidGrant = "invalid_grant"
|
|
||||||
ErrorInvalidRequest = "invalid_request"
|
|
||||||
ErrorServerError = "server_error"
|
|
||||||
ErrorUnauthorizedClient = "unauthorized_client"
|
|
||||||
ErrorUnsupportedGrantType = "unsupported_grant_type"
|
|
||||||
ErrorUnsupportedResponseType = "unsupported_response_type"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Type string `json:"error"`
|
|
||||||
Description string `json:"error_description,omitempty"`
|
|
||||||
State string `json:"state,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
|
||||||
if e.Description != "" {
|
|
||||||
return e.Type + ": " + e.Description
|
|
||||||
}
|
|
||||||
return e.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewError(typ string) *Error {
|
|
||||||
return &Error{Type: typ}
|
|
||||||
}
|
|
|
@ -1,416 +0,0 @@
|
||||||
package oauth2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
phttp "github.com/coreos/go-oidc/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResponseTypesEqual compares two response_type values. If either
|
|
||||||
// contains a space, it is treated as an unordered list. For example,
|
|
||||||
// comparing "code id_token" and "id_token code" would evaluate to true.
|
|
||||||
func ResponseTypesEqual(r1, r2 string) bool {
|
|
||||||
if !strings.Contains(r1, " ") || !strings.Contains(r2, " ") {
|
|
||||||
// fast route, no split needed
|
|
||||||
return r1 == r2
|
|
||||||
}
|
|
||||||
|
|
||||||
// split, sort, and compare
|
|
||||||
r1Fields := strings.Fields(r1)
|
|
||||||
r2Fields := strings.Fields(r2)
|
|
||||||
if len(r1Fields) != len(r2Fields) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
sort.Strings(r1Fields)
|
|
||||||
sort.Strings(r2Fields)
|
|
||||||
for i, r1Field := range r1Fields {
|
|
||||||
if r1Field != r2Fields[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// OAuth2.0 response types registered by OIDC.
|
|
||||||
//
|
|
||||||
// See: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#RegistryContents
|
|
||||||
ResponseTypeCode = "code"
|
|
||||||
ResponseTypeCodeIDToken = "code id_token"
|
|
||||||
ResponseTypeCodeIDTokenToken = "code id_token token"
|
|
||||||
ResponseTypeIDToken = "id_token"
|
|
||||||
ResponseTypeIDTokenToken = "id_token token"
|
|
||||||
ResponseTypeToken = "token"
|
|
||||||
ResponseTypeNone = "none"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
GrantTypeAuthCode = "authorization_code"
|
|
||||||
GrantTypeClientCreds = "client_credentials"
|
|
||||||
GrantTypeUserCreds = "password"
|
|
||||||
GrantTypeImplicit = "implicit"
|
|
||||||
GrantTypeRefreshToken = "refresh_token"
|
|
||||||
|
|
||||||
AuthMethodClientSecretPost = "client_secret_post"
|
|
||||||
AuthMethodClientSecretBasic = "client_secret_basic"
|
|
||||||
AuthMethodClientSecretJWT = "client_secret_jwt"
|
|
||||||
AuthMethodPrivateKeyJWT = "private_key_jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Credentials ClientCredentials
|
|
||||||
Scope []string
|
|
||||||
RedirectURL string
|
|
||||||
AuthURL string
|
|
||||||
TokenURL string
|
|
||||||
|
|
||||||
// Must be one of the AuthMethodXXX methods above. Right now, only
|
|
||||||
// AuthMethodClientSecretPost and AuthMethodClientSecretBasic are supported.
|
|
||||||
AuthMethod string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
hc phttp.Client
|
|
||||||
creds ClientCredentials
|
|
||||||
scope []string
|
|
||||||
authURL *url.URL
|
|
||||||
redirectURL *url.URL
|
|
||||||
tokenURL *url.URL
|
|
||||||
authMethod string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientCredentials struct {
|
|
||||||
ID string
|
|
||||||
Secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(hc phttp.Client, cfg Config) (c *Client, err error) {
|
|
||||||
if len(cfg.Credentials.ID) == 0 {
|
|
||||||
err = errors.New("missing client id")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.Credentials.Secret) == 0 {
|
|
||||||
err = errors.New("missing client secret")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.AuthMethod == "" {
|
|
||||||
cfg.AuthMethod = AuthMethodClientSecretBasic
|
|
||||||
} else if cfg.AuthMethod != AuthMethodClientSecretPost && cfg.AuthMethod != AuthMethodClientSecretBasic {
|
|
||||||
err = fmt.Errorf("auth method %q is not supported", cfg.AuthMethod)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
au, err := phttp.ParseNonEmptyURL(cfg.AuthURL)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tu, err := phttp.ParseNonEmptyURL(cfg.TokenURL)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow empty redirect URL in the case where the client
|
|
||||||
// only needs to verify a given token.
|
|
||||||
ru, err := url.Parse(cfg.RedirectURL)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c = &Client{
|
|
||||||
creds: cfg.Credentials,
|
|
||||||
scope: cfg.Scope,
|
|
||||||
redirectURL: ru,
|
|
||||||
authURL: au,
|
|
||||||
tokenURL: tu,
|
|
||||||
hc: hc,
|
|
||||||
authMethod: cfg.AuthMethod,
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the embedded HTTP client
|
|
||||||
func (c *Client) HttpClient() phttp.Client {
|
|
||||||
return c.hc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the url for initial redirect to oauth provider.
|
|
||||||
func (c *Client) AuthCodeURL(state, accessType, prompt string) string {
|
|
||||||
v := c.commonURLValues()
|
|
||||||
v.Set("state", state)
|
|
||||||
if strings.ToLower(accessType) == "offline" {
|
|
||||||
v.Set("access_type", "offline")
|
|
||||||
}
|
|
||||||
|
|
||||||
if prompt != "" {
|
|
||||||
v.Set("prompt", prompt)
|
|
||||||
}
|
|
||||||
v.Set("response_type", "code")
|
|
||||||
|
|
||||||
q := v.Encode()
|
|
||||||
u := *c.authURL
|
|
||||||
if u.RawQuery == "" {
|
|
||||||
u.RawQuery = q
|
|
||||||
} else {
|
|
||||||
u.RawQuery += "&" + q
|
|
||||||
}
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) commonURLValues() url.Values {
|
|
||||||
return url.Values{
|
|
||||||
"redirect_uri": {c.redirectURL.String()},
|
|
||||||
"scope": {strings.Join(c.scope, " ")},
|
|
||||||
"client_id": {c.creds.ID},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newAuthenticatedRequest(urlToken string, values url.Values) (*http.Request, error) {
|
|
||||||
var req *http.Request
|
|
||||||
var err error
|
|
||||||
switch c.authMethod {
|
|
||||||
case AuthMethodClientSecretPost:
|
|
||||||
values.Set("client_secret", c.creds.Secret)
|
|
||||||
req, err = http.NewRequest("POST", urlToken, strings.NewReader(values.Encode()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case AuthMethodClientSecretBasic:
|
|
||||||
req, err = http.NewRequest("POST", urlToken, strings.NewReader(values.Encode()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
encodedID := url.QueryEscape(c.creds.ID)
|
|
||||||
encodedSecret := url.QueryEscape(c.creds.Secret)
|
|
||||||
req.SetBasicAuth(encodedID, encodedSecret)
|
|
||||||
default:
|
|
||||||
panic("misconfigured client: auth method not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
return req, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientCredsToken posts the client id and secret to obtain a token scoped to the OAuth2 client via the "client_credentials" grant type.
|
|
||||||
// May not be supported by all OAuth2 servers.
|
|
||||||
func (c *Client) ClientCredsToken(scope []string) (result TokenResponse, err error) {
|
|
||||||
v := url.Values{
|
|
||||||
"scope": {strings.Join(scope, " ")},
|
|
||||||
"grant_type": {GrantTypeClientCreds},
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return parseTokenResponse(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserCredsToken posts the username and password to obtain a token scoped to the OAuth2 client via the "password" grant_type
|
|
||||||
// May not be supported by all OAuth2 servers.
|
|
||||||
func (c *Client) UserCredsToken(username, password string) (result TokenResponse, err error) {
|
|
||||||
v := url.Values{
|
|
||||||
"scope": {strings.Join(c.scope, " ")},
|
|
||||||
"grant_type": {GrantTypeUserCreds},
|
|
||||||
"username": {username},
|
|
||||||
"password": {password},
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return parseTokenResponse(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestToken requests a token from the Token Endpoint with the specified grantType.
|
|
||||||
// If 'grantType' == GrantTypeAuthCode, then 'value' should be the authorization code.
|
|
||||||
// If 'grantType' == GrantTypeRefreshToken, then 'value' should be the refresh token.
|
|
||||||
func (c *Client) RequestToken(grantType, value string) (result TokenResponse, err error) {
|
|
||||||
v := c.commonURLValues()
|
|
||||||
|
|
||||||
v.Set("grant_type", grantType)
|
|
||||||
v.Set("client_secret", c.creds.Secret)
|
|
||||||
switch grantType {
|
|
||||||
case GrantTypeAuthCode:
|
|
||||||
v.Set("code", value)
|
|
||||||
case GrantTypeRefreshToken:
|
|
||||||
v.Set("refresh_token", value)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unsupported grant_type: %v", grantType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return parseTokenResponse(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTokenResponse(resp *http.Response) (result TokenResponse, err error) {
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
badStatusCode := resp.StatusCode < 200 || resp.StatusCode > 299
|
|
||||||
|
|
||||||
contentType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result = TokenResponse{
|
|
||||||
RawBody: body,
|
|
||||||
}
|
|
||||||
|
|
||||||
newError := func(typ, desc, state string) error {
|
|
||||||
if typ == "" {
|
|
||||||
return fmt.Errorf("unrecognized error %s", body)
|
|
||||||
}
|
|
||||||
return &Error{typ, desc, state}
|
|
||||||
}
|
|
||||||
|
|
||||||
if contentType == "application/x-www-form-urlencoded" || contentType == "text/plain" {
|
|
||||||
var vals url.Values
|
|
||||||
vals, err = url.ParseQuery(string(body))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if error := vals.Get("error"); error != "" || badStatusCode {
|
|
||||||
err = newError(error, vals.Get("error_description"), vals.Get("state"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e := vals.Get("expires_in")
|
|
||||||
if e == "" {
|
|
||||||
e = vals.Get("expires")
|
|
||||||
}
|
|
||||||
if e != "" {
|
|
||||||
result.Expires, err = strconv.Atoi(e)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.AccessToken = vals.Get("access_token")
|
|
||||||
result.TokenType = vals.Get("token_type")
|
|
||||||
result.IDToken = vals.Get("id_token")
|
|
||||||
result.RefreshToken = vals.Get("refresh_token")
|
|
||||||
result.Scope = vals.Get("scope")
|
|
||||||
} else {
|
|
||||||
var r struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
IDToken string `json:"id_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
State string `json:"state"`
|
|
||||||
ExpiresIn json.Number `json:"expires_in"` // Azure AD returns string
|
|
||||||
Expires int `json:"expires"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
Desc string `json:"error_description"`
|
|
||||||
}
|
|
||||||
if err = json.Unmarshal(body, &r); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Error != "" || badStatusCode {
|
|
||||||
err = newError(r.Error, r.Desc, r.State)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result.AccessToken = r.AccessToken
|
|
||||||
result.TokenType = r.TokenType
|
|
||||||
result.IDToken = r.IDToken
|
|
||||||
result.RefreshToken = r.RefreshToken
|
|
||||||
result.Scope = r.Scope
|
|
||||||
if expiresIn, err := r.ExpiresIn.Int64(); err != nil {
|
|
||||||
result.Expires = r.Expires
|
|
||||||
} else {
|
|
||||||
result.Expires = int(expiresIn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenResponse struct {
|
|
||||||
AccessToken string
|
|
||||||
TokenType string
|
|
||||||
Expires int
|
|
||||||
IDToken string
|
|
||||||
RefreshToken string // OPTIONAL.
|
|
||||||
Scope string // OPTIONAL, if identical to the scope requested by the client, otherwise, REQUIRED.
|
|
||||||
RawBody []byte // In case callers need some other non-standard info from the token response
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthCodeRequest struct {
|
|
||||||
ResponseType string
|
|
||||||
ClientID string
|
|
||||||
RedirectURL *url.URL
|
|
||||||
Scope []string
|
|
||||||
State string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseAuthCodeRequest(q url.Values) (AuthCodeRequest, error) {
|
|
||||||
acr := AuthCodeRequest{
|
|
||||||
ResponseType: q.Get("response_type"),
|
|
||||||
ClientID: q.Get("client_id"),
|
|
||||||
State: q.Get("state"),
|
|
||||||
Scope: make([]string, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
qs := strings.TrimSpace(q.Get("scope"))
|
|
||||||
if qs != "" {
|
|
||||||
acr.Scope = strings.Split(qs, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := func() error {
|
|
||||||
if acr.ClientID == "" {
|
|
||||||
return NewError(ErrorInvalidRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectURL := q.Get("redirect_uri")
|
|
||||||
if redirectURL != "" {
|
|
||||||
ru, err := url.Parse(redirectURL)
|
|
||||||
if err != nil {
|
|
||||||
return NewError(ErrorInvalidRequest)
|
|
||||||
}
|
|
||||||
acr.RedirectURL = ru
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
return acr, err
|
|
||||||
}
|
|
|
@ -1,846 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/mail"
|
|
||||||
"net/url"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
phttp "github.com/coreos/go-oidc/http"
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
"github.com/coreos/go-oidc/key"
|
|
||||||
"github.com/coreos/go-oidc/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// amount of time that must pass after the last key sync
|
|
||||||
// completes before another attempt may begin
|
|
||||||
keySyncWindow = 5 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
DefaultScope = []string{"openid", "email", "profile"}
|
|
||||||
|
|
||||||
supportedAuthMethods = map[string]struct{}{
|
|
||||||
oauth2.AuthMethodClientSecretBasic: struct{}{},
|
|
||||||
oauth2.AuthMethodClientSecretPost: struct{}{},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClientCredentials oauth2.ClientCredentials
|
|
||||||
|
|
||||||
type ClientIdentity struct {
|
|
||||||
Credentials ClientCredentials
|
|
||||||
Metadata ClientMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
type JWAOptions struct {
|
|
||||||
// SigningAlg specifies an JWA alg for signing JWTs.
|
|
||||||
//
|
|
||||||
// Specifying this field implies different actions depending on the context. It may
|
|
||||||
// require objects be serialized and signed as a JWT instead of plain JSON, or
|
|
||||||
// require an existing JWT object use the specified alg.
|
|
||||||
//
|
|
||||||
// See: http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
|
|
||||||
SigningAlg string
|
|
||||||
// EncryptionAlg, if provided, specifies that the returned or sent object be stored
|
|
||||||
// (or nested) within a JWT object and encrypted with the provided JWA alg.
|
|
||||||
EncryptionAlg string
|
|
||||||
// EncryptionEnc specifies the JWA enc algorithm to use with EncryptionAlg. If
|
|
||||||
// EncryptionAlg is provided and EncryptionEnc is omitted, this field defaults
|
|
||||||
// to A128CBC-HS256.
|
|
||||||
//
|
|
||||||
// If EncryptionEnc is provided EncryptionAlg must also be specified.
|
|
||||||
EncryptionEnc string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt JWAOptions) valid() error {
|
|
||||||
if opt.EncryptionEnc != "" && opt.EncryptionAlg == "" {
|
|
||||||
return errors.New("encryption encoding provided with no encryption algorithm")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt JWAOptions) defaults() JWAOptions {
|
|
||||||
if opt.EncryptionAlg != "" && opt.EncryptionEnc == "" {
|
|
||||||
opt.EncryptionEnc = jose.EncA128CBCHS256
|
|
||||||
}
|
|
||||||
return opt
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Ensure ClientMetadata satisfies these interfaces.
|
|
||||||
_ json.Marshaler = &ClientMetadata{}
|
|
||||||
_ json.Unmarshaler = &ClientMetadata{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClientMetadata holds metadata that the authorization server associates
|
|
||||||
// with a client identifier. The fields range from human-facing display
|
|
||||||
// strings such as client name, to items that impact the security of the
|
|
||||||
// protocol, such as the list of valid redirect URIs.
|
|
||||||
//
|
|
||||||
// See http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
|
|
||||||
//
|
|
||||||
// TODO: support language specific claim representations
|
|
||||||
// http://openid.net/specs/openid-connect-registration-1_0.html#LanguagesAndScripts
|
|
||||||
type ClientMetadata struct {
|
|
||||||
RedirectURIs []url.URL // Required
|
|
||||||
|
|
||||||
// A list of OAuth 2.0 "response_type" values that the client wishes to restrict
|
|
||||||
// itself to. Either "code", "token", or another registered extension.
|
|
||||||
//
|
|
||||||
// If omitted, only "code" will be used.
|
|
||||||
ResponseTypes []string
|
|
||||||
// A list of OAuth 2.0 grant types the client wishes to restrict itself to.
|
|
||||||
// The grant type values used by OIDC are "authorization_code", "implicit",
|
|
||||||
// and "refresh_token".
|
|
||||||
//
|
|
||||||
// If ommitted, only "authorization_code" will be used.
|
|
||||||
GrantTypes []string
|
|
||||||
// "native" or "web". If omitted, "web".
|
|
||||||
ApplicationType string
|
|
||||||
|
|
||||||
// List of email addresses.
|
|
||||||
Contacts []mail.Address
|
|
||||||
// Name of client to be presented to the end-user.
|
|
||||||
ClientName string
|
|
||||||
// URL that references a logo for the Client application.
|
|
||||||
LogoURI *url.URL
|
|
||||||
// URL of the home page of the Client.
|
|
||||||
ClientURI *url.URL
|
|
||||||
// Profile data policies and terms of use to be provided to the end user.
|
|
||||||
PolicyURI *url.URL
|
|
||||||
TermsOfServiceURI *url.URL
|
|
||||||
|
|
||||||
// URL to or the value of the client's JSON Web Key Set document.
|
|
||||||
JWKSURI *url.URL
|
|
||||||
JWKS *jose.JWKSet
|
|
||||||
|
|
||||||
// URL referencing a flie with a single JSON array of redirect URIs.
|
|
||||||
SectorIdentifierURI *url.URL
|
|
||||||
|
|
||||||
SubjectType string
|
|
||||||
|
|
||||||
// Options to restrict the JWS alg and enc values used for server responses and requests.
|
|
||||||
IDTokenResponseOptions JWAOptions
|
|
||||||
UserInfoResponseOptions JWAOptions
|
|
||||||
RequestObjectOptions JWAOptions
|
|
||||||
|
|
||||||
// Client requested authorization method and signing options for the token endpoint.
|
|
||||||
//
|
|
||||||
// Defaults to "client_secret_basic"
|
|
||||||
TokenEndpointAuthMethod string
|
|
||||||
TokenEndpointAuthSigningAlg string
|
|
||||||
|
|
||||||
// DefaultMaxAge specifies the maximum amount of time in seconds before an authorized
|
|
||||||
// user must reauthroize.
|
|
||||||
//
|
|
||||||
// If 0, no limitation is placed on the maximum.
|
|
||||||
DefaultMaxAge int64
|
|
||||||
// RequireAuthTime specifies if the auth_time claim in the ID token is required.
|
|
||||||
RequireAuthTime bool
|
|
||||||
|
|
||||||
// Default Authentication Context Class Reference values for authentication requests.
|
|
||||||
DefaultACRValues []string
|
|
||||||
|
|
||||||
// URI that a third party can use to initiate a login by the relaying party.
|
|
||||||
//
|
|
||||||
// See: http://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin
|
|
||||||
InitiateLoginURI *url.URL
|
|
||||||
// Pre-registered request_uri values that may be cached by the server.
|
|
||||||
RequestURIs []url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defaults returns a shallow copy of ClientMetadata with default
|
|
||||||
// values replacing omitted fields.
|
|
||||||
func (m ClientMetadata) Defaults() ClientMetadata {
|
|
||||||
if len(m.ResponseTypes) == 0 {
|
|
||||||
m.ResponseTypes = []string{oauth2.ResponseTypeCode}
|
|
||||||
}
|
|
||||||
if len(m.GrantTypes) == 0 {
|
|
||||||
m.GrantTypes = []string{oauth2.GrantTypeAuthCode}
|
|
||||||
}
|
|
||||||
if m.ApplicationType == "" {
|
|
||||||
m.ApplicationType = "web"
|
|
||||||
}
|
|
||||||
if m.TokenEndpointAuthMethod == "" {
|
|
||||||
m.TokenEndpointAuthMethod = oauth2.AuthMethodClientSecretBasic
|
|
||||||
}
|
|
||||||
m.IDTokenResponseOptions = m.IDTokenResponseOptions.defaults()
|
|
||||||
m.UserInfoResponseOptions = m.UserInfoResponseOptions.defaults()
|
|
||||||
m.RequestObjectOptions = m.RequestObjectOptions.defaults()
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ClientMetadata) MarshalJSON() ([]byte, error) {
|
|
||||||
e := m.toEncodableStruct()
|
|
||||||
return json.Marshal(&e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ClientMetadata) UnmarshalJSON(data []byte) error {
|
|
||||||
var e encodableClientMetadata
|
|
||||||
if err := json.Unmarshal(data, &e); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
meta, err := e.toStruct()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := meta.Valid(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*m = meta
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type encodableClientMetadata struct {
|
|
||||||
RedirectURIs []string `json:"redirect_uris"` // Required
|
|
||||||
ResponseTypes []string `json:"response_types,omitempty"`
|
|
||||||
GrantTypes []string `json:"grant_types,omitempty"`
|
|
||||||
ApplicationType string `json:"application_type,omitempty"`
|
|
||||||
Contacts []string `json:"contacts,omitempty"`
|
|
||||||
ClientName string `json:"client_name,omitempty"`
|
|
||||||
LogoURI string `json:"logo_uri,omitempty"`
|
|
||||||
ClientURI string `json:"client_uri,omitempty"`
|
|
||||||
PolicyURI string `json:"policy_uri,omitempty"`
|
|
||||||
TermsOfServiceURI string `json:"tos_uri,omitempty"`
|
|
||||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
|
||||||
JWKS *jose.JWKSet `json:"jwks,omitempty"`
|
|
||||||
SectorIdentifierURI string `json:"sector_identifier_uri,omitempty"`
|
|
||||||
SubjectType string `json:"subject_type,omitempty"`
|
|
||||||
IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"`
|
|
||||||
IDTokenEncryptedResponseAlg string `json:"id_token_encrypted_response_alg,omitempty"`
|
|
||||||
IDTokenEncryptedResponseEnc string `json:"id_token_encrypted_response_enc,omitempty"`
|
|
||||||
UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"`
|
|
||||||
UserInfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"`
|
|
||||||
UserInfoEncryptedResponseEnc string `json:"userinfo_encrypted_response_enc,omitempty"`
|
|
||||||
RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"`
|
|
||||||
RequestObjectEncryptionAlg string `json:"request_object_encryption_alg,omitempty"`
|
|
||||||
RequestObjectEncryptionEnc string `json:"request_object_encryption_enc,omitempty"`
|
|
||||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
|
||||||
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"`
|
|
||||||
DefaultMaxAge int64 `json:"default_max_age,omitempty"`
|
|
||||||
RequireAuthTime bool `json:"require_auth_time,omitempty"`
|
|
||||||
DefaultACRValues []string `json:"default_acr_values,omitempty"`
|
|
||||||
InitiateLoginURI string `json:"initiate_login_uri,omitempty"`
|
|
||||||
RequestURIs []string `json:"request_uris,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *encodableClientMetadata) toStruct() (ClientMetadata, error) {
|
|
||||||
p := stickyErrParser{}
|
|
||||||
m := ClientMetadata{
|
|
||||||
RedirectURIs: p.parseURIs(c.RedirectURIs, "redirect_uris"),
|
|
||||||
ResponseTypes: c.ResponseTypes,
|
|
||||||
GrantTypes: c.GrantTypes,
|
|
||||||
ApplicationType: c.ApplicationType,
|
|
||||||
Contacts: p.parseEmails(c.Contacts, "contacts"),
|
|
||||||
ClientName: c.ClientName,
|
|
||||||
LogoURI: p.parseURI(c.LogoURI, "logo_uri"),
|
|
||||||
ClientURI: p.parseURI(c.ClientURI, "client_uri"),
|
|
||||||
PolicyURI: p.parseURI(c.PolicyURI, "policy_uri"),
|
|
||||||
TermsOfServiceURI: p.parseURI(c.TermsOfServiceURI, "tos_uri"),
|
|
||||||
JWKSURI: p.parseURI(c.JWKSURI, "jwks_uri"),
|
|
||||||
JWKS: c.JWKS,
|
|
||||||
SectorIdentifierURI: p.parseURI(c.SectorIdentifierURI, "sector_identifier_uri"),
|
|
||||||
SubjectType: c.SubjectType,
|
|
||||||
TokenEndpointAuthMethod: c.TokenEndpointAuthMethod,
|
|
||||||
TokenEndpointAuthSigningAlg: c.TokenEndpointAuthSigningAlg,
|
|
||||||
DefaultMaxAge: c.DefaultMaxAge,
|
|
||||||
RequireAuthTime: c.RequireAuthTime,
|
|
||||||
DefaultACRValues: c.DefaultACRValues,
|
|
||||||
InitiateLoginURI: p.parseURI(c.InitiateLoginURI, "initiate_login_uri"),
|
|
||||||
RequestURIs: p.parseURIs(c.RequestURIs, "request_uris"),
|
|
||||||
IDTokenResponseOptions: JWAOptions{
|
|
||||||
c.IDTokenSignedResponseAlg,
|
|
||||||
c.IDTokenEncryptedResponseAlg,
|
|
||||||
c.IDTokenEncryptedResponseEnc,
|
|
||||||
},
|
|
||||||
UserInfoResponseOptions: JWAOptions{
|
|
||||||
c.UserInfoSignedResponseAlg,
|
|
||||||
c.UserInfoEncryptedResponseAlg,
|
|
||||||
c.UserInfoEncryptedResponseEnc,
|
|
||||||
},
|
|
||||||
RequestObjectOptions: JWAOptions{
|
|
||||||
c.RequestObjectSigningAlg,
|
|
||||||
c.RequestObjectEncryptionAlg,
|
|
||||||
c.RequestObjectEncryptionEnc,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if p.firstErr != nil {
|
|
||||||
return ClientMetadata{}, p.firstErr
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// stickyErrParser parses URIs and email addresses. Once it encounters
|
|
||||||
// a parse error, subsequent calls become no-op.
|
|
||||||
type stickyErrParser struct {
|
|
||||||
firstErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *stickyErrParser) parseURI(s, field string) *url.URL {
|
|
||||||
if p.firstErr != nil || s == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
u, err := url.Parse(s)
|
|
||||||
if err == nil {
|
|
||||||
if u.Host == "" {
|
|
||||||
err = errors.New("no host in URI")
|
|
||||||
} else if u.Scheme != "http" && u.Scheme != "https" {
|
|
||||||
err = errors.New("invalid URI scheme")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
p.firstErr = fmt.Errorf("failed to parse %s: %v", field, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *stickyErrParser) parseURIs(s []string, field string) []url.URL {
|
|
||||||
if p.firstErr != nil || len(s) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
uris := make([]url.URL, len(s))
|
|
||||||
for i, val := range s {
|
|
||||||
if val == "" {
|
|
||||||
p.firstErr = fmt.Errorf("invalid URI in field %s", field)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if u := p.parseURI(val, field); u != nil {
|
|
||||||
uris[i] = *u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uris
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *stickyErrParser) parseEmails(s []string, field string) []mail.Address {
|
|
||||||
if p.firstErr != nil || len(s) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
addrs := make([]mail.Address, len(s))
|
|
||||||
for i, addr := range s {
|
|
||||||
if addr == "" {
|
|
||||||
p.firstErr = fmt.Errorf("invalid email in field %s", field)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
a, err := mail.ParseAddress(addr)
|
|
||||||
if err != nil {
|
|
||||||
p.firstErr = fmt.Errorf("invalid email in field %s: %v", field, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
addrs[i] = *a
|
|
||||||
}
|
|
||||||
return addrs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ClientMetadata) toEncodableStruct() encodableClientMetadata {
|
|
||||||
return encodableClientMetadata{
|
|
||||||
RedirectURIs: urisToStrings(m.RedirectURIs),
|
|
||||||
ResponseTypes: m.ResponseTypes,
|
|
||||||
GrantTypes: m.GrantTypes,
|
|
||||||
ApplicationType: m.ApplicationType,
|
|
||||||
Contacts: emailsToStrings(m.Contacts),
|
|
||||||
ClientName: m.ClientName,
|
|
||||||
LogoURI: uriToString(m.LogoURI),
|
|
||||||
ClientURI: uriToString(m.ClientURI),
|
|
||||||
PolicyURI: uriToString(m.PolicyURI),
|
|
||||||
TermsOfServiceURI: uriToString(m.TermsOfServiceURI),
|
|
||||||
JWKSURI: uriToString(m.JWKSURI),
|
|
||||||
JWKS: m.JWKS,
|
|
||||||
SectorIdentifierURI: uriToString(m.SectorIdentifierURI),
|
|
||||||
SubjectType: m.SubjectType,
|
|
||||||
IDTokenSignedResponseAlg: m.IDTokenResponseOptions.SigningAlg,
|
|
||||||
IDTokenEncryptedResponseAlg: m.IDTokenResponseOptions.EncryptionAlg,
|
|
||||||
IDTokenEncryptedResponseEnc: m.IDTokenResponseOptions.EncryptionEnc,
|
|
||||||
UserInfoSignedResponseAlg: m.UserInfoResponseOptions.SigningAlg,
|
|
||||||
UserInfoEncryptedResponseAlg: m.UserInfoResponseOptions.EncryptionAlg,
|
|
||||||
UserInfoEncryptedResponseEnc: m.UserInfoResponseOptions.EncryptionEnc,
|
|
||||||
RequestObjectSigningAlg: m.RequestObjectOptions.SigningAlg,
|
|
||||||
RequestObjectEncryptionAlg: m.RequestObjectOptions.EncryptionAlg,
|
|
||||||
RequestObjectEncryptionEnc: m.RequestObjectOptions.EncryptionEnc,
|
|
||||||
TokenEndpointAuthMethod: m.TokenEndpointAuthMethod,
|
|
||||||
TokenEndpointAuthSigningAlg: m.TokenEndpointAuthSigningAlg,
|
|
||||||
DefaultMaxAge: m.DefaultMaxAge,
|
|
||||||
RequireAuthTime: m.RequireAuthTime,
|
|
||||||
DefaultACRValues: m.DefaultACRValues,
|
|
||||||
InitiateLoginURI: uriToString(m.InitiateLoginURI),
|
|
||||||
RequestURIs: urisToStrings(m.RequestURIs),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func uriToString(u *url.URL) string {
|
|
||||||
if u == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func urisToStrings(urls []url.URL) []string {
|
|
||||||
if len(urls) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sli := make([]string, len(urls))
|
|
||||||
for i, u := range urls {
|
|
||||||
sli[i] = u.String()
|
|
||||||
}
|
|
||||||
return sli
|
|
||||||
}
|
|
||||||
|
|
||||||
func emailsToStrings(addrs []mail.Address) []string {
|
|
||||||
if len(addrs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sli := make([]string, len(addrs))
|
|
||||||
for i, addr := range addrs {
|
|
||||||
sli[i] = addr.String()
|
|
||||||
}
|
|
||||||
return sli
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid determines if a ClientMetadata conforms with the OIDC specification.
|
|
||||||
//
|
|
||||||
// Valid is called by UnmarshalJSON.
|
|
||||||
//
|
|
||||||
// NOTE(ericchiang): For development purposes Valid does not mandate 'https' for
|
|
||||||
// URLs fields where the OIDC spec requires it. This may change in future releases
|
|
||||||
// of this package. See: https://github.com/coreos/go-oidc/issues/34
|
|
||||||
func (m *ClientMetadata) Valid() error {
|
|
||||||
if len(m.RedirectURIs) == 0 {
|
|
||||||
return errors.New("zero redirect URLs")
|
|
||||||
}
|
|
||||||
|
|
||||||
validURI := func(u *url.URL, fieldName string) error {
|
|
||||||
if u.Host == "" {
|
|
||||||
return fmt.Errorf("no host for uri field %s", fieldName)
|
|
||||||
}
|
|
||||||
if u.Scheme != "http" && u.Scheme != "https" {
|
|
||||||
return fmt.Errorf("uri field %s scheme is not http or https", fieldName)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
uris := []struct {
|
|
||||||
val *url.URL
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
{m.LogoURI, "logo_uri"},
|
|
||||||
{m.ClientURI, "client_uri"},
|
|
||||||
{m.PolicyURI, "policy_uri"},
|
|
||||||
{m.TermsOfServiceURI, "tos_uri"},
|
|
||||||
{m.JWKSURI, "jwks_uri"},
|
|
||||||
{m.SectorIdentifierURI, "sector_identifier_uri"},
|
|
||||||
{m.InitiateLoginURI, "initiate_login_uri"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, uri := range uris {
|
|
||||||
if uri.val == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := validURI(uri.val, uri.name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uriLists := []struct {
|
|
||||||
vals []url.URL
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
{m.RedirectURIs, "redirect_uris"},
|
|
||||||
{m.RequestURIs, "request_uris"},
|
|
||||||
}
|
|
||||||
for _, list := range uriLists {
|
|
||||||
for _, uri := range list.vals {
|
|
||||||
if err := validURI(&uri, list.name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
options := []struct {
|
|
||||||
option JWAOptions
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
{m.IDTokenResponseOptions, "id_token response"},
|
|
||||||
{m.UserInfoResponseOptions, "userinfo response"},
|
|
||||||
{m.RequestObjectOptions, "request_object"},
|
|
||||||
}
|
|
||||||
for _, option := range options {
|
|
||||||
if err := option.option.valid(); err != nil {
|
|
||||||
return fmt.Errorf("invalid JWA values for %s: %v", option.name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientRegistrationResponse struct {
|
|
||||||
ClientID string // Required
|
|
||||||
ClientSecret string
|
|
||||||
RegistrationAccessToken string
|
|
||||||
RegistrationClientURI string
|
|
||||||
// If IsZero is true, unspecified.
|
|
||||||
ClientIDIssuedAt time.Time
|
|
||||||
// Time at which the client_secret will expire.
|
|
||||||
// If IsZero is true, it will not expire.
|
|
||||||
ClientSecretExpiresAt time.Time
|
|
||||||
|
|
||||||
ClientMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
type encodableClientRegistrationResponse struct {
|
|
||||||
ClientID string `json:"client_id"` // Required
|
|
||||||
ClientSecret string `json:"client_secret,omitempty"`
|
|
||||||
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
|
|
||||||
RegistrationClientURI string `json:"registration_client_uri,omitempty"`
|
|
||||||
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
|
|
||||||
// Time at which the client_secret will expire, in seconds since the epoch.
|
|
||||||
// If 0 it will not expire.
|
|
||||||
ClientSecretExpiresAt int64 `json:"client_secret_expires_at"` // Required
|
|
||||||
|
|
||||||
encodableClientMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func unixToSec(t time.Time) int64 {
|
|
||||||
if t.IsZero() {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return t.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientRegistrationResponse) MarshalJSON() ([]byte, error) {
|
|
||||||
e := encodableClientRegistrationResponse{
|
|
||||||
ClientID: c.ClientID,
|
|
||||||
ClientSecret: c.ClientSecret,
|
|
||||||
RegistrationAccessToken: c.RegistrationAccessToken,
|
|
||||||
RegistrationClientURI: c.RegistrationClientURI,
|
|
||||||
ClientIDIssuedAt: unixToSec(c.ClientIDIssuedAt),
|
|
||||||
ClientSecretExpiresAt: unixToSec(c.ClientSecretExpiresAt),
|
|
||||||
encodableClientMetadata: c.ClientMetadata.toEncodableStruct(),
|
|
||||||
}
|
|
||||||
return json.Marshal(&e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func secToUnix(sec int64) time.Time {
|
|
||||||
if sec == 0 {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return time.Unix(sec, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientRegistrationResponse) UnmarshalJSON(data []byte) error {
|
|
||||||
var e encodableClientRegistrationResponse
|
|
||||||
if err := json.Unmarshal(data, &e); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.ClientID == "" {
|
|
||||||
return errors.New("no client_id in client registration response")
|
|
||||||
}
|
|
||||||
metadata, err := e.encodableClientMetadata.toStruct()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*c = ClientRegistrationResponse{
|
|
||||||
ClientID: e.ClientID,
|
|
||||||
ClientSecret: e.ClientSecret,
|
|
||||||
RegistrationAccessToken: e.RegistrationAccessToken,
|
|
||||||
RegistrationClientURI: e.RegistrationClientURI,
|
|
||||||
ClientIDIssuedAt: secToUnix(e.ClientIDIssuedAt),
|
|
||||||
ClientSecretExpiresAt: secToUnix(e.ClientSecretExpiresAt),
|
|
||||||
ClientMetadata: metadata,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientConfig struct {
|
|
||||||
HTTPClient phttp.Client
|
|
||||||
Credentials ClientCredentials
|
|
||||||
Scope []string
|
|
||||||
RedirectURL string
|
|
||||||
ProviderConfig ProviderConfig
|
|
||||||
KeySet key.PublicKeySet
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(cfg ClientConfig) (*Client, error) {
|
|
||||||
// Allow empty redirect URL in the case where the client
|
|
||||||
// only needs to verify a given token.
|
|
||||||
ru, err := url.Parse(cfg.RedirectURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid redirect URL: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := Client{
|
|
||||||
credentials: cfg.Credentials,
|
|
||||||
httpClient: cfg.HTTPClient,
|
|
||||||
scope: cfg.Scope,
|
|
||||||
redirectURL: ru.String(),
|
|
||||||
providerConfig: newProviderConfigRepo(cfg.ProviderConfig),
|
|
||||||
keySet: cfg.KeySet,
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.httpClient == nil {
|
|
||||||
c.httpClient = http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.scope == nil {
|
|
||||||
c.scope = make([]string, len(DefaultScope))
|
|
||||||
copy(c.scope, DefaultScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
httpClient phttp.Client
|
|
||||||
providerConfig *providerConfigRepo
|
|
||||||
credentials ClientCredentials
|
|
||||||
redirectURL string
|
|
||||||
scope []string
|
|
||||||
keySet key.PublicKeySet
|
|
||||||
providerSyncer *ProviderConfigSyncer
|
|
||||||
|
|
||||||
keySetSyncMutex sync.RWMutex
|
|
||||||
lastKeySetSync time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Healthy() error {
|
|
||||||
now := time.Now().UTC()
|
|
||||||
|
|
||||||
cfg := c.providerConfig.Get()
|
|
||||||
|
|
||||||
if cfg.Empty() {
|
|
||||||
return errors.New("oidc client provider config empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.ExpiresAt.IsZero() && cfg.ExpiresAt.Before(now) {
|
|
||||||
return errors.New("oidc client provider config expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) OAuthClient() (*oauth2.Client, error) {
|
|
||||||
cfg := c.providerConfig.Get()
|
|
||||||
authMethod, err := chooseAuthMethod(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ocfg := oauth2.Config{
|
|
||||||
Credentials: oauth2.ClientCredentials(c.credentials),
|
|
||||||
RedirectURL: c.redirectURL,
|
|
||||||
AuthURL: cfg.AuthEndpoint.String(),
|
|
||||||
TokenURL: cfg.TokenEndpoint.String(),
|
|
||||||
Scope: c.scope,
|
|
||||||
AuthMethod: authMethod,
|
|
||||||
}
|
|
||||||
|
|
||||||
return oauth2.NewClient(c.httpClient, ocfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func chooseAuthMethod(cfg ProviderConfig) (string, error) {
|
|
||||||
if len(cfg.TokenEndpointAuthMethodsSupported) == 0 {
|
|
||||||
return oauth2.AuthMethodClientSecretBasic, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, authMethod := range cfg.TokenEndpointAuthMethodsSupported {
|
|
||||||
if _, ok := supportedAuthMethods[authMethod]; ok {
|
|
||||||
return authMethod, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("no supported auth methods")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncProviderConfig starts the provider config syncer
|
|
||||||
func (c *Client) SyncProviderConfig(discoveryURL string) chan struct{} {
|
|
||||||
r := NewHTTPProviderConfigGetter(c.httpClient, discoveryURL)
|
|
||||||
s := NewProviderConfigSyncer(r, c.providerConfig)
|
|
||||||
stop := s.Run()
|
|
||||||
s.WaitUntilInitialSync()
|
|
||||||
return stop
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) maybeSyncKeys() error {
|
|
||||||
tooSoon := func() bool {
|
|
||||||
return time.Now().UTC().Before(c.lastKeySetSync.Add(keySyncWindow))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore request to sync keys if a sync operation has been
|
|
||||||
// attempted too recently
|
|
||||||
if tooSoon() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.keySetSyncMutex.Lock()
|
|
||||||
defer c.keySetSyncMutex.Unlock()
|
|
||||||
|
|
||||||
// check again, as another goroutine may have been holding
|
|
||||||
// the lock while updating the keys
|
|
||||||
if tooSoon() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := c.providerConfig.Get()
|
|
||||||
r := NewRemotePublicKeyRepo(c.httpClient, cfg.KeysEndpoint.String())
|
|
||||||
w := &clientKeyRepo{client: c}
|
|
||||||
_, err := key.Sync(r, w)
|
|
||||||
c.lastKeySetSync = time.Now().UTC()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientKeyRepo struct {
|
|
||||||
client *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *clientKeyRepo) Set(ks key.KeySet) error {
|
|
||||||
pks, ok := ks.(*key.PublicKeySet)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("unable to cast to PublicKey")
|
|
||||||
}
|
|
||||||
r.client.keySet = *pks
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ClientCredsToken(scope []string) (jose.JWT, error) {
|
|
||||||
cfg := c.providerConfig.Get()
|
|
||||||
|
|
||||||
if !cfg.SupportsGrantType(oauth2.GrantTypeClientCreds) {
|
|
||||||
return jose.JWT{}, fmt.Errorf("%v grant type is not supported", oauth2.GrantTypeClientCreds)
|
|
||||||
}
|
|
||||||
|
|
||||||
oac, err := c.OAuthClient()
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := oac.ClientCredsToken(scope)
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt, err := jose.ParseJWT(t.IDToken)
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwt, c.VerifyJWT(jwt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExchangeAuthCode exchanges an OAuth2 auth code for an OIDC JWT ID token.
|
|
||||||
func (c *Client) ExchangeAuthCode(code string) (jose.JWT, error) {
|
|
||||||
oac, err := c.OAuthClient()
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := oac.RequestToken(oauth2.GrantTypeAuthCode, code)
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt, err := jose.ParseJWT(t.IDToken)
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwt, c.VerifyJWT(jwt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshToken uses a refresh token to exchange for a new OIDC JWT ID Token.
|
|
||||||
func (c *Client) RefreshToken(refreshToken string) (jose.JWT, error) {
|
|
||||||
oac, err := c.OAuthClient()
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := oac.RequestToken(oauth2.GrantTypeRefreshToken, refreshToken)
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt, err := jose.ParseJWT(t.IDToken)
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwt, c.VerifyJWT(jwt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) VerifyJWT(jwt jose.JWT) error {
|
|
||||||
var keysFunc func() []key.PublicKey
|
|
||||||
if kID, ok := jwt.KeyID(); ok {
|
|
||||||
keysFunc = c.keysFuncWithID(kID)
|
|
||||||
} else {
|
|
||||||
keysFunc = c.keysFuncAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
v := NewJWTVerifier(
|
|
||||||
c.providerConfig.Get().Issuer.String(),
|
|
||||||
c.credentials.ID,
|
|
||||||
c.maybeSyncKeys, keysFunc)
|
|
||||||
|
|
||||||
return v.Verify(jwt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// keysFuncWithID returns a function that retrieves at most unexpired
|
|
||||||
// public key from the Client that matches the provided ID
|
|
||||||
func (c *Client) keysFuncWithID(kID string) func() []key.PublicKey {
|
|
||||||
return func() []key.PublicKey {
|
|
||||||
c.keySetSyncMutex.RLock()
|
|
||||||
defer c.keySetSyncMutex.RUnlock()
|
|
||||||
|
|
||||||
if c.keySet.ExpiresAt().Before(time.Now()) {
|
|
||||||
return []key.PublicKey{}
|
|
||||||
}
|
|
||||||
|
|
||||||
k := c.keySet.Key(kID)
|
|
||||||
if k == nil {
|
|
||||||
return []key.PublicKey{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []key.PublicKey{*k}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// keysFuncAll returns a function that retrieves all unexpired public
|
|
||||||
// keys from the Client
|
|
||||||
func (c *Client) keysFuncAll() func() []key.PublicKey {
|
|
||||||
return func() []key.PublicKey {
|
|
||||||
c.keySetSyncMutex.RLock()
|
|
||||||
defer c.keySetSyncMutex.RUnlock()
|
|
||||||
|
|
||||||
if c.keySet.ExpiresAt().Before(time.Now()) {
|
|
||||||
return []key.PublicKey{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.keySet.Keys()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type providerConfigRepo struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
config ProviderConfig // do not access directly, use Get()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newProviderConfigRepo(pc ProviderConfig) *providerConfigRepo {
|
|
||||||
return &providerConfigRepo{sync.RWMutex{}, pc}
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns an error to implement ProviderConfigSetter
|
|
||||||
func (r *providerConfigRepo) Set(cfg ProviderConfig) error {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
r.config = cfg
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *providerConfigRepo) Get() ProviderConfig {
|
|
||||||
r.mu.RLock()
|
|
||||||
defer r.mu.RUnlock()
|
|
||||||
return r.config
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
// Package oidc is DEPRECATED. Use github.com/coreos/go-oidc instead.
|
|
||||||
package oidc
|
|
|
@ -1,44 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Identity struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
ExpiresAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func IdentityFromClaims(claims jose.Claims) (*Identity, error) {
|
|
||||||
if claims == nil {
|
|
||||||
return nil, errors.New("nil claim set")
|
|
||||||
}
|
|
||||||
|
|
||||||
var ident Identity
|
|
||||||
var err error
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
if ident.ID, ok, err = claims.StringClaim("sub"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !ok {
|
|
||||||
return nil, errors.New("missing required claim: sub")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ident.Email, _, err = claims.StringClaim("email"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
exp, ok, err := claims.TimeClaim("exp")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if ok {
|
|
||||||
ident.ExpiresAt = exp
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ident, nil
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
type LoginFunc func(ident Identity, sessionKey string) (redirectURL string, err error)
|
|
|
@ -1,67 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
phttp "github.com/coreos/go-oidc/http"
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
"github.com/coreos/go-oidc/key"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultPublicKeySetTTL is the default TTL set on the PublicKeySet if no
|
|
||||||
// Cache-Control header is provided by the JWK Set document endpoint.
|
|
||||||
const DefaultPublicKeySetTTL = 24 * time.Hour
|
|
||||||
|
|
||||||
// NewRemotePublicKeyRepo is responsible for fetching the JWK Set document.
|
|
||||||
func NewRemotePublicKeyRepo(hc phttp.Client, ep string) *remotePublicKeyRepo {
|
|
||||||
return &remotePublicKeyRepo{hc: hc, ep: ep}
|
|
||||||
}
|
|
||||||
|
|
||||||
type remotePublicKeyRepo struct {
|
|
||||||
hc phttp.Client
|
|
||||||
ep string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a PublicKeySet fetched from the JWK Set document endpoint. A TTL
|
|
||||||
// is set on the Key Set to avoid it having to be re-retrieved for every
|
|
||||||
// encryption event. This TTL is typically controlled by the endpoint returning
|
|
||||||
// a Cache-Control header, but defaults to 24 hours if no Cache-Control header
|
|
||||||
// is found.
|
|
||||||
func (r *remotePublicKeyRepo) Get() (key.KeySet, error) {
|
|
||||||
req, err := http.NewRequest("GET", r.ep, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := r.hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var d struct {
|
|
||||||
Keys []jose.JWK `json:"keys"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(d.Keys) == 0 {
|
|
||||||
return nil, errors.New("zero keys in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
ttl, ok, err := phttp.Cacheable(resp.Header)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
ttl = DefaultPublicKeySetTTL
|
|
||||||
}
|
|
||||||
|
|
||||||
exp := time.Now().UTC().Add(ttl)
|
|
||||||
ks := key.NewPublicKeySet(d.Keys, exp)
|
|
||||||
return ks, nil
|
|
||||||
}
|
|
|
@ -1,687 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/pkg/timeutil"
|
|
||||||
"github.com/jonboulle/clockwork"
|
|
||||||
|
|
||||||
phttp "github.com/coreos/go-oidc/http"
|
|
||||||
"github.com/coreos/go-oidc/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Subject Identifier types defined by the OIDC spec. Specifies if the provider
|
|
||||||
// should provide the same sub claim value to all clients (public) or a unique
|
|
||||||
// value for each client (pairwise).
|
|
||||||
//
|
|
||||||
// See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
|
|
||||||
SubjectTypePublic = "public"
|
|
||||||
SubjectTypePairwise = "pairwise"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Default values for omitted provider config fields.
|
|
||||||
//
|
|
||||||
// Use ProviderConfig's Defaults method to fill a provider config with these values.
|
|
||||||
DefaultGrantTypesSupported = []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeImplicit}
|
|
||||||
DefaultResponseModesSupported = []string{"query", "fragment"}
|
|
||||||
DefaultTokenEndpointAuthMethodsSupported = []string{oauth2.AuthMethodClientSecretBasic}
|
|
||||||
DefaultClaimTypesSupported = []string{"normal"}
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MaximumProviderConfigSyncInterval = 24 * time.Hour
|
|
||||||
MinimumProviderConfigSyncInterval = time.Minute
|
|
||||||
|
|
||||||
discoveryConfigPath = "/.well-known/openid-configuration"
|
|
||||||
)
|
|
||||||
|
|
||||||
// internally configurable for tests
|
|
||||||
var minimumProviderConfigSyncInterval = MinimumProviderConfigSyncInterval
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Ensure ProviderConfig satisfies these interfaces.
|
|
||||||
_ json.Marshaler = &ProviderConfig{}
|
|
||||||
_ json.Unmarshaler = &ProviderConfig{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProviderConfig represents the OpenID Provider Metadata specifying what
|
|
||||||
// configurations a provider supports.
|
|
||||||
//
|
|
||||||
// See: http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
|
||||||
type ProviderConfig struct {
|
|
||||||
Issuer *url.URL // Required
|
|
||||||
AuthEndpoint *url.URL // Required
|
|
||||||
TokenEndpoint *url.URL // Required if grant types other than "implicit" are supported
|
|
||||||
UserInfoEndpoint *url.URL
|
|
||||||
KeysEndpoint *url.URL // Required
|
|
||||||
RegistrationEndpoint *url.URL
|
|
||||||
EndSessionEndpoint *url.URL
|
|
||||||
CheckSessionIFrame *url.URL
|
|
||||||
|
|
||||||
// Servers MAY choose not to advertise some supported scope values even when this
|
|
||||||
// parameter is used, although those defined in OpenID Core SHOULD be listed, if supported.
|
|
||||||
ScopesSupported []string
|
|
||||||
// OAuth2.0 response types supported.
|
|
||||||
ResponseTypesSupported []string // Required
|
|
||||||
// OAuth2.0 response modes supported.
|
|
||||||
//
|
|
||||||
// If omitted, defaults to DefaultResponseModesSupported.
|
|
||||||
ResponseModesSupported []string
|
|
||||||
// OAuth2.0 grant types supported.
|
|
||||||
//
|
|
||||||
// If omitted, defaults to DefaultGrantTypesSupported.
|
|
||||||
GrantTypesSupported []string
|
|
||||||
ACRValuesSupported []string
|
|
||||||
// SubjectTypesSupported specifies strategies for providing values for the sub claim.
|
|
||||||
SubjectTypesSupported []string // Required
|
|
||||||
|
|
||||||
// JWA signing and encryption algorith values supported for ID tokens.
|
|
||||||
IDTokenSigningAlgValues []string // Required
|
|
||||||
IDTokenEncryptionAlgValues []string
|
|
||||||
IDTokenEncryptionEncValues []string
|
|
||||||
|
|
||||||
// JWA signing and encryption algorith values supported for user info responses.
|
|
||||||
UserInfoSigningAlgValues []string
|
|
||||||
UserInfoEncryptionAlgValues []string
|
|
||||||
UserInfoEncryptionEncValues []string
|
|
||||||
|
|
||||||
// JWA signing and encryption algorith values supported for request objects.
|
|
||||||
ReqObjSigningAlgValues []string
|
|
||||||
ReqObjEncryptionAlgValues []string
|
|
||||||
ReqObjEncryptionEncValues []string
|
|
||||||
|
|
||||||
TokenEndpointAuthMethodsSupported []string
|
|
||||||
TokenEndpointAuthSigningAlgValuesSupported []string
|
|
||||||
DisplayValuesSupported []string
|
|
||||||
ClaimTypesSupported []string
|
|
||||||
ClaimsSupported []string
|
|
||||||
ServiceDocs *url.URL
|
|
||||||
ClaimsLocalsSupported []string
|
|
||||||
UILocalsSupported []string
|
|
||||||
ClaimsParameterSupported bool
|
|
||||||
RequestParameterSupported bool
|
|
||||||
RequestURIParamaterSupported bool
|
|
||||||
RequireRequestURIRegistration bool
|
|
||||||
|
|
||||||
Policy *url.URL
|
|
||||||
TermsOfService *url.URL
|
|
||||||
|
|
||||||
// Not part of the OpenID Provider Metadata
|
|
||||||
ExpiresAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defaults returns a shallow copy of ProviderConfig with default
|
|
||||||
// values replacing omitted fields.
|
|
||||||
//
|
|
||||||
// var cfg oidc.ProviderConfig
|
|
||||||
// // Fill provider config with default values for omitted fields.
|
|
||||||
// cfg = cfg.Defaults()
|
|
||||||
//
|
|
||||||
func (p ProviderConfig) Defaults() ProviderConfig {
|
|
||||||
setDefault := func(val *[]string, defaultVal []string) {
|
|
||||||
if len(*val) == 0 {
|
|
||||||
*val = defaultVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setDefault(&p.GrantTypesSupported, DefaultGrantTypesSupported)
|
|
||||||
setDefault(&p.ResponseModesSupported, DefaultResponseModesSupported)
|
|
||||||
setDefault(&p.TokenEndpointAuthMethodsSupported, DefaultTokenEndpointAuthMethodsSupported)
|
|
||||||
setDefault(&p.ClaimTypesSupported, DefaultClaimTypesSupported)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProviderConfig) MarshalJSON() ([]byte, error) {
|
|
||||||
e := p.toEncodableStruct()
|
|
||||||
return json.Marshal(&e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProviderConfig) UnmarshalJSON(data []byte) error {
|
|
||||||
var e encodableProviderConfig
|
|
||||||
if err := json.Unmarshal(data, &e); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
conf, err := e.toStruct()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := conf.Valid(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*p = conf
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type encodableProviderConfig struct {
|
|
||||||
Issuer string `json:"issuer"`
|
|
||||||
AuthEndpoint string `json:"authorization_endpoint"`
|
|
||||||
TokenEndpoint string `json:"token_endpoint"`
|
|
||||||
UserInfoEndpoint string `json:"userinfo_endpoint,omitempty"`
|
|
||||||
KeysEndpoint string `json:"jwks_uri"`
|
|
||||||
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
|
||||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
|
||||||
CheckSessionIFrame string `json:"check_session_iframe,omitempty"`
|
|
||||||
|
|
||||||
// Use 'omitempty' for all slices as per OIDC spec:
|
|
||||||
// "Claims that return multiple values are represented as JSON arrays.
|
|
||||||
// Claims with zero elements MUST be omitted from the response."
|
|
||||||
// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
|
|
||||||
|
|
||||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
|
||||||
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
|
||||||
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
|
|
||||||
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
|
||||||
ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
|
|
||||||
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
|
||||||
|
|
||||||
IDTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
|
||||||
IDTokenEncryptionAlgValues []string `json:"id_token_encryption_alg_values_supported,omitempty"`
|
|
||||||
IDTokenEncryptionEncValues []string `json:"id_token_encryption_enc_values_supported,omitempty"`
|
|
||||||
UserInfoSigningAlgValues []string `json:"userinfo_signing_alg_values_supported,omitempty"`
|
|
||||||
UserInfoEncryptionAlgValues []string `json:"userinfo_encryption_alg_values_supported,omitempty"`
|
|
||||||
UserInfoEncryptionEncValues []string `json:"userinfo_encryption_enc_values_supported,omitempty"`
|
|
||||||
ReqObjSigningAlgValues []string `json:"request_object_signing_alg_values_supported,omitempty"`
|
|
||||||
ReqObjEncryptionAlgValues []string `json:"request_object_encryption_alg_values_supported,omitempty"`
|
|
||||||
ReqObjEncryptionEncValues []string `json:"request_object_encryption_enc_values_supported,omitempty"`
|
|
||||||
|
|
||||||
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
|
|
||||||
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
|
|
||||||
|
|
||||||
DisplayValuesSupported []string `json:"display_values_supported,omitempty"`
|
|
||||||
ClaimTypesSupported []string `json:"claim_types_supported,omitempty"`
|
|
||||||
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
|
||||||
ServiceDocs string `json:"service_documentation,omitempty"`
|
|
||||||
ClaimsLocalsSupported []string `json:"claims_locales_supported,omitempty"`
|
|
||||||
UILocalsSupported []string `json:"ui_locales_supported,omitempty"`
|
|
||||||
ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"`
|
|
||||||
RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
|
|
||||||
RequestURIParamaterSupported bool `json:"request_uri_parameter_supported,omitempty"`
|
|
||||||
RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"`
|
|
||||||
|
|
||||||
Policy string `json:"op_policy_uri,omitempty"`
|
|
||||||
TermsOfService string `json:"op_tos_uri,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg ProviderConfig) toEncodableStruct() encodableProviderConfig {
|
|
||||||
return encodableProviderConfig{
|
|
||||||
Issuer: uriToString(cfg.Issuer),
|
|
||||||
AuthEndpoint: uriToString(cfg.AuthEndpoint),
|
|
||||||
TokenEndpoint: uriToString(cfg.TokenEndpoint),
|
|
||||||
UserInfoEndpoint: uriToString(cfg.UserInfoEndpoint),
|
|
||||||
KeysEndpoint: uriToString(cfg.KeysEndpoint),
|
|
||||||
RegistrationEndpoint: uriToString(cfg.RegistrationEndpoint),
|
|
||||||
EndSessionEndpoint: uriToString(cfg.EndSessionEndpoint),
|
|
||||||
CheckSessionIFrame: uriToString(cfg.CheckSessionIFrame),
|
|
||||||
ScopesSupported: cfg.ScopesSupported,
|
|
||||||
ResponseTypesSupported: cfg.ResponseTypesSupported,
|
|
||||||
ResponseModesSupported: cfg.ResponseModesSupported,
|
|
||||||
GrantTypesSupported: cfg.GrantTypesSupported,
|
|
||||||
ACRValuesSupported: cfg.ACRValuesSupported,
|
|
||||||
SubjectTypesSupported: cfg.SubjectTypesSupported,
|
|
||||||
IDTokenSigningAlgValues: cfg.IDTokenSigningAlgValues,
|
|
||||||
IDTokenEncryptionAlgValues: cfg.IDTokenEncryptionAlgValues,
|
|
||||||
IDTokenEncryptionEncValues: cfg.IDTokenEncryptionEncValues,
|
|
||||||
UserInfoSigningAlgValues: cfg.UserInfoSigningAlgValues,
|
|
||||||
UserInfoEncryptionAlgValues: cfg.UserInfoEncryptionAlgValues,
|
|
||||||
UserInfoEncryptionEncValues: cfg.UserInfoEncryptionEncValues,
|
|
||||||
ReqObjSigningAlgValues: cfg.ReqObjSigningAlgValues,
|
|
||||||
ReqObjEncryptionAlgValues: cfg.ReqObjEncryptionAlgValues,
|
|
||||||
ReqObjEncryptionEncValues: cfg.ReqObjEncryptionEncValues,
|
|
||||||
TokenEndpointAuthMethodsSupported: cfg.TokenEndpointAuthMethodsSupported,
|
|
||||||
TokenEndpointAuthSigningAlgValuesSupported: cfg.TokenEndpointAuthSigningAlgValuesSupported,
|
|
||||||
DisplayValuesSupported: cfg.DisplayValuesSupported,
|
|
||||||
ClaimTypesSupported: cfg.ClaimTypesSupported,
|
|
||||||
ClaimsSupported: cfg.ClaimsSupported,
|
|
||||||
ServiceDocs: uriToString(cfg.ServiceDocs),
|
|
||||||
ClaimsLocalsSupported: cfg.ClaimsLocalsSupported,
|
|
||||||
UILocalsSupported: cfg.UILocalsSupported,
|
|
||||||
ClaimsParameterSupported: cfg.ClaimsParameterSupported,
|
|
||||||
RequestParameterSupported: cfg.RequestParameterSupported,
|
|
||||||
RequestURIParamaterSupported: cfg.RequestURIParamaterSupported,
|
|
||||||
RequireRequestURIRegistration: cfg.RequireRequestURIRegistration,
|
|
||||||
Policy: uriToString(cfg.Policy),
|
|
||||||
TermsOfService: uriToString(cfg.TermsOfService),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e encodableProviderConfig) toStruct() (ProviderConfig, error) {
|
|
||||||
p := stickyErrParser{}
|
|
||||||
conf := ProviderConfig{
|
|
||||||
Issuer: p.parseURI(e.Issuer, "issuer"),
|
|
||||||
AuthEndpoint: p.parseURI(e.AuthEndpoint, "authorization_endpoint"),
|
|
||||||
TokenEndpoint: p.parseURI(e.TokenEndpoint, "token_endpoint"),
|
|
||||||
UserInfoEndpoint: p.parseURI(e.UserInfoEndpoint, "userinfo_endpoint"),
|
|
||||||
KeysEndpoint: p.parseURI(e.KeysEndpoint, "jwks_uri"),
|
|
||||||
RegistrationEndpoint: p.parseURI(e.RegistrationEndpoint, "registration_endpoint"),
|
|
||||||
EndSessionEndpoint: p.parseURI(e.EndSessionEndpoint, "end_session_endpoint"),
|
|
||||||
CheckSessionIFrame: p.parseURI(e.CheckSessionIFrame, "check_session_iframe"),
|
|
||||||
ScopesSupported: e.ScopesSupported,
|
|
||||||
ResponseTypesSupported: e.ResponseTypesSupported,
|
|
||||||
ResponseModesSupported: e.ResponseModesSupported,
|
|
||||||
GrantTypesSupported: e.GrantTypesSupported,
|
|
||||||
ACRValuesSupported: e.ACRValuesSupported,
|
|
||||||
SubjectTypesSupported: e.SubjectTypesSupported,
|
|
||||||
IDTokenSigningAlgValues: e.IDTokenSigningAlgValues,
|
|
||||||
IDTokenEncryptionAlgValues: e.IDTokenEncryptionAlgValues,
|
|
||||||
IDTokenEncryptionEncValues: e.IDTokenEncryptionEncValues,
|
|
||||||
UserInfoSigningAlgValues: e.UserInfoSigningAlgValues,
|
|
||||||
UserInfoEncryptionAlgValues: e.UserInfoEncryptionAlgValues,
|
|
||||||
UserInfoEncryptionEncValues: e.UserInfoEncryptionEncValues,
|
|
||||||
ReqObjSigningAlgValues: e.ReqObjSigningAlgValues,
|
|
||||||
ReqObjEncryptionAlgValues: e.ReqObjEncryptionAlgValues,
|
|
||||||
ReqObjEncryptionEncValues: e.ReqObjEncryptionEncValues,
|
|
||||||
TokenEndpointAuthMethodsSupported: e.TokenEndpointAuthMethodsSupported,
|
|
||||||
TokenEndpointAuthSigningAlgValuesSupported: e.TokenEndpointAuthSigningAlgValuesSupported,
|
|
||||||
DisplayValuesSupported: e.DisplayValuesSupported,
|
|
||||||
ClaimTypesSupported: e.ClaimTypesSupported,
|
|
||||||
ClaimsSupported: e.ClaimsSupported,
|
|
||||||
ServiceDocs: p.parseURI(e.ServiceDocs, "service_documentation"),
|
|
||||||
ClaimsLocalsSupported: e.ClaimsLocalsSupported,
|
|
||||||
UILocalsSupported: e.UILocalsSupported,
|
|
||||||
ClaimsParameterSupported: e.ClaimsParameterSupported,
|
|
||||||
RequestParameterSupported: e.RequestParameterSupported,
|
|
||||||
RequestURIParamaterSupported: e.RequestURIParamaterSupported,
|
|
||||||
RequireRequestURIRegistration: e.RequireRequestURIRegistration,
|
|
||||||
Policy: p.parseURI(e.Policy, "op_policy-uri"),
|
|
||||||
TermsOfService: p.parseURI(e.TermsOfService, "op_tos_uri"),
|
|
||||||
}
|
|
||||||
if p.firstErr != nil {
|
|
||||||
return ProviderConfig{}, p.firstErr
|
|
||||||
}
|
|
||||||
return conf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty returns if a ProviderConfig holds no information.
|
|
||||||
//
|
|
||||||
// This case generally indicates a ProviderConfigGetter has experienced an error
|
|
||||||
// and has nothing to report.
|
|
||||||
func (p ProviderConfig) Empty() bool {
|
|
||||||
return p.Issuer == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(sli []string, ele string) bool {
|
|
||||||
for _, s := range sli {
|
|
||||||
if s == ele {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid determines if a ProviderConfig conforms with the OIDC specification.
|
|
||||||
// If Valid returns successfully it guarantees required field are non-nil and
|
|
||||||
// URLs are well formed.
|
|
||||||
//
|
|
||||||
// Valid is called by UnmarshalJSON.
|
|
||||||
//
|
|
||||||
// NOTE(ericchiang): For development purposes Valid does not mandate 'https' for
|
|
||||||
// URLs fields where the OIDC spec requires it. This may change in future releases
|
|
||||||
// of this package. See: https://github.com/coreos/go-oidc/issues/34
|
|
||||||
func (p ProviderConfig) Valid() error {
|
|
||||||
grantTypes := p.GrantTypesSupported
|
|
||||||
if len(grantTypes) == 0 {
|
|
||||||
grantTypes = DefaultGrantTypesSupported
|
|
||||||
}
|
|
||||||
implicitOnly := true
|
|
||||||
for _, grantType := range grantTypes {
|
|
||||||
if grantType != oauth2.GrantTypeImplicit {
|
|
||||||
implicitOnly = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.SubjectTypesSupported) == 0 {
|
|
||||||
return errors.New("missing required field subject_types_supported")
|
|
||||||
}
|
|
||||||
if len(p.IDTokenSigningAlgValues) == 0 {
|
|
||||||
return errors.New("missing required field id_token_signing_alg_values_supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.ScopesSupported) != 0 && !contains(p.ScopesSupported, "openid") {
|
|
||||||
return errors.New("scoped_supported must be unspecified or include 'openid'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !contains(p.IDTokenSigningAlgValues, "RS256") {
|
|
||||||
return errors.New("id_token_signing_alg_values_supported must include 'RS256'")
|
|
||||||
}
|
|
||||||
|
|
||||||
uris := []struct {
|
|
||||||
val *url.URL
|
|
||||||
name string
|
|
||||||
required bool
|
|
||||||
}{
|
|
||||||
{p.Issuer, "issuer", true},
|
|
||||||
{p.AuthEndpoint, "authorization_endpoint", true},
|
|
||||||
{p.TokenEndpoint, "token_endpoint", !implicitOnly},
|
|
||||||
{p.UserInfoEndpoint, "userinfo_endpoint", false},
|
|
||||||
{p.KeysEndpoint, "jwks_uri", true},
|
|
||||||
{p.RegistrationEndpoint, "registration_endpoint", false},
|
|
||||||
{p.EndSessionEndpoint, "end_session_endpoint", false},
|
|
||||||
{p.CheckSessionIFrame, "check_session_iframe", false},
|
|
||||||
{p.ServiceDocs, "service_documentation", false},
|
|
||||||
{p.Policy, "op_policy_uri", false},
|
|
||||||
{p.TermsOfService, "op_tos_uri", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, uri := range uris {
|
|
||||||
if uri.val == nil {
|
|
||||||
if !uri.required {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return fmt.Errorf("empty value for required uri field %s", uri.name)
|
|
||||||
}
|
|
||||||
if uri.val.Host == "" {
|
|
||||||
return fmt.Errorf("no host for uri field %s", uri.name)
|
|
||||||
}
|
|
||||||
if uri.val.Scheme != "http" && uri.val.Scheme != "https" {
|
|
||||||
return fmt.Errorf("uri field %s schemeis not http or https", uri.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supports determines if provider supports a client given their respective metadata.
|
|
||||||
func (p ProviderConfig) Supports(c ClientMetadata) error {
|
|
||||||
if err := p.Valid(); err != nil {
|
|
||||||
return fmt.Errorf("invalid provider config: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.Valid(); err != nil {
|
|
||||||
return fmt.Errorf("invalid client config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill default values for omitted fields
|
|
||||||
c = c.Defaults()
|
|
||||||
p = p.Defaults()
|
|
||||||
|
|
||||||
// Do the supported values list the requested one?
|
|
||||||
supports := []struct {
|
|
||||||
supported []string
|
|
||||||
requested string
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
{p.IDTokenSigningAlgValues, c.IDTokenResponseOptions.SigningAlg, "id_token_signed_response_alg"},
|
|
||||||
{p.IDTokenEncryptionAlgValues, c.IDTokenResponseOptions.EncryptionAlg, "id_token_encryption_response_alg"},
|
|
||||||
{p.IDTokenEncryptionEncValues, c.IDTokenResponseOptions.EncryptionEnc, "id_token_encryption_response_enc"},
|
|
||||||
{p.UserInfoSigningAlgValues, c.UserInfoResponseOptions.SigningAlg, "userinfo_signed_response_alg"},
|
|
||||||
{p.UserInfoEncryptionAlgValues, c.UserInfoResponseOptions.EncryptionAlg, "userinfo_encryption_response_alg"},
|
|
||||||
{p.UserInfoEncryptionEncValues, c.UserInfoResponseOptions.EncryptionEnc, "userinfo_encryption_response_enc"},
|
|
||||||
{p.ReqObjSigningAlgValues, c.RequestObjectOptions.SigningAlg, "request_object_signing_alg"},
|
|
||||||
{p.ReqObjEncryptionAlgValues, c.RequestObjectOptions.EncryptionAlg, "request_object_encryption_alg"},
|
|
||||||
{p.ReqObjEncryptionEncValues, c.RequestObjectOptions.EncryptionEnc, "request_object_encryption_enc"},
|
|
||||||
}
|
|
||||||
for _, field := range supports {
|
|
||||||
if field.requested == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !contains(field.supported, field.requested) {
|
|
||||||
return fmt.Errorf("provider does not support requested value for field %s", field.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stringsEqual := func(s1, s2 string) bool { return s1 == s2 }
|
|
||||||
|
|
||||||
// For lists, are the list of requested values a subset of the supported ones?
|
|
||||||
supportsAll := []struct {
|
|
||||||
supported []string
|
|
||||||
requested []string
|
|
||||||
name string
|
|
||||||
// OAuth2.0 response_type can be space separated lists where order doesn't matter.
|
|
||||||
// For example "id_token token" is the same as "token id_token"
|
|
||||||
// Support a custom compare method.
|
|
||||||
comp func(s1, s2 string) bool
|
|
||||||
}{
|
|
||||||
{p.GrantTypesSupported, c.GrantTypes, "grant_types", stringsEqual},
|
|
||||||
{p.ResponseTypesSupported, c.ResponseTypes, "response_type", oauth2.ResponseTypesEqual},
|
|
||||||
}
|
|
||||||
for _, field := range supportsAll {
|
|
||||||
requestLoop:
|
|
||||||
for _, req := range field.requested {
|
|
||||||
for _, sup := range field.supported {
|
|
||||||
if field.comp(req, sup) {
|
|
||||||
continue requestLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("provider does not support requested value for field %s", field.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(ericchiang): Are there more checks we feel comfortable with begin strict about?
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p ProviderConfig) SupportsGrantType(grantType string) bool {
|
|
||||||
var supported []string
|
|
||||||
if len(p.GrantTypesSupported) == 0 {
|
|
||||||
supported = DefaultGrantTypesSupported
|
|
||||||
} else {
|
|
||||||
supported = p.GrantTypesSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range supported {
|
|
||||||
if t == grantType {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProviderConfigGetter interface {
|
|
||||||
Get() (ProviderConfig, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProviderConfigSetter interface {
|
|
||||||
Set(ProviderConfig) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProviderConfigSyncer struct {
|
|
||||||
from ProviderConfigGetter
|
|
||||||
to ProviderConfigSetter
|
|
||||||
clock clockwork.Clock
|
|
||||||
|
|
||||||
initialSyncDone bool
|
|
||||||
initialSyncWait sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProviderConfigSyncer(from ProviderConfigGetter, to ProviderConfigSetter) *ProviderConfigSyncer {
|
|
||||||
return &ProviderConfigSyncer{
|
|
||||||
from: from,
|
|
||||||
to: to,
|
|
||||||
clock: clockwork.NewRealClock(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProviderConfigSyncer) Run() chan struct{} {
|
|
||||||
stop := make(chan struct{})
|
|
||||||
|
|
||||||
var next pcsStepper
|
|
||||||
next = &pcsStepNext{aft: time.Duration(0)}
|
|
||||||
|
|
||||||
s.initialSyncWait.Add(1)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-s.clock.After(next.after()):
|
|
||||||
next = next.step(s.sync)
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return stop
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProviderConfigSyncer) WaitUntilInitialSync() {
|
|
||||||
s.initialSyncWait.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProviderConfigSyncer) sync() (time.Duration, error) {
|
|
||||||
cfg, err := s.from.Get()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.to.Set(cfg); err != nil {
|
|
||||||
return 0, fmt.Errorf("error setting provider config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.initialSyncDone {
|
|
||||||
s.initialSyncWait.Done()
|
|
||||||
s.initialSyncDone = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextSyncAfter(cfg.ExpiresAt, s.clock), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type pcsStepFunc func() (time.Duration, error)
|
|
||||||
|
|
||||||
type pcsStepper interface {
|
|
||||||
after() time.Duration
|
|
||||||
step(pcsStepFunc) pcsStepper
|
|
||||||
}
|
|
||||||
|
|
||||||
type pcsStepNext struct {
|
|
||||||
aft time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *pcsStepNext) after() time.Duration {
|
|
||||||
return n.aft
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *pcsStepNext) step(fn pcsStepFunc) (next pcsStepper) {
|
|
||||||
ttl, err := fn()
|
|
||||||
if err == nil {
|
|
||||||
next = &pcsStepNext{aft: ttl}
|
|
||||||
} else {
|
|
||||||
next = &pcsStepRetry{aft: time.Second}
|
|
||||||
log.Printf("go-oidc: provider config sync failed, retrying in %v: %v", next.after(), err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type pcsStepRetry struct {
|
|
||||||
aft time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *pcsStepRetry) after() time.Duration {
|
|
||||||
return r.aft
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *pcsStepRetry) step(fn pcsStepFunc) (next pcsStepper) {
|
|
||||||
ttl, err := fn()
|
|
||||||
if err == nil {
|
|
||||||
next = &pcsStepNext{aft: ttl}
|
|
||||||
} else {
|
|
||||||
next = &pcsStepRetry{aft: timeutil.ExpBackoff(r.aft, time.Minute)}
|
|
||||||
log.Printf("go-oidc: provider config sync failed, retrying in %v: %v", next.after(), err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func nextSyncAfter(exp time.Time, clock clockwork.Clock) time.Duration {
|
|
||||||
if exp.IsZero() {
|
|
||||||
return MaximumProviderConfigSyncInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
t := exp.Sub(clock.Now()) / 2
|
|
||||||
if t > MaximumProviderConfigSyncInterval {
|
|
||||||
t = MaximumProviderConfigSyncInterval
|
|
||||||
} else if t < minimumProviderConfigSyncInterval {
|
|
||||||
t = minimumProviderConfigSyncInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpProviderConfigGetter struct {
|
|
||||||
hc phttp.Client
|
|
||||||
issuerURL string
|
|
||||||
clock clockwork.Clock
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPProviderConfigGetter(hc phttp.Client, issuerURL string) *httpProviderConfigGetter {
|
|
||||||
return &httpProviderConfigGetter{
|
|
||||||
hc: hc,
|
|
||||||
issuerURL: issuerURL,
|
|
||||||
clock: clockwork.NewRealClock(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *httpProviderConfigGetter) Get() (cfg ProviderConfig, err error) {
|
|
||||||
// If the Issuer value contains a path component, any terminating / MUST be removed before
|
|
||||||
// appending /.well-known/openid-configuration.
|
|
||||||
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
|
|
||||||
discoveryURL := strings.TrimSuffix(r.issuerURL, "/") + discoveryConfigPath
|
|
||||||
req, err := http.NewRequest("GET", discoveryURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := r.hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if err = json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var ttl time.Duration
|
|
||||||
var ok bool
|
|
||||||
ttl, ok, err = phttp.Cacheable(resp.Header)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
} else if ok {
|
|
||||||
cfg.ExpiresAt = r.clock.Now().UTC().Add(ttl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The issuer value returned MUST be identical to the Issuer URL that was directly used to retrieve the configuration information.
|
|
||||||
// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation
|
|
||||||
if !urlEqual(cfg.Issuer.String(), r.issuerURL) {
|
|
||||||
err = fmt.Errorf(`"issuer" in config (%v) does not match provided issuer URL (%v)`, cfg.Issuer, r.issuerURL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchProviderConfig(hc phttp.Client, issuerURL string) (ProviderConfig, error) {
|
|
||||||
if hc == nil {
|
|
||||||
hc = http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
g := NewHTTPProviderConfigGetter(hc, issuerURL)
|
|
||||||
return g.Get()
|
|
||||||
}
|
|
||||||
|
|
||||||
func WaitForProviderConfig(hc phttp.Client, issuerURL string) (pcfg ProviderConfig) {
|
|
||||||
return waitForProviderConfig(hc, issuerURL, clockwork.NewRealClock())
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForProviderConfig(hc phttp.Client, issuerURL string, clock clockwork.Clock) (pcfg ProviderConfig) {
|
|
||||||
var sleep time.Duration
|
|
||||||
var err error
|
|
||||||
for {
|
|
||||||
pcfg, err = FetchProviderConfig(hc, issuerURL)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep = timeutil.ExpBackoff(sleep, time.Minute)
|
|
||||||
fmt.Printf("Failed fetching provider config, trying again in %v: %v\n", sleep, err)
|
|
||||||
time.Sleep(sleep)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
phttp "github.com/coreos/go-oidc/http"
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TokenRefresher interface {
|
|
||||||
// Verify checks if the provided token is currently valid or not.
|
|
||||||
Verify(jose.JWT) error
|
|
||||||
|
|
||||||
// Refresh attempts to authenticate and retrieve a new token.
|
|
||||||
Refresh() (jose.JWT, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientCredsTokenRefresher struct {
|
|
||||||
Issuer string
|
|
||||||
OIDCClient *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientCredsTokenRefresher) Verify(jwt jose.JWT) (err error) {
|
|
||||||
_, err = VerifyClientClaims(jwt, c.Issuer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientCredsTokenRefresher) Refresh() (jwt jose.JWT, err error) {
|
|
||||||
if err = c.OIDCClient.Healthy(); err != nil {
|
|
||||||
err = fmt.Errorf("unable to authenticate, unhealthy OIDC client: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt, err = c.OIDCClient.ClientCredsToken([]string{"openid"})
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("unable to verify auth code with issuer: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthenticatedTransport struct {
|
|
||||||
TokenRefresher
|
|
||||||
http.RoundTripper
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
jwt jose.JWT
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AuthenticatedTransport) verifiedJWT() (jose.JWT, error) {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
if t.TokenRefresher.Verify(t.jwt) == nil {
|
|
||||||
return t.jwt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt, err := t.TokenRefresher.Refresh()
|
|
||||||
if err != nil {
|
|
||||||
return jose.JWT{}, fmt.Errorf("unable to acquire valid JWT: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.jwt = jwt
|
|
||||||
return t.jwt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetJWT sets the JWT held by the Transport.
|
|
||||||
// This is useful for cases in which you want to set an initial JWT.
|
|
||||||
func (t *AuthenticatedTransport) SetJWT(jwt jose.JWT) {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
t.jwt = jwt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *AuthenticatedTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
||||||
jwt, err := t.verifiedJWT()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req := phttp.CopyRequest(r)
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt.Encode()))
|
|
||||||
return t.RoundTripper.RoundTrip(req)
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RequestTokenExtractor funcs extract a raw encoded token from a request.
|
|
||||||
type RequestTokenExtractor func(r *http.Request) (string, error)
|
|
||||||
|
|
||||||
// ExtractBearerToken is a RequestTokenExtractor which extracts a bearer token from a request's
|
|
||||||
// Authorization header.
|
|
||||||
func ExtractBearerToken(r *http.Request) (string, error) {
|
|
||||||
ah := r.Header.Get("Authorization")
|
|
||||||
if ah == "" {
|
|
||||||
return "", errors.New("missing Authorization header")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ah) <= 6 || strings.ToUpper(ah[0:6]) != "BEARER" {
|
|
||||||
return "", errors.New("should be a bearer token")
|
|
||||||
}
|
|
||||||
|
|
||||||
val := ah[7:]
|
|
||||||
if len(val) == 0 {
|
|
||||||
return "", errors.New("bearer token is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CookieTokenExtractor returns a RequestTokenExtractor which extracts a token from the named cookie in a request.
|
|
||||||
func CookieTokenExtractor(cookieName string) RequestTokenExtractor {
|
|
||||||
return func(r *http.Request) (string, error) {
|
|
||||||
ck, err := r.Cookie(cookieName)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("token cookie not found in request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ck.Value == "" {
|
|
||||||
return "", errors.New("token cookie found but is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ck.Value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClaims(iss, sub string, aud interface{}, iat, exp time.Time) jose.Claims {
|
|
||||||
return jose.Claims{
|
|
||||||
// required
|
|
||||||
"iss": iss,
|
|
||||||
"sub": sub,
|
|
||||||
"aud": aud,
|
|
||||||
"iat": iat.Unix(),
|
|
||||||
"exp": exp.Unix(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenClientID(hostport string) (string, error) {
|
|
||||||
b, err := randBytes(32)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var host string
|
|
||||||
if strings.Contains(hostport, ":") {
|
|
||||||
host, _, err = net.SplitHostPort(hostport)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
host = hostport
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s@%s", base64.URLEncoding.EncodeToString(b), host), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func randBytes(n int) ([]byte, error) {
|
|
||||||
b := make([]byte, n)
|
|
||||||
got, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if n != got {
|
|
||||||
return nil, errors.New("unable to generate enough random data")
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// urlEqual checks two urls for equality using only the host and path portions.
|
|
||||||
func urlEqual(url1, url2 string) bool {
|
|
||||||
u1, err := url.Parse(url1)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
u2, err := url.Parse(url2)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.ToLower(u1.Host+u1.Path) == strings.ToLower(u2.Host+u2.Path)
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jonboulle/clockwork"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
"github.com/coreos/go-oidc/key"
|
|
||||||
)
|
|
||||||
|
|
||||||
func VerifySignature(jwt jose.JWT, keys []key.PublicKey) (bool, error) {
|
|
||||||
jwtBytes := []byte(jwt.Data())
|
|
||||||
for _, k := range keys {
|
|
||||||
v, err := k.Verifier()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if v.Verify(jwt.Signature, jwtBytes) == nil {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// containsString returns true if the given string(needle) is found
|
|
||||||
// in the string array(haystack).
|
|
||||||
func containsString(needle string, haystack []string) bool {
|
|
||||||
for _, v := range haystack {
|
|
||||||
if v == needle {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify claims in accordance with OIDC spec
|
|
||||||
// http://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation
|
|
||||||
func VerifyClaims(jwt jose.JWT, issuer, clientID string) error {
|
|
||||||
now := time.Now().UTC()
|
|
||||||
|
|
||||||
claims, err := jwt.Claims()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ident, err := IdentityFromClaims(claims)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ident.ExpiresAt.Before(now) {
|
|
||||||
return errors.New("token is expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
// iss REQUIRED. Issuer Identifier for the Issuer of the response.
|
|
||||||
// The iss value is a case sensitive URL using the https scheme that contains scheme,
|
|
||||||
// host, and optionally, port number and path components and no query or fragment components.
|
|
||||||
if iss, exists := claims["iss"].(string); exists {
|
|
||||||
if !urlEqual(iss, issuer) {
|
|
||||||
return fmt.Errorf("invalid claim value: 'iss'. expected=%s, found=%s.", issuer, iss)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return errors.New("missing claim: 'iss'")
|
|
||||||
}
|
|
||||||
|
|
||||||
// iat REQUIRED. Time at which the JWT was issued.
|
|
||||||
// Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z
|
|
||||||
// as measured in UTC until the date/time.
|
|
||||||
if _, exists := claims["iat"].(float64); !exists {
|
|
||||||
return errors.New("missing claim: 'iat'")
|
|
||||||
}
|
|
||||||
|
|
||||||
// aud REQUIRED. Audience(s) that this ID Token is intended for.
|
|
||||||
// It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value.
|
|
||||||
// It MAY also contain identifiers for other audiences. In the general case, the aud
|
|
||||||
// value is an array of case sensitive strings. In the common special case when there
|
|
||||||
// is one audience, the aud value MAY be a single case sensitive string.
|
|
||||||
if aud, ok, err := claims.StringClaim("aud"); err == nil && ok {
|
|
||||||
if aud != clientID {
|
|
||||||
return fmt.Errorf("invalid claims, 'aud' claim and 'client_id' do not match, aud=%s, client_id=%s", aud, clientID)
|
|
||||||
}
|
|
||||||
} else if aud, ok, err := claims.StringsClaim("aud"); err == nil && ok {
|
|
||||||
if !containsString(clientID, aud) {
|
|
||||||
return fmt.Errorf("invalid claims, cannot find 'client_id' in 'aud' claim, aud=%v, client_id=%s", aud, clientID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return errors.New("invalid claim value: 'aud' is required, and should be either string or string array")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyClientClaims verifies all the required claims are valid for a "client credentials" JWT.
|
|
||||||
// Returns the client ID if valid, or an error if invalid.
|
|
||||||
func VerifyClientClaims(jwt jose.JWT, issuer string) (string, error) {
|
|
||||||
claims, err := jwt.Claims()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to parse JWT claims: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
iss, ok, err := claims.StringClaim("iss")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to parse 'iss' claim: %v", err)
|
|
||||||
} else if !ok {
|
|
||||||
return "", errors.New("missing required 'iss' claim")
|
|
||||||
} else if !urlEqual(iss, issuer) {
|
|
||||||
return "", fmt.Errorf("'iss' claim does not match expected issuer, iss=%s", iss)
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, ok, err := claims.StringClaim("sub")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to parse 'sub' claim: %v", err)
|
|
||||||
} else if !ok {
|
|
||||||
return "", errors.New("missing required 'sub' claim")
|
|
||||||
}
|
|
||||||
|
|
||||||
if aud, ok, err := claims.StringClaim("aud"); err == nil && ok {
|
|
||||||
if aud != sub {
|
|
||||||
return "", fmt.Errorf("invalid claims, 'aud' claim and 'sub' claim do not match, aud=%s, sub=%s", aud, sub)
|
|
||||||
}
|
|
||||||
} else if aud, ok, err := claims.StringsClaim("aud"); err == nil && ok {
|
|
||||||
if !containsString(sub, aud) {
|
|
||||||
return "", fmt.Errorf("invalid claims, cannot find 'sud' in 'aud' claim, aud=%v, sub=%s", aud, sub)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return "", errors.New("invalid claim value: 'aud' is required, and should be either string or string array")
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().UTC()
|
|
||||||
exp, ok, err := claims.TimeClaim("exp")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to parse 'exp' claim: %v", err)
|
|
||||||
} else if !ok {
|
|
||||||
return "", errors.New("missing required 'exp' claim")
|
|
||||||
} else if exp.Before(now) {
|
|
||||||
return "", fmt.Errorf("token already expired at: %v", exp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type JWTVerifier struct {
|
|
||||||
issuer string
|
|
||||||
clientID string
|
|
||||||
syncFunc func() error
|
|
||||||
keysFunc func() []key.PublicKey
|
|
||||||
clock clockwork.Clock
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJWTVerifier(issuer, clientID string, syncFunc func() error, keysFunc func() []key.PublicKey) JWTVerifier {
|
|
||||||
return JWTVerifier{
|
|
||||||
issuer: issuer,
|
|
||||||
clientID: clientID,
|
|
||||||
syncFunc: syncFunc,
|
|
||||||
keysFunc: keysFunc,
|
|
||||||
clock: clockwork.NewRealClock(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *JWTVerifier) Verify(jwt jose.JWT) error {
|
|
||||||
// Verify claims before verifying the signature. This is an optimization to throw out
|
|
||||||
// tokens we know are invalid without undergoing an expensive signature check and
|
|
||||||
// possibly a re-sync event.
|
|
||||||
if err := VerifyClaims(jwt, v.issuer, v.clientID); err != nil {
|
|
||||||
return fmt.Errorf("oidc: JWT claims invalid: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := VerifySignature(jwt, v.keysFunc())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("oidc: JWT signature verification failed: %v", err)
|
|
||||||
} else if ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = v.syncFunc(); err != nil {
|
|
||||||
return fmt.Errorf("oidc: failed syncing KeySet: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err = VerifySignature(jwt, v.keysFunc())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("oidc: JWT signature verification failed: %v", err)
|
|
||||||
} else if !ok {
|
|
||||||
return errors.New("oidc: unable to verify JWT signature: no matching keys")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -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,5 +0,0 @@
|
||||||
CoreOS Project
|
|
||||||
Copyright 2014 CoreOS, Inc
|
|
||||||
|
|
||||||
This product includes software developed at CoreOS, Inc.
|
|
||||||
(http://www.coreos.com/).
|
|
|
@ -1,11 +0,0 @@
|
||||||
health
|
|
||||||
====
|
|
||||||
|
|
||||||
A simple framework for implementing an HTTP health check endpoint on servers.
|
|
||||||
|
|
||||||
Users implement their `health.Checkable` types, and create a `health.Checker`, from which they can get an `http.HandlerFunc` using `health.Checker.MakeHealthHandlerFunc`.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
For more details, visit the docs on [gopkgdoc](http://godoc.org/github.com/coreos/pkg/health)
|
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
package health
|
|
||||||
|
|
||||||
import (
|
|
||||||
"expvar"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/coreos/pkg/httputil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Checkables should return nil when the thing they are checking is healthy, and an error otherwise.
|
|
||||||
type Checkable interface {
|
|
||||||
Healthy() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checker provides a way to make an endpoint which can be probed for system health.
|
|
||||||
type Checker struct {
|
|
||||||
// Checks are the Checkables to be checked when probing.
|
|
||||||
Checks []Checkable
|
|
||||||
|
|
||||||
// Unhealthyhandler is called when one or more of the checks are unhealthy.
|
|
||||||
// If not provided DefaultUnhealthyHandler is called.
|
|
||||||
UnhealthyHandler UnhealthyHandler
|
|
||||||
|
|
||||||
// HealthyHandler is called when all checks are healthy.
|
|
||||||
// If not provided, DefaultHealthyHandler is called.
|
|
||||||
HealthyHandler http.HandlerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Checker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
unhealthyHandler := c.UnhealthyHandler
|
|
||||||
if unhealthyHandler == nil {
|
|
||||||
unhealthyHandler = DefaultUnhealthyHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
successHandler := c.HealthyHandler
|
|
||||||
if successHandler == nil {
|
|
||||||
successHandler = DefaultHealthyHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method != "GET" {
|
|
||||||
w.Header().Set("Allow", "GET")
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := Check(c.Checks); err != nil {
|
|
||||||
unhealthyHandler(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
successHandler(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnhealthyHandler func(w http.ResponseWriter, r *http.Request, err error)
|
|
||||||
|
|
||||||
type StatusResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Details *StatusResponseDetails `json:"details,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusResponseDetails struct {
|
|
||||||
Code int `json:"code,omitempty"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Check(checks []Checkable) (err error) {
|
|
||||||
errs := []error{}
|
|
||||||
for _, c := range checks {
|
|
||||||
if e := c.Healthy(); e != nil {
|
|
||||||
errs = append(errs, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(errs) {
|
|
||||||
case 0:
|
|
||||||
err = nil
|
|
||||||
case 1:
|
|
||||||
err = errs[0]
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("multiple health check failure: %v", errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultHealthyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := httputil.WriteJSONResponse(w, http.StatusOK, StatusResponse{
|
|
||||||
Status: "ok",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// TODO(bobbyrullo): replace with logging from new logging pkg,
|
|
||||||
// once it lands.
|
|
||||||
log.Printf("Failed to write JSON response: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultUnhealthyHandler(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
writeErr := httputil.WriteJSONResponse(w, http.StatusInternalServerError, StatusResponse{
|
|
||||||
Status: "error",
|
|
||||||
Details: &StatusResponseDetails{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
Message: err.Error(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if writeErr != nil {
|
|
||||||
// TODO(bobbyrullo): replace with logging from new logging pkg,
|
|
||||||
// once it lands.
|
|
||||||
log.Printf("Failed to write JSON response: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpvarHandler is copied from https://golang.org/src/expvar/expvar.go, where it's sadly unexported.
|
|
||||||
func ExpvarHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
fmt.Fprintf(w, "{\n")
|
|
||||||
first := true
|
|
||||||
expvar.Do(func(kv expvar.KeyValue) {
|
|
||||||
if !first {
|
|
||||||
fmt.Fprintf(w, ",\n")
|
|
||||||
}
|
|
||||||
first = false
|
|
||||||
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
|
|
||||||
})
|
|
||||||
fmt.Fprintf(w, "\n}\n")
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
httputil
|
|
||||||
====
|
|
||||||
|
|
||||||
Common code for dealing with HTTP.
|
|
||||||
|
|
||||||
Includes:
|
|
||||||
|
|
||||||
* Code for returning JSON responses.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
Visit the docs on [gopkgdoc](http://godoc.org/github.com/coreos/pkg/httputil)
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
package httputil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeleteCookies effectively deletes all named cookies
|
|
||||||
// by wiping all data and setting to expire immediately.
|
|
||||||
func DeleteCookies(w http.ResponseWriter, cookieNames ...string) {
|
|
||||||
for _, n := range cookieNames {
|
|
||||||
c := &http.Cookie{
|
|
||||||
Name: n,
|
|
||||||
Value: "",
|
|
||||||
Path: "/",
|
|
||||||
MaxAge: -1,
|
|
||||||
Expires: time.Time{},
|
|
||||||
}
|
|
||||||
http.SetCookie(w, c)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package httputil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
JSONContentType = "application/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WriteJSONResponse(w http.ResponseWriter, code int, resp interface{}) error {
|
|
||||||
enc, err := json.Marshal(resp)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", JSONContentType)
|
|
||||||
w.WriteHeader(code)
|
|
||||||
|
|
||||||
_, err = w.Write(enc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package timeutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExpBackoff(prev, max time.Duration) time.Duration {
|
|
||||||
if prev == 0 {
|
|
||||||
return time.Second
|
|
||||||
}
|
|
||||||
if prev > max/2 {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return 2 * prev
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
*.test
|
|
||||||
|
|
||||||
*.swp
|
|
|
@ -1,5 +0,0 @@
|
||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.3
|
|
||||||
|
|
||||||
sudo: false
|
|
|
@ -1,201 +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,61 +0,0 @@
|
||||||
clockwork
|
|
||||||
=========
|
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/jonboulle/clockwork.png?branch=master)](https://travis-ci.org/jonboulle/clockwork)
|
|
||||||
[![godoc](https://godoc.org/github.com/jonboulle/clockwork?status.svg)](http://godoc.org/github.com/jonboulle/clockwork)
|
|
||||||
|
|
||||||
a simple fake clock for golang
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
Replace uses of the `time` package with the `clockwork.Clock` interface instead.
|
|
||||||
|
|
||||||
For example, instead of using `time.Sleep` directly:
|
|
||||||
|
|
||||||
```
|
|
||||||
func my_func() {
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
do_something()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
inject a clock and use its `Sleep` method instead:
|
|
||||||
|
|
||||||
```
|
|
||||||
func my_func(clock clockwork.Clock) {
|
|
||||||
clock.Sleep(3 * time.Second)
|
|
||||||
do_something()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you can easily test `my_func` with a `FakeClock`:
|
|
||||||
|
|
||||||
```
|
|
||||||
func TestMyFunc(t *testing.T) {
|
|
||||||
c := clockwork.NewFakeClock()
|
|
||||||
|
|
||||||
// Start our sleepy function
|
|
||||||
my_func(c)
|
|
||||||
|
|
||||||
// Ensure we wait until my_func is sleeping
|
|
||||||
c.BlockUntil(1)
|
|
||||||
|
|
||||||
assert_state()
|
|
||||||
|
|
||||||
// Advance the FakeClock forward in time
|
|
||||||
c.Advance(3)
|
|
||||||
|
|
||||||
assert_state()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
and in production builds, simply inject the real clock instead:
|
|
||||||
```
|
|
||||||
my_func(clockwork.NewRealClock())
|
|
||||||
```
|
|
||||||
|
|
||||||
See [example_test.go](example_test.go) for a full example.
|
|
||||||
|
|
||||||
# Credits
|
|
||||||
|
|
||||||
clockwork is inspired by @wickman's [threaded fake clock](https://gist.github.com/wickman/3840816), and the [Golang playground](http://blog.golang.org/playground#Faking time)
|
|
|
@ -1,169 +0,0 @@
|
||||||
package clockwork
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Clock provides an interface that packages can use instead of directly
|
|
||||||
// using the time module, so that chronology-related behavior can be tested
|
|
||||||
type Clock interface {
|
|
||||||
After(d time.Duration) <-chan time.Time
|
|
||||||
Sleep(d time.Duration)
|
|
||||||
Now() time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// FakeClock provides an interface for a clock which can be
|
|
||||||
// manually advanced through time
|
|
||||||
type FakeClock interface {
|
|
||||||
Clock
|
|
||||||
// Advance advances the FakeClock to a new point in time, ensuring any existing
|
|
||||||
// sleepers are notified appropriately before returning
|
|
||||||
Advance(d time.Duration)
|
|
||||||
// BlockUntil will block until the FakeClock has the given number of
|
|
||||||
// sleepers (callers of Sleep or After)
|
|
||||||
BlockUntil(n int)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRealClock returns a Clock which simply delegates calls to the actual time
|
|
||||||
// package; it should be used by packages in production.
|
|
||||||
func NewRealClock() Clock {
|
|
||||||
return &realClock{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFakeClock returns a FakeClock implementation which can be
|
|
||||||
// manually advanced through time for testing. The initial time of the
|
|
||||||
// FakeClock will be an arbitrary non-zero time.
|
|
||||||
func NewFakeClock() FakeClock {
|
|
||||||
// use a fixture that does not fulfill Time.IsZero()
|
|
||||||
return NewFakeClockAt(time.Date(1984, time.April, 4, 0, 0, 0, 0, time.UTC))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFakeClockAt returns a FakeClock initialised at the given time.Time.
|
|
||||||
func NewFakeClockAt(t time.Time) FakeClock {
|
|
||||||
return &fakeClock{
|
|
||||||
time: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type realClock struct{}
|
|
||||||
|
|
||||||
func (rc *realClock) After(d time.Duration) <-chan time.Time {
|
|
||||||
return time.After(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *realClock) Sleep(d time.Duration) {
|
|
||||||
time.Sleep(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *realClock) Now() time.Time {
|
|
||||||
return time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeClock struct {
|
|
||||||
sleepers []*sleeper
|
|
||||||
blockers []*blocker
|
|
||||||
time time.Time
|
|
||||||
|
|
||||||
l sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// sleeper represents a caller of After or Sleep
|
|
||||||
type sleeper struct {
|
|
||||||
until time.Time
|
|
||||||
done chan time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// blocker represents a caller of BlockUntil
|
|
||||||
type blocker struct {
|
|
||||||
count int
|
|
||||||
ch chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// After mimics time.After; it waits for the given duration to elapse on the
|
|
||||||
// fakeClock, then sends the current time on the returned channel.
|
|
||||||
func (fc *fakeClock) After(d time.Duration) <-chan time.Time {
|
|
||||||
fc.l.Lock()
|
|
||||||
defer fc.l.Unlock()
|
|
||||||
now := fc.time
|
|
||||||
done := make(chan time.Time, 1)
|
|
||||||
if d.Nanoseconds() == 0 {
|
|
||||||
// special case - trigger immediately
|
|
||||||
done <- now
|
|
||||||
} else {
|
|
||||||
// otherwise, add to the set of sleepers
|
|
||||||
s := &sleeper{
|
|
||||||
until: now.Add(d),
|
|
||||||
done: done,
|
|
||||||
}
|
|
||||||
fc.sleepers = append(fc.sleepers, s)
|
|
||||||
// and notify any blockers
|
|
||||||
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
|
||||||
}
|
|
||||||
return done
|
|
||||||
}
|
|
||||||
|
|
||||||
// notifyBlockers notifies all the blockers waiting until the
|
|
||||||
// given number of sleepers are waiting on the fakeClock. It
|
|
||||||
// returns an updated slice of blockers (i.e. those still waiting)
|
|
||||||
func notifyBlockers(blockers []*blocker, count int) (newBlockers []*blocker) {
|
|
||||||
for _, b := range blockers {
|
|
||||||
if b.count == count {
|
|
||||||
close(b.ch)
|
|
||||||
} else {
|
|
||||||
newBlockers = append(newBlockers, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep blocks until the given duration has passed on the fakeClock
|
|
||||||
func (fc *fakeClock) Sleep(d time.Duration) {
|
|
||||||
<-fc.After(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time returns the current time of the fakeClock
|
|
||||||
func (fc *fakeClock) Now() time.Time {
|
|
||||||
fc.l.RLock()
|
|
||||||
t := fc.time
|
|
||||||
fc.l.RUnlock()
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance advances fakeClock to a new point in time, ensuring channels from any
|
|
||||||
// previous invocations of After are notified appropriately before returning
|
|
||||||
func (fc *fakeClock) Advance(d time.Duration) {
|
|
||||||
fc.l.Lock()
|
|
||||||
defer fc.l.Unlock()
|
|
||||||
end := fc.time.Add(d)
|
|
||||||
var newSleepers []*sleeper
|
|
||||||
for _, s := range fc.sleepers {
|
|
||||||
if end.Sub(s.until) >= 0 {
|
|
||||||
s.done <- end
|
|
||||||
} else {
|
|
||||||
newSleepers = append(newSleepers, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fc.sleepers = newSleepers
|
|
||||||
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
|
||||||
fc.time = end
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlockUntil will block until the fakeClock has the given number of sleepers
|
|
||||||
// (callers of Sleep or After)
|
|
||||||
func (fc *fakeClock) BlockUntil(n int) {
|
|
||||||
fc.l.Lock()
|
|
||||||
// Fast path: current number of sleepers is what we're looking for
|
|
||||||
if len(fc.sleepers) == n {
|
|
||||||
fc.l.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Otherwise, set up a new blocker
|
|
||||||
b := &blocker{
|
|
||||||
count: n,
|
|
||||||
ch: make(chan struct{}),
|
|
||||||
}
|
|
||||||
fc.blockers = append(fc.blockers, b)
|
|
||||||
fc.l.Unlock()
|
|
||||||
<-b.ch
|
|
||||||
}
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Package require implements the same assertions as the `assert` package but
|
||||||
|
// stops test execution when a test fails.
|
||||||
|
//
|
||||||
|
// Example Usage
|
||||||
|
//
|
||||||
|
// The following is a complete example using require in a standard test function:
|
||||||
|
// import (
|
||||||
|
// "testing"
|
||||||
|
// "github.com/stretchr/testify/require"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// func TestSomething(t *testing.T) {
|
||||||
|
//
|
||||||
|
// var a string = "Hello"
|
||||||
|
// var b string = "Hello"
|
||||||
|
//
|
||||||
|
// require.Equal(t, a, b, "The two words should be the same.")
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Assertions
|
||||||
|
//
|
||||||
|
// The `require` package have same global functions as in the `assert` package,
|
||||||
|
// but instead of returning a boolean result they call `t.FailNow()`.
|
||||||
|
//
|
||||||
|
// Every assertion function also takes an optional string message as the final argument,
|
||||||
|
// allowing custom error messages to be appended to the message the assertion method outputs.
|
||||||
|
package require
|
16
vendor/github.com/stretchr/testify/require/forward_requirements.go
generated
vendored
Normal file
16
vendor/github.com/stretchr/testify/require/forward_requirements.go
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package require
|
||||||
|
|
||||||
|
// Assertions provides assertion methods around the
|
||||||
|
// TestingT interface.
|
||||||
|
type Assertions struct {
|
||||||
|
t TestingT
|
||||||
|
}
|
||||||
|
|
||||||
|
// New makes a new Assertions object for the specified TestingT.
|
||||||
|
func New(t TestingT) *Assertions {
|
||||||
|
return &Assertions{
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate go run ../_codegen/main.go -output-package=require -template=require_forward.go.tmpl -include-format-funcs
|
|
@ -0,0 +1,867 @@
|
||||||
|
/*
|
||||||
|
* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen
|
||||||
|
* THIS FILE MUST NOT BE EDITED BY HAND
|
||||||
|
*/
|
||||||
|
|
||||||
|
package require
|
||||||
|
|
||||||
|
import (
|
||||||
|
assert "github.com/stretchr/testify/assert"
|
||||||
|
http "net/http"
|
||||||
|
url "net/url"
|
||||||
|
time "time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Condition uses a Comparison to assert a complex condition.
|
||||||
|
func Condition(t TestingT, comp assert.Comparison, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Condition(t, comp, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditionf uses a Comparison to assert a complex condition.
|
||||||
|
func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interface{}) {
|
||||||
|
if !assert.Conditionf(t, comp, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains asserts that the specified string, list(array, slice...) or map contains the
|
||||||
|
// specified substring or element.
|
||||||
|
//
|
||||||
|
// assert.Contains(t, "Hello World", "World")
|
||||||
|
// assert.Contains(t, ["Hello", "World"], "World")
|
||||||
|
// assert.Contains(t, {"Hello": "World"}, "Hello")
|
||||||
|
func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Contains(t, s, contains, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containsf asserts that the specified string, list(array, slice...) or map contains the
|
||||||
|
// specified substring or element.
|
||||||
|
//
|
||||||
|
// assert.Containsf(t, "Hello World", "World", "error message %s", "formatted")
|
||||||
|
// assert.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted")
|
||||||
|
// assert.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted")
|
||||||
|
func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Containsf(t, s, contains, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists.
|
||||||
|
func DirExists(t TestingT, path string, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.DirExists(t, path, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists.
|
||||||
|
func DirExistsf(t TestingT, path string, msg string, args ...interface{}) {
|
||||||
|
if !assert.DirExistsf(t, path, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified
|
||||||
|
// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements,
|
||||||
|
// the number of appearances of each of them in both lists should match.
|
||||||
|
//
|
||||||
|
// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2])
|
||||||
|
func ElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.ElementsMatch(t, listA, listB, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified
|
||||||
|
// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements,
|
||||||
|
// the number of appearances of each of them in both lists should match.
|
||||||
|
//
|
||||||
|
// assert.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted")
|
||||||
|
func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.ElementsMatchf(t, listA, listB, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either
|
||||||
|
// a slice or a channel with len == 0.
|
||||||
|
//
|
||||||
|
// assert.Empty(t, obj)
|
||||||
|
func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Empty(t, object, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either
|
||||||
|
// a slice or a channel with len == 0.
|
||||||
|
//
|
||||||
|
// assert.Emptyf(t, obj, "error message %s", "formatted")
|
||||||
|
func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Emptyf(t, object, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal asserts that two objects are equal.
|
||||||
|
//
|
||||||
|
// assert.Equal(t, 123, 123)
|
||||||
|
//
|
||||||
|
// Pointer variable equality is determined based on the equality of the
|
||||||
|
// referenced values (as opposed to the memory addresses). Function equality
|
||||||
|
// cannot be determined and will always fail.
|
||||||
|
func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Equal(t, expected, actual, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualError asserts that a function returned an error (i.e. not `nil`)
|
||||||
|
// and that it is equal to the provided error.
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// assert.EqualError(t, err, expectedErrorString)
|
||||||
|
func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.EqualError(t, theError, errString, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualErrorf asserts that a function returned an error (i.e. not `nil`)
|
||||||
|
// and that it is equal to the provided error.
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted")
|
||||||
|
func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) {
|
||||||
|
if !assert.EqualErrorf(t, theError, errString, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualValues asserts that two objects are equal or convertable to the same types
|
||||||
|
// and equal.
|
||||||
|
//
|
||||||
|
// assert.EqualValues(t, uint32(123), int32(123))
|
||||||
|
func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.EqualValues(t, expected, actual, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualValuesf asserts that two objects are equal or convertable to the same types
|
||||||
|
// and equal.
|
||||||
|
//
|
||||||
|
// assert.EqualValuesf(t, uint32(123, "error message %s", "formatted"), int32(123))
|
||||||
|
func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.EqualValuesf(t, expected, actual, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equalf asserts that two objects are equal.
|
||||||
|
//
|
||||||
|
// assert.Equalf(t, 123, 123, "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Pointer variable equality is determined based on the equality of the
|
||||||
|
// referenced values (as opposed to the memory addresses). Function equality
|
||||||
|
// cannot be determined and will always fail.
|
||||||
|
func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Equalf(t, expected, actual, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error asserts that a function returned an error (i.e. not `nil`).
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// if assert.Error(t, err) {
|
||||||
|
// assert.Equal(t, expectedError, err)
|
||||||
|
// }
|
||||||
|
func Error(t TestingT, err error, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Error(t, err, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf asserts that a function returned an error (i.e. not `nil`).
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// if assert.Errorf(t, err, "error message %s", "formatted") {
|
||||||
|
// assert.Equal(t, expectedErrorf, err)
|
||||||
|
// }
|
||||||
|
func Errorf(t TestingT, err error, msg string, args ...interface{}) {
|
||||||
|
if !assert.Errorf(t, err, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactly asserts that two objects are equal in value and type.
|
||||||
|
//
|
||||||
|
// assert.Exactly(t, int32(123), int64(123))
|
||||||
|
func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Exactly(t, expected, actual, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactlyf asserts that two objects are equal in value and type.
|
||||||
|
//
|
||||||
|
// assert.Exactlyf(t, int32(123, "error message %s", "formatted"), int64(123))
|
||||||
|
func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Exactlyf(t, expected, actual, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail reports a failure through
|
||||||
|
func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Fail(t, failureMessage, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailNow fails test
|
||||||
|
func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.FailNow(t, failureMessage, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailNowf fails test
|
||||||
|
func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) {
|
||||||
|
if !assert.FailNowf(t, failureMessage, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failf reports a failure through
|
||||||
|
func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) {
|
||||||
|
if !assert.Failf(t, failureMessage, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// False asserts that the specified value is false.
|
||||||
|
//
|
||||||
|
// assert.False(t, myBool)
|
||||||
|
func False(t TestingT, value bool, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.False(t, value, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falsef asserts that the specified value is false.
|
||||||
|
//
|
||||||
|
// assert.Falsef(t, myBool, "error message %s", "formatted")
|
||||||
|
func Falsef(t TestingT, value bool, msg string, args ...interface{}) {
|
||||||
|
if !assert.Falsef(t, value, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file.
|
||||||
|
func FileExists(t TestingT, path string, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.FileExists(t, path, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file.
|
||||||
|
func FileExistsf(t TestingT, path string, msg string, args ...interface{}) {
|
||||||
|
if !assert.FileExistsf(t, path, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBodyContains asserts that a specified handler returns a
|
||||||
|
// body that contains a string.
|
||||||
|
//
|
||||||
|
// assert.HTTPBodyContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.HTTPBodyContains(t, handler, method, url, values, str, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBodyContainsf asserts that a specified handler returns a
|
||||||
|
// body that contains a string.
|
||||||
|
//
|
||||||
|
// assert.HTTPBodyContainsf(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.HTTPBodyContainsf(t, handler, method, url, values, str, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBodyNotContains asserts that a specified handler returns a
|
||||||
|
// body that does not contain a string.
|
||||||
|
//
|
||||||
|
// assert.HTTPBodyNotContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.HTTPBodyNotContains(t, handler, method, url, values, str, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBodyNotContainsf asserts that a specified handler returns a
|
||||||
|
// body that does not contain a string.
|
||||||
|
//
|
||||||
|
// assert.HTTPBodyNotContainsf(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.HTTPBodyNotContainsf(t, handler, method, url, values, str, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPError asserts that a specified handler returns an error status code.
|
||||||
|
//
|
||||||
|
// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.HTTPError(t, handler, method, url, values, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPErrorf asserts that a specified handler returns an error status code.
|
||||||
|
//
|
||||||
|
// assert.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false).
|
||||||
|
func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) {
|
||||||
|
if !assert.HTTPErrorf(t, handler, method, url, values, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPRedirect asserts that a specified handler returns a redirect status code.
|
||||||
|
//
|
||||||
|
// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.HTTPRedirect(t, handler, method, url, values, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPRedirectf asserts that a specified handler returns a redirect status code.
|
||||||
|
//
|
||||||
|
// assert.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false).
|
||||||
|
func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) {
|
||||||
|
if !assert.HTTPRedirectf(t, handler, method, url, values, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPSuccess asserts that a specified handler returns a success status code.
|
||||||
|
//
|
||||||
|
// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil)
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.HTTPSuccess(t, handler, method, url, values, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPSuccessf asserts that a specified handler returns a success status code.
|
||||||
|
//
|
||||||
|
// assert.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) {
|
||||||
|
if !assert.HTTPSuccessf(t, handler, method, url, values, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements asserts that an object is implemented by the specified interface.
|
||||||
|
//
|
||||||
|
// assert.Implements(t, (*MyInterface)(nil), new(MyObject))
|
||||||
|
func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Implements(t, interfaceObject, object, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementsf asserts that an object is implemented by the specified interface.
|
||||||
|
//
|
||||||
|
// assert.Implementsf(t, (*MyInterface, "error message %s", "formatted")(nil), new(MyObject))
|
||||||
|
func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Implementsf(t, interfaceObject, object, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDelta asserts that the two numerals are within delta of each other.
|
||||||
|
//
|
||||||
|
// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01)
|
||||||
|
func InDelta(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.InDelta(t, expected, actual, delta, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys.
|
||||||
|
func InDeltaMapValues(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.InDeltaMapValues(t, expected, actual, delta, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys.
|
||||||
|
func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) {
|
||||||
|
if !assert.InDeltaMapValuesf(t, expected, actual, delta, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaSlice is the same as InDelta, except it compares two slices.
|
||||||
|
func InDeltaSlice(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.InDeltaSlice(t, expected, actual, delta, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaSlicef is the same as InDelta, except it compares two slices.
|
||||||
|
func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) {
|
||||||
|
if !assert.InDeltaSlicef(t, expected, actual, delta, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaf asserts that the two numerals are within delta of each other.
|
||||||
|
//
|
||||||
|
// assert.InDeltaf(t, math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01)
|
||||||
|
func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) {
|
||||||
|
if !assert.InDeltaf(t, expected, actual, delta, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InEpsilon asserts that expected and actual have a relative error less than epsilon
|
||||||
|
func InEpsilon(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.InEpsilon(t, expected, actual, epsilon, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices.
|
||||||
|
func InEpsilonSlice(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.InEpsilonSlice(t, expected, actual, epsilon, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices.
|
||||||
|
func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) {
|
||||||
|
if !assert.InEpsilonSlicef(t, expected, actual, epsilon, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InEpsilonf asserts that expected and actual have a relative error less than epsilon
|
||||||
|
func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) {
|
||||||
|
if !assert.InEpsilonf(t, expected, actual, epsilon, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsType asserts that the specified objects are of the same type.
|
||||||
|
func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.IsType(t, expectedType, object, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTypef asserts that the specified objects are of the same type.
|
||||||
|
func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.IsTypef(t, expectedType, object, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONEq asserts that two JSON strings are equivalent.
|
||||||
|
//
|
||||||
|
// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)
|
||||||
|
func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.JSONEq(t, expected, actual, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONEqf asserts that two JSON strings are equivalent.
|
||||||
|
//
|
||||||
|
// assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted")
|
||||||
|
func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) {
|
||||||
|
if !assert.JSONEqf(t, expected, actual, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len asserts that the specified object has specific length.
|
||||||
|
// Len also fails if the object has a type that len() not accept.
|
||||||
|
//
|
||||||
|
// assert.Len(t, mySlice, 3)
|
||||||
|
func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Len(t, object, length, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lenf asserts that the specified object has specific length.
|
||||||
|
// Lenf also fails if the object has a type that len() not accept.
|
||||||
|
//
|
||||||
|
// assert.Lenf(t, mySlice, 3, "error message %s", "formatted")
|
||||||
|
func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) {
|
||||||
|
if !assert.Lenf(t, object, length, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nil asserts that the specified object is nil.
|
||||||
|
//
|
||||||
|
// assert.Nil(t, err)
|
||||||
|
func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Nil(t, object, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nilf asserts that the specified object is nil.
|
||||||
|
//
|
||||||
|
// assert.Nilf(t, err, "error message %s", "formatted")
|
||||||
|
func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Nilf(t, object, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoError asserts that a function returned no error (i.e. `nil`).
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// if assert.NoError(t, err) {
|
||||||
|
// assert.Equal(t, expectedObj, actualObj)
|
||||||
|
// }
|
||||||
|
func NoError(t TestingT, err error, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NoError(t, err, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoErrorf asserts that a function returned no error (i.e. `nil`).
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// if assert.NoErrorf(t, err, "error message %s", "formatted") {
|
||||||
|
// assert.Equal(t, expectedObj, actualObj)
|
||||||
|
// }
|
||||||
|
func NoErrorf(t TestingT, err error, msg string, args ...interface{}) {
|
||||||
|
if !assert.NoErrorf(t, err, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the
|
||||||
|
// specified substring or element.
|
||||||
|
//
|
||||||
|
// assert.NotContains(t, "Hello World", "Earth")
|
||||||
|
// assert.NotContains(t, ["Hello", "World"], "Earth")
|
||||||
|
// assert.NotContains(t, {"Hello": "World"}, "Earth")
|
||||||
|
func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NotContains(t, s, contains, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the
|
||||||
|
// specified substring or element.
|
||||||
|
//
|
||||||
|
// assert.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted")
|
||||||
|
// assert.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted")
|
||||||
|
// assert.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted")
|
||||||
|
func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.NotContainsf(t, s, contains, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
|
||||||
|
// a slice or a channel with len == 0.
|
||||||
|
//
|
||||||
|
// if assert.NotEmpty(t, obj) {
|
||||||
|
// assert.Equal(t, "two", obj[1])
|
||||||
|
// }
|
||||||
|
func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NotEmpty(t, object, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
|
||||||
|
// a slice or a channel with len == 0.
|
||||||
|
//
|
||||||
|
// if assert.NotEmptyf(t, obj, "error message %s", "formatted") {
|
||||||
|
// assert.Equal(t, "two", obj[1])
|
||||||
|
// }
|
||||||
|
func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.NotEmptyf(t, object, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqual asserts that the specified values are NOT equal.
|
||||||
|
//
|
||||||
|
// assert.NotEqual(t, obj1, obj2)
|
||||||
|
//
|
||||||
|
// Pointer variable equality is determined based on the equality of the
|
||||||
|
// referenced values (as opposed to the memory addresses).
|
||||||
|
func NotEqual(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NotEqual(t, expected, actual, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqualf asserts that the specified values are NOT equal.
|
||||||
|
//
|
||||||
|
// assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Pointer variable equality is determined based on the equality of the
|
||||||
|
// referenced values (as opposed to the memory addresses).
|
||||||
|
func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.NotEqualf(t, expected, actual, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotNil asserts that the specified object is not nil.
|
||||||
|
//
|
||||||
|
// assert.NotNil(t, err)
|
||||||
|
func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NotNil(t, object, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotNilf asserts that the specified object is not nil.
|
||||||
|
//
|
||||||
|
// assert.NotNilf(t, err, "error message %s", "formatted")
|
||||||
|
func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.NotNilf(t, object, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic.
|
||||||
|
//
|
||||||
|
// assert.NotPanics(t, func(){ RemainCalm() })
|
||||||
|
func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NotPanics(t, f, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic.
|
||||||
|
//
|
||||||
|
// assert.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted")
|
||||||
|
func NotPanicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) {
|
||||||
|
if !assert.NotPanicsf(t, f, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotRegexp asserts that a specified regexp does not match a string.
|
||||||
|
//
|
||||||
|
// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting")
|
||||||
|
// assert.NotRegexp(t, "^start", "it's not starting")
|
||||||
|
func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NotRegexp(t, rx, str, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotRegexpf asserts that a specified regexp does not match a string.
|
||||||
|
//
|
||||||
|
// assert.NotRegexpf(t, regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting")
|
||||||
|
// assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted")
|
||||||
|
func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.NotRegexpf(t, rx, str, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotSubset asserts that the specified list(array, slice...) contains not all
|
||||||
|
// elements given in the specified subset(array, slice...).
|
||||||
|
//
|
||||||
|
// assert.NotSubset(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]")
|
||||||
|
func NotSubset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NotSubset(t, list, subset, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotSubsetf asserts that the specified list(array, slice...) contains not all
|
||||||
|
// elements given in the specified subset(array, slice...).
|
||||||
|
//
|
||||||
|
// assert.NotSubsetf(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted")
|
||||||
|
func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.NotSubsetf(t, list, subset, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotZero asserts that i is not the zero value for its type.
|
||||||
|
func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.NotZero(t, i, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotZerof asserts that i is not the zero value for its type.
|
||||||
|
func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.NotZerof(t, i, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panics asserts that the code inside the specified PanicTestFunc panics.
|
||||||
|
//
|
||||||
|
// assert.Panics(t, func(){ GoCrazy() })
|
||||||
|
func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Panics(t, f, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that
|
||||||
|
// the recovered panic value equals the expected panic value.
|
||||||
|
//
|
||||||
|
// assert.PanicsWithValue(t, "crazy error", func(){ GoCrazy() })
|
||||||
|
func PanicsWithValue(t TestingT, expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.PanicsWithValue(t, expected, f, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that
|
||||||
|
// the recovered panic value equals the expected panic value.
|
||||||
|
//
|
||||||
|
// assert.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted")
|
||||||
|
func PanicsWithValuef(t TestingT, expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) {
|
||||||
|
if !assert.PanicsWithValuef(t, expected, f, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panicsf asserts that the code inside the specified PanicTestFunc panics.
|
||||||
|
//
|
||||||
|
// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted")
|
||||||
|
func Panicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) {
|
||||||
|
if !assert.Panicsf(t, f, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regexp asserts that a specified regexp matches a string.
|
||||||
|
//
|
||||||
|
// assert.Regexp(t, regexp.MustCompile("start"), "it's starting")
|
||||||
|
// assert.Regexp(t, "start...$", "it's not starting")
|
||||||
|
func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Regexp(t, rx, str, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regexpf asserts that a specified regexp matches a string.
|
||||||
|
//
|
||||||
|
// assert.Regexpf(t, regexp.MustCompile("start", "error message %s", "formatted"), "it's starting")
|
||||||
|
// assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted")
|
||||||
|
func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Regexpf(t, rx, str, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subset asserts that the specified list(array, slice...) contains all
|
||||||
|
// elements given in the specified subset(array, slice...).
|
||||||
|
//
|
||||||
|
// assert.Subset(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]")
|
||||||
|
func Subset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Subset(t, list, subset, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsetf asserts that the specified list(array, slice...) contains all
|
||||||
|
// elements given in the specified subset(array, slice...).
|
||||||
|
//
|
||||||
|
// assert.Subsetf(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted")
|
||||||
|
func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Subsetf(t, list, subset, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// True asserts that the specified value is true.
|
||||||
|
//
|
||||||
|
// assert.True(t, myBool)
|
||||||
|
func True(t TestingT, value bool, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.True(t, value, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truef asserts that the specified value is true.
|
||||||
|
//
|
||||||
|
// assert.Truef(t, myBool, "error message %s", "formatted")
|
||||||
|
func Truef(t TestingT, value bool, msg string, args ...interface{}) {
|
||||||
|
if !assert.Truef(t, value, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithinDuration asserts that the two times are within duration delta of each other.
|
||||||
|
//
|
||||||
|
// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second)
|
||||||
|
func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.WithinDuration(t, expected, actual, delta, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithinDurationf asserts that the two times are within duration delta of each other.
|
||||||
|
//
|
||||||
|
// assert.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted")
|
||||||
|
func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) {
|
||||||
|
if !assert.WithinDurationf(t, expected, actual, delta, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero asserts that i is the zero value for its type.
|
||||||
|
func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) {
|
||||||
|
if !assert.Zero(t, i, msgAndArgs...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zerof asserts that i is the zero value for its type.
|
||||||
|
func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) {
|
||||||
|
if !assert.Zerof(t, i, msg, args...) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{{.Comment}}
|
||||||
|
func {{.DocInfo.Name}}(t TestingT, {{.Params}}) {
|
||||||
|
if !assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,687 @@
|
||||||
|
/*
|
||||||
|
* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen
|
||||||
|
* THIS FILE MUST NOT BE EDITED BY HAND
|
||||||
|
*/
|
||||||
|
|
||||||
|
package require
|
||||||
|
|
||||||
|
import (
|
||||||
|
assert "github.com/stretchr/testify/assert"
|
||||||
|
http "net/http"
|
||||||
|
url "net/url"
|
||||||
|
time "time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Condition uses a Comparison to assert a complex condition.
|
||||||
|
func (a *Assertions) Condition(comp assert.Comparison, msgAndArgs ...interface{}) {
|
||||||
|
Condition(a.t, comp, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditionf uses a Comparison to assert a complex condition.
|
||||||
|
func (a *Assertions) Conditionf(comp assert.Comparison, msg string, args ...interface{}) {
|
||||||
|
Conditionf(a.t, comp, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains asserts that the specified string, list(array, slice...) or map contains the
|
||||||
|
// specified substring or element.
|
||||||
|
//
|
||||||
|
// a.Contains("Hello World", "World")
|
||||||
|
// a.Contains(["Hello", "World"], "World")
|
||||||
|
// a.Contains({"Hello": "World"}, "Hello")
|
||||||
|
func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Contains(a.t, s, contains, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containsf asserts that the specified string, list(array, slice...) or map contains the
|
||||||
|
// specified substring or element.
|
||||||
|
//
|
||||||
|
// a.Containsf("Hello World", "World", "error message %s", "formatted")
|
||||||
|
// a.Containsf(["Hello", "World"], "World", "error message %s", "formatted")
|
||||||
|
// a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) {
|
||||||
|
Containsf(a.t, s, contains, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists.
|
||||||
|
func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) {
|
||||||
|
DirExists(a.t, path, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists.
|
||||||
|
func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) {
|
||||||
|
DirExistsf(a.t, path, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified
|
||||||
|
// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements,
|
||||||
|
// the number of appearances of each of them in both lists should match.
|
||||||
|
//
|
||||||
|
// a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2])
|
||||||
|
func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) {
|
||||||
|
ElementsMatch(a.t, listA, listB, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified
|
||||||
|
// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements,
|
||||||
|
// the number of appearances of each of them in both lists should match.
|
||||||
|
//
|
||||||
|
// a.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted")
|
||||||
|
func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) {
|
||||||
|
ElementsMatchf(a.t, listA, listB, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either
|
||||||
|
// a slice or a channel with len == 0.
|
||||||
|
//
|
||||||
|
// a.Empty(obj)
|
||||||
|
func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Empty(a.t, object, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either
|
||||||
|
// a slice or a channel with len == 0.
|
||||||
|
//
|
||||||
|
// a.Emptyf(obj, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) {
|
||||||
|
Emptyf(a.t, object, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal asserts that two objects are equal.
|
||||||
|
//
|
||||||
|
// a.Equal(123, 123)
|
||||||
|
//
|
||||||
|
// Pointer variable equality is determined based on the equality of the
|
||||||
|
// referenced values (as opposed to the memory addresses). Function equality
|
||||||
|
// cannot be determined and will always fail.
|
||||||
|
func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Equal(a.t, expected, actual, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualError asserts that a function returned an error (i.e. not `nil`)
|
||||||
|
// and that it is equal to the provided error.
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// a.EqualError(err, expectedErrorString)
|
||||||
|
func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) {
|
||||||
|
EqualError(a.t, theError, errString, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualErrorf asserts that a function returned an error (i.e. not `nil`)
|
||||||
|
// and that it is equal to the provided error.
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) {
|
||||||
|
EqualErrorf(a.t, theError, errString, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualValues asserts that two objects are equal or convertable to the same types
|
||||||
|
// and equal.
|
||||||
|
//
|
||||||
|
// a.EqualValues(uint32(123), int32(123))
|
||||||
|
func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
|
||||||
|
EqualValues(a.t, expected, actual, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualValuesf asserts that two objects are equal or convertable to the same types
|
||||||
|
// and equal.
|
||||||
|
//
|
||||||
|
// a.EqualValuesf(uint32(123, "error message %s", "formatted"), int32(123))
|
||||||
|
func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) {
|
||||||
|
EqualValuesf(a.t, expected, actual, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equalf asserts that two objects are equal.
|
||||||
|
//
|
||||||
|
// a.Equalf(123, 123, "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Pointer variable equality is determined based on the equality of the
|
||||||
|
// referenced values (as opposed to the memory addresses). Function equality
|
||||||
|
// cannot be determined and will always fail.
|
||||||
|
func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) {
|
||||||
|
Equalf(a.t, expected, actual, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error asserts that a function returned an error (i.e. not `nil`).
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// if a.Error(err) {
|
||||||
|
// assert.Equal(t, expectedError, err)
|
||||||
|
// }
|
||||||
|
func (a *Assertions) Error(err error, msgAndArgs ...interface{}) {
|
||||||
|
Error(a.t, err, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf asserts that a function returned an error (i.e. not `nil`).
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// if a.Errorf(err, "error message %s", "formatted") {
|
||||||
|
// assert.Equal(t, expectedErrorf, err)
|
||||||
|
// }
|
||||||
|
func (a *Assertions) Errorf(err error, msg string, args ...interface{}) {
|
||||||
|
Errorf(a.t, err, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactly asserts that two objects are equal in value and type.
|
||||||
|
//
|
||||||
|
// a.Exactly(int32(123), int64(123))
|
||||||
|
func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Exactly(a.t, expected, actual, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactlyf asserts that two objects are equal in value and type.
|
||||||
|
//
|
||||||
|
// a.Exactlyf(int32(123, "error message %s", "formatted"), int64(123))
|
||||||
|
func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) {
|
||||||
|
Exactlyf(a.t, expected, actual, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail reports a failure through
|
||||||
|
func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) {
|
||||||
|
Fail(a.t, failureMessage, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailNow fails test
|
||||||
|
func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) {
|
||||||
|
FailNow(a.t, failureMessage, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailNowf fails test
|
||||||
|
func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) {
|
||||||
|
FailNowf(a.t, failureMessage, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failf reports a failure through
|
||||||
|
func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) {
|
||||||
|
Failf(a.t, failureMessage, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// False asserts that the specified value is false.
|
||||||
|
//
|
||||||
|
// a.False(myBool)
|
||||||
|
func (a *Assertions) False(value bool, msgAndArgs ...interface{}) {
|
||||||
|
False(a.t, value, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falsef asserts that the specified value is false.
|
||||||
|
//
|
||||||
|
// a.Falsef(myBool, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) {
|
||||||
|
Falsef(a.t, value, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file.
|
||||||
|
func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) {
|
||||||
|
FileExists(a.t, path, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file.
|
||||||
|
func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) {
|
||||||
|
FileExistsf(a.t, path, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBodyContains asserts that a specified handler returns a
|
||||||
|
// body that contains a string.
|
||||||
|
//
|
||||||
|
// a.HTTPBodyContains(myHandler, "www.google.com", nil, "I'm Feeling Lucky")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) {
|
||||||
|
HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBodyContainsf asserts that a specified handler returns a
|
||||||
|
// body that contains a string.
|
||||||
|
//
|
||||||
|
// a.HTTPBodyContainsf(myHandler, "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) {
|
||||||
|
HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBodyNotContains asserts that a specified handler returns a
|
||||||
|
// body that does not contain a string.
|
||||||
|
//
|
||||||
|
// a.HTTPBodyNotContains(myHandler, "www.google.com", nil, "I'm Feeling Lucky")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) {
|
||||||
|
HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBodyNotContainsf asserts that a specified handler returns a
|
||||||
|
// body that does not contain a string.
|
||||||
|
//
|
||||||
|
// a.HTTPBodyNotContainsf(myHandler, "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) {
|
||||||
|
HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPError asserts that a specified handler returns an error status code.
|
||||||
|
//
|
||||||
|
// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) {
|
||||||
|
HTTPError(a.t, handler, method, url, values, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPErrorf asserts that a specified handler returns an error status code.
|
||||||
|
//
|
||||||
|
// a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false).
|
||||||
|
func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) {
|
||||||
|
HTTPErrorf(a.t, handler, method, url, values, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPRedirect asserts that a specified handler returns a redirect status code.
|
||||||
|
//
|
||||||
|
// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) {
|
||||||
|
HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPRedirectf asserts that a specified handler returns a redirect status code.
|
||||||
|
//
|
||||||
|
// a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false).
|
||||||
|
func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) {
|
||||||
|
HTTPRedirectf(a.t, handler, method, url, values, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPSuccess asserts that a specified handler returns a success status code.
|
||||||
|
//
|
||||||
|
// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil)
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) {
|
||||||
|
HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPSuccessf asserts that a specified handler returns a success status code.
|
||||||
|
//
|
||||||
|
// a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
|
func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) {
|
||||||
|
HTTPSuccessf(a.t, handler, method, url, values, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements asserts that an object is implemented by the specified interface.
|
||||||
|
//
|
||||||
|
// a.Implements((*MyInterface)(nil), new(MyObject))
|
||||||
|
func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Implements(a.t, interfaceObject, object, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementsf asserts that an object is implemented by the specified interface.
|
||||||
|
//
|
||||||
|
// a.Implementsf((*MyInterface, "error message %s", "formatted")(nil), new(MyObject))
|
||||||
|
func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) {
|
||||||
|
Implementsf(a.t, interfaceObject, object, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDelta asserts that the two numerals are within delta of each other.
|
||||||
|
//
|
||||||
|
// a.InDelta(math.Pi, (22 / 7.0), 0.01)
|
||||||
|
func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
|
||||||
|
InDelta(a.t, expected, actual, delta, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys.
|
||||||
|
func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
|
||||||
|
InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys.
|
||||||
|
func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) {
|
||||||
|
InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaSlice is the same as InDelta, except it compares two slices.
|
||||||
|
func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
|
||||||
|
InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaSlicef is the same as InDelta, except it compares two slices.
|
||||||
|
func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) {
|
||||||
|
InDeltaSlicef(a.t, expected, actual, delta, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InDeltaf asserts that the two numerals are within delta of each other.
|
||||||
|
//
|
||||||
|
// a.InDeltaf(math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01)
|
||||||
|
func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) {
|
||||||
|
InDeltaf(a.t, expected, actual, delta, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InEpsilon asserts that expected and actual have a relative error less than epsilon
|
||||||
|
func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) {
|
||||||
|
InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices.
|
||||||
|
func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) {
|
||||||
|
InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices.
|
||||||
|
func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) {
|
||||||
|
InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InEpsilonf asserts that expected and actual have a relative error less than epsilon
|
||||||
|
func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) {
|
||||||
|
InEpsilonf(a.t, expected, actual, epsilon, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsType asserts that the specified objects are of the same type.
|
||||||
|
func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
IsType(a.t, expectedType, object, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTypef asserts that the specified objects are of the same type.
|
||||||
|
func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) {
|
||||||
|
IsTypef(a.t, expectedType, object, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONEq asserts that two JSON strings are equivalent.
|
||||||
|
//
|
||||||
|
// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)
|
||||||
|
func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) {
|
||||||
|
JSONEq(a.t, expected, actual, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONEqf asserts that two JSON strings are equivalent.
|
||||||
|
//
|
||||||
|
// a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) {
|
||||||
|
JSONEqf(a.t, expected, actual, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len asserts that the specified object has specific length.
|
||||||
|
// Len also fails if the object has a type that len() not accept.
|
||||||
|
//
|
||||||
|
// a.Len(mySlice, 3)
|
||||||
|
func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) {
|
||||||
|
Len(a.t, object, length, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lenf asserts that the specified object has specific length.
|
||||||
|
// Lenf also fails if the object has a type that len() not accept.
|
||||||
|
//
|
||||||
|
// a.Lenf(mySlice, 3, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) {
|
||||||
|
Lenf(a.t, object, length, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nil asserts that the specified object is nil.
|
||||||
|
//
|
||||||
|
// a.Nil(err)
|
||||||
|
func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Nil(a.t, object, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nilf asserts that the specified object is nil.
|
||||||
|
//
|
||||||
|
// a.Nilf(err, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) {
|
||||||
|
Nilf(a.t, object, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoError asserts that a function returned no error (i.e. `nil`).
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// if a.NoError(err) {
|
||||||
|
// assert.Equal(t, expectedObj, actualObj)
|
||||||
|
// }
|
||||||
|
func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) {
|
||||||
|
NoError(a.t, err, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoErrorf asserts that a function returned no error (i.e. `nil`).
|
||||||
|
//
|
||||||
|
// actualObj, err := SomeFunction()
|
||||||
|
// if a.NoErrorf(err, "error message %s", "formatted") {
|
||||||
|
// assert.Equal(t, expectedObj, actualObj)
|
||||||
|
// }
|
||||||
|
func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) {
|
||||||
|
NoErrorf(a.t, err, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the
|
||||||
|
// specified substring or element.
|
||||||
|
//
|
||||||
|
// a.NotContains("Hello World", "Earth")
|
||||||
|
// a.NotContains(["Hello", "World"], "Earth")
|
||||||
|
// a.NotContains({"Hello": "World"}, "Earth")
|
||||||
|
func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) {
|
||||||
|
NotContains(a.t, s, contains, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the
|
||||||
|
// specified substring or element.
|
||||||
|
//
|
||||||
|
// a.NotContainsf("Hello World", "Earth", "error message %s", "formatted")
|
||||||
|
// a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted")
|
||||||
|
// a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted")
|
||||||
|
func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) {
|
||||||
|
NotContainsf(a.t, s, contains, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
|
||||||
|
// a slice or a channel with len == 0.
|
||||||
|
//
|
||||||
|
// if a.NotEmpty(obj) {
|
||||||
|
// assert.Equal(t, "two", obj[1])
|
||||||
|
// }
|
||||||
|
func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
NotEmpty(a.t, object, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
|
||||||
|
// a slice or a channel with len == 0.
|
||||||
|
//
|
||||||
|
// if a.NotEmptyf(obj, "error message %s", "formatted") {
|
||||||
|
// assert.Equal(t, "two", obj[1])
|
||||||
|
// }
|
||||||
|
func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) {
|
||||||
|
NotEmptyf(a.t, object, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqual asserts that the specified values are NOT equal.
|
||||||
|
//
|
||||||
|
// a.NotEqual(obj1, obj2)
|
||||||
|
//
|
||||||
|
// Pointer variable equality is determined based on the equality of the
|
||||||
|
// referenced values (as opposed to the memory addresses).
|
||||||
|
func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
|
||||||
|
NotEqual(a.t, expected, actual, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqualf asserts that the specified values are NOT equal.
|
||||||
|
//
|
||||||
|
// a.NotEqualf(obj1, obj2, "error message %s", "formatted")
|
||||||
|
//
|
||||||
|
// Pointer variable equality is determined based on the equality of the
|
||||||
|
// referenced values (as opposed to the memory addresses).
|
||||||
|
func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) {
|
||||||
|
NotEqualf(a.t, expected, actual, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotNil asserts that the specified object is not nil.
|
||||||
|
//
|
||||||
|
// a.NotNil(err)
|
||||||
|
func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) {
|
||||||
|
NotNil(a.t, object, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotNilf asserts that the specified object is not nil.
|
||||||
|
//
|
||||||
|
// a.NotNilf(err, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) {
|
||||||
|
NotNilf(a.t, object, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic.
|
||||||
|
//
|
||||||
|
// a.NotPanics(func(){ RemainCalm() })
|
||||||
|
func (a *Assertions) NotPanics(f assert.PanicTestFunc, msgAndArgs ...interface{}) {
|
||||||
|
NotPanics(a.t, f, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic.
|
||||||
|
//
|
||||||
|
// a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) NotPanicsf(f assert.PanicTestFunc, msg string, args ...interface{}) {
|
||||||
|
NotPanicsf(a.t, f, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotRegexp asserts that a specified regexp does not match a string.
|
||||||
|
//
|
||||||
|
// a.NotRegexp(regexp.MustCompile("starts"), "it's starting")
|
||||||
|
// a.NotRegexp("^start", "it's not starting")
|
||||||
|
func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) {
|
||||||
|
NotRegexp(a.t, rx, str, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotRegexpf asserts that a specified regexp does not match a string.
|
||||||
|
//
|
||||||
|
// a.NotRegexpf(regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting")
|
||||||
|
// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted")
|
||||||
|
func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) {
|
||||||
|
NotRegexpf(a.t, rx, str, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotSubset asserts that the specified list(array, slice...) contains not all
|
||||||
|
// elements given in the specified subset(array, slice...).
|
||||||
|
//
|
||||||
|
// a.NotSubset([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]")
|
||||||
|
func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) {
|
||||||
|
NotSubset(a.t, list, subset, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotSubsetf asserts that the specified list(array, slice...) contains not all
|
||||||
|
// elements given in the specified subset(array, slice...).
|
||||||
|
//
|
||||||
|
// a.NotSubsetf([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted")
|
||||||
|
func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) {
|
||||||
|
NotSubsetf(a.t, list, subset, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotZero asserts that i is not the zero value for its type.
|
||||||
|
func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) {
|
||||||
|
NotZero(a.t, i, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotZerof asserts that i is not the zero value for its type.
|
||||||
|
func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) {
|
||||||
|
NotZerof(a.t, i, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panics asserts that the code inside the specified PanicTestFunc panics.
|
||||||
|
//
|
||||||
|
// a.Panics(func(){ GoCrazy() })
|
||||||
|
func (a *Assertions) Panics(f assert.PanicTestFunc, msgAndArgs ...interface{}) {
|
||||||
|
Panics(a.t, f, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that
|
||||||
|
// the recovered panic value equals the expected panic value.
|
||||||
|
//
|
||||||
|
// a.PanicsWithValue("crazy error", func(){ GoCrazy() })
|
||||||
|
func (a *Assertions) PanicsWithValue(expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) {
|
||||||
|
PanicsWithValue(a.t, expected, f, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that
|
||||||
|
// the recovered panic value equals the expected panic value.
|
||||||
|
//
|
||||||
|
// a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) PanicsWithValuef(expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) {
|
||||||
|
PanicsWithValuef(a.t, expected, f, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panicsf asserts that the code inside the specified PanicTestFunc panics.
|
||||||
|
//
|
||||||
|
// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Panicsf(f assert.PanicTestFunc, msg string, args ...interface{}) {
|
||||||
|
Panicsf(a.t, f, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regexp asserts that a specified regexp matches a string.
|
||||||
|
//
|
||||||
|
// a.Regexp(regexp.MustCompile("start"), "it's starting")
|
||||||
|
// a.Regexp("start...$", "it's not starting")
|
||||||
|
func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Regexp(a.t, rx, str, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regexpf asserts that a specified regexp matches a string.
|
||||||
|
//
|
||||||
|
// a.Regexpf(regexp.MustCompile("start", "error message %s", "formatted"), "it's starting")
|
||||||
|
// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) {
|
||||||
|
Regexpf(a.t, rx, str, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subset asserts that the specified list(array, slice...) contains all
|
||||||
|
// elements given in the specified subset(array, slice...).
|
||||||
|
//
|
||||||
|
// a.Subset([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]")
|
||||||
|
func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Subset(a.t, list, subset, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsetf asserts that the specified list(array, slice...) contains all
|
||||||
|
// elements given in the specified subset(array, slice...).
|
||||||
|
//
|
||||||
|
// a.Subsetf([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) {
|
||||||
|
Subsetf(a.t, list, subset, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// True asserts that the specified value is true.
|
||||||
|
//
|
||||||
|
// a.True(myBool)
|
||||||
|
func (a *Assertions) True(value bool, msgAndArgs ...interface{}) {
|
||||||
|
True(a.t, value, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truef asserts that the specified value is true.
|
||||||
|
//
|
||||||
|
// a.Truef(myBool, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) Truef(value bool, msg string, args ...interface{}) {
|
||||||
|
Truef(a.t, value, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithinDuration asserts that the two times are within duration delta of each other.
|
||||||
|
//
|
||||||
|
// a.WithinDuration(time.Now(), time.Now(), 10*time.Second)
|
||||||
|
func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) {
|
||||||
|
WithinDuration(a.t, expected, actual, delta, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithinDurationf asserts that the two times are within duration delta of each other.
|
||||||
|
//
|
||||||
|
// a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted")
|
||||||
|
func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) {
|
||||||
|
WithinDurationf(a.t, expected, actual, delta, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero asserts that i is the zero value for its type.
|
||||||
|
func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) {
|
||||||
|
Zero(a.t, i, msgAndArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zerof asserts that i is the zero value for its type.
|
||||||
|
func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) {
|
||||||
|
Zerof(a.t, i, msg, args...)
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{{.CommentWithoutT "a"}}
|
||||||
|
func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) {
|
||||||
|
{{.DocInfo.Name}}(a.t, {{.ForwardedParams}})
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package require
|
||||||
|
|
||||||
|
// TestingT is an interface wrapper around *testing.T
|
||||||
|
type TestingT interface {
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate go run ../_codegen/main.go -output-package=require -template=require.go.tmpl -include-format-funcs
|
Loading…
Reference in New Issue