diff --git a/.gitignore b/.gitignore index b47a4e92d..eecd054bd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ tests/db.bolt test.sock tests/redis.conf tests/*.csr +*.orig +debug *.iml config.yml diff --git a/.travis.yml b/.travis.yml index d24b9c9f6..a198ce270 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ services: - docker language: go go: -- 1.7 - 1.8 install: - go get github.com/tools/godep diff --git a/AUTHORS b/AUTHORS index 0cc4d58cf..eac1818fd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,7 +1,9 @@ Allan Degnan Ben Marvell Chris Nesbitt-Smith +Jiten Bhagat johanneslanger +Naveen Rémi Vion Rohith Rohith Jayawardene diff --git a/CHANGELOG.md b/CHANGELOG.md index 76eca2ce7..4df74c296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +#### **2.1.0** + +FIXES: +* fixed the parsing of slices for command line arguments (i.e. --cors-origins etc) +* fixed any accidental proxying on the /oauth or /debug URI +* removed all references to the underlining web framework in tests + +FEATURES +* changed the routing engine from gin to echo +* we now normalize all inbound URI before applying the protection middleware +* the order of the resources are no longer important, the framework will handle the routing +* improved the overall spec of the proxy by removing URL inspection and prefix checking +* removed the CORS implementation and using the default echo middles, which is more compliant + +BREAKING CHANGES: +* the proxy no longer uses prefixes for resources, if you wish to use wildcard urls you need + to specify it, i.e. --resource=/ becomes --resource=/* or =admin/ becomes =admin/* or /admin*; + a full set of routing details can bt found at https://echo.labstack.com/guide/routing +* removed the --enable-cors-global option, CORS is now handled the default echo middleware +* changed option from log-requests -> enable-logging +* changed option from json-format -> enable-json-logging #### **2.0.5 (unreleased)** diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index e15e5c423..dbea0b349 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -69,6 +69,11 @@ "ImportPath": "github.com/davecgh/go-spew/spew", "Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d" }, + { + "ImportPath": "github.com/dgrijalva/jwt-go", + "Comment": "v3.0.0-17-g2268707", + "Rev": "2268707a8f0843315e2004ee4f1d021dc08baedf" + }, { "ImportPath": "github.com/fsnotify/fsnotify", "Comment": "v1.4.2-2-gfd9ec7d", @@ -79,21 +84,6 @@ "Comment": "v1.0-87-gc3b6ff1", "Rev": "c3b6ff1178d68ec9e93f7c996f41a3df89931d9f" }, - { - "ImportPath": "github.com/gin-gonic/gin", - "Comment": "v1.1.4", - "Rev": "e2212d40c62a98b388a5eb48ecbdcf88534688ba" - }, - { - "ImportPath": "github.com/gin-gonic/gin/binding", - "Comment": "v1.1.4", - "Rev": "e2212d40c62a98b388a5eb48ecbdcf88534688ba" - }, - { - "ImportPath": "github.com/gin-gonic/gin/render", - "Comment": "v1.1.4", - "Rev": "e2212d40c62a98b388a5eb48ecbdcf88534688ba" - }, { "ImportPath": "github.com/go-resty/resty", "Comment": "v0.7-5-g39c3db9", @@ -108,8 +98,38 @@ "Rev": "ed104f61ea4877bea08af6f759805674861e968d" }, { - "ImportPath": "github.com/manucorporat/sse", - "Rev": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d" + "ImportPath": "github.com/labstack/echo", + "Comment": "v3.1.0-rc.1-23-geac431d", + "Rev": "eac431df0dbad8ba6cc313fba37f1d4275c317e8" + }, + { + "ImportPath": "github.com/labstack/echo/middleware", + "Comment": "v3.1.0-rc.1-23-geac431d", + "Rev": "eac431df0dbad8ba6cc313fba37f1d4275c317e8" + }, + { + "ImportPath": "github.com/labstack/gommon/bytes", + "Comment": "v0.1.0-13-ge8995fb", + "Rev": "e8995fb26e646187d33cff439b18609cfba23088" + }, + { + "ImportPath": "github.com/labstack/gommon/color", + "Comment": "v0.1.0-13-ge8995fb", + "Rev": "e8995fb26e646187d33cff439b18609cfba23088" + }, + { + "ImportPath": "github.com/labstack/gommon/log", + "Comment": "v0.1.0-13-ge8995fb", + "Rev": "e8995fb26e646187d33cff439b18609cfba23088" + }, + { + "ImportPath": "github.com/labstack/gommon/random", + "Comment": "v0.1.0-13-ge8995fb", + "Rev": "e8995fb26e646187d33cff439b18609cfba23088" + }, + { + "ImportPath": "github.com/mattn/go-colorable", + "Rev": "9cbef7c35391cca05f15f8181dc0b18bc9736dbb" }, { "ImportPath": "github.com/mattn/go-isatty", @@ -122,6 +142,7 @@ }, { "ImportPath": "github.com/pmezard/go-difflib/difflib", + "Comment": "v1.0.0", "Rev": "792786c7400a136282c1664665ae0a8db921c6c2" }, { @@ -164,10 +185,30 @@ "Comment": "v1.19.1", "Rev": "0bdeddeeb0f650497d603c4ad7b20cfe685682f6" }, + { + "ImportPath": "github.com/valyala/bytebufferpool", + "Rev": "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7" + }, + { + "ImportPath": "github.com/valyala/fasttemplate", + "Rev": "dcecefd839c4193db0d35b88ec65b4c12d360ab0" + }, + { + "ImportPath": "golang.org/x/crypto/acme", + "Rev": "453249f01cfeb54c3d549ddb75ff152ca243f9d8" + }, + { + "ImportPath": "golang.org/x/crypto/acme/autocert", + "Rev": "453249f01cfeb54c3d549ddb75ff152ca243f9d8" + }, { "ImportPath": "golang.org/x/net/context", "Rev": "bc3663df0ac92f928d419e31e0d2af22e683a5a2" }, + { + "ImportPath": "golang.org/x/net/context/ctxhttp", + "Rev": "bc3663df0ac92f928d419e31e0d2af22e683a5a2" + }, { "ImportPath": "golang.org/x/net/idna", "Rev": "bc3663df0ac92f928d419e31e0d2af22e683a5a2" @@ -196,11 +237,6 @@ "ImportPath": "gopkg.in/bsm/ratelimit.v1", "Rev": "db14e161995a5177acef654cb0dd785e8ee8bc22" }, - { - "ImportPath": "gopkg.in/go-playground/validator.v8", - "Comment": "v8.17.1", - "Rev": "014792cf3e266caff1e916876be12282b33059e0" - }, { "ImportPath": "gopkg.in/redis.v4", "Comment": "v3.6.1-18-g889409d", diff --git a/Makefile b/Makefile index c9b0dfe5d..c02bc1ada 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME=keycloak-proxy AUTHOR=gambol99 AUTHOR_EMAIL=gambol99@gmail.com REGISTRY=quay.io -GOVERSION ?= 1.7.3 +GOVERSION ?= 1.8.0 SUDO= ROOT_DIR=${PWD} HARDWARE=$(shell uname -m) diff --git a/README.md b/README.md index 7407b3f25..961f0372d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Keycloak-proxy is a proxy service which at the risk of stating the obvious integrates with the [Keycloak](https://github.com/keycloak/keycloak) authentication service. Although technically the service has no dependency on Keycloak itself and would quite happily work with any OpenID provider. The service supports both access tokens in browser cookie or bearer tokens. ```shell -[jest@starfury keycloak-proxy]$ bin/keycloak-proxy help +[jest@starfury keycloak-proxy]$ bin/keycloak-proxy --help NAME: keycloak-proxy - is a proxy using the keycloak service for auth and authorization @@ -32,7 +32,7 @@ USAGE: keycloak-proxy [options] VERSION: - v2.0.2 (git+sha: 260d2c8-dirty) + v2.1.0 (git+sha: f74c713) AUTHOR: Rohith @@ -47,14 +47,15 @@ GLOBAL OPTIONS: --discovery-url value discovery url to retrieve the openid configuration [$PROXY_DISCOVERY_URL] --client-id value client id used to authenticate to the oauth service [$PROXY_CLIENT_ID] --client-secret value client secret used to authenticate to the oauth service [$PROXY_CLIENT_SECRET] - --redirection-url value redirection url for the oauth callback url [$PROXY_REDIRECTION_URL] + --redirection-url value redirection url for the oauth callback url, defaults to host header is absent [$PROXY_REDIRECTION_URL] --revocation-url value url for the revocation endpoint to revoke refresh token [$PROXY_REVOCATION_URL] --skip-openid-provider-tls-verify skip the verification of any TLS communication with the openid provider (default: false) --scopes value list of scopes requested when authenticating the user --upstream-url value url for the upstream endpoint you wish to proxy [$PROXY_UPSTREAM_URL] --resources value list of resources 'uri=/admin|methods=GET,PUT|roles=role1,role2' --headers value custom headers to the upstream request, key=value - --enable-cors-global inject the CORs headers into all responses (default: false) [$PROXY_ENABLE_CORS_GLOBAL] + --enable-logging enable http logging of the requests (default: false) + --enable-json-logging switch on json logging rather than text (default: false) --enable-forwarding enables the forwarding proxy mode, signing outbound request (default: false) --enable-security-filter enables the security filter handler (default: false) --enable-refresh-tokens nables the handling of the refresh tokens (default: false) [$PROXY_ENABLE_SECURITY_FILTER] @@ -68,6 +69,7 @@ GLOBAL OPTIONS: --filter-frame-deny enable to the frame deny header (default: false) --content-security-policy value specify the content security policy --localhost-metrics enforces the metrics page can only been requested from 127.0.0.1 (default: false) + --access-token-duration value fallback cookie duration for the access token when using refresh tokens (default: 720h0m0s) --cookie-domain value domain the access cookie is available to, defaults host header --cookie-access-name value name of the cookie use to hold the access token (default: "kc-access") --cookie-refresh-name value name of the cookie used to hold the encrypted refresh token (default: "kc-state") @@ -89,9 +91,7 @@ GLOBAL OPTIONS: --cors-max-age value max age applied to cors headers (Access-Control-Max-Age) (default: 0s) --hostnames value list of hostnames the service will respond to --store-url value url for the storage subsystem, e.g redis://127.0.0.1:6379, file:///etc/tokens.file - --encryption-key value encryption key used to encrpytion the session state - --log-requests enable http logging of the requests (default: false) - --json-format switch on json logging rather than text (default: false) + --encryption-key value encryption key used to encryption the session state [$PROXY_ENCRYPTION_KEY] --no-redirects do not have back redirects when no authentication is present, 401 them (default: false) --skip-token-verification TESTING ONLY; bypass token verification, only expiration and roles enforced (default: false) --upstream-keepalives enables or disables the keepalive connections for upstream endpoint (default: false) @@ -122,7 +122,7 @@ Docker image is available at [https://quay.io/repository/gambol99/keycloak-proxy Configuration can come from a yaml/json file and or the command line options (note, command options have a higher priority and will override or merge any options referenced in a config file) ```YAML -# is the url for retrieve the openid configuration - normally the /auth/realm/ +# is the url for retrieve the OpenID configuration - normally the /auth/realm/ discovery-url: https://keycloak.example.com/auth/realms/ # the client id for the 'client' application client-id: @@ -158,7 +158,7 @@ resources: - openvpn:vpn-user - openvpn:prod-vpn - test -- uri: /admin +- uri: /admin/* methods: - GET roles: @@ -188,13 +188,13 @@ redirection-url: http://127.0.0.1:3000 encryption_key: AgXa7xRcoClDEU0ZDSH4X0XhL5Qy2Z2j upstream-url: http://127.0.0.1:80 resources: -- uri: /admin +- uri: /admin* methods: - GET roles: - client:test1 - client:test2 -- uri: /backend +- uri: /backend* roles: - client:test1 ``` @@ -211,10 +211,20 @@ bin/keycloak-proxy \ --enable-refresh-token=true \ --encryption-key=AgXa7xRcoClDEU0ZDSH4X0XhL5Qy2Z2j \ --upstream-url=http://127.0.0.1:80 \ - --resources="uri=/admin|methods=GET|roles=test1,test2" \ - --resources="uri=/backend|roles=test1" + --resources="uri=/admin*|methods=GET|roles=test1,test2" \ + --resources="uri=/backend*|roles=test1" ``` +#### **HTTP Routing** + +By default all requests will be proxyed on to the upstream, if you wish to ensure all requests are authentication you can use + +```shell +--resource=uri=/* +``` + +Note the HTTP routing rules following the guidelines from [echo](https://echo.labstack.com/guide/routing). Its also worth nothing the ordering of the resource do not matter, the router will handle that for you. + #### **Google OAuth** Although the role extensions do require a Keycloak IDP or at the very least a IDP that produces a token which contains roles, there's nothing stopping you from using it against any OpenID providers, such as Google. Go to the Google Developers Console and create a new application *(via "Enable and Manage APIs -> Credentials)*. Once you've created the application, take the client id, secret and make sure you've added the callback url to the application scope *(using the default this would be http://127.0.0.1:3000/oauth/callback)* @@ -223,7 +233,7 @@ bin/keycloak-proxy \ --discovery-url=https://accounts.google.com/.well-known/openid-configuration \ --client-id= \ --client-secret= \ - --resources="uri=/" \ + --resources="uri=/*" \ --verbose=true ``` @@ -403,13 +413,13 @@ By default the proxy will immediately redirect you for authentication and hand b #### **White-listed URL's** -Depending on how the application url's are laid out, you might want protect the root / url but have exceptions on a list of paths, i.e. /health etc. Although you should probably fix this by fixing up the paths, you can add excepts to the protected resources. (Note: it's an array, so the order is important) +Depending on how the application url's are laid out, you might want protect the root / url but have exceptions on a list of paths, i.e. /health etc. Although you should probably fix this by fixing up the paths, you can add excepts to the protected resources. ```YAML resources: - url: /some_white_listed_url white-listed: true - - url: / + - url: /* methods: - GET roles: @@ -421,7 +431,7 @@ Or on the command line ```shell --resources "uri=/some_white_listed_url|white-listed=true" - --resources "uri=/" # requires authentication on the rest + --resources "uri=/*" # requires authentication on the rest --resources "uri=/admin|roles=admin,superuser|methods=POST,DELETE ``` @@ -445,7 +455,7 @@ A /oauth/logout?redirect=url is provided as a helper to logout the users. Aside #### **Cross Origin Resource Sharing (CORS)** -You can add CORS header via the --cors-[method] command line or configuration options. By default this will inject CORS header into all response from the /oauth/* and any authentication required redirects, though you can enable these globally for all responses via the --enable-cors-global option. +You can add CORS header via the --cors-[method] command line or configuration options. * Access-Control-Allow-Origin * Access-Control-Allow-Methods @@ -479,8 +489,8 @@ You can control the upstream endpoint via the --upstream-url option. Both http a #### **Endpoints** -* **/oauth/authorize** is authentication endpoint which will generate the openid redirect to the provider -* **/oauth/callback** is provider openid callback endpoint +* **/oauth/authorize** is authentication endpoint which will generate the OpenID redirect to the provider +* **/oauth/callback** is provider OpenID callback endpoint * **/oauth/expired** is a helper endpoint to check if a access token has expired, 200 for ok and, 401 for no token and 401 for expired * **/oauth/health** is the health checking endpoint for the proxy, you can also grab version from headers * **/oauth/login** provides a relay endpoint to login via grant_type=password i.e. POST /oauth/login form values are username=USERNAME&password=PASSWORD (must be enabled) diff --git a/cli.go b/cli.go index 330c77bae..b7743134a 100644 --- a/cli.go +++ b/cli.go @@ -169,9 +169,7 @@ func parseCLIOptions(cx *cli.Context, config *Config) (err error) { case reflect.String: reflect.ValueOf(config).Elem().FieldByName(field.Name).SetString(cx.String(name)) case reflect.Slice: - for _, x := range cx.StringSlice(name) { - reflect.Append(reflect.ValueOf(config).Elem().FieldByName(field.Name), reflect.ValueOf(x)) - } + reflect.ValueOf(config).Elem().FieldByName(field.Name).Set(reflect.ValueOf(cx.StringSlice(name))) case reflect.Int64: switch field.Type.String() { case "time.Duration": diff --git a/cli_test.go b/cli_test.go index 309c0d233..6a8b0037a 100644 --- a/cli_test.go +++ b/cli_test.go @@ -18,9 +18,15 @@ package main import ( "testing" + "github.com/stretchr/testify/assert" "github.com/urfave/cli" ) +func TestNewOauthProxyApp(t *testing.T) { + a := newOauthProxyApp() + assert.NotNil(t, a) +} + func TestGetCLIOptions(t *testing.T) { if flags := getCommandLineOptions(); flags == nil { t.Error("we should have received some flags options") diff --git a/config_sample.yml b/config_sample.yml index 7655b7c71..5e9fe5a37 100644 --- a/config_sample.yml +++ b/config_sample.yml @@ -11,9 +11,9 @@ listen: 127.0.0.1:3000 # whether to request offline access and use a refresh token enable-refresh-tokens: true # log all incoming requests -log-requests: true +enable-logging: true # log in json format -json-format: true +enable-json-logging: true # do not redirec the request, simple 307 it no-redirects: false # the location of a certificate you wish the proxy to use for TLS support @@ -40,7 +40,7 @@ skip-upstream-tls-verify: true|false scopes: [] # enables a more extra secuirty features enable-security-filter: true -# headers permits you to inject custom headers into all request +# headers permits you to inject custom headers into all requests headers: myheader_name: my_header_value # a map of claims that MUST exist in the token presented and the value is it MUST match @@ -61,20 +61,17 @@ resources: - GET # a list of roles the user must have in order to accces urls under the above roles: - - openvpn:vpn-user - - openvpn:prod-vpn + - openvpn:vpn-test - uri: /admin/white_listed # permits a url prefix through, bypassing the admission controls white-listed: true - - uri: /admin + - uri: /admin/* methods: - GET roles: - openvpn:vpn-user - openvpn:prod-vpn -# indicates you want cors headers injects for ALL responses -enable-cors-global: false # an array of origins (Access-Control-Allow-Origin) cors-origins: [] # an array of headers to apply (Access-Control-Allow-Headers) diff --git a/cookies.go b/cookies.go index f342c6ebf..06d902c8c 100644 --- a/cookies.go +++ b/cookies.go @@ -19,14 +19,12 @@ import ( "net/http" "strings" "time" - - "github.com/gin-gonic/gin" ) // dropCookie drops a cookie into the response -func (r *oauthProxy) dropCookie(cx *gin.Context, name, value string, duration time.Duration) { +func (r *oauthProxy) dropCookie(w http.ResponseWriter, host, name, value string, duration time.Duration) { // step: default to the host header, else the config domain - domain := strings.Split(cx.Request.Host, ":")[0] + domain := strings.Split(host, ":")[0] if r.config.CookieDomain != "" { domain = r.config.CookieDomain } @@ -42,31 +40,31 @@ func (r *oauthProxy) dropCookie(cx *gin.Context, name, value string, duration ti cookie.Expires = time.Now().Add(duration) } - http.SetCookie(cx.Writer, cookie) + http.SetCookie(w, cookie) } // dropAccessTokenCookie drops a access token cookie into the response -func (r *oauthProxy) dropAccessTokenCookie(cx *gin.Context, value string, duration time.Duration) { - r.dropCookie(cx, r.config.CookieAccessName, value, duration) +func (r *oauthProxy) dropAccessTokenCookie(req *http.Request, w http.ResponseWriter, value string, duration time.Duration) { + r.dropCookie(w, req.Host, r.config.CookieAccessName, value, duration) } // dropRefreshTokenCookie drops a refresh token cookie into the response -func (r *oauthProxy) dropRefreshTokenCookie(cx *gin.Context, value string, duration time.Duration) { - r.dropCookie(cx, r.config.CookieRefreshName, value, duration) +func (r *oauthProxy) dropRefreshTokenCookie(req *http.Request, w http.ResponseWriter, value string, duration time.Duration) { + r.dropCookie(w, req.Host, r.config.CookieRefreshName, value, duration) } // clearAllCookies is just a helper function for the below -func (r *oauthProxy) clearAllCookies(cx *gin.Context) { - r.clearAccessTokenCookie(cx) - r.clearRefreshTokenCookie(cx) +func (r *oauthProxy) clearAllCookies(req *http.Request, w http.ResponseWriter) { + r.clearAccessTokenCookie(req, w) + r.clearRefreshTokenCookie(req, w) } // clearRefreshSessionCookie clears the session cookie -func (r *oauthProxy) clearRefreshTokenCookie(cx *gin.Context) { - r.dropCookie(cx, r.config.CookieRefreshName, "", time.Duration(-10*time.Hour)) +func (r *oauthProxy) clearRefreshTokenCookie(req *http.Request, w http.ResponseWriter) { + r.dropCookie(w, req.Host, r.config.CookieRefreshName, "", time.Duration(-10*time.Hour)) } // clearAccessTokenCookie clears the session cookie -func (r *oauthProxy) clearAccessTokenCookie(cx *gin.Context) { - r.dropCookie(cx, r.config.CookieAccessName, "", time.Duration(-10*time.Hour)) +func (r *oauthProxy) clearAccessTokenCookie(req *http.Request, w http.ResponseWriter) { + r.dropCookie(w, req.Host, r.config.CookieAccessName, "", time.Duration(-10*time.Hour)) } diff --git a/cookies_test.go b/cookies_test.go index b6556a924..343f811b9 100644 --- a/cookies_test.go +++ b/cookies_test.go @@ -17,6 +17,7 @@ package main import ( "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/assert" @@ -58,78 +59,99 @@ func TestCookieDomain(t *testing.T) { func TestDropCookie(t *testing.T) { p, _, _ := newTestProxyService(nil) - context := newFakeGinContext("GET", "/admin") - p.dropCookie(context, "test-cookie", "test-value", 0) + req := newFakeHTTPRequest("GET", "/admin") + resp := httptest.NewRecorder() + p.dropCookie(resp, req.Host, "test-cookie", "test-value", 0) - assert.Equal(t, context.Writer.Header().Get("Set-Cookie"), + assert.Equal(t, resp.Header().Get("Set-Cookie"), "test-cookie=test-value; Path=/; Domain=127.0.0.1", - "we have not set the cookie, headers: %v", context.Writer.Header()) + "we have not set the cookie, headers: %v", resp.Header()) - context = newFakeGinContext("GET", "/admin") + req = newFakeHTTPRequest("GET", "/admin") + resp = httptest.NewRecorder() p.config.SecureCookie = false - p.dropCookie(context, "test-cookie", "test-value", 0) + p.dropCookie(resp, req.Host, "test-cookie", "test-value", 0) - assert.Equal(t, context.Writer.Header().Get("Set-Cookie"), + assert.Equal(t, resp.Header().Get("Set-Cookie"), "test-cookie=test-value; Path=/; Domain=127.0.0.1", - "we have not set the cookie, headers: %v", context.Writer.Header()) + "we have not set the cookie, headers: %v", resp.Header()) - context = newFakeGinContext("GET", "/admin") + req = newFakeHTTPRequest("GET", "/admin") + resp = httptest.NewRecorder() p.config.SecureCookie = true - p.dropCookie(context, "test-cookie", "test-value", 0) - assert.NotEqual(t, context.Writer.Header().Get("Set-Cookie"), + p.dropCookie(resp, req.Host, "test-cookie", "test-value", 0) + assert.NotEqual(t, resp.Header().Get("Set-Cookie"), "test-cookie=test-value; Path=/; Domain=127.0.0.2; HttpOnly; Secure", - "we have not set the cookie, headers: %v", context.Writer.Header()) + "we have not set the cookie, headers: %v", resp.Header()) p.config.CookieDomain = "test.com" - p.dropCookie(context, "test-cookie", "test-value", 0) + p.dropCookie(resp, req.Host, "test-cookie", "test-value", 0) p.config.SecureCookie = false - assert.NotEqual(t, context.Writer.Header().Get("Set-Cookie"), + assert.NotEqual(t, resp.Header().Get("Set-Cookie"), "test-cookie=test-value; Path=/; Domain=test.com;", - "we have not set the cookie, headers: %v", context.Writer.Header()) + "we have not set the cookie, headers: %v", resp.Header()) +} + +func TestDropRefreshCookie(t *testing.T) { + p, _, _ := newTestProxyService(nil) + + req := newFakeHTTPRequest("GET", "/admin") + resp := httptest.NewRecorder() + p.dropRefreshTokenCookie(req, resp, "test", 0) + + assert.Equal(t, resp.Header().Get("Set-Cookie"), + "kc-state=test; Path=/; Domain=127.0.0.1", + "we have not set the cookie, headers: %v", resp.Header()) } func TestHTTPOnlyCookie(t *testing.T) { p, _, _ := newTestProxyService(nil) - context := newFakeGinContext("GET", "/admin") - p.dropCookie(context, "test-cookie", "test-value", 0) + req := newFakeHTTPRequest("GET", "/admin") + resp := httptest.NewRecorder() + p.dropCookie(resp, req.Host, "test-cookie", "test-value", 0) - assert.Equal(t, context.Writer.Header().Get("Set-Cookie"), + assert.Equal(t, resp.Header().Get("Set-Cookie"), "test-cookie=test-value; Path=/; Domain=127.0.0.1", - "we have not set the cookie, headers: %v", context.Writer.Header()) + "we have not set the cookie, headers: %v", resp.Header()) - context = newFakeGinContext("GET", "/admin") + req = newFakeHTTPRequest("GET", "/admin") + resp = httptest.NewRecorder() p.config.HTTPOnlyCookie = true - p.dropCookie(context, "test-cookie", "test-value", 0) + p.dropCookie(resp, req.Host, "test-cookie", "test-value", 0) - assert.Equal(t, context.Writer.Header().Get("Set-Cookie"), + assert.Equal(t, resp.Header().Get("Set-Cookie"), "test-cookie=test-value; Path=/; Domain=127.0.0.1; HttpOnly", - "we have not set the cookie, headers: %v", context.Writer.Header()) + "we have not set the cookie, headers: %v", resp.Header()) } func TestClearAccessTokenCookie(t *testing.T) { p, _, _ := newTestProxyService(nil) - context := newFakeGinContext("GET", "/admin") - p.clearAccessTokenCookie(context) - assert.Contains(t, context.Writer.Header().Get("Set-Cookie"), + + req := newFakeHTTPRequest("GET", "/admin") + resp := httptest.NewRecorder() + p.clearAccessTokenCookie(req, resp) + assert.Contains(t, resp.Header().Get("Set-Cookie"), "kc-access=; Path=/; Domain=127.0.0.1; Expires=", - "we have not cleared the, headers: %v", context.Writer.Header()) + "we have not cleared the, headers: %v", resp.Header()) } func TestClearRefreshAccessTokenCookie(t *testing.T) { p, _, _ := newTestProxyService(nil) - context := newFakeGinContext("GET", "/admin") - p.clearRefreshTokenCookie(context) - assert.Contains(t, context.Writer.Header().Get("Set-Cookie"), + req := newFakeHTTPRequest("GET", "/admin") + resp := httptest.NewRecorder() + p.clearRefreshTokenCookie(req, resp) + assert.Contains(t, resp.Header().Get("Set-Cookie"), "kc-state=; Path=/; Domain=127.0.0.1; Expires=", - "we have not cleared the, headers: %v", context.Writer.Header()) + "we have not cleared the, headers: %v", resp.Header()) } func TestClearAllCookies(t *testing.T) { p, _, _ := newTestProxyService(nil) - context := newFakeGinContext("GET", "/admin") - p.clearAllCookies(context) - assert.Contains(t, context.Writer.Header().Get("Set-Cookie"), + req := newFakeHTTPRequest("GET", "/admin") + resp := httptest.NewRecorder() + p.clearAllCookies(req, resp) + assert.Contains(t, resp.Header().Get("Set-Cookie"), "kc-access=; Path=/; Domain=127.0.0.1; Expires=", - "we have not cleared the, headers: %v", context.Writer.Header()) + "we have not cleared the, headers: %v", resp.Header()) } diff --git a/doc.go b/doc.go index 47cd03dac..404e31a92 100644 --- a/doc.go +++ b/doc.go @@ -24,7 +24,7 @@ import ( ) var ( - release = "v2.0.4" + release = "v2.1.0" gitsha = "no gitsha provided" version = release + " (git+sha: " + gitsha + ")" ) @@ -38,6 +38,7 @@ const ( headerUpgrade = "Upgrade" userContextName = "identity" + revokeContextName = "revoke" authorizationHeader = "Authorization" versionHeader = "X-Auth-Proxy-Version" envPrefix = "PROXY_" @@ -45,12 +46,12 @@ const ( oauthURL = "/oauth" authorizationURL = "/authorize" callbackURL = "/callback" - healthURL = "/health" - tokenURL = "/token" expiredURL = "/expired" - logoutURL = "/logout" + healthURL = "/health" loginURL = "/login" + logoutURL = "/logout" metricsURL = "/metrics" + tokenURL = "/token" claimPreferredName = "preferred_username" claimAudience = "aud" @@ -86,22 +87,6 @@ type Resource struct { Roles []string `json:"roles" yaml:"roles"` } -// Cors access controls -type Cors struct { - // Origins is a list of origins permitted - Origins []string `json:"origins" yaml:"origins"` - // Methods is a set of access control methods - Methods []string `json:"methods" yaml:"methods"` - // Headers is a set of cors headers - Headers []string `json:"headers" yaml:"headers"` - // ExposedHeaders are the exposed header fields - ExposedHeaders []string `json:"exposed-headers" yaml:"exposed-headers"` - // Credentials set the creds flag - Credentials bool `json:"credentials" yaml:"credentials"` - // MaxAge is the age for CORS - MaxAge time.Duration `json:"max-age" yaml:"max-age"` -} - // Config is the configuration for the proxy type Config struct { // ConfigFile is the binding interface @@ -131,8 +116,10 @@ type Config struct { // Headers permits adding customs headers across the board Headers map[string]string `json:"headers" yaml:"headers" usage:"custom headers to the upstream request, key=value"` - // EnableCorsGlobal enables the CORs header in all response headers - EnableCorsGlobal bool `json:"enable-cors-global" yaml:"enable-cors-global" usage:"inject the CORs headers into all responses" env:"ENABLE_CORS_GLOBAL"` + // EnableLogging indicates if we should log all the requests + EnableLogging bool `json:"enable-logging" yaml:"enable-logging" usage:"enable http logging of the requests"` + // EnableJSONLogging is the logging format + EnableJSONLogging bool `json:"enable-json-logging" yaml:"enable-json-logging" usage:"switch on json logging rather than text"` // EnableForwarding enables the forwarding proxy EnableForwarding bool `json:"enable-forwarding" yaml:"enable-forwarding" usage:"enables the forwarding proxy mode, signing outbound request"` // EnableSecurityFilter enabled the security handler @@ -212,10 +199,6 @@ type Config struct { // EncryptionKey is the encryption key used to encrypt the refresh token EncryptionKey string `json:"encryption-key" yaml:"encryption-key" usage:"encryption key used to encryption the session state" env:"ENCRYPTION_KEY"` - // LogRequests indicates if we should log all the requests - LogRequests bool `json:"log-requests" yaml:"log-requests" usage:"enable http logging of the requests"` - // LogFormat is the logging format - LogJSONFormat bool `json:"json-format" yaml:"json-format" usage:"switch on json logging rather than text"` // NoRedirects informs we should hand back a 401 not a redirect NoRedirects bool `json:"no-redirects" yaml:"no-redirects" usage:"do not have back redirects when no authentication is present, 401 them"` // SkipTokenVerification tells the service to skipp verifying the access token - for testing purposes @@ -246,7 +229,7 @@ type Config struct { ForwardingDomains []string `json:"forwarding-domains" yaml:"forwarding-domains" usage:"list of domains which should be signed; everything else is relayed unsigned"` } -// store is used to hold the offline refresh token, assuming you don't want to use +// storage is used to hold the offline refresh token, assuming you don't want to use // the default practice of a encrypted cookie type storage interface { // Add the token to the store diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 000000000..77ac6b9fe --- /dev/null +++ b/errors_test.go @@ -0,0 +1,28 @@ +/* +Copyright 2015 All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewAPIError(t *testing.T) { + err := newAPIError("testing", 1) + assert.Error(t, err) + assert.Equal(t, "testing", err.Error()) +} diff --git a/forwarding.go b/forwarding.go index f93c7b3b8..9c93eac2e 100644 --- a/forwarding.go +++ b/forwarding.go @@ -23,38 +23,48 @@ import ( log "github.com/Sirupsen/logrus" "github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/oidc" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" ) -// reverseProxyMiddleware is responsible for handles reverse proxy request to the upstream endpoint -func (r *oauthProxy) reverseProxyMiddleware() gin.HandlerFunc { - return func(cx *gin.Context) { - // step: continue the flow - cx.Next() - // step: check its cool to continue - if cx.IsAborted() { - return - } +// proxyMiddleware is responsible for handles reverse proxy request to the upstream endpoint +func (r *oauthProxy) proxyMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + // step: permit the flow + next(cx) + // step: refuse to proxy + if found := cx.Get(revokeContextName); found != nil { + return nil + } - // step: is this connection upgrading? - if isUpgradedConnection(cx.Request) { - log.Debugf("upgrading the connnection to %s", cx.Request.Header.Get(headerUpgrade)) - if err := tryUpdateConnection(cx, r.endpoint); err != nil { - log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to upgrade the connection") - cx.AbortWithStatus(http.StatusInternalServerError) - return + // step: is this connection upgrading? + if isUpgradedConnection(cx.Request()) { + log.Debugf("upgrading the connnection to %s", cx.Request().Header.Get(headerUpgrade)) + if err := tryUpdateConnection(cx.Request(), cx.Response().Writer, r.endpoint); err != nil { + log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to upgrade the connection") + cx.NoContent(http.StatusInternalServerError) + return nil + } + return nil } - cx.Abort() - return - } + // step: add any custom headers to the request + for k, v := range r.config.Headers { + cx.Request().Header.Set(k, v) + } + + // By default goproxy only provides a forwarding proxy, thus all requests have to be absolute + // and we must update the host headers + cx.Request().URL.Host = r.endpoint.Host + cx.Request().URL.Scheme = r.endpoint.Scheme + cx.Request().Host = r.endpoint.Host - // By default goproxy only provides a forwarding proxy, thus all requests have to be absolute - // and we must update the host headers - cx.Request.URL.Host = r.endpoint.Host - cx.Request.URL.Scheme = r.endpoint.Scheme - cx.Request.Host = r.endpoint.Host + cx.Request().Header.Add("X-Forwarded-For", cx.RealIP()) + cx.Request().Header.Set("X-Forwarded-Host", cx.Request().URL.Host) + cx.Request().Header.Set("X-Forwarded-Proto", cx.Request().Header.Get("X-Forwarded-Proto")) - r.upstream.ServeHTTP(cx.Writer, cx.Request) + r.upstream.ServeHTTP(cx.Response().Writer, cx.Request()) + return nil + } } } @@ -194,7 +204,7 @@ func (r *oauthProxy) forwardProxyHandler() func(*http.Request, *http.Response) { // step: wait for an expiration to come close if state.wait { // step: set the expiration of the access token within a random 85% of actual expiration - duration := getWithin(state.expiration, 0.80) + duration := getWithin(state.expiration, 0.85) log.WithFields(log.Fields{ "token_expiration": state.expiration.Format(time.RFC3339), diff --git a/handlers.go b/handlers.go index 9291e47b6..45be01ff9 100644 --- a/handlers.go +++ b/handlers.go @@ -31,24 +31,24 @@ import ( log "github.com/Sirupsen/logrus" "github.com/coreos/go-oidc/oauth2" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" ) // getRedirectionURL returns the redirectionURL for the oauth flow -func (r *oauthProxy) getRedirectionURL(cx *gin.Context) string { +func (r *oauthProxy) getRedirectionURL(cx echo.Context) string { var redirect string switch r.config.RedirectionURL { case "": // need to determine the scheme, cx.Request.URL.Scheme doesn't have it, best way is to default // and then check for TLS scheme := "http" - if cx.Request.TLS != nil { + if !cx.IsTLS() { scheme = "https" } // @QUESTION: should I use the X-Forwarded-
?? .. redirect = fmt.Sprintf("%s://%s", - defaultTo(cx.Request.Header.Get("X-Forwarded-Proto"), scheme), - defaultTo(cx.Request.Header.Get("X-Forwarded-Host"), cx.Request.Host)) + defaultTo(cx.Request().Header.Get("X-Forwarded-Proto"), scheme), + defaultTo(cx.Request().Header.Get("X-Forwarded-Host"), cx.Request().Host)) default: redirect = r.config.RedirectionURL } @@ -56,22 +56,59 @@ func (r *oauthProxy) getRedirectionURL(cx *gin.Context) string { return fmt.Sprintf("%s/oauth/callback", redirect) } +// oauthHandler is required due to the fact the echo router does not run middleware if no handler +// is found for a group https://github.com/labstack/echo/issues/856 +func (r *oauthProxy) oauthHandler(cx echo.Context) error { + handler := fmt.Sprintf("/%s", strings.TrimLeft(cx.Param("name"), "/")) + r.revokeProxy(cx) + switch cx.Request().Method { + case http.MethodGet: + switch handler { + case authorizationURL: + return r.oauthAuthorizationHandler(cx) + case callbackURL: + return r.oauthCallbackHandler(cx) + case expiredURL: + return r.expirationHandler(cx) + case healthURL: + return r.healthHandler(cx) + case logoutURL: + return r.logoutHandler(cx) + case tokenURL: + return r.tokenHandler(cx) + case metricsURL: + if r.config.EnableMetrics { + return r.metricsHandler(cx) + } + default: + return cx.NoContent(http.StatusNotFound) + } + case http.MethodPost: + switch handler { + case loginURL: + return r.loginHandler(cx) + default: + return cx.NoContent(http.StatusNotFound) + } + default: + return cx.NoContent(http.StatusMethodNotAllowed) + } + + return nil +} + // oauthAuthorizationHandler is responsible for performing the redirection to oauth provider -func (r *oauthProxy) oauthAuthorizationHandler(cx *gin.Context) { - // step: we can skip all of this if were not verifying the token +func (r *oauthProxy) oauthAuthorizationHandler(cx echo.Context) error { if r.config.SkipTokenVerification { - cx.AbortWithStatus(http.StatusNotAcceptable) - return + return cx.NoContent(http.StatusNotAcceptable) } - // step: create a oauth client client, err := r.getOAuthClient(r.getRedirectionURL(cx)) if err != nil { log.WithFields(log.Fields{ "error": err.Error(), }).Errorf("failed to retrieve the oauth client for authorization") - cx.AbortWithStatus(http.StatusInternalServerError) - return + return cx.NoContent(http.StatusInternalServerError) } // step: set the access type of the session @@ -80,39 +117,34 @@ func (r *oauthProxy) oauthAuthorizationHandler(cx *gin.Context) { accessType = "offline" } - authURL := client.AuthCodeURL(cx.Query("state"), accessType, "") - + authURL := client.AuthCodeURL(cx.QueryParam("state"), accessType, "") log.WithFields(log.Fields{ - "client_ip": cx.ClientIP(), "access_type": accessType, "auth-url": authURL, - }).Debugf("incoming authorization request from client address: %s", cx.ClientIP()) + "client_ip": cx.RealIP(), + }).Debugf("incoming authorization request from client address: %s", cx.RealIP()) // step: if we have a custom sign in page, lets display that if r.config.hasCustomSignInPage() { - // step: inject any custom tags into the context for the template model := make(map[string]string, 0) model["redirect"] = authURL - cx.HTML(http.StatusOK, path.Base(r.config.SignInPage), mergeMaps(model, r.config.Tags)) - return + return cx.Render(http.StatusOK, path.Base(r.config.SignInPage), mergeMaps(model, r.config.Tags)) } - r.redirectToURL(authURL, cx) + return r.redirectToURL(authURL, cx) } // oauthCallbackHandler is responsible for handling the response from oauth service -func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) { +func (r *oauthProxy) oauthCallbackHandler(cx echo.Context) error { // step: is token verification switched on? if r.config.SkipTokenVerification { - cx.AbortWithStatus(http.StatusNotAcceptable) - return + return cx.NoContent(http.StatusNotAcceptable) } // step: ensure we have a authorization code to exchange - code := cx.Request.URL.Query().Get("code") + code := cx.QueryParam("code") if code == "" { - cx.AbortWithStatus(http.StatusBadRequest) - return + return cx.NoContent(http.StatusBadRequest) } // step: create a oauth client @@ -120,8 +152,7 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) { if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to create a oauth2 client") - cx.AbortWithStatus(http.StatusInternalServerError) - return + return cx.NoContent(http.StatusInternalServerError) } // step: exchange the authorization for a access token @@ -129,8 +160,7 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) { if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to exchange code for access token") - r.accessForbidden(cx) - return + return r.accessForbidden(cx) } // step: parse decode the identity token @@ -138,16 +168,14 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) { if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to parse id token for identity") - r.accessForbidden(cx) - return + return r.accessForbidden(cx) } // step: verify the token is valid if err = verifyToken(r.client, token); err != nil { log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to verify the id token") - r.accessForbidden(cx) - return + return r.accessForbidden(cx) } // step: attempt to decode the access token else we default to the id token @@ -172,12 +200,12 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) { if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to encrypt the refresh token") - cx.AbortWithStatus(http.StatusInternalServerError) - return + return cx.NoContent(http.StatusInternalServerError) } // drop in the access token - cookie expiration = access token - r.dropAccessTokenCookie(cx, token.Encode(), r.getAccessCookieExpiration(token, resp.RefreshToken)) + r.dropAccessTokenCookie(cx.Request(), cx.Response().Writer, token.Encode(), + r.getAccessCookieExpiration(token, resp.RefreshToken)) switch r.useStore() { case true: @@ -188,22 +216,22 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) { // notes: not all idp refresh tokens are readable, google for example, so we attempt to decode into // a jwt and if possible extract the expiration, else we default to 10 days if _, ident, err := parseToken(resp.RefreshToken); err != nil { - r.dropRefreshTokenCookie(cx, encrypted, time.Duration(240)*time.Hour) + r.dropRefreshTokenCookie(cx.Request(), cx.Response().Writer, encrypted, time.Duration(240)*time.Hour) } else { - r.dropRefreshTokenCookie(cx, encrypted, ident.ExpiresAt.Sub(time.Now())) + r.dropRefreshTokenCookie(cx.Request(), cx.Response().Writer, encrypted, ident.ExpiresAt.Sub(time.Now())) } } } else { - r.dropAccessTokenCookie(cx, token.Encode(), identity.ExpiresAt.Sub(time.Now())) + r.dropAccessTokenCookie(cx.Request(), cx.Response().Writer, token.Encode(), identity.ExpiresAt.Sub(time.Now())) } // step: decode the state variable state := "/" - if cx.Request.URL.Query().Get("state") != "" { - decoded, err := base64.StdEncoding.DecodeString(cx.Request.URL.Query().Get("state")) + if cx.QueryParam("state") != "" { + decoded, err := base64.StdEncoding.DecodeString(cx.QueryParam("state")) if err != nil { log.WithFields(log.Fields{ - "state": cx.Request.URL.Query().Get("state"), + "state": cx.QueryParam("state"), "error": err.Error(), }).Warnf("unable to decode the state parameter") } else { @@ -211,11 +239,11 @@ func (r *oauthProxy) oauthCallbackHandler(cx *gin.Context) { } } - r.redirectToURL(state, cx) + return r.redirectToURL(state, cx) } // loginHandler provide's a generic endpoint for clients to perform a user_credentials login to the provider -func (r *oauthProxy) loginHandler(cx *gin.Context) { +func (r *oauthProxy) loginHandler(cx echo.Context) error { errorMsg, code, err := func() (string, int, error) { // step: check if the handler is disable if !r.config.EnableLoginHandler { @@ -223,8 +251,8 @@ func (r *oauthProxy) loginHandler(cx *gin.Context) { } // step: parse the client credentials - username := cx.Request.PostFormValue("username") - password := cx.Request.PostFormValue("password") + username := cx.Request().PostFormValue("username") + password := cx.Request().PostFormValue("password") if username == "" || password == "" { return "request does not have both username and password", http.StatusBadRequest, errors.New("no credentials") } @@ -249,7 +277,7 @@ func (r *oauthProxy) loginHandler(cx *gin.Context) { return "unable to decode the access token", http.StatusNotImplemented, err } - r.dropAccessTokenCookie(cx, token.AccessToken, identity.ExpiresAt.Sub(time.Now())) + r.dropAccessTokenCookie(cx.Request(), cx.Response().Writer, token.AccessToken, identity.ExpiresAt.Sub(time.Now())) cx.JSON(http.StatusOK, tokenResponse{ IDToken: token.IDToken, @@ -263,12 +291,19 @@ func (r *oauthProxy) loginHandler(cx *gin.Context) { }() if err != nil { log.WithFields(log.Fields{ - "client_ip": cx.Request.RemoteAddr, + "client_ip": cx.RealIP(), "error": err.Error, }).Errorf(errorMsg) - cx.AbortWithStatus(code) + return cx.NoContent(code) } + + return nil +} + +// emptyHandler is responsible for doing nothing +func emptyHandler(cx echo.Context) error { + return nil } // @@ -277,25 +312,24 @@ func (r *oauthProxy) loginHandler(cx *gin.Context) { // - if the user has a refresh token, the token is invalidated by the provider // - optionally, the user can be redirected by to a url // -func (r *oauthProxy) logoutHandler(cx *gin.Context) { +func (r *oauthProxy) logoutHandler(cx echo.Context) error { // the user can specify a url to redirect the back - redirectURL := cx.Request.URL.Query().Get("redirect") + redirectURL := cx.QueryParam("redirect") // step: drop the access token - user, err := r.getIdentity(cx.Request) + user, err := r.getIdentity(cx.Request()) if err != nil { - cx.AbortWithStatus(http.StatusBadRequest) - return + return cx.NoContent(http.StatusBadRequest) } // step: can either use the id token or the refresh token identityToken := user.token.Encode() - if refresh, err := r.retrieveRefreshToken(cx.Request, user); err == nil { + if refresh, err := r.retrieveRefreshToken(cx.Request(), user); err == nil { identityToken = refresh } - r.clearAllCookies(cx) + r.clearAllCookies(cx.Request(), cx.Response().Writer) - // step: check if the user has a state session and if so, revoke it + // step: check if the user has a state session and if so revoke it if r.useStore() { go func() { if err := r.DeleteRefreshToken(user.token); err != nil { @@ -315,8 +349,7 @@ func (r *oauthProxy) logoutHandler(cx *gin.Context) { if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to retrieve the openid client") - cx.AbortWithStatus(http.StatusInternalServerError) - return + return cx.NoContent(http.StatusInternalServerError) } // step: add the authentication headers @@ -329,9 +362,7 @@ func (r *oauthProxy) logoutHandler(cx *gin.Context) { bytes.NewBufferString(fmt.Sprintf("refresh_token=%s", identityToken))) if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to construct the revocation request") - - cx.AbortWithStatus(http.StatusInternalServerError) - return + return cx.NoContent(http.StatusInternalServerError) } // step: add the authentication headers and content-type @@ -342,8 +373,7 @@ func (r *oauthProxy) logoutHandler(cx *gin.Context) { response, err := client.HttpClient().Do(request) if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Errorf("unable to post to revocation endpoint") - - return + return nil } // step: add a log for debugging @@ -363,58 +393,47 @@ func (r *oauthProxy) logoutHandler(cx *gin.Context) { // step: should we redirect the user if redirectURL != "" { - r.redirectToURL(redirectURL, cx) - return + return r.redirectToURL(redirectURL, cx) } - cx.AbortWithStatus(http.StatusOK) + return cx.NoContent(http.StatusOK) } -// // expirationHandler checks if the token has expired -// -func (r *oauthProxy) expirationHandler(cx *gin.Context) { - // step: get the access token from the request - user, err := r.getIdentity(cx.Request) +func (r *oauthProxy) expirationHandler(cx echo.Context) error { + user, err := r.getIdentity(cx.Request()) if err != nil { - cx.AbortWithError(http.StatusUnauthorized, err) - return + return cx.NoContent(http.StatusUnauthorized) } - // step: check the access is not expired if user.isExpired() { - cx.AbortWithError(http.StatusUnauthorized, err) - return + return cx.NoContent(http.StatusUnauthorized) } - cx.AbortWithStatus(http.StatusOK) + return cx.NoContent(http.StatusOK) } -// // tokenHandler display access token to screen -// -func (r *oauthProxy) tokenHandler(cx *gin.Context) { - // step: extract the access token from the request - user, err := r.getIdentity(cx.Request) +func (r *oauthProxy) tokenHandler(cx echo.Context) error { + user, err := r.getIdentity(cx.Request()) if err != nil { - cx.AbortWithError(http.StatusBadRequest, fmt.Errorf("unable to retrieve session, error: %s", err)) - return + return cx.String(http.StatusBadRequest, fmt.Sprintf("unable to retrieve session, error: %s", err)) } + cx.Response().Writer.Header().Set("Content-Type", "application/json") - // step: write the json content - cx.Writer.Header().Set("Content-Type", "application/json") - cx.String(http.StatusOK, fmt.Sprintf("%s", user.token.Payload)) + return cx.String(http.StatusOK, fmt.Sprintf("%s", user.token.Payload)) } // healthHandler is a health check handler for the service -func (r *oauthProxy) healthHandler(cx *gin.Context) { - cx.Writer.Header().Set(versionHeader, version) - cx.String(http.StatusOK, "OK\n") +func (r *oauthProxy) healthHandler(cx echo.Context) error { + cx.Response().Writer.Header().Set(versionHeader, version) + + return cx.String(http.StatusOK, "OK\n") } // debugHandler is responsible for providing the pprof -func (r *oauthProxy) debugHandler(cx *gin.Context) { +func (r *oauthProxy) debugHandler(cx echo.Context) error { name := cx.Param("name") - switch cx.Request.Method { + switch cx.Request().Method { case http.MethodGet: switch name { case "heap": @@ -424,40 +443,40 @@ func (r *oauthProxy) debugHandler(cx *gin.Context) { case "block": fallthrough case "threadcreate": - pprof.Handler(name).ServeHTTP(cx.Writer, cx.Request) + pprof.Handler(name).ServeHTTP(cx.Response().Writer, cx.Request()) case "cmdline": - pprof.Cmdline(cx.Writer, cx.Request) + pprof.Cmdline(cx.Response().Writer, cx.Request()) case "profile": - pprof.Profile(cx.Writer, cx.Request) + pprof.Profile(cx.Response().Writer, cx.Request()) case "trace": - pprof.Trace(cx.Writer, cx.Request) + pprof.Trace(cx.Response().Writer, cx.Request()) case "symbol": - pprof.Symbol(cx.Writer, cx.Request) + pprof.Symbol(cx.Response().Writer, cx.Request()) default: - cx.AbortWithStatus(http.StatusNotFound) + cx.NoContent(http.StatusNotFound) } case http.MethodPost: switch name { case "symbol": - pprof.Symbol(cx.Writer, cx.Request) + pprof.Symbol(cx.Response().Writer, cx.Request()) default: - cx.AbortWithStatus(http.StatusNotFound) + cx.NoContent(http.StatusNotFound) } } - cx.Abort() + return nil } // metricsHandler forwards the request into the prometheus handler -func (r *oauthProxy) metricsHandler(cx *gin.Context) { +func (r *oauthProxy) metricsHandler(cx echo.Context) error { if r.config.LocalhostMetrics { - if !net.ParseIP(cx.ClientIP()).IsLoopback() { - r.accessForbidden(cx) - return + if !net.ParseIP(cx.RealIP()).IsLoopback() { + return r.accessForbidden(cx) } } + r.prometheusHandler.ServeHTTP(cx.Response().Writer, cx.Request()) - r.prometheusHandler.ServeHTTP(cx.Writer, cx.Request) + return nil } // retrieveRefreshToken retrieves the refresh token from store or cookie @@ -465,7 +484,6 @@ func (r *oauthProxy) retrieveRefreshToken(req *http.Request, user *userContext) var token string var err error - // step: get the refresh token from the store or cookie switch r.useStore() { case true: token, err = r.GetRefreshToken(user.token) diff --git a/handlers_test.go b/handlers_test.go index cc36ffc9d..e4a4a11de 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -16,332 +16,291 @@ limitations under the License. package main import ( - "errors" "net/http" - "net/url" "testing" "time" - - "github.com/go-resty/resty" - "github.com/stretchr/testify/assert" ) func TestExpirationHandler(t *testing.T) { - _, idp, svc := newTestProxyService(nil) - cases := []struct { - ExpireIn time.Duration - Expects int - }{ + uri := oauthURL + expiredURL + requests := []fakeRequest{ { - Expects: http.StatusUnauthorized, + URI: uri, + ExpectedCode: http.StatusUnauthorized, }, { - ExpireIn: time.Duration(-24 * time.Hour), - Expects: http.StatusUnauthorized, + URI: uri, + HasToken: true, + Expires: time.Duration(-48 * time.Hour), + ExpectedCode: http.StatusUnauthorized, }, { - ExpireIn: time.Duration(14 * time.Hour), - Expects: http.StatusOK, + URI: uri, + HasToken: true, + Expires: time.Duration(14 * time.Hour), + ExpectedCode: http.StatusOK, }, } + newFakeProxy(nil).RunTests(t, requests) +} - for i, c := range cases { - token := newTestToken(idp.getLocation()) - token.setExpiration(time.Now().Add(c.ExpireIn)) - // sign the token - signed, _ := idp.signToken(token.claims) - // make the request - resp, err := resty.New().SetAuthToken(signed.Encode()).R().Get(svc + oauthURL + expiredURL) - if !assert.NoError(t, err, "case %d unable to make the request, error: %s", i, err) { - continue - } - assert.Equal(t, c.Expects, resp.StatusCode(), "case %d, expects: %d but got: %d", i, c.Expects, resp.StatusCode()) +func TestOauthRequestNotProxying(t *testing.T) { + requests := []fakeRequest{ + {URI: "/oauth/test"}, + {URI: "/oauth/..//oauth/test/"}, + {URI: "/oauth/expired", Method: http.MethodPost, ExpectedCode: http.StatusNotFound}, + {URI: "/oauth/expiring", Method: http.MethodPost}, + {URI: "/oauth%2F///../test%2F%2Foauth"}, } + newFakeProxy(nil).RunTests(t, requests) } func TestLoginHandlerDisabled(t *testing.T) { - config := newFakeKeycloakConfig() - config.EnableLoginHandler = false - - _, _, url := newTestProxyService(config) - resp, err := resty.DefaultClient.R().Post(url + "/oauth/login") - assert.NoError(t, err) - assert.NotNil(t, resp) - assert.Equal(t, http.StatusNotImplemented, resp.StatusCode()) + c := newFakeKeycloakConfig() + c.EnableLoginHandler = false + requests := []fakeRequest{ + {URI: oauthURL + loginURL, Method: http.MethodPost, ExpectedCode: http.StatusNotImplemented}, + {URI: oauthURL + loginURL, ExpectedCode: http.StatusNotFound}, + } + newFakeProxy(c).RunTests(t, requests) } func TestLoginHandlerNotDisabled(t *testing.T) { - config := newFakeKeycloakConfig() - config.EnableLoginHandler = true - _, _, url := newTestProxyService(config) - resp, err := http.Post(url+"/oauth/login", "", nil) - assert.NoError(t, err) - assert.NotNil(t, resp) - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + c := newFakeKeycloakConfig() + c.EnableLoginHandler = true + requests := []fakeRequest{ + {URI: "/oauth/login", Method: http.MethodPost, ExpectedCode: http.StatusBadRequest}, + } + newFakeProxy(c).RunTests(t, requests) } func TestLoginHandler(t *testing.T) { - _, _, u := newTestProxyService(nil) - - cs := []struct { - Username string - Password string - ExpectedCode int - }{ - { - Username: "", - Password: "", + uri := oauthURL + loginURL + requests := []fakeRequest{ + { + URI: uri, + Method: http.MethodPost, ExpectedCode: http.StatusBadRequest, }, { - Username: "test", - Password: "", + URI: uri, + Method: http.MethodPost, + FormValues: map[string]string{"username": "test"}, ExpectedCode: http.StatusBadRequest, }, { - Username: "", - Password: "test", + URI: uri, + Method: http.MethodPost, + FormValues: map[string]string{"password": "test"}, ExpectedCode: http.StatusBadRequest, }, { - Username: "test", - Password: "test", + URI: uri, + Method: http.MethodPost, + FormValues: map[string]string{ + "password": "test", + "username": "test", + }, ExpectedCode: http.StatusOK, }, { - Username: "test", - Password: "notmypassword", + URI: uri, + Method: http.MethodPost, + FormValues: map[string]string{ + "password": "test", + "username": "notmypassword", + }, ExpectedCode: http.StatusUnauthorized, }, } - - for i, x := range cs { - uri := u + oauthURL + loginURL - values := url.Values{} - if x.Username != "" { - values.Add("username", x.Username) - } - if x.Password != "" { - values.Add("password", x.Password) - } - - resp, err := http.PostForm(uri, values) - if err != nil { - t.Errorf("case %d, unable to make requets, error: %s", i, err) - continue - } - assert.Equal(t, x.ExpectedCode, resp.StatusCode, "case %d, expect: %v, got: %d", - i, x.ExpectedCode, resp.StatusCode) - } + newFakeProxy(nil).RunTests(t, requests) } func TestLogoutHandlerBadRequest(t *testing.T) { - _, _, u := newTestProxyService(nil) - - res, err := http.Get(u + oauthURL + logoutURL) - assert.NoError(t, err) - assert.NotNil(t, res) - assert.Equal(t, res.StatusCode, http.StatusBadRequest) + requests := []fakeRequest{ + {URI: oauthURL + logoutURL, ExpectedCode: http.StatusBadRequest}, + } + newFakeProxy(nil).RunTests(t, requests) } func TestLogoutHandlerBadToken(t *testing.T) { - u := newTestService() - req, err := http.NewRequest("GET", u+oauthURL+logoutURL, nil) - assert.NoError(t, err) - req.Header.Add("kc-access", "this.is.a.bad.token") - res, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - assert.NotNil(t, res) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) + requests := []fakeRequest{ + { + URI: oauthURL + logoutURL, + ExpectedCode: http.StatusBadRequest, + }, + { + URI: oauthURL + logoutURL, + HasCookieToken: true, + RawToken: "this.is.a.bad.token", + ExpectedCode: http.StatusBadRequest, + }, + { + URI: oauthURL + logoutURL, + RawToken: "this.is.a.bad.token", + ExpectedCode: http.StatusBadRequest, + }, + } + newFakeProxy(nil).RunTests(t, requests) } func TestLogoutHandlerGood(t *testing.T) { - u := newTestService() - // step: first we login and get a token - token, err := makeTestOauthLogin(u + "/admin") - if !assert.NoError(t, err) { - t.Errorf("failed to perform oauth login, reason: %s", err) - t.Fail() - } - - // step: attempt to logout - res, err := resty.DefaultClient.R(). - SetAuthToken(token). - Get(u + oauthURL + logoutURL) - if !assert.NoError(t, err) { - t.Fail() + requests := []fakeRequest{ + { + URI: oauthURL + logoutURL, + HasToken: true, + ExpectedCode: http.StatusOK, + }, + { + URI: oauthURL + logoutURL + "?redirect=http://example.com", + HasToken: true, + ExpectedCode: http.StatusTemporaryRedirect, + ExpectedLocation: "http://example.com", + }, } - assert.Equal(t, http.StatusOK, res.StatusCode()) + newFakeProxy(nil).RunTests(t, requests) } func TestTokenHandler(t *testing.T) { - token := newFakeAccessToken(nil, 0) - svc := newTestService() - cs := []struct { - Token string - Cookie string - Expected int - }{ + uri := oauthURL + tokenURL + requests := []fakeRequest{ { - Token: token.Encode(), - Expected: http.StatusOK, + URI: uri, + HasToken: true, + ExpectedCode: http.StatusOK, }, { - Expected: http.StatusBadRequest, + URI: uri, + ExpectedCode: http.StatusBadRequest, }, { - Token: "niothing", - Expected: http.StatusBadRequest, + URI: uri, + RawToken: "niothing", + ExpectedCode: http.StatusBadRequest, }, { - Cookie: token.Encode(), - Expected: http.StatusOK, + URI: uri, + HasToken: true, + HasCookieToken: true, + ExpectedCode: http.StatusOK, }, } - requrl := svc + oauthURL + tokenURL - for _, c := range cs { - client := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) - if c.Token != "" { - client.SetAuthToken(c.Token) - } - if c.Cookie != "" { - client.SetCookie(&http.Cookie{ - Name: "kc-access", - Path: "/", - Value: c.Cookie, - }) - } - resp, err := client.R().Get(requrl) - assert.NoError(t, err) - assert.Equal(t, c.Expected, resp.StatusCode()) - } + newFakeProxy(nil).RunTests(t, requests) } -func TestNoRedirect(t *testing.T) { - p, _, svc := newTestProxyService(nil) - p.config.NoRedirects = true - - req, _ := http.NewRequest("GET", svc+"/admin", nil) - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - assert.NotNil(t, resp) - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) +func TestServiceRedirect(t *testing.T) { + requests := []fakeRequest{ + { + URI: "/admin", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, + ExpectedLocation: "/oauth/authorize?state=L2FkbWlu", + }, + { + URI: "/admin", + ExpectedCode: http.StatusUnauthorized, + }, + } + newFakeProxy(nil).RunTests(t, requests) +} - p.config.NoRedirects = false - req, _ = http.NewRequest("GET", svc+"/admin", nil) - resp, err = http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - assert.NotNil(t, resp) - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) +func TestAuthorizationURLWithSkipToken(t *testing.T) { + c := newFakeKeycloakConfig() + c.SkipTokenVerification = true + newFakeProxy(c).RunTests(t, []fakeRequest{ + { + URI: oauthURL + authorizationURL, + ExpectedCode: http.StatusNotAcceptable, + }, + }) } func TestAuthorizationURL(t *testing.T) { - _, _, u := newTestProxyService(nil) - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return errors.New("no redirect") - }, - } - cs := []struct { - URL string - ExpectedURL string - ExpectedCode int - }{ + requests := []fakeRequest{ { - URL: "/admin", - ExpectedURL: "/oauth/authorize?state=L2FkbWlu", - ExpectedCode: http.StatusTemporaryRedirect, + URI: "/admin", + Redirects: true, + ExpectedLocation: "/oauth/authorize?state=L2FkbWlu", + ExpectedCode: http.StatusTemporaryRedirect, }, { - URL: "/admin/test", - ExpectedURL: "/oauth/authorize?state=L2FkbWluL3Rlc3Q=", - ExpectedCode: http.StatusTemporaryRedirect, + URI: "/admin/test", + Redirects: true, + ExpectedLocation: "/oauth/authorize?state=L2FkbWluL3Rlc3Q=", + ExpectedCode: http.StatusTemporaryRedirect, }, { - URL: "/help/../admin", - ExpectedURL: "/oauth/authorize?state=L2FkbWlu", - ExpectedCode: http.StatusTemporaryRedirect, + URI: "/help/../admin", + Redirects: true, + ExpectedLocation: "/oauth/authorize?state=L2FkbWlu", + ExpectedCode: http.StatusTemporaryRedirect, }, { - URL: "/admin?test=yes&test1=test", - ExpectedURL: "/oauth/authorize?state=L2FkbWluP3Rlc3Q9eWVzJnRlc3QxPXRlc3Q=", - ExpectedCode: http.StatusTemporaryRedirect, + URI: "/admin?test=yes&test1=test", + Redirects: true, + ExpectedLocation: "/oauth/authorize?state=L2FkbWluP3Rlc3Q9eWVzJnRlc3QxPXRlc3Q=", + ExpectedCode: http.StatusTemporaryRedirect, }, { - URL: "/oauth/test", + URI: "/oauth/test", + Redirects: true, ExpectedCode: http.StatusNotFound, }, { - URL: "/oauth/callback/..//test", + URI: "/oauth/callback/..//test", + Redirects: true, ExpectedCode: http.StatusNotFound, }, } - for i, x := range cs { - resp, _ := client.Get(u + x.URL) - assert.Equal(t, x.ExpectedCode, resp.StatusCode, "case %d, expect: %v, got: %s", i, x.ExpectedCode, resp.StatusCode) - assert.Equal(t, x.ExpectedURL, resp.Header.Get("Location"), "case %d, expect: %v, got: %s", i, x.ExpectedURL, resp.Header.Get("Location")) - assert.Empty(t, resp.Header.Get(testProxyAccepted)) - } + newFakeProxy(nil).RunTests(t, requests) } func TestCallbackURL(t *testing.T) { - _, _, u := newTestProxyService(nil) - - cs := []struct { - URL string - ExpectedURL string - }{ + cfg := newFakeKeycloakConfig() + requests := []fakeRequest{ + { + URI: oauthURL + callbackURL, + Method: http.MethodPost, + ExpectedCode: http.StatusNotFound, + }, { - URL: "/oauth/authorize?state=L2FkbWlu", - ExpectedURL: "/admin", + URI: oauthURL + callbackURL, + ExpectedCode: http.StatusBadRequest, }, { - URL: "/oauth/authorize", - ExpectedURL: "/", + URI: oauthURL + callbackURL + "?code=fake", + ExpectedCookies: []string{cfg.CookieAccessName}, + ExpectedLocation: "/", + ExpectedCode: http.StatusTemporaryRedirect, }, { - URL: "/oauth/authorize?state=L2FkbWluL3Rlc3QxP3Rlc3QxJmhlbGxv", - ExpectedURL: "/admin/test1?test1&hello", + URI: oauthURL + callbackURL + "?code=fake&state=/admin", + ExpectedCookies: []string{cfg.CookieAccessName}, + ExpectedLocation: "/", + ExpectedCode: http.StatusTemporaryRedirect, + }, + { + URI: oauthURL + callbackURL + "?code=fake&state=L2FkbWlu", + ExpectedCookies: []string{cfg.CookieAccessName}, + ExpectedLocation: "/admin", + ExpectedCode: http.StatusTemporaryRedirect, }, } - for i, x := range cs { - // step: call the authorization endpoint - req, err := http.NewRequest("GET", u+x.URL, nil) - if err != nil { - continue - } - resp, err := http.DefaultTransport.RoundTrip(req) - if !assert.NoError(t, err, "case %d, should not have failed", i) { - continue - } - openIDURL := resp.Header.Get("Location") - if !assert.NotEmpty(t, openIDURL, "case %d, the open id redirection url is empty", i) { - continue - } - req, _ = http.NewRequest("GET", openIDURL, nil) - resp, err = http.DefaultTransport.RoundTrip(req) - if !assert.NoError(t, err, "case %d, should not have failed calling the opend id url", i) { - continue - } - callbackURL := resp.Header.Get("Location") - if !assert.NotEmpty(t, callbackURL, "case %d, should have received a callback url", i) { - continue - } - // step: call the callback url - req, _ = http.NewRequest("GET", callbackURL, nil) - resp, err = http.DefaultTransport.RoundTrip(req) - if !assert.NoError(t, err, "case %d, unable to call the callback url", i) { - continue - } - // step: check the callback location is as expected - assert.Contains(t, resp.Header.Get("Location"), x.ExpectedURL) - } + newFakeProxy(cfg).RunTests(t, requests) } func TestHealthHandler(t *testing.T) { - svc := newTestService() - resp, err := resty.DefaultClient.R().Get(svc + oauthURL + healthURL) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode()) - assert.Equal(t, version, resp.Header().Get(versionHeader)) + requests := []fakeRequest{ + { + URI: oauthURL + healthURL, + ExpectedCode: http.StatusOK, + ExpectedContent: "OK\n", + }, + { + URI: oauthURL + healthURL, + Method: http.MethodHead, + ExpectedCode: http.StatusMethodNotAllowed, + }, + } + newFakeProxy(nil).RunTests(t, requests) } diff --git a/kube/forwarding-rc.yml b/kube/forward.yml similarity index 61% rename from kube/forwarding-rc.yml rename to kube/forward.yml index e122934c5..cc3a338ba 100644 --- a/kube/forwarding-rc.yml +++ b/kube/forward.yml @@ -1,20 +1,18 @@ -apiVersion: v1 -kind: ReplicationController +apiVersion: extensions/v1beta1 +kind: Deployment metadata: - name: keycloak-proxy - labels: - name: keycloak-proxy + name: proxy spec: replicas: 1 - selector: - name: keycloak-proxy template: metadata: labels: - name: keycloak-proxy + name: proxy + annotations: + repository: https://github.com/gambol99/keycloak-proxy spec: containers: - - name: keycloak-proxy + - name: proxy image: quay.io/gambol99/keycloak-proxy:latest imagePullPolicy: Always args: @@ -23,11 +21,11 @@ spec: - --client-id broker - --client-secret - --listen 127.0.0.1:3000 - - --enable-forwarding true - - --forwarding-username username - - --forwarding-password password - - --log-requests true - - --log-json-format true + - --enable-forwarding=true + - --forwarding-username=username + - --forwarding-password=password + - --enable-logging=true + - --enable-json-logging true - --verbose true volumeMounts: - name: secrets diff --git a/kube/reverse.yml b/kube/reverse.yml new file mode 100644 index 000000000..569affa80 --- /dev/null +++ b/kube/reverse.yml @@ -0,0 +1,33 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: proxy +spec: + replicas: 1 + template: + metadata: + labels: + name: proxy + annotations: + repository: https://github.com/gambol99/keycloak-proxy + spec: + containers: + - name: proxy + image: quay.io/gambol99/keycloak-proxy:latest + imagePullPolicy: Always + args: + - --config=/etc/secrets/config.yml + - --discovery-url=https://sso.example.com/auth/realms/hod-test + - --client-id=broker + - --client-secret=secret + - --listen=127.0.0.1:3000 + - --enable-logging=true + - --enable-json-logging=true + - --upstream-url=http://127.0.0.1:8080 + volumeMounts: + - name: secrets + mountPath: /etc/secrets + volumes: + - name: secrets + secret: + secretName: config diff --git a/kube/secure-proxy-rc.yml b/kube/secure-proxy-rc.yml deleted file mode 100644 index eaafb9f6e..000000000 --- a/kube/secure-proxy-rc.yml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: v1 -kind: ReplicationController -metadata: - name: keycloak-proxy - labels: - name: keycloak-proxy -spec: - replicas: 1 - selector: - name: keycloak-proxy - template: - metadata: - labels: - name: keycloak-proxy - spec: - containers: - - name: keycloak-proxy - image: quay.io/gambol99/keycloak-proxy:latest - imagePullPolicy: Always - args: - - --config /etc/secrets/forwarding.yml - - --discovery-url https://sso.example.com/auth/realms/hod-test - - --client-id broker - - --client-secret - - --listen 127.0.0.1:3000 - - --log-requests true - - --log-json-format true - - --upstream-url=http://127.0.0.1:8080 - volumeMounts: - - name: secrets - mountPath: /etc/secrets - volumes: - - name: secrets - secret: - secretName: config diff --git a/middleware.go b/middleware.go index fb06824e4..6127592b9 100644 --- a/middleware.go +++ b/middleware.go @@ -17,6 +17,7 @@ package main import ( "fmt" + "net/http" "regexp" "strings" "time" @@ -24,58 +25,75 @@ import ( "github.com/PuerkitoBio/purell" log "github.com/Sirupsen/logrus" "github.com/coreos/go-oidc/jose" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" "github.com/prometheus/client_golang/prometheus" "github.com/unrolled/secure" ) -const ( - // cxEnforce is the tag name for a request requiring - cxEnforce = "Enforcing" -) - const normalizeFlags purell.NormalizationFlags = purell.FlagRemoveDotSegments | purell.FlagRemoveDuplicateSlashes +// proxyRevokeMiddleware is just a helper to drop all requests proxying +func (r *oauthProxy) proxyRevokeMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + r.revokeProxy(cx) + cx.NoContent(http.StatusForbidden) + return next(cx) + } + } +} + // filterMiddleware is custom filtering for incoming requests -func (r *oauthProxy) filterMiddleware() gin.HandlerFunc { - return func(cx *gin.Context) { - // step: keep a copy of the original - orig := cx.Request.URL.Path - // step: normalize the url - purell.NormalizeURL(cx.Request.URL, normalizeFlags) - // step: continue the flow - cx.Next() - // step: place back the original - cx.Request.URL.Path = orig +func (r *oauthProxy) filterMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + // step: keep a copy of the original + keep := cx.Request().URL.Path + purell.NormalizeURL(cx.Request().URL, normalizeFlags) + cx.Request().URL.RawPath = cx.Request().URL.Path + cx.Request().RequestURI = cx.Request().URL.Path + // step: continue the flow + next(cx) + // step: place back the original uri for proxying request + cx.Request().URL.Path = keep + cx.Request().URL.RawPath = keep + cx.Request().RequestURI = keep + + return nil + } } } // loggingMiddleware is a custom http logger -func (r *oauthProxy) loggingMiddleware() gin.HandlerFunc { - return func(cx *gin.Context) { - start := time.Now() - cx.Next() - latency := time.Now().Sub(start) - - log.WithFields(log.Fields{ - "client_ip": cx.ClientIP(), - "method": cx.Request.Method, - "status": cx.Writer.Status(), - "bytes": cx.Writer.Size(), - "path": cx.Request.URL.Path, - "latency": latency.String(), - }).Infof("[%d] |%s| |%10v| %-5s %s", cx.Writer.Status(), cx.ClientIP(), latency, cx.Request.Method, cx.Request.URL.Path) +func (r *oauthProxy) loggingMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + start := time.Now() + next(cx) + latency := time.Now().Sub(start) + addr := cx.RealIP() + log.WithFields(log.Fields{ + "client_ip": addr, + "method": cx.Request().Method, + "status": cx.Response().Status, + "bytes": cx.Response().Size, + "path": cx.Request().URL.Path, + "latency": latency.String(), + }).Infof("[%d] |%s| |%10v| %-5s %s", cx.Response().Status, addr, latency, cx.Request().Method, cx.Request().URL.Path) + + return nil + } } } // metricsMiddleware is responsible for collecting metrics -func (r *oauthProxy) metricsMiddleware() gin.HandlerFunc { +func (r *oauthProxy) metricsMiddleware() echo.MiddlewareFunc { log.Infof("enabled the service metrics middleware, available on %s%s", oauthURL, metricsURL) statusMetrics := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "http_request_total", - Help: "The HTTP requests broken partitioned by status code", + Help: "The HTTP requests partitioned by status code", }, []string{"code", "method"}, ) @@ -83,357 +101,273 @@ func (r *oauthProxy) metricsMiddleware() gin.HandlerFunc { // step: register the metric with prometheus prometheus.MustRegisterOrGet(statusMetrics) - return func(cx *gin.Context) { - // step: permit to next stage - cx.Next() - // step: update the metrics - statusMetrics.WithLabelValues(fmt.Sprintf("%d", cx.Writer.Status()), cx.Request.Method).Inc() - } -} - -// entrypointMiddleware checks to see if the request requires authentication -func (r *oauthProxy) entrypointMiddleware() gin.HandlerFunc { - return func(cx *gin.Context) { - // step: we can skip if under oauth prefix - if strings.HasPrefix(cx.Request.URL.Path, oauthURL) { - cx.Abort() - return - } - - // step: check if authentication is required - gin doesn't support wildcard url - // so we have to use prefixes - for _, resource := range r.config.Resources { - if strings.HasPrefix(cx.Request.URL.Path, resource.URL) { - if resource.WhiteListed { - break - } - // step: inject the resource into the context, saves us from doing this again - if containedIn("ANY", resource.Methods) || containedIn(cx.Request.Method, resource.Methods) { - cx.Set(cxEnforce, resource) - } - break - } + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + statusMetrics.WithLabelValues(fmt.Sprintf("%d", cx.Response().Status), cx.Request().Method).Inc() + return next(cx) } } } // authenticationMiddleware is responsible for verifying the access token -func (r *oauthProxy) authenticationMiddleware() gin.HandlerFunc { - return func(cx *gin.Context) { - // step: grab the client ip address - quicker to do once - clientIP := cx.ClientIP() - - // step: is authentication required on this uri? - if _, found := cx.Get(cxEnforce); !found { - log.WithFields(log.Fields{ - "uri": cx.Request.URL.Path, - }).Debugf("skipping the authentication as resource not protected") - - return - } +func (r *oauthProxy) authenticationMiddleware(resource *Resource) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + clientIP := cx.RealIP() - // step: grab the user identity from the request - user, err := r.getIdentity(cx.Request) - if err != nil { - log.WithFields(log.Fields{ - "error": err.Error(), - }).Errorf("no session found in request, redirecting for authorization") - - r.redirectToAuthorization(cx) - return - } - - // step: inject the user into the context - cx.Set(userContextName, user) - - // step: skipif we are running skip-token-verification - if r.config.SkipTokenVerification { - log.Warnf("skip token verification enabled, skipping verification process - FOR TESTING ONLY") - - if user.isExpired() { - log.WithFields(log.Fields{ - "client_ip": clientIP, - "username": user.name, - "expired_on": user.expiresAt.String(), - }).Errorf("the session has expired and verification switch off") - - r.redirectToAuthorization(cx) - } - - return - } - - if err := verifyToken(r.client, user.token); err != nil { - // step: if the error post verification is anything other than a token expired error - // we immediately throw an access forbidden - as there is something messed up in the token - if err != ErrAccessTokenExpired { + // step: grab the user identity from the request + user, err := r.getIdentity(cx.Request()) + if err != nil { log.WithFields(log.Fields{ - "client_ip": clientIP, - "error": err.Error(), - }).Errorf("access token failed verification") + "error": err.Error(), + }).Errorf("no session found in request, redirecting for authorization") - r.accessForbidden(cx) - return + return r.redirectToAuthorization(cx) } + // step: inject the user into the context + cx.Set(userContextName, user) - // step: check if we are refreshing the access tokens and if not re-auth - if !r.config.EnableRefreshTokens { - log.WithFields(log.Fields{ - "email": user.name, - "expired_on": user.expiresAt.String(), - "client_ip": clientIP, - }).Errorf("session expired and access token refreshing is disabled") + // step: skip if we are running skip-token-verification + if r.config.SkipTokenVerification { + log.Warnf("skip token verification enabled, skipping verification - TESTING ONLY") - r.redirectToAuthorization(cx) - return - } + if user.isExpired() { + log.WithFields(log.Fields{ + "client_ip": clientIP, + "username": user.name, + "expired_on": user.expiresAt.String(), + }).Errorf("the session has expired and verification switch off") - log.WithFields(log.Fields{ - "email": user.email, - "client_ip": clientIP, - }).Infof("accces token for user has expired, attemping to refresh the token") + return r.redirectToAuthorization(cx) + } + } else { + if err := verifyToken(r.client, user.token); err != nil { + // step: if the error post verification is anything other than a token + // expired error we immediately throw an access forbidden - as there is + // something messed up in the token + if err != ErrAccessTokenExpired { + log.WithFields(log.Fields{ + "client_ip": clientIP, + "error": err.Error(), + }).Errorf("access token failed verification") + + return r.accessForbidden(cx) + } - // step: check if the user has refresh token - refresh, err := r.retrieveRefreshToken(cx.Request, user) - if err != nil { - log.WithFields(log.Fields{ - "email": user.email, - "error": err.Error(), - "client_ip": clientIP, - }).Errorf("unable to find a refresh token for user") + // step: check if we are refreshing the access tokens and if not re-auth + if !r.config.EnableRefreshTokens { + log.WithFields(log.Fields{ + "client_ip": clientIP, + "email": user.name, + "expired_on": user.expiresAt.String(), + }).Errorf("session expired and access token refreshing is disabled") - r.redirectToAuthorization(cx) - return - } + return r.redirectToAuthorization(cx) + } - // attempt to refresh the access token - token, _, err := getRefreshedToken(r.client, refresh) - if err != nil { - switch err { - case ErrRefreshTokenExpired: log.WithFields(log.Fields{ - "email": user.email, "client_ip": clientIP, - }).Warningf("refresh token has expired, cannot retrieve access token") - - r.clearAllCookies(cx) - default: - log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to refresh the access token") - } - - r.redirectToAuthorization(cx) - return - } - - // get the expiration of the new access token - expiresIn := r.getAccessCookieExpiration(token, refresh) + "email": user.email, + }).Infof("accces token for user has expired, attemping to refresh the token") + + // step: check if the user has refresh token + refresh, err := r.retrieveRefreshToken(cx.Request(), user) + if err != nil { + log.WithFields(log.Fields{ + "client_ip": clientIP, + "email": user.email, + "error": err.Error(), + }).Errorf("unable to find a refresh token for user") + + return r.redirectToAuthorization(cx) + } - log.WithFields(log.Fields{ - "client_ip": clientIP, - "cookie_name": r.config.CookieAccessName, - "email": user.email, - "expires_in": expiresIn.String(), - }).Infof("injecting the refreshed access token cookie") - - // step: inject the refreshed access token - r.dropAccessTokenCookie(cx, token.Encode(), expiresIn) - - if r.useStore() { - go func(old, new jose.JWT, state string) { - if err := r.DeleteRefreshToken(old); err != nil { - log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to remove old token") + // attempt to refresh the access token + token, _, err := getRefreshedToken(r.client, refresh) + if err != nil { + switch err { + case ErrRefreshTokenExpired: + log.WithFields(log.Fields{ + "client_ip": clientIP, + "email": user.email, + }).Warningf("refresh token has expired, cannot retrieve access token") + + r.clearAllCookies(cx.Request(), cx.Response().Writer) + default: + log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to refresh the access token") + } + + return r.redirectToAuthorization(cx) } - if err := r.StoreRefreshToken(new, state); err != nil { - log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to store refresh token") - return + // get the expiration of the new access token + expiresIn := r.getAccessCookieExpiration(token, refresh) + + log.WithFields(log.Fields{ + "client_ip": clientIP, + "cookie_name": r.config.CookieAccessName, + "email": user.email, + "expires_in": expiresIn.String(), + }).Infof("injecting the refreshed access token cookie") + + // step: inject the refreshed access token + r.dropAccessTokenCookie(cx.Request(), cx.Response().Writer, token.Encode(), expiresIn) + + if r.useStore() { + go func(old, new jose.JWT, state string) { + if err := r.DeleteRefreshToken(old); err != nil { + log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to remove old token") + } + if err := r.StoreRefreshToken(new, state); err != nil { + log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed to store refresh token") + return + } + }(user.token, token, refresh) } - }(user.token, token, refresh) + // step: update the with the new access token + user.token = token + // step: inject the user into the context + cx.Set(userContextName, user) + } } - - // step: update the with the new access token - user.token = token - - // step: inject the user into the context - cx.Set(userContextName, user) + return next(cx) } - - cx.Next() } } // admissionMiddleware is responsible checking the access token against the protected resource -func (r *oauthProxy) admissionMiddleware() gin.HandlerFunc { - // step: compile the regex's for the claims +func (r *oauthProxy) admissionMiddleware(resource *Resource) echo.MiddlewareFunc { claimMatches := make(map[string]*regexp.Regexp, 0) for k, v := range r.config.MatchClaims { claimMatches[k] = regexp.MustCompile(v) } - return func(cx *gin.Context) { - // step: is this resource enforcing? - if _, found := cx.Get(cxEnforce); !found { - return - } - - resource := cx.MustGet(cxEnforce).(*Resource) - user := cx.MustGet(userContextName).(*userContext) - - // step: check the audience for the token is us - if r.config.ClientID != "" && !user.isAudience(r.config.ClientID) { - log.WithFields(log.Fields{ - "email": user.email, - "expired_on": user.expiresAt.String(), - "issuer": user.audience, - "client_id": r.config.ClientID, - }).Warnf("access token audience is not us, redirecting back for authentication") - - r.accessForbidden(cx) - return - } - - // step: we need to check the roles - if roles := len(resource.Roles); roles > 0 { - if !hasRoles(resource.Roles, user.roles) { - log.WithFields(log.Fields{ - "access": "denied", - "email": user.email, - "resource": resource.URL, - "required": resource.getRoles(), - }).Warnf("access denied, invalid roles") - - r.accessForbidden(cx) - return + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + if found := cx.Get(revokeContextName); found != nil { + return nil } - } + user := cx.Get(userContextName).(*userContext) - // step: if we have any claim matching, lets validate the tokens has the claims - for claimName, match := range claimMatches { - // step: if the claim is NOT in the token, we access deny - value, found, err := user.claims.StringClaim(claimName) - if err != nil { + // step: check the audience for the token is us + if r.config.ClientID != "" && !user.isAudience(r.config.ClientID) { log.WithFields(log.Fields{ - "access": "denied", - "email": user.email, - "resource": resource.URL, - "error": err.Error(), - }).Errorf("unable to extract the claim from token") - - r.accessForbidden(cx) - return + "client_id": r.config.ClientID, + "email": user.email, + "expired_on": user.expiresAt.String(), + "issuer": user.audience, + }).Warnf("access token audience is not us, redirecting back for authentication") + + return r.accessForbidden(cx) } - if !found { - log.WithFields(log.Fields{ - "access": "denied", - "email": user.email, - "resource": resource.URL, - "claim": claimName, - }).Warnf("the token does not have the claim") - - r.accessForbidden(cx) - return + // step: we need to check the roles + if roles := len(resource.Roles); roles > 0 { + if !hasRoles(resource.Roles, user.roles) { + log.WithFields(log.Fields{ + "access": "denied", + "email": user.email, + "resource": resource.URL, + "required": resource.getRoles(), + }).Warnf("access denied, invalid roles") + + return r.accessForbidden(cx) + } } - // step: check the claim is the same - if !match.MatchString(value) { - log.WithFields(log.Fields{ - "access": "denied", - "email": user.email, - "resource": resource.URL, - "claim": claimName, - "issued": value, - "required": match, - }).Warnf("the token claims does not match claim requirement") - - r.accessForbidden(cx) - return + // step: if we have any claim matching, lets validate the tokens has the claims + for claimName, match := range claimMatches { + // step: if the claim is NOT in the token, we access deny + value, found, err := user.claims.StringClaim(claimName) + if err != nil { + log.WithFields(log.Fields{ + "access": "denied", + "email": user.email, + "resource": resource.URL, + "error": err.Error(), + }).Errorf("unable to extract the claim from token") + + return r.accessForbidden(cx) + } + + if !found { + log.WithFields(log.Fields{ + "access": "denied", + "claim": claimName, + "email": user.email, + "resource": resource.URL, + }).Warnf("the token does not have the claim") + + return r.accessForbidden(cx) + } + + // step: check the claim is the same + if !match.MatchString(value) { + log.WithFields(log.Fields{ + "access": "denied", + "claim": claimName, + "email": user.email, + "issued": value, + "required": match, + "resource": resource.URL, + }).Warnf("the token claims does not match claim requirement") + + return r.accessForbidden(cx) + } } - } - log.WithFields(log.Fields{ - "access": "permitted", - "email": user.email, - "resource": resource.URL, - "expires": user.expiresAt.Sub(time.Now()).String(), - }).Debugf("access permitted to resource") - } -} + log.WithFields(log.Fields{ + "access": "permitted", + "email": user.email, + "expires": user.expiresAt.Sub(time.Now()).String(), + "resource": resource.URL, + }).Debugf("access permitted to resource") -// corsMiddleware injects the CORS headers, if set, for request made to /oauth -func (r *oauthProxy) corsMiddleware(c Cors) gin.HandlerFunc { - return func(cx *gin.Context) { - if len(c.Origins) > 0 { - cx.Writer.Header().Set("Access-Control-Allow-Origin", strings.Join(c.Origins, ",")) - } - if c.Credentials { - cx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - } - if len(c.ExposedHeaders) > 0 { - cx.Writer.Header().Set("Access-Control-Expose-Headers", strings.Join(c.ExposedHeaders, ",")) - } - if len(c.Methods) > 0 { - cx.Writer.Header().Set("Access-Control-Allow-Methods", strings.Join(c.Methods, ",")) - } - if len(c.Headers) > 0 { - cx.Writer.Header().Set("Access-Control-Allow-Headers", strings.Join(c.Headers, ",")) - } - if c.MaxAge > 0 { - cx.Writer.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", int(c.MaxAge.Seconds()))) + return next(cx) } } } -// // headersMiddleware is responsible for add the authentication headers for the upstream -// -func (r *oauthProxy) headersMiddleware(custom []string) gin.HandlerFunc { - // step: we don't wanna do this every time, quicker to perform once +func (r *oauthProxy) headersMiddleware(custom []string) echo.MiddlewareFunc { customClaims := make(map[string]string) for _, x := range custom { customClaims[x] = fmt.Sprintf("X-Auth-%s", toHeader(x)) } - return func(cx *gin.Context) { - // step: add any custom headers to the request - for k, v := range r.config.Headers { - cx.Request.Header.Set(k, v) - } - - // step: retrieve the user context if any - if user, found := cx.Get(userContextName); found { - id := user.(*userContext) - - cx.Request.Header.Set("X-Auth-Userid", id.name) - cx.Request.Header.Set("X-Auth-Subject", id.id) - cx.Request.Header.Set("X-Auth-Username", id.name) - cx.Request.Header.Set("X-Auth-Email", id.email) - cx.Request.Header.Set("X-Auth-ExpiresIn", id.expiresAt.String()) - cx.Request.Header.Set("X-Auth-Token", id.token.Encode()) - cx.Request.Header.Set("X-Auth-Roles", strings.Join(id.roles, ",")) - - // step: add the authorization header if requested - if r.config.EnableAuthorizationHeader { - cx.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", id.token.Encode())) - } + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + + // step: retrieve the user context if any + if user := cx.Get(userContextName); user != nil { + id := user.(*userContext) + cx.Request().Header.Set("X-Auth-Email", id.email) + cx.Request().Header.Set("X-Auth-ExpiresIn", id.expiresAt.String()) + cx.Request().Header.Set("X-Auth-Roles", strings.Join(id.roles, ",")) + cx.Request().Header.Set("X-Auth-Subject", id.id) + cx.Request().Header.Set("X-Auth-Token", id.token.Encode()) + cx.Request().Header.Set("X-Auth-Userid", id.name) + cx.Request().Header.Set("X-Auth-Username", id.name) + + // step: add the authorization header if requested + if r.config.EnableAuthorizationHeader { + cx.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", id.token.Encode())) + } - // step: inject any custom claims - for claim, header := range customClaims { - if claim, found := id.claims[claim]; found { - cx.Request.Header.Set(header, fmt.Sprintf("%v", claim)) + // step: inject any custom claims + for claim, header := range customClaims { + if claim, found := id.claims[claim]; found { + cx.Request().Header.Set(header, fmt.Sprintf("%v", claim)) + } } } + return next(cx) } - - cx.Request.Header.Add("X-Forwarded-For", cx.Request.RemoteAddr) - cx.Request.Header.Set("X-Forwarded-Host", cx.Request.Host) - cx.Request.Header.Set("X-Forwarded-Proto", cx.Request.Header.Get("X-Forwarded-Proto")) } } // securityMiddleware performs numerous security checks on the request -func (r *oauthProxy) securityMiddleware() gin.HandlerFunc { +func (r *oauthProxy) securityMiddleware() echo.MiddlewareFunc { log.Info("enabling the security filter middleware") - // step: create the security options secure := secure.New(secure.Options{ AllowedHosts: r.config.Hostnames, BrowserXssFilter: r.config.EnableBrowserXSSFilter, @@ -443,11 +377,13 @@ func (r *oauthProxy) securityMiddleware() gin.HandlerFunc { SSLRedirect: r.config.EnableHTTPSRedirect, }) - return func(cx *gin.Context) { - if err := secure.Process(cx.Writer, cx.Request); err != nil { - log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed security middleware") - - cx.Abort() + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(cx echo.Context) error { + if err := secure.Process(cx.Response().Writer, cx.Request()); err != nil { + log.WithFields(log.Fields{"error": err.Error()}).Errorf("failed security middleware") + return r.accessForbidden(cx) + } + return next(cx) } } } diff --git a/middleware_test.go b/middleware_test.go index d257738e3..1a8bed635 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -16,487 +16,765 @@ limitations under the License. package main import ( + "io/ioutil" "net/http" + "net/http/httptest" "strings" "testing" "time" + log "github.com/Sirupsen/logrus" "github.com/coreos/go-oidc/jose" "github.com/go-resty/resty" + "github.com/labstack/echo/middleware" "github.com/stretchr/testify/assert" ) type fakeRequest struct { - URI string - Method string - Redirects bool - HasToken bool - NotSigned bool - Expires time.Duration - Roles []string - Expects int + BasicAuth bool + Cookies []*http.Cookie + Expires time.Duration + FormValues map[string]string + HasCookieToken bool + HasLogin bool + HasToken bool + Headers map[string]string + Method string + NotSigned bool + OnResponse func(int, *resty.Request, *resty.Response) + Password string + ProxyRequest bool + RawToken string + Redirects bool + Roles []string + TokenClaims jose.Claims + URI string + URL string + Username string + ExpectedCode int + ExpectedContent string + ExpectedContentContains string + ExpectedCookies []string + ExpectedHeaders map[string]string + ExpectedProxyHeaders map[string]string + ExpectedLocation string + ExpectedProxy bool } -func makeFakesRequests(t *testing.T, reqs []fakeRequest, cfg *Config) { - cfg.SkipTokenVerification = false - px, idp, svc := newTestProxyService(cfg) - for i, c := range reqs { - px.config.NoRedirects = !c.Redirects - // step: make the client - hc := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) +type fakeProxy struct { + config *Config + idp *fakeAuthServer + proxy *oauthProxy + server *httptest.Server + cookies map[string]*http.Cookie +} + +func newFakeProxy(c *Config) *fakeProxy { + log.SetOutput(ioutil.Discard) + if c == nil { + c = newFakeKeycloakConfig() + } + auth := newFakeAuthServer() + c.DiscoveryURL = auth.getLocation() + c.RevocationEndpoint = auth.getRevocationURL() + proxy, err := newProxy(c) + if err != nil { + panic("failed to create fake proxy service, error: " + err.Error()) + } + proxy.upstream = &fakeUpstreamService{} + service := httptest.NewServer(proxy.router) + c.RedirectionURL = service.URL + // step: we need to update the client configs + if proxy.client, proxy.idp, proxy.idpClient, err = newOpenIDClient(c); err != nil { + panic("failed to recreate the openid client, error: " + err.Error()) + } + + return &fakeProxy{c, auth, proxy, service, make(map[string]*http.Cookie, 0)} +} + +// RunTests performs a series of requests against a fake proxy service +func (f *fakeProxy) RunTests(t *testing.T, requests []fakeRequest) { + defer func() { + f.idp.Close() + f.server.Close() + }() + for i, c := range requests { + var upstream fakeUpstreamResponse + + f.config.NoRedirects = !c.Redirects + // we need to set any defaults + if c.Method == "" { + c.Method = http.MethodGet + } + // create a http client + client := resty.New() + request := client.SetRedirectPolicy(resty.NoRedirectPolicy()).R() + + // are we performing a oauth login beforehand + if c.HasLogin { + if err := f.performUserLogin(c.URI); err != nil { + t.Errorf("case %d, unable to login to oauth server, error: %s", i, err) + return + } + } + if len(f.cookies) > 0 { + for _, k := range f.cookies { + client.SetCookie(k) + } + } + if c.ExpectedProxy { + request.SetResult(&upstream) + } + if c.ProxyRequest { + request.SetProxy(f.server.URL) + } + if c.BasicAuth { + request.SetBasicAuth(c.Username, c.Password) + } + if c.RawToken != "" { + setRequestAuthentication(f.config, client, request, &c, c.RawToken) + } + if len(c.Cookies) > 0 { + client.SetCookies(c.Cookies) + } + if len(c.Headers) > 0 { + request.SetHeaders(c.Headers) + } + if c.FormValues != nil { + request.SetFormData(c.FormValues) + } if c.HasToken { - token := newTestToken(idp.getLocation()) + token := newTestToken(f.idp.getLocation()) + if c.TokenClaims != nil && len(c.TokenClaims) > 0 { + token.merge(c.TokenClaims) + } if len(c.Roles) > 0 { - token.setRealmsRoles(c.Roles) + token.addRealmRoles(c.Roles) } - if c.Expires > 0 { + if c.Expires > 0 || c.Expires < 0 { token.setExpiration(time.Now().Add(c.Expires)) } - if !c.NotSigned { - signed, err := idp.signToken(token.claims) - if !assert.NoError(t, err, "case %d, unable to sign the token, error: %s", i, err) { - continue - } - hc.SetAuthToken(signed.Encode()) + if c.NotSigned { + authToken := token.getToken() + setRequestAuthentication(f.config, client, request, &c, authToken.Encode()) } else { - jwt := token.getToken() - hc.SetAuthToken(jwt.Encode()) + signed, _ := f.idp.signToken(token.claims) + setRequestAuthentication(f.config, client, request, &c, signed.Encode()) } } - // step: make the request - resp, err := hc.R().Execute(c.Method, svc+c.URI) + + // step: execute the request + var resp *resty.Response + var err error + switch c.URL { + case "": + resp, err = request.Execute(c.Method, f.server.URL+c.URI) + default: + resp, err = request.Execute(c.Method, c.URL) + } if err != nil { if !strings.Contains(err.Error(), "Auto redirect is disable") { assert.NoError(t, err, "case %d, unable to make request, error: %s", i, err) continue } } - // step: check against the expected - assert.Equal(t, c.Expects, resp.StatusCode(), "case %d, uri: %s, expected: %d, got: %d", - i, c.URI, c.Expects, resp.StatusCode()) + status := resp.StatusCode() + if c.ExpectedCode != 0 { + assert.Equal(t, c.ExpectedCode, status, "case %d, expected: %d, got: %d", i, c.URI, c.ExpectedCode, status) + } + if c.ExpectedLocation != "" { + l := resp.Header().Get("Location") + assert.Equal(t, c.ExpectedLocation, l, "case %d, expected location: %s, got: %s", i, c.ExpectedLocation, l) + } + if len(c.ExpectedHeaders) > 0 { + for k, v := range c.ExpectedHeaders { + e := resp.Header().Get(k) + assert.Equal(t, v, e, "case %d, expected header %s=%s, got: %s", i, k, v, e) + } + } + if c.ExpectedProxy { + assert.NotEmpty(t, resp.Header().Get(testProxyAccepted), "case %d, did not proxy request", i) + } else { + assert.Empty(t, resp.Header().Get(testProxyAccepted), "case %d, should NOT proxy request", i) + } + if c.ExpectedProxyHeaders != nil && len(c.ExpectedProxyHeaders) > 0 { + for k, v := range c.ExpectedProxyHeaders { + headers := upstream.Headers + assert.Equal(t, v, headers.Get(k), "case %d, expected proxy header %s=%s, got: %s", i, k, v, headers.Get(k)) + } + } + if c.ExpectedContent != "" { + e := string(resp.Body()) + assert.Equal(t, c.ExpectedContent, e, "case %d, expected content: %s, got: %s", i, c.ExpectedContent, e) + } + if c.ExpectedContentContains != "" { + e := string(resp.Body()) + assert.Contains(t, e, c.ExpectedContentContains, "case %d, expected content: %s, got: %s", i, c.ExpectedContentContains, e) + } + if len(c.ExpectedCookies) > 0 { + l := len(c.ExpectedCookies) + g := len(resp.Cookies()) + assert.Equal(t, l, g, "case %d, expected %d cookies, got: %d", i, l, g) + for _, x := range c.ExpectedCookies { + assert.NotNil(t, findCookie(x, resp.Cookies()), "case %d, expected cookie %s not found", i, x) + } + } + if c.OnResponse != nil { + c.OnResponse(i, request, resp) + } + } +} + +func (f *fakeProxy) performUserLogin(uri string) error { + resp, err := makeTestCodeFlowLogin(f.server.URL + uri) + if err != nil { + return err + } + for _, c := range resp.Cookies() { + if c.Name == f.config.CookieAccessName || c.Name == f.config.CookieRefreshName { + f.cookies[c.Name] = &http.Cookie{ + Name: c.Name, + Path: "/", + Domain: "127.0.0.1", + Value: c.Value, + } + } } + + return nil +} + +func setRequestAuthentication(cfg *Config, client *resty.Client, request *resty.Request, c *fakeRequest, token string) { + switch c.HasCookieToken { + case true: + client.SetCookie(&http.Cookie{ + Name: cfg.CookieAccessName, + Path: "/", + Value: token, + }) + default: + request.SetAuthToken(token) + } +} + +func TestMetricsMiddleware(t *testing.T) { + cfg := newFakeKeycloakConfig() + cfg.EnableMetrics = true + cfg.LocalhostMetrics = true + requests := []fakeRequest{ + { + URI: oauthURL + metricsURL, + ExpectedCode: http.StatusOK, + ExpectedContentContains: "http_request_total", + }, + { + URI: oauthURL + metricsURL, + Headers: map[string]string{ + "X-Forwarded-For": "10.0.0.1", + }, + ExpectedCode: http.StatusForbidden, + }, + } + newFakeProxy(cfg).RunTests(t, requests) } func TestOauthRequests(t *testing.T) { cfg := newFakeKeycloakConfig() requests := []fakeRequest{ { - URI: "/oauth/authorize", - Redirects: true, - Expects: http.StatusTemporaryRedirect, + URI: "/oauth/authorize", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, }, { - URI: "/oauth/callback", - Redirects: true, - Expects: http.StatusBadRequest, + URI: "/oauth/callback", + Redirects: true, + ExpectedCode: http.StatusBadRequest, }, { - URI: "/oauth/health", - Redirects: true, - Expects: http.StatusOK, + URI: "/oauth/health", + Redirects: true, + ExpectedCode: http.StatusOK, }, } - makeFakesRequests(t, requests, cfg) + newFakeProxy(cfg).RunTests(t, requests) } func TestStrangeAdminRequests(t *testing.T) { cfg := newFakeKeycloakConfig() cfg.Resources = []*Resource{ { - URL: "/admin", - Methods: []string{"ANY"}, + URL: "/admin*", + Methods: allHTTPMethods, Roles: []string{fakeAdminRole}, }, } requests := []fakeRequest{ { // check for escaping - URI: "//admin%2Ftest", - Redirects: true, - Expects: http.StatusTemporaryRedirect, + URI: "//admin%2Ftest", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, }, { // check for escaping - URI: "/admin%2Ftest", - Redirects: true, - Expects: http.StatusTemporaryRedirect, + URI: "///admin/../admin//%2Ftest", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, }, - { // check for prefix slashs - URI: "//admin/test", - Redirects: true, - Expects: http.StatusTemporaryRedirect, + { // check for escaping + URI: "/admin%2Ftest", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, }, { // check for prefix slashs - URI: "/admin//test", - Redirects: true, - Expects: http.StatusTemporaryRedirect, + URI: "//admin/test", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, }, - { // check for prefix slashs - URI: "/admin//test", - Redirects: false, - HasToken: true, - Expects: http.StatusForbidden, + { // check for double slashs + URI: "/admin//test", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, + }, + { // check for double slashs no redirects + URI: "/admin//test", + Redirects: false, + HasToken: true, + ExpectedCode: http.StatusForbidden, }, { // check for dodgy url - URI: "//admin/../admin/test", - Redirects: true, - Expects: http.StatusTemporaryRedirect, + URI: "//admin/../admin/test", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, }, { // check for it works - URI: "//admin/test", - HasToken: true, - Roles: []string{fakeAdminRole}, - Expects: http.StatusOK, + URI: "//admin/test", + HasToken: true, + Roles: []string{fakeAdminRole}, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, { // check for it works - URI: "//admin//test", - HasToken: true, - Roles: []string{fakeAdminRole}, - Expects: http.StatusOK, + URI: "//admin//test", + HasToken: true, + Roles: []string{fakeAdminRole}, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, { - URI: "/help/../admin/test/21", - Redirects: false, - Expects: http.StatusUnauthorized, + URI: "/help/../admin/test/21", + Redirects: false, + ExpectedCode: http.StatusUnauthorized, }, } - makeFakesRequests(t, requests, cfg) + newFakeProxy(cfg).RunTests(t, requests) } func TestWhiteListedRequests(t *testing.T) { cfg := newFakeKeycloakConfig() cfg.Resources = []*Resource{ { - URL: "/whitelist", - WhiteListed: true, - Methods: []string{"GET"}, - Roles: []string{}, - }, - { - URL: "/", - Methods: []string{"ANY"}, + URL: "/*", + Methods: allHTTPMethods, Roles: []string{fakeTestRole}, }, { - URL: "/whitelisted", + URL: "/whitelist*", WhiteListed: true, - Methods: []string{"ANY"}, - Roles: []string{fakeTestRole}, + Methods: allHTTPMethods, }, } requests := []fakeRequest{ { // check whitelisted is passed - URI: "/whitelist", - Expects: http.StatusOK, + URI: "/whitelist", + ExpectedCode: http.StatusOK, + ExpectedProxy: true, }, { // check whitelisted is passed - URI: "/whitelist/test", - Expects: http.StatusOK, + URI: "/whitelist/test", + ExpectedCode: http.StatusOK, + ExpectedProxy: true, + }, + { + URI: "/test", + HasToken: true, + Roles: []string{"nothing"}, + ExpectedCode: http.StatusForbidden, + }, + { + URI: "/", + ExpectedCode: http.StatusUnauthorized, }, { - URI: "/", - Expects: http.StatusUnauthorized, + URI: "/", + HasToken: true, + ExpectedProxy: true, + Roles: []string{fakeTestRole}, + ExpectedCode: http.StatusOK, }, } - makeFakesRequests(t, requests, cfg) + newFakeProxy(cfg).RunTests(t, requests) } func TestRolePermissionsMiddleware(t *testing.T) { cfg := newFakeKeycloakConfig() cfg.Resources = []*Resource{ { - URL: "/admin", - Methods: []string{"ANY"}, + URL: "/admin*", + Methods: allHTTPMethods, Roles: []string{fakeAdminRole}, }, { - URL: "/test", + URL: "/test*", Methods: []string{"GET"}, Roles: []string{fakeTestRole}, }, { - URL: "/test_admin_role", + URL: "/test_admin_role*", Methods: []string{"GET"}, Roles: []string{fakeAdminRole, fakeTestRole}, }, { - URL: "/whitelist", - WhiteListed: true, - Methods: []string{"GET"}, - Roles: []string{}, + URL: "/section/*", + Methods: allHTTPMethods, + Roles: []string{fakeAdminRole}, }, { - URL: "/", - Methods: []string{"ANY"}, + URL: "/section/one", + Methods: allHTTPMethods, + Roles: []string{"one"}, + }, + { + URL: "/whitelist", + Methods: []string{"GET"}, + Roles: []string{}, + }, + { + URL: "/*", + Methods: allHTTPMethods, Roles: []string{fakeTestRole}, }, } - // test cases requests := []fakeRequest{ { - URI: "/", - Expects: http.StatusUnauthorized, + URI: "/", + ExpectedCode: http.StatusUnauthorized, }, { // check for redirect - URI: "/", - Redirects: true, - Expects: http.StatusTemporaryRedirect, + URI: "/", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, }, - { // check with a token - URI: "/", - Redirects: false, - HasToken: true, - Expects: http.StatusForbidden, + { // check with a token but not test role + URI: "/", + Redirects: false, + HasToken: true, + ExpectedCode: http.StatusForbidden, }, { // check with a token and wrong roles - URI: "/", - Redirects: false, - HasToken: true, - Roles: []string{"one", "two"}, - Expects: http.StatusForbidden, + URI: "/", + Redirects: false, + HasToken: true, + Roles: []string{"one", "two"}, + ExpectedCode: http.StatusForbidden, }, { // token, wrong roles - URI: "/test", - Redirects: false, - HasToken: true, - Roles: []string{"bad_role"}, - Expects: http.StatusForbidden, - }, - { // token, wrong roles, no 'get' method - URI: "/test", - Method: http.MethodPost, - Redirects: false, - HasToken: true, - Roles: []string{"bad_role"}, - Expects: http.StatusOK, + URI: "/test", + Redirects: false, + HasToken: true, + Roles: []string{"bad_role"}, + ExpectedCode: http.StatusForbidden, + }, + { // token, wrong roles, no 'get' method (5) + URI: "/test", + Method: http.MethodPost, + Redirects: false, + HasToken: true, + Roles: []string{"bad_role"}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, }, { // check with correct token - URI: "/test", - Redirects: false, - HasToken: true, - Roles: []string{fakeTestRole}, - Expects: http.StatusOK, - }, - { // check with correct token - URI: "/", - Redirects: false, - HasToken: true, - Roles: []string{fakeTestRole}, - Expects: http.StatusOK, + URI: "/test", + Redirects: false, + HasToken: true, + Roles: []string{fakeTestRole}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, + }, + { // check with correct token on base + URI: "/", + Redirects: false, + HasToken: true, + Roles: []string{fakeTestRole}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, }, { // check with correct token, not signed - URI: "/", - Redirects: false, - HasToken: true, - NotSigned: true, - Roles: []string{fakeTestRole}, - Expects: http.StatusForbidden, + URI: "/", + Redirects: false, + HasToken: true, + NotSigned: true, + Roles: []string{fakeTestRole}, + ExpectedCode: http.StatusForbidden, }, { // check with correct token, signed - URI: "/admin/page", - Method: http.MethodPost, - Redirects: false, - HasToken: true, - Roles: []string{fakeTestRole}, - Expects: http.StatusForbidden, + URI: "/admin/page", + Method: http.MethodPost, + Redirects: false, + HasToken: true, + Roles: []string{fakeTestRole}, + ExpectedCode: http.StatusForbidden, + }, + { // check with correct token, signed, wrong roles (10) + URI: "/admin/page", + Redirects: false, + HasToken: true, + Roles: []string{fakeTestRole}, + ExpectedCode: http.StatusForbidden, }, { // check with correct token, signed, wrong roles - URI: "/admin/page", - Redirects: false, - HasToken: true, - Roles: []string{fakeTestRole}, - Expects: http.StatusForbidden, - }, - { // check with correct token, signed, wrong roles - URI: "/admin/page", - Redirects: false, - HasToken: true, - Roles: []string{fakeTestRole, fakeAdminRole}, - Expects: http.StatusOK, + URI: "/admin/page", + Redirects: false, + HasToken: true, + Roles: []string{fakeTestRole, fakeAdminRole}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, }, { // strange url - URI: "/admin/..//admin/page", - Redirects: false, - Expects: http.StatusUnauthorized, + URI: "/admin/..//admin/page", + Redirects: false, + ExpectedCode: http.StatusUnauthorized, }, { // strange url, token - URI: "/admin/../admin", - Redirects: false, - HasToken: true, - Roles: []string{"hehe"}, - Expects: http.StatusForbidden, + URI: "/admin/../admin", + Redirects: false, + HasToken: true, + Roles: []string{"hehe"}, + ExpectedCode: http.StatusForbidden, }, { // strange url, token - URI: "/test/../admin", - Redirects: false, - HasToken: true, - Expects: http.StatusForbidden, - }, - { // strange url, token, role - URI: "/test/../admin", - Redirects: false, - HasToken: true, - Roles: []string{fakeAdminRole}, - Expects: http.StatusOK, - }, - { // strange url, token, wrong roles - URI: "/test/../admin", - Redirects: false, - HasToken: true, - Roles: []string{fakeAdminRole}, - Expects: http.StatusOK, + URI: "/test/../admin", + Redirects: false, + HasToken: true, + ExpectedCode: http.StatusForbidden, + }, + { // strange url, token, role (15) + URI: "/test/../admin", + Redirects: false, + HasToken: true, + Roles: []string{fakeAdminRole}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, + }, + { // strange url, token, but good token + URI: "/test/../admin", + Redirects: false, + HasToken: true, + Roles: []string{fakeAdminRole}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, }, { // strange url, token, wrong roles - URI: "/test/../admin", - Redirects: false, - HasToken: true, - Roles: []string{fakeTestRole}, - Expects: http.StatusForbidden, + URI: "/test/../admin", + Redirects: false, + HasToken: true, + Roles: []string{fakeTestRole}, + ExpectedCode: http.StatusForbidden, + }, + { // check with a token admin test role + URI: "/test_admin_role", + Redirects: false, + HasToken: true, + ExpectedCode: http.StatusForbidden, + }, + { // check with a token but without both roles + URI: "/test_admin_role", + Redirects: false, + HasToken: true, + ExpectedCode: http.StatusForbidden, + Roles: []string{fakeAdminRole}, + }, + { // check with a token with both roles (20) + URI: "/test_admin_role", + Redirects: false, + HasToken: true, + Roles: []string{fakeAdminRole, fakeTestRole}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, + }, + { + URI: "/section/test1", + Redirects: false, + HasToken: true, + Roles: []string{}, + ExpectedCode: http.StatusForbidden, + }, + { + URI: "/section/test", + Redirects: false, + HasToken: true, + Roles: []string{fakeTestRole, fakeAdminRole}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, + }, + { + URI: "/section/one", + Redirects: false, + HasToken: true, + Roles: []string{fakeTestRole, fakeAdminRole}, + ExpectedCode: http.StatusForbidden, + }, + { + URI: "/section/one", + Redirects: false, + HasToken: true, + Roles: []string{"one"}, + ExpectedCode: http.StatusOK, + ExpectedProxy: true, }, } - makeFakesRequests(t, requests, cfg) + newFakeProxy(cfg).RunTests(t, requests) } func TestCrossSiteHandler(t *testing.T) { cases := []struct { - Cors Cors - Headers map[string]string + Cors middleware.CORSConfig + Request fakeRequest }{ { - Cors: Cors{ - Origins: []string{"*"}, + Cors: middleware.CORSConfig{ + AllowOrigins: []string{"*"}, }, - Headers: map[string]string{ - "Access-Control-Allow-Origin": "*", + Request: fakeRequest{ + URI: fakeAuthAllURL, + ExpectedHeaders: map[string]string{ + "Access-Control-Allow-Origin": "*", + }, }, }, { - Cors: Cors{ - Origins: []string{"*", "https://examples.com"}, + Cors: middleware.CORSConfig{ + AllowOrigins: []string{"*", "https://examples.com"}, }, - Headers: map[string]string{ - "Access-Control-Allow-Origin": "*,https://examples.com", + Request: fakeRequest{ + URI: fakeAuthAllURL, + ExpectedHeaders: map[string]string{ + "Access-Control-Allow-Origin": "*", + }, }, }, { - Cors: Cors{ - Origins: []string{"*", "https://examples.com"}, - Methods: []string{"GET", "POST"}, + Cors: middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST"}, }, - Headers: map[string]string{ - "Access-Control-Allow-Origin": "*,https://examples.com", - "Access-Control-Allow-Methods": "GET,POST", + Request: fakeRequest{ + URI: fakeAuthAllURL, + Method: http.MethodOptions, + ExpectedHeaders: map[string]string{ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,POST", + }, }, }, } - for i, c := range cases { + for _, c := range cases { cfg := newFakeKeycloakConfig() - // update the cors options - cfg.EnableCorsGlobal = true - cfg.NoRedirects = false - cfg.CorsCredentials = c.Cors.Credentials - cfg.CorsExposedHeaders = c.Cors.ExposedHeaders - cfg.CorsHeaders = c.Cors.Headers - cfg.CorsMaxAge = c.Cors.MaxAge - cfg.CorsMethods = c.Cors.Methods - cfg.CorsOrigins = c.Cors.Origins - // create the test service - svc := newTestServiceWithConfig(cfg) - // login and get a token - token, err := makeTestOauthLogin(svc + fakeAuthAllURL) - if err != nil { - t.Errorf("case %d, unable to login to service, error: %s", i, err) - continue - } - // make a request and check the response - var response testUpstreamResponse - resp, err := resty.New().R(). - SetHeader("Content-Type", "application/json"). - SetAuthToken(token). - SetResult(&response). - Get(svc + fakeAuthAllURL) - if !assert.NoError(t, err, "case %d, unable to make request, error: %s", i, err) { - continue - } - // make sure we got a successfully response - if !assert.Equal(t, http.StatusOK, resp.StatusCode(), "case %d expected response: %d, got: %d", i, http.StatusOK, resp.StatusCode()) { - continue - } - // parse the response - assert.NotEmpty(t, response.Headers, "case %d the headers should not be empty", i) - // check the headers are present - for k, v := range c.Headers { - assert.NotEmpty(t, resp.Header().Get(k), "case %d did not find header: %s", i, k) - assert.Equal(t, v, resp.Header().Get(k), "case %d expected: %s, got: %s", i, v, resp.Header().Get(k)) + cfg.CorsCredentials = c.Cors.AllowCredentials + cfg.CorsExposedHeaders = c.Cors.ExposeHeaders + cfg.CorsHeaders = c.Cors.AllowHeaders + cfg.CorsMaxAge = time.Duration(time.Duration(c.Cors.MaxAge) * time.Second) + cfg.CorsMethods = c.Cors.AllowMethods + cfg.CorsOrigins = c.Cors.AllowOrigins + + newFakeProxy(cfg).RunTests(t, []fakeRequest{c.Request}) + } +} + +func TestCheckRefreshTokens(t *testing.T) { + cfg := newFakeKeycloakConfig() + cfg.EnableRefreshTokens = true + cfg.EncryptionKey = "ZSeCYDUxIlhDrmPpa1Ldc7il384esSF2" + fn := func(no int, req *resty.Request, resp *resty.Response) { + if no == 0 { + <-time.After(1000 * time.Millisecond) } } + p := newFakeProxy(cfg) + p.idp.setTokenExpiration(time.Duration(1000 * time.Millisecond)) + + requests := []fakeRequest{ + { + URI: fakeAuthAllURL, + HasLogin: true, + Redirects: true, + OnResponse: fn, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, + }, + { + URI: fakeAuthAllURL, + Redirects: false, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, + ExpectedCookies: []string{cfg.CookieAccessName}, + }, + } + p.RunTests(t, requests) } func TestCustomHeadersHandler(t *testing.T) { - cs := []struct { + requests := []struct { Match []string - Claims jose.Claims - Expects map[string]string - }{ /* - { - Match: []string{"subject", "userid", "email", "username"}, - Claims: jose.Claims{ - "id": "test-subject", - "name": "rohith", - "email": "gambol99@gmail.com", + Request fakeRequest + }{ + { + Match: []string{"subject", "userid", "email", "username"}, + Request: fakeRequest{ + URI: fakeAuthAllURL, + HasToken: true, + TokenClaims: jose.Claims{ + "sub": "test-subject", + "username": "rohith", + "preferred_username": "rohith", + "email": "gambol99@gmail.com", }, - Expects: map[string]string{ + ExpectedProxyHeaders: map[string]string{ "X-Auth-Subject": "test-subject", "X-Auth-Userid": "rohith", "X-Auth-Email": "gambol99@gmail.com", "X-Auth-Username": "rohith", }, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, - { - Match: []string{"roles"}, - Claims: jose.Claims{ - "roles": []string{"a", "b", "c"}, - }, - Expects: map[string]string{ - "X-Auth-Roles": "a,b,c", - }, - },*/ + }, { Match: []string{"given_name", "family_name"}, - Claims: jose.Claims{ - "email": "gambol99@gmail.com", - "name": "Rohith Jayawardene", - "family_name": "Jayawardene", - "preferred_username": "rjayawardene", - "given_name": "Rohith", - }, - Expects: map[string]string{ - "X-Auth-Given-Name": "Rohith", - "X-Auth-Family-Name": "Jayawardene", + Request: fakeRequest{ + URI: fakeAuthAllURL, + HasToken: true, + TokenClaims: jose.Claims{ + "email": "gambol99@gmail.com", + "name": "Rohith Jayawardene", + "family_name": "Jayawardene", + "preferred_username": "rjayawardene", + "given_name": "Rohith", + }, + ExpectedProxyHeaders: map[string]string{ + "X-Auth-Given-Name": "Rohith", + "X-Auth-Family-Name": "Jayawardene", + }, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, }, } - for i, x := range cs { + for _, c := range requests { cfg := newFakeKeycloakConfig() - cfg.AddClaims = x.Match - _, idp, svc := newTestProxyService(cfg) - // create a token with those clams - token := newTestToken(idp.getLocation()) - token.mergeClaims(x.Claims) - signed, _ := idp.signToken(token.claims) - // make the request - var response testUpstreamResponse - resp, err := resty.New().SetAuthToken(signed.Encode()).R().SetResult(&response).Get(svc + fakeAuthAllURL) - if !assert.NoError(t, err, "case %d, unable to make the request, error: %s", i, err) { - continue - } - // ensure the headers - if !assert.Equal(t, http.StatusOK, resp.StatusCode(), "case %d, expected: %d, got: %d", i, http.StatusOK, resp.StatusCode()) { - continue - } - for k, v := range x.Expects { - assert.NotEmpty(t, response.Headers.Get(k), "case %d, did not have header: %s", i, k) - assert.Equal(t, v, response.Headers.Get(k), "case %d, expected: %s, got: %s", i, v, response.Headers.Get(k)) - } + cfg.AddClaims = c.Match + newFakeProxy(cfg).RunTests(t, []fakeRequest{c.Request}) } } @@ -506,7 +784,7 @@ func TestAdmissionHandlerRoles(t *testing.T) { cfg.Resources = []*Resource{ { URL: "/admin", - Methods: []string{"ANY"}, + Methods: allHTTPMethods, Roles: []string{"admin"}, }, { @@ -516,157 +794,201 @@ func TestAdmissionHandlerRoles(t *testing.T) { }, { URL: "/either", - Methods: []string{"ANY"}, + Methods: allHTTPMethods, Roles: []string{"admin", "test"}, }, { URL: "/", - Methods: []string{"ANY"}, + Methods: allHTTPMethods, }, } - _, idp, svc := newTestProxyService(cfg) - cs := []struct { - Method string - URL string - Roles []string - Expected int - }{ + requests := []fakeRequest{ { - URL: "/admin", - Roles: []string{}, - Expected: http.StatusForbidden, + URI: "/admin", + Roles: []string{}, + HasToken: true, + ExpectedCode: http.StatusForbidden, }, { - URL: "/admin", - Roles: []string{"admin"}, - Expected: http.StatusOK, + URI: "/admin", + Roles: []string{"admin"}, + HasToken: true, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, { - URL: "/test", - Expected: http.StatusOK, - Roles: []string{"test"}, + URI: "/test", + Roles: []string{"test"}, + HasToken: true, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, { - URL: "/either", - Expected: http.StatusOK, - Roles: []string{"test", "admin"}, + URI: "/either", + Roles: []string{"test", "admin"}, + HasToken: true, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, { - URL: "/either", - Expected: http.StatusForbidden, - Roles: []string{"no_roles"}, + URI: "/either", + Roles: []string{"no_roles"}, + HasToken: true, + ExpectedCode: http.StatusForbidden, }, { - URL: "/", - Expected: http.StatusOK, + URI: "/", + HasToken: true, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, } - - for _, c := range cs { - // step: create token from the toles - token := newTestToken(idp.getLocation()) - if len(c.Roles) > 0 { - token.setRealmsRoles(c.Roles) - } - jwt, err := idp.signToken(token.claims) - if !assert.NoError(t, err) { - continue - } - - // step: make the request - resp, err := resty.New().R(). - SetAuthToken(jwt.Encode()). - Get(svc + c.URL) - if !assert.NoError(t, err) { - continue - } - assert.Equal(t, c.Expected, resp.StatusCode()) - if c.Expected == http.StatusOK { - assert.NotEmpty(t, resp.Header().Get(testProxyAccepted)) - } - } + newFakeProxy(cfg).RunTests(t, requests) } -func TestRolesAdmissionHandlerClaims(t *testing.T) { - cfg := newFakeKeycloakConfig() - cfg.NoRedirects = true - cfg.Resources = []*Resource{ +func TestCustomHeaders(t *testing.T) { + uri := "/admin/test" + requests := []struct { + Headers map[string]string + Request fakeRequest + }{ { - URL: "/admin", - Methods: []string{"ANY"}, + Headers: map[string]string{ + "TestHeaderOne": "one", + }, + Request: fakeRequest{ + URI: "/test.html", + ExpectedProxy: true, + ExpectedProxyHeaders: map[string]string{ + "TestHeaderOne": "one", + }, + }, }, + { + Headers: map[string]string{ + "TestHeader": "test", + }, + Request: fakeRequest{ + URI: uri, + HasToken: true, + ExpectedProxy: true, + ExpectedProxyHeaders: map[string]string{ + "TestHeader": "test", + }, + }, + }, + { + Headers: map[string]string{ + "TestHeaderOne": "one", + "TestHeaderTwo": "two", + }, + Request: fakeRequest{ + URI: uri, + HasToken: true, + ExpectedProxy: true, + ExpectedProxyHeaders: map[string]string{ + "TestHeaderOne": "one", + "TestHeaderTwo": "two", + }, + }, + }, + } + for _, c := range requests { + cfg := newFakeKeycloakConfig() + cfg.Resources = []*Resource{{URL: "/admin*", Methods: allHTTPMethods}} + cfg.Headers = c.Headers + newFakeProxy(cfg).RunTests(t, []fakeRequest{c.Request}) } - cs := []struct { - Matches map[string]string - Claims jose.Claims - Expected int +} + +func TestRolesAdmissionHandlerClaims(t *testing.T) { + uri := "/admin/test" + requests := []struct { + Matches map[string]string + Request fakeRequest }{ { - Matches: map[string]string{"cal": "test"}, - Claims: jose.Claims{}, - Expected: http.StatusForbidden, + Matches: map[string]string{"cal": "test"}, + Request: fakeRequest{ + URI: uri, + HasToken: true, + ExpectedCode: http.StatusForbidden, + }, }, { - Matches: map[string]string{"item": "^tes$"}, - Claims: jose.Claims{}, - Expected: http.StatusForbidden, + Matches: map[string]string{"item": "^tes$"}, + Request: fakeRequest{ + URI: uri, + HasToken: true, + ExpectedCode: http.StatusForbidden, + }, }, { Matches: map[string]string{"item": "^tes$"}, - Claims: jose.Claims{ - "item": "tes", + Request: fakeRequest{ + URI: uri, + HasToken: true, + TokenClaims: jose.Claims{"item": "tes"}, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, + }, + }, + { + Matches: map[string]string{"item": "not_match"}, + Request: fakeRequest{ + URI: uri, + HasToken: true, + TokenClaims: jose.Claims{"item": "test"}, + ExpectedCode: http.StatusForbidden, }, - Expected: http.StatusOK, }, { Matches: map[string]string{"item": "^test", "found": "something"}, - Claims: jose.Claims{ - "item": "test", + Request: fakeRequest{ + URI: uri, + HasToken: true, + TokenClaims: jose.Claims{"item": "test"}, + ExpectedCode: http.StatusForbidden, }, - Expected: http.StatusForbidden, }, { Matches: map[string]string{"item": "^test", "found": "something"}, - Claims: jose.Claims{ - "item": "tester", - "found": "something", + Request: fakeRequest{ + URI: uri, + HasToken: true, + TokenClaims: jose.Claims{ + "item": "tester", + "found": "something", + }, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, - Expected: http.StatusOK, }, { Matches: map[string]string{"item": ".*"}, - Claims: jose.Claims{ - "item": "test", + Request: fakeRequest{ + URI: uri, + HasToken: true, + TokenClaims: jose.Claims{"item": "test"}, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, }, - Expected: http.StatusOK, }, { - Matches: map[string]string{"item": "^t.*$"}, - Claims: jose.Claims{"item": "test"}, - Expected: http.StatusOK, + Matches: map[string]string{"item": "^t.*$"}, + Request: fakeRequest{ + URI: uri, + HasToken: true, + TokenClaims: jose.Claims{"item": "test"}, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, + }, }, } - - for i, c := range cs { + for _, c := range requests { + cfg := newFakeKeycloakConfig() + cfg.Resources = []*Resource{{URL: "/admin*", Methods: allHTTPMethods}} cfg.MatchClaims = c.Matches - _, idp, svc := newTestProxyService(cfg) - - token := newTestToken(idp.getLocation()) - token.mergeClaims(c.Claims) - jwt, err := idp.signToken(token.claims) - if !assert.NoError(t, err) { - continue - } - // step: inject a resource - resp, err := resty.New().R(). - SetAuthToken(jwt.Encode()). - Get(svc + "/admin") - if !assert.NoError(t, err) { - continue - } - assert.Equal(t, c.Expected, resp.StatusCode(), "case %d failed, expected: %d but got: %d", i, c.Expected, resp.StatusCode()) - if c.Expected == http.StatusOK { - assert.NotEmpty(t, resp.Header().Get(testProxyAccepted)) - } + newFakeProxy(cfg).RunTests(t, []fakeRequest{c.Request}) } } diff --git a/misc.go b/misc.go index 06ce7ba14..cf2d578e5 100644 --- a/misc.go +++ b/misc.go @@ -24,45 +24,58 @@ import ( log "github.com/Sirupsen/logrus" "github.com/coreos/go-oidc/jose" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" ) +// revokeProxy is responsible to stopping the middleware from proxying the request +func (r *oauthProxy) revokeProxy(cx echo.Context) { + cx.Set(revokeContextName, true) +} + // accessForbidden redirects the user to the forbidden page -func (r *oauthProxy) accessForbidden(cx *gin.Context) { +func (r *oauthProxy) accessForbidden(cx echo.Context) error { + r.revokeProxy(cx) + if r.config.hasCustomForbiddenPage() { - cx.HTML(http.StatusForbidden, path.Base(r.config.ForbiddenPage), r.config.Tags) - cx.Abort() - return + tplName := path.Base(r.config.ForbiddenPage) + err := cx.Render(http.StatusForbidden, tplName, r.config.Tags) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "template": tplName, + }).Error("unable to render the template") + } + + return err } - cx.AbortWithStatus(http.StatusForbidden) + return cx.NoContent(http.StatusForbidden) } // redirectToURL redirects the user and aborts the context -func (r *oauthProxy) redirectToURL(url string, cx *gin.Context) { - cx.Redirect(http.StatusTemporaryRedirect, url) - cx.Abort() +func (r *oauthProxy) redirectToURL(url string, cx echo.Context) error { + r.revokeProxy(cx) + + return cx.Redirect(http.StatusTemporaryRedirect, url) } // redirectToAuthorization redirects the user to authorization handler -func (r *oauthProxy) redirectToAuthorization(cx *gin.Context) { +func (r *oauthProxy) redirectToAuthorization(cx echo.Context) error { + r.revokeProxy(cx) + if r.config.NoRedirects { - cx.AbortWithStatus(http.StatusUnauthorized) - return + return cx.NoContent(http.StatusUnauthorized) } - // step: add a state referrer to the authorization page - authQuery := fmt.Sprintf("?state=%s", base64.StdEncoding.EncodeToString([]byte(cx.Request.URL.RequestURI()))) + authQuery := fmt.Sprintf("?state=%s", base64.StdEncoding.EncodeToString([]byte(cx.Request().URL.RequestURI()))) // step: if verification is switched off, we can't authorization if r.config.SkipTokenVerification { log.Errorf("refusing to redirection to authorization endpoint, skip token verification switched on") - - cx.AbortWithStatus(http.StatusForbidden) - return + return cx.NoContent(http.StatusForbidden) } - r.redirectToURL(oauthURL+authorizationURL+authQuery, cx) + return r.redirectToURL(oauthURL+authorizationURL+authQuery, cx) } // getAccessCookieExpiration calucates the expiration of the access token cookie diff --git a/misc_test.go b/misc_test.go index e17103f8c..9adb121f6 100644 --- a/misc_test.go +++ b/misc_test.go @@ -18,34 +18,32 @@ package main import ( "net/http" "testing" - - "github.com/go-resty/resty" - "github.com/stretchr/testify/assert" ) func TestRedirectToAuthorizationUnauthorized(t *testing.T) { - p, _, svc := newTestProxyService(nil) - p.config.SkipTokenVerification = false - p.config.NoRedirects = true - - resp, err := resty.DefaultClient.R().Get(svc + "/admin") - assert.NoError(t, err) - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode()) + requests := []fakeRequest{ + {URI: "/admin", ExpectedCode: http.StatusUnauthorized}, + } + newFakeProxy(nil).RunTests(t, requests) } func TestRedirectToAuthorization(t *testing.T) { - p, _, svc := newTestProxyService(nil) - p.config.SkipTokenVerification = false - p.config.NoRedirects = false - - resp, _ := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()).R().Get(svc + "/admin") - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode()) + requests := []fakeRequest{ + { + URI: "/admin", + Redirects: true, + ExpectedLocation: "/oauth/authorize?state=L2FkbWlu", + ExpectedCode: http.StatusTemporaryRedirect, + }, + } + newFakeProxy(nil).RunTests(t, requests) } func TestRedirectToAuthorizationSkipToken(t *testing.T) { - p, _, svc := newTestProxyService(nil) - p.config.SkipTokenVerification = true - - resp, _ := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()).R().Get(svc + "/admin") - assert.Equal(t, http.StatusForbidden, resp.StatusCode()) + requests := []fakeRequest{ + {URI: "/admin", ExpectedCode: http.StatusUnauthorized}, + } + c := newFakeKeycloakConfig() + c.SkipTokenVerification = true + newFakeProxy(c).RunTests(t, requests) } diff --git a/oauth.go b/oauth.go index 5cc217102..c965856fc 100644 --- a/oauth.go +++ b/oauth.go @@ -16,6 +16,10 @@ limitations under the License. package main import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" "net/http" "strings" "time" @@ -32,22 +36,20 @@ func (r *oauthProxy) getOAuthClient(redirectionURL string) (*oauth2.Client, erro ID: r.config.ClientID, Secret: r.config.ClientSecret, }, - RedirectURL: redirectionURL, + AuthMethod: oauth2.AuthMethodClientSecretBasic, AuthURL: r.idp.AuthEndpoint.String(), - TokenURL: r.idp.TokenEndpoint.String(), + RedirectURL: redirectionURL, Scope: append(r.config.Scopes, oidc.DefaultScope...), - AuthMethod: oauth2.AuthMethodClientSecretBasic, + TokenURL: r.idp.TokenEndpoint.String(), }) } // verifyToken verify that the token in the user context is valid func verifyToken(client *oidc.Client, token jose.JWT) error { - // step: verify the token is whom they say they are if err := client.VerifyJWT(token); err != nil { if strings.Contains(err.Error(), "token is expired") { return ErrAccessTokenExpired } - return err } @@ -56,7 +58,6 @@ func verifyToken(client *oidc.Client, token jose.JWT) error { // getRefreshedToken attempts to refresh the access token, returning the parsed token and the time it expires or a error func getRefreshedToken(client *oidc.Client, t string) (jose.JWT, time.Time, error) { - // step: retrieve the client cl, err := client.OAuthClient() if err != nil { return jose.JWT{}, time.Time{}, err @@ -69,7 +70,6 @@ func getRefreshedToken(client *oidc.Client, t string) (jose.JWT, time.Time, erro return jose.JWT{}, time.Time{}, err } - // step: parse the access token token, identity, err := parseToken(response.AccessToken) if err != nil { return jose.JWT{}, time.Time{}, err @@ -83,47 +83,49 @@ func exchangeAuthenticationCode(client *oauth2.Client, code string) (oauth2.Toke return getToken(client, oauth2.GrantTypeAuthCode, code) } -// getUserinfo is responsible for getting the userinfo from the iDP -func getUserinfo(client *oauth2.Client, provider *oidc.ProviderConfig) (interface{}, error) { - // step: creating the http request - req, err := http.NewRequest(http.MethodGet, provider.UserInfoEndpoint.String(), nil) +// getUserinfo is responsible for getting the userinfo from the IDPD +func getUserinfo(client *oauth2.Client, endpoint string, token string) (jose.Claims, error) { + req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, err } - // step: make the resposne + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + // step: make the request resp, err := client.HttpClient().Do(req) if err != nil { return nil, err } - // step: check the status code returned if resp.StatusCode != http.StatusOK { - return nil, newAPIError("token not validate by userinfo endpoint", resp.StatusCode) + return nil, errors.New("token not validate by userinfo endpoint") + } + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var claims jose.Claims + if err := json.Unmarshal([]byte(content), &claims); err != nil { + return nil, newAPIError("unable to decode response", resp.StatusCode) } - return nil, nil + return claims, nil } // getToken retrieves a code from the provider, extracts and verified the token func getToken(client *oauth2.Client, grantType, code string) (oauth2.TokenResponse, error) { - // step: request a token from the authentication server return client.RequestToken(grantType, code) } // parseToken retrieve the user identity from the token func parseToken(t string) (jose.JWT, *oidc.Identity, error) { - // step: parse and return the token token, err := jose.ParseJWT(t) if err != nil { return jose.JWT{}, nil, err } - - // step: parse the claims claims, err := token.Claims() if err != nil { return jose.JWT{}, nil, err } - - // step: get the identity identity, err := oidc.IdentityFromClaims(claims) if err != nil { return jose.JWT{}, nil, err diff --git a/oauth_test.go b/oauth_test.go index 0f1516396..3f36d4f63 100644 --- a/oauth_test.go +++ b/oauth_test.go @@ -16,7 +16,6 @@ limitations under the License. package main import ( - "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" @@ -24,27 +23,22 @@ import ( "net/http" "net/http/httptest" "net/url" - "sync" + "strings" "testing" "time" "github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/oauth2" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" + "github.com/stretchr/testify/assert" ) -type fakeOAuthServer struct { - sync.Mutex - // the location of the service - location *url.URL - // the private key - privateKey *rsa.PrivateKey - // the jwk key - key jose.JWK - // the signer - signer jose.Signer - // the claims - claims jose.Claims +type fakeAuthServer struct { + location *url.URL + key jose.JWK + signer jose.Signer + server *httptest.Server + expiration time.Duration } const fakePrivateKey = ` @@ -77,11 +71,6 @@ Ka0WPQGKjQJhZRtqDAT3sfnrEEUa34+MkXQeKFCu6Yi0dRFic4iqOYU= -----END RSA PRIVATE KEY----- ` -const ( - validUsername = "test" - validPassword = "test" -) - type fakeDiscoveryResponse struct { AuthorizationEndpoint string `json:"authorization_endpoint"` EndSessionEndpoint string `json:"end_session_endpoint"` @@ -100,37 +89,15 @@ type fakeDiscoveryResponse struct { var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") -// -// newFakeOAuthServer simulates a oauth service -// -func newFakeOAuthServer() *fakeOAuthServer { +// newFakeAuthServer simulates a oauth service +func newFakeAuthServer() *fakeAuthServer { // step: load the private key block, _ := pem.Decode([]byte(fakePrivateKey)) - // step: parse the private key privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { panic("failed to parse the private key, error: " + err.Error()) } - - service := &fakeOAuthServer{ - claims: jose.Claims{ - "jti": "4ee75b8e-3ee6-4382-92d4-3390b4b4937b", - "exp": int(time.Now().Add(time.Duration(10) * time.Hour).Unix()), - "nbf": 0, - "iat": float64(1450372669), - "aud": "test", - "sub": "1e11e539-8256-4b3b-bda8-cc0d56cddb48", - "typ": "Bearer", - "azp": "clientid", - "session_state": "98f4c3d2-1b8c-4932-b8c4-92ec0ea7e195", - "client_session": "f0105893-369a-46bc-9661-ad8c747b1a69", - "email": "gambol99@gmail.com", - "name": "Rohith Jayawardene", - "family_name": "Jayawardene", - "preferred_username": "rjayawardene", - "given_name": "Rohith", - }, - privateKey: privateKey, + service := &fakeAuthServer{ key: jose.JWK{ ID: "test-kid", Type: "RSA", @@ -143,154 +110,167 @@ func newFakeOAuthServer() *fakeOAuthServer { signer: jose.NewSignerRSA("test-kid", *privateKey), } - gin.SetMode(gin.ReleaseMode) - r := gin.New() + r := echo.New() r.GET("auth/realms/hod-test/.well-known/openid-configuration", service.discoveryHandler) r.GET("auth/realms/hod-test/protocol/openid-connect/certs", service.keysHandler) r.GET("auth/realms/hod-test/protocol/openid-connect/token", service.tokenHandler) r.POST("auth/realms/hod-test/protocol/openid-connect/token", service.tokenHandler) r.GET("auth/realms/hod-test/protocol/openid-connect/auth", service.authHandler) r.POST("auth/realms/hod-test/protocol/openid-connect/logout", service.logoutHandler) - r.GET("auth/realms/hod-test/protocol/openid-connect/userinfo", service.userinfoHandler) + r.GET("auth/realms/hod-test/protocol/openid-connect/userinfo", service.userInfoHandler) - location, err := url.Parse(httptest.NewServer(r).URL) + service.server = httptest.NewServer(r) + location, err := url.Parse(service.server.URL) if err != nil { panic("unable to create fake oauth service, error: " + err.Error()) } service.location = location - service.claims["iss"] = service.getLocation() + service.expiration = time.Duration(1) * time.Hour return service } -func (r *fakeOAuthServer) getLocation() string { +func (r *fakeAuthServer) Close() { + r.server.Close() +} + +func (r *fakeAuthServer) getLocation() string { return fmt.Sprintf("%s://%s/auth/realms/hod-test", r.location.Scheme, r.location.Host) } -func (r *fakeOAuthServer) getRevocationURL() string { +func (r *fakeAuthServer) getRevocationURL() string { return fmt.Sprintf("%s://%s/auth/realms/hod-test/protocol/openid-connect/logout", r.location.Scheme, r.location.Host) } -func (r *fakeOAuthServer) signToken(claims jose.Claims) (*jose.JWT, error) { +func (r *fakeAuthServer) signToken(claims jose.Claims) (*jose.JWT, error) { return jose.NewSignedJWT(claims, r.signer) } -func (r *fakeOAuthServer) setUserRealmRoles(roles []string) *fakeOAuthServer { - r.claims["realm_access"] = map[string]interface{}{ - "roles": roles, - } - return r -} - -func (r *fakeOAuthServer) setUserExpiration(duration time.Duration) *fakeOAuthServer { - r.claims["exp"] = time.Now().Add(duration).Second() +func (r *fakeAuthServer) setTokenExpiration(tm time.Duration) *fakeAuthServer { + r.expiration = tm return r } -func (r *fakeOAuthServer) discoveryHandler(cx *gin.Context) { - cx.JSON(http.StatusOK, fakeDiscoveryResponse{ +func (r *fakeAuthServer) discoveryHandler(cx echo.Context) error { + return cx.JSON(http.StatusOK, fakeDiscoveryResponse{ + AuthorizationEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/auth", r.location.Host), + EndSessionEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/logout", r.location.Host), + Issuer: fmt.Sprintf("http://%s/auth/realms/hod-test", r.location.Host), + JwksURI: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/certs", r.location.Host), + RegistrationEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/clients-registrations/openid-connect", r.location.Host), + TokenEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/token", r.location.Host), + TokenIntrospectionEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/token/introspect", r.location.Host), + UserinfoEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/userinfo", r.location.Host), + GrantTypesSupported: []string{"authorization_code", "implicit", "refresh_token", "password", "client_credentials"}, IDTokenSigningAlgValuesSupported: []string{"RS256"}, - Issuer: fmt.Sprintf("http://%s/auth/realms/hod-test", r.location.Host), - AuthorizationEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/auth", r.location.Host), - TokenEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/token", r.location.Host), - RegistrationEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/clients-registrations/openid-connect", r.location.Host), - TokenIntrospectionEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/token/introspect", r.location.Host), - UserinfoEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/userinfo", r.location.Host), - EndSessionEndpoint: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/logout", r.location.Host), - JwksURI: fmt.Sprintf("http://%s/auth/realms/hod-test/protocol/openid-connect/certs", r.location.Host), - GrantTypesSupported: []string{"authorization_code", "implicit", "refresh_token", "password", "client_credentials"}, - ResponseModesSupported: []string{"query", "fragment", "form_post"}, - ResponseTypesSupported: []string{"code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"}, - SubjectTypesSupported: []string{"public"}, + ResponseModesSupported: []string{"query", "fragment", "form_post"}, + ResponseTypesSupported: []string{"code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"}, + SubjectTypesSupported: []string{"public"}, }) } -func (r *fakeOAuthServer) keysHandler(cx *gin.Context) { - cx.JSON(http.StatusOK, jose.JWKSet{Keys: []jose.JWK{r.key}}) +func (r *fakeAuthServer) keysHandler(cx echo.Context) error { + return cx.JSON(http.StatusOK, jose.JWKSet{Keys: []jose.JWK{r.key}}) } -func (r *fakeOAuthServer) authHandler(cx *gin.Context) { - state := cx.Query("state") - redirect := cx.Query("redirect_uri") - +func (r *fakeAuthServer) authHandler(cx echo.Context) error { + state := cx.QueryParam("state") + redirect := cx.QueryParam("redirect_uri") if redirect == "" { - cx.AbortWithStatus(http.StatusInternalServerError) - return + return cx.NoContent(http.StatusInternalServerError) } if state == "" { state = "/" } - // step: generate a random authentication code redirectionURL := fmt.Sprintf("%s?state=%s&code=%s", redirect, state, getRandomString(32)) - cx.Redirect(http.StatusTemporaryRedirect, redirectionURL) + return cx.Redirect(http.StatusTemporaryRedirect, redirectionURL) } -func (r *fakeOAuthServer) logoutHandler(cx *gin.Context) { - if refreshToken := cx.PostForm("refresh_token"); refreshToken == "" { - cx.AbortWithStatus(http.StatusBadRequest) - return +func (r *fakeAuthServer) logoutHandler(cx echo.Context) error { + if refreshToken := cx.FormValue("refresh_token"); refreshToken == "" { + return cx.NoContent(http.StatusBadRequest) } - cx.AbortWithStatus(http.StatusNoContent) + return cx.NoContent(http.StatusNoContent) } -func (r *fakeOAuthServer) userinfoHandler(cx *gin.Context) { - cx.JSON(http.StatusOK, map[string]string{ - "sub": "0d69648e-380f-48c0-90cd-91e55fe68452", - "name": "Rohith Jayawardene", - "given_name": "Rohith", - "family_name": "Jayawardene", - "preferred_username": "gambol99@gmail.com", - "email": "gambol99@gmail.com", - "picture": "http://gambol99.com/gambol99/me.jpg", +func (r *fakeAuthServer) userInfoHandler(cx echo.Context) error { + items := strings.Split(cx.Request().Header.Get("Authorization"), " ") + if len(items) != 2 { + return echo.ErrUnauthorized + } + decoded, err := jose.ParseJWT(items[1]) + if err != nil { + return echo.ErrUnauthorized + } + claims, err := decoded.Claims() + if err != nil { + return echo.ErrUnauthorized + } + + return cx.JSON(http.StatusOK, map[string]interface{}{ + "sub": claims["sub"], + "name": claims["name"], + "given_name": claims["given_name"], + "family_name": claims["familty_name"], + "preferred_username": claims["preferred_username"], + "email": claims["email"], + "picture": claims["picture"], }) } -func (r *fakeOAuthServer) tokenHandler(cx *gin.Context) { - expiration := time.Now().Add(time.Duration(1) * time.Hour) +func (r *fakeAuthServer) tokenHandler(cx echo.Context) error { + expires := time.Now().Add(r.expiration) + unsigned := newTestToken(r.getLocation()) + unsigned.setExpiration(expires) - token, err := jose.NewSignedJWT(r.claims, r.signer) + // sign the token with the private key + token, err := jose.NewSignedJWT(unsigned.claims, r.signer) if err != nil { - cx.AbortWithError(http.StatusInternalServerError, err) - return + return cx.NoContent(http.StatusInternalServerError) } - switch cx.PostForm("grant_type") { + switch cx.FormValue("grant_type") { case oauth2.GrantTypeUserCreds: - username := cx.PostForm("username") - password := cx.PostForm("password") + username := cx.FormValue("username") + password := cx.FormValue("password") if username == "" || password == "" { - cx.AbortWithStatus(http.StatusBadRequest) - return + return cx.NoContent(http.StatusBadRequest) } if username == validUsername && password == validPassword { - cx.JSON(http.StatusOK, tokenResponse{ + return cx.JSON(http.StatusOK, tokenResponse{ IDToken: token.Encode(), AccessToken: token.Encode(), RefreshToken: token.Encode(), - ExpiresIn: expiration.Second(), + ExpiresIn: expires.UTC().Second(), }) - return } - cx.JSON(http.StatusUnauthorized, gin.H{ + return cx.JSON(http.StatusUnauthorized, map[string]string{ "error": "invalid_grant", "error_description": "Invalid user credentials", }) + case oauth2.GrantTypeRefreshToken: + fallthrough case oauth2.GrantTypeAuthCode: - cx.JSON(http.StatusOK, tokenResponse{ + return cx.JSON(http.StatusOK, tokenResponse{ IDToken: token.Encode(), AccessToken: token.Encode(), RefreshToken: token.Encode(), - ExpiresIn: expiration.Second(), + ExpiresIn: expires.Second(), }) default: - cx.AbortWithStatus(http.StatusBadRequest) + return cx.NoContent(http.StatusBadRequest) } } func TestGetUserinfo(t *testing.T) { - + px, idp, _ := newTestProxyService(nil) + token := newTestToken(idp.getLocation()).getToken() + client, _ := px.client.OAuthClient() + claims, err := getUserinfo(client, px.idp.UserInfoEndpoint.String(), token.Encode()) + assert.NoError(t, err) + assert.NotEmpty(t, claims) } func TestTokenExpired(t *testing.T) { diff --git a/resource.go b/resource.go index d1fca8918..1b01dcd55 100644 --- a/resource.go +++ b/resource.go @@ -78,7 +78,7 @@ func (r *Resource) valid() error { // step: add any of no methods if len(r.Methods) <= 0 { - r.Methods = append(r.Methods, "ANY") + r.Methods = allHTTPMethods } // step: check the method is valid diff --git a/resource_test.go b/resource_test.go index 8a5bc3486..97531db27 100644 --- a/resource_test.go +++ b/resource_test.go @@ -27,6 +27,22 @@ func TestDecodeResource(t *testing.T) { Ok bool Resource *Resource }{ + { + Option: "unknown=bad", + Ok: false, + }, + { + Option: "uri=/|unknown=bad", + Ok: false, + }, + { + Option: "uri", + Ok: false, + }, + { + Option: "uri=/|white-listed=ERROR", + Ok: false, + }, { Option: "uri=/admin", Ok: true, @@ -111,7 +127,7 @@ func TestIsValid(t *testing.T) { for i, c := range testCases { err := c.Resource.valid() if err != nil && c.Ok { - t.Errorf("case %d should not have failed", i) + t.Errorf("case %d should not have failed, error: %s", i, err) } } } diff --git a/server.go b/server.go index 309bd9e71..509ba02c8 100644 --- a/server.go +++ b/server.go @@ -19,6 +19,8 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "html/template" + "io" "io/ioutil" "net" "net/http" @@ -34,14 +36,15 @@ import ( "github.com/armon/go-proxyproto" "github.com/coreos/go-oidc/oidc" "github.com/gambol99/goproxy" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" + "github.com/labstack/echo/middleware" "github.com/prometheus/client_golang/prometheus" ) type oauthProxy struct { // the proxy configuration config *Config - // the gin service + // the http service router http.Handler // the opened client client *oidc.Client @@ -53,6 +56,8 @@ type oauthProxy struct { upstream reverseProxy // the upstream endpoint url endpoint *url.URL + // the templates for the custom pages + templates *template.Template // the store interface store storage // the prometheus handler @@ -69,18 +74,13 @@ func init() { // newProxy create's a new proxy from configuration func newProxy(config *Config) (*oauthProxy, error) { var err error - // step: set the logger + // step: set the logging httplog.SetOutput(ioutil.Discard) - - // step: set the logging level - if config.LogJSONFormat { + if config.EnableJSONLogging { log.SetFormatter(&log.JSONFormatter{}) } - // step: set the logging level - gin.SetMode(gin.ReleaseMode) if config.Verbose { log.SetLevel(log.DebugLevel) - gin.SetMode(gin.DebugMode) httplog.SetOutput(os.Stderr) } @@ -113,7 +113,7 @@ func newProxy(config *Config) (*oauthProxy, error) { } if config.ClientID == "" && config.ClientSecret == "" { - log.Warnf("Note: client credentials are not set, depending on provider (confidential|public) you might be unable to auth") + log.Warnf("client credentials are not set, depending on provider (confidential|public) you might be unable to auth") } // step: are we running in forwarding more? @@ -134,90 +134,69 @@ func newProxy(config *Config) (*oauthProxy, error) { // createReverseProxy creates a reverse proxy func (r *oauthProxy) createReverseProxy() error { log.Infof("enabled reverse proxy mode, upstream url: %s", r.config.Upstream) - - // step: display the protected resources - for _, resource := range r.config.Resources { - log.Infof("protecting resources under uri: %s", resource) - } - for name, value := range r.config.MatchClaims { - log.Infof("the token must container the claim: %s, required: %s", name, value) - } - if r.config.RedirectionURL == "" { - log.Warnf("no redirection url has been set, will use host headers") - } - - // step: initialize the reverse http proxy if err := r.createUpstreamProxy(r.endpoint); err != nil { return err } - // step: create the gin router - engine := gin.New() - engine.Use(gin.Recovery()) - // step: custom filtering - engine.Use(r.filterMiddleware(), r.reverseProxyMiddleware()) + // step: create the router + engine := echo.New() + engine.Pre(r.filterMiddleware()) + engine.Use(middleware.Recover()) - // step: is profiling enabled? if r.config.EnableProfiling { - log.Warn("Enabling the debug profiling on /debug/pprof") - engine.Any("/debug/pprof/:name", r.debugHandler) + log.Warn("enabling the debug profiling on /debug/pprof") + engine.Any("/debug/pprof/:name", r.debugHandler, r.proxyRevokeMiddleware()) } - // step: are we logging the traffic? - if r.config.LogRequests { + if r.config.EnableLogging { engine.Use(r.loggingMiddleware()) } - // step: enabling the metrics? if r.config.EnableMetrics { engine.Use(r.metricsMiddleware()) } - // step: enabling the security filter? if r.config.EnableSecurityFilter { engine.Use(r.securityMiddleware()) } - cors := Cors{ - Origins: r.config.CorsOrigins, - Methods: r.config.CorsMethods, - Headers: r.config.CorsHeaders, - ExposedHeaders: r.config.CorsExposedHeaders, - Credentials: r.config.CorsCredentials, - MaxAge: r.config.CorsMaxAge, - } - // step: enabling globaling? - if r.config.EnableCorsGlobal { - log.Info("enabling CORs header injection globally") - engine.Use(r.corsMiddleware(cors)) - } - // step: add the routing and cors middleware - oauth := engine.Group(oauthURL) - if !r.config.EnableCorsGlobal { - oauth.Use(r.corsMiddleware(cors)) - } - oauth.GET(authorizationURL, r.oauthAuthorizationHandler) - oauth.GET(callbackURL, r.oauthCallbackHandler) - oauth.GET(healthURL, r.healthHandler) - oauth.GET(tokenURL, r.tokenHandler) - oauth.GET(expiredURL, r.expirationHandler) - oauth.GET(logoutURL, r.logoutHandler) - oauth.POST(loginURL, r.loginHandler) - // step: enable the metric page? - if r.config.EnableMetrics { - oauth.GET(metricsURL, r.metricsHandler) + if len(r.config.CorsOrigins) > 0 { + engine.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: r.config.CorsOrigins, + AllowMethods: r.config.CorsMethods, + AllowHeaders: r.config.CorsHeaders, + AllowCredentials: r.config.CorsCredentials, + ExposeHeaders: r.config.CorsExposedHeaders, + MaxAge: int(r.config.CorsMaxAge.Seconds())})) } - // step: add the middleware - engine.Use(r.entrypointMiddleware(), - r.authenticationMiddleware(), - r.admissionMiddleware(), - r.headersMiddleware(r.config.AddClaims)) - - // step: set the handler + // step: add the routing for aouth + engine.Group(oauthURL, r.proxyRevokeMiddleware()) + engine.Any(oauthURL+"/:name", r.oauthHandler) r.router = engine - // step: load the templates + // step: load the templates if any if err := r.createTemplates(); err != nil { return err } + // step: provision in the protected resources + for _, resource := range r.config.Resources { + log.Infof("protecting resource: %s", resource) + switch resource.WhiteListed { + case false: + engine.Match(resource.Methods, resource.URL, emptyHandler, + r.authenticationMiddleware(resource), + r.admissionMiddleware(resource), + r.headersMiddleware(r.config.AddClaims)) + default: + engine.Match(resource.Methods, resource.URL, emptyHandler) + } + } + for name, value := range r.config.MatchClaims { + log.Infof("the token must container the claim: %s, required: %s", name, value) + } + if r.config.RedirectionURL == "" { + log.Warnf("no redirection url has been set, will use host headers") + } + engine.Use(r.proxyMiddleware()) + return nil } @@ -228,13 +207,9 @@ func (r *oauthProxy) createForwardingProxy() error { if r.config.SkipUpstreamTLSVerify { log.Warnf("TLS verification switched off. In forward signing mode it's recommended you verify! (--skip-upstream-tls-verify=false)") } - - // step: initialize the reverse http proxy if err := r.createUpstreamProxy(nil); err != nil { return err } - - // step: setup and initialize the handler forwardingHandler := r.forwardProxyHandler() // step: set the http handler @@ -247,6 +222,7 @@ func (r *oauthProxy) createForwardingProxy() error { if err != nil { return fmt.Errorf("unable to load certificate authority, error: %s", err) } + // step: implement the goproxy connect method proxy.OnRequest().HandleConnectFunc( func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { @@ -263,7 +239,7 @@ func (r *oauthProxy) createForwardingProxy() error { proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { // @NOTES, somewhat annoying but goproxy hands back a nil response on proxy client errors - if resp != nil && r.config.LogRequests { + if resp != nil && r.config.EnableLogging { start := ctx.UserData.(time.Time) latency := time.Now().Sub(start) @@ -283,7 +259,6 @@ func (r *oauthProxy) createForwardingProxy() error { ctx.UserData = time.Now() // step: forward into the handler forwardingHandler(req, ctx.Resp) - return req, ctx.Resp }) @@ -292,7 +267,6 @@ func (r *oauthProxy) createForwardingProxy() error { // Run starts the proxy service func (r *oauthProxy) Run() error { - // step: create the service listener listener, err := createHTTPListener(listenerConfig{ listen: r.config.Listen, certificate: r.config.TLSCertificate, @@ -331,7 +305,7 @@ func (r *oauthProxy) Run() error { } httpsvc := &http.Server{ Addr: r.config.ListenHTTP, - Handler: r.router, + Handler: http.Handler(r.router), } go func() { if err := httpsvc.Serve(httpListener); err != nil { @@ -363,7 +337,6 @@ func createHTTPListener(config listenerConfig) (net.Listener, error) { // step: are we create a unix socket or tcp listener? if strings.HasPrefix(config.listen, "unix://") { socket := strings.Trim(config.listen, "unix://") - // step: delete the socket if it exists if exists := fileExists(socket); exists { if err = os.Remove(socket); err != nil { return nil, err @@ -421,7 +394,6 @@ func createHTTPListener(config listenerConfig) (net.Listener, error) { // createUpstreamProxy create a reverse http proxy from the upstream func (r *oauthProxy) createUpstreamProxy(upstream *url.URL) error { - // step: create the default dialer dialer := (&net.Dialer{ KeepAlive: r.config.UpstreamKeepaliveTimeout, Timeout: r.config.UpstreamTimeout, @@ -454,8 +426,6 @@ func (r *oauthProxy) createUpstreamProxy(upstream *url.URL) error { } pool := x509.NewCertPool() pool.AppendCertsFromPEM(cert) - - // step: update the upstream tls to use the client certificate tlsConfig.ClientCAs = pool tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } @@ -491,8 +461,14 @@ func (r *oauthProxy) createTemplates() error { if len(list) > 0 { log.Infof("loading the custom templates: %s", strings.Join(list, ",")) - r.router.(*gin.Engine).LoadHTMLFiles(list...) + r.templates = template.Must(template.ParseFiles(list...)) + r.router.(*echo.Echo).Renderer = r } return nil } + +// Render implements the echo Render interface +func (r *oauthProxy) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return r.templates.ExecuteTemplate(w, name, data) +} diff --git a/server_test.go b/server_test.go index b2e24814f..0d1e1e313 100644 --- a/server_test.go +++ b/server_test.go @@ -16,13 +16,10 @@ limitations under the License. package main import ( - "bufio" - "bytes" "encoding/json" "errors" "fmt" "io/ioutil" - "net" "net/http" "net/http/httptest" "net/url" @@ -32,159 +29,193 @@ import ( log "github.com/Sirupsen/logrus" "github.com/coreos/go-oidc/jose" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) const ( - fakeClientID = "test" - fakeSecret = fakeClientID - - fakeAdminRoleURL = "/admin" - fakeTestRoleURL = "/test_role" + fakeAdminRole = "role:admin" + fakeAdminRoleURL = "/admin*" + fakeAuthAllURL = "/auth_all/*" + fakeClientID = "test" + fakeSecret = "test" fakeTestAdminRolesURL = "/test_admin_roles" - fakeAuthAllURL = "/auth_all" - fakeTestWhitelistedURL = fakeAuthAllURL + "/white_listed" - fakeTestListenOrdered = fakeAuthAllURL + "/bad_order" - - fakeAdminRole = "role:admin" - fakeTestRole = "role:test" + fakeTestRole = "role:test" + fakeTestRoleURL = "/test_role" + fakeTestWhitelistedURL = "/auth_all/white_listed*" + testProxyAccepted = "Proxy-Accepted" + validUsername = "test" + validPassword = "test" ) var ( - defaultFakeClaims = jose.Claims{ - "jti": "4ee75b8e-3ee6-4382-92d4-3390b4b4937b", - "nbf": 0, - "iat": "1450372669", - "iss": "https://keycloak.example.com/auth/realms/commons", - "aud": "test", - "sub": "1e11e539-8256-4b3b-bda8-cc0d56cddb48", - "typ": "Bearer", - "azp": "clientid", - "session_state": "98f4c3d2-1b8c-4932-b8c4-92ec0ea7e195", - "client_session": "f0105893-369a-46bc-9661-ad8c747b1a69", - "resource_access": map[string]interface{}{ - "openvpn": map[string]interface{}{ - "roles": []string{ - "dev-vpn", - }, - }, - }, - "email": "gambol99@gmail.com", - "name": "Rohith Jayawardene", - "family_name": "Jayawardene", - "preferred_username": "rjayawardene", - "given_name": "Rohith", - } - defaultTestTokenClaims = jose.Claims{ - "jti": "4ee75b8e-3ee6-4382-92d4-3390b4b4937b", - "nbf": 0, - "iat": "1450372669", - "iss": "test", "aud": "test", - "sub": "1e11e539-8256-4b3b-bda8-cc0d56cddb48", - "typ": "Bearer", "azp": "clientid", - "session_state": "98f4c3d2-1b8c-4932-b8c4-92ec0ea7e195", "client_session": "f0105893-369a-46bc-9661-ad8c747b1a69", "email": "gambol99@gmail.com", - "name": "Rohith Jayawardene", "family_name": "Jayawardene", - "preferred_username": "rjayawardene", "given_name": "Rohith", - } - - defaultFakeRealmClaims = jose.Claims{ - "jti": "4ee75b8e-3ee6-4382-92d4-3390b4b4937b", - "nbf": 0, - "iat": "1450372669", - "iss": "https://keycloak.example.com/auth/realms/commons", - "aud": "test", - "sub": "1e11e539-8256-4b3b-bda8-cc0d56cddb48", - "typ": "Bearer", - "azp": "clientid", - "session_state": "98f4c3d2-1b8c-4932-b8c4-92ec0ea7e195", - "client_session": "f0105893-369a-46bc-9661-ad8c747b1a69", - "realm_access": map[string]interface{}{ - "roles": []string{ - "dsp-dev-vpn", - "vpn-user", - "dsp-prod-vpn", - }, - }, - "resource_access": map[string]interface{}{ - "openvpn": map[string]interface{}{ - "roles": []string{ - "dev-vpn", - }, - }, - }, - "email": "gambol99@gmail.com", + "iat": "1450372669", + "iss": "test", + "jti": "4ee75b8e-3ee6-4382-92d4-3390b4b4937b", "name": "Rohith Jayawardene", - "family_name": "Jayawardene", + "nbf": 0, "preferred_username": "rjayawardene", - "given_name": "Rohith", + "session_state": "98f4c3d2-1b8c-4932-b8c4-92ec0ea7e195", + "sub": "1e11e539-8256-4b3b-bda8-cc0d56cddb48", + "typ": "Bearer", } ) -type testJWTToken struct { - claims jose.Claims -} - -func newTestToken(issuer string) *testJWTToken { - claims := defaultTestTokenClaims - claims.Add("exp", float64(time.Now().Add(1*time.Hour).Unix())) - claims.Add("iat", float64(time.Now().Unix())) - claims.Add("iss", issuer) +func TestNewKeycloakProxy(t *testing.T) { + cfg := newFakeKeycloakConfig() + cfg.DiscoveryURL = newFakeAuthServer().getLocation() + cfg.Listen = "127.0.0.1:0" + cfg.ListenHTTP = "" - return &testJWTToken{claims: claims} + proxy, err := newProxy(cfg) + assert.NoError(t, err) + assert.NotNil(t, proxy) + assert.NotNil(t, proxy.config) + assert.NotNil(t, proxy.router) + assert.NotNil(t, proxy.endpoint) + assert.NoError(t, proxy.Run()) } -func (t *testJWTToken) mergeClaims(claims jose.Claims) { - for k, v := range claims { - t.claims.Add(k, v) +func TestReverseProxyHeaders(t *testing.T) { + p := newFakeProxy(nil) + token := newTestToken(p.idp.getLocation()) + token.addRealmRoles([]string{fakeAdminRole}) + signed, _ := p.idp.signToken(token.claims) + requests := []fakeRequest{ + { + URI: fakeAuthAllURL, + RawToken: signed.Encode(), + ExpectedProxy: true, + ExpectedProxyHeaders: map[string]string{ + "X-Auth-Email": "gambol99@gmail.com", + "X-Auth-Roles": "role:admin", + "X-Auth-Subject": token.claims["sub"].(string), + "X-Auth-Token": signed.Encode(), + "X-Auth-Userid": "rjayawardene", + "X-Auth-Username": "rjayawardene", + "X-Forwarded-For": "127.0.0.1", + }, + ExpectedCode: http.StatusOK, + }, } + p.RunTests(t, requests) } -func (t *testJWTToken) getToken() jose.JWT { - tk, _ := jose.NewJWT(jose.JOSEHeader{"alg": "RS256"}, t.claims) - return tk +func TestForwardingProxy(t *testing.T) { + cfg := newFakeKeycloakConfig() + cfg.EnableForwarding = true + cfg.ForwardingDomains = []string{} + cfg.ForwardingUsername = "test" + cfg.ForwardingPassword = "test" + s := httptest.NewServer(&fakeUpstreamService{}) + requests := []fakeRequest{ + { + URL: s.URL + "/test", + ProxyRequest: true, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, + ExpectedContentContains: "Bearer ey", + }, + } + p := newFakeProxy(cfg) + <-time.After(time.Duration(100) * time.Millisecond) + p.RunTests(t, requests) } -func (t *testJWTToken) setExpiration(tm time.Time) { - t.claims.Add("exp", float64(tm.Unix())) +func TestForbiddenTemplate(t *testing.T) { + cfg := newFakeKeycloakConfig() + cfg.ForbiddenPage = "templates/forbidden.html.tmpl" + cfg.Resources = []*Resource{ + { + URL: "/*", + Methods: allHTTPMethods, + Roles: []string{fakeAdminRole}, + }, + } + requests := []fakeRequest{ + { + URI: "/", + Redirects: false, + HasToken: true, + ExpectedCode: http.StatusForbidden, + ExpectedContentContains: "403 Permission Denied", + }, + } + newFakeProxy(cfg).RunTests(t, requests) } -func (t *testJWTToken) setRealmsRoles(roles []string) { - t.claims.Add("realm_access", map[string]interface{}{ - "roles": roles, - }) +func TestAuthorizationTemplate(t *testing.T) { + cfg := newFakeKeycloakConfig() + cfg.SignInPage = "templates/sign_in.html.tmpl" + cfg.Resources = []*Resource{ + { + URL: "/*", + Methods: allHTTPMethods, + Roles: []string{fakeAdminRole}, + }, + } + requests := []fakeRequest{ + { + URI: oauthURL + authorizationURL, + Redirects: true, + ExpectedCode: http.StatusOK, + ExpectedContentContains: "Sign In", + }, + } + newFakeProxy(cfg).RunTests(t, requests) } -func (t *testJWTToken) setClientRoles(client string, roles []string) { - t.claims.Add("resource_access", map[string]interface{}{ - "openvpn": map[string]interface{}{ - "roles": roles, - }, - }) +func newTestService() string { + _, _, u := newTestProxyService(nil) + return u } -func newFakeAccessToken(claims *jose.Claims, expire time.Duration) jose.JWT { - if claims == nil { - claims = &defaultFakeClaims +func newTestProxyService(config *Config) (*oauthProxy, *fakeAuthServer, string) { + log.SetOutput(ioutil.Discard) + auth := newFakeAuthServer() + if config == nil { + config = newFakeKeycloakConfig() + } + config.DiscoveryURL = auth.getLocation() + config.RevocationEndpoint = auth.getRevocationURL() + config.Verbose = false + config.EnableLogging = false + + proxy, err := newProxy(config) + if err != nil { + panic("failed to create proxy service, error: " + err.Error()) } - claims.Add("exp", float64(time.Now().Add(10*time.Hour).Unix())) - if expire > 0 { - claims.Add("exp", float64(time.Now().Add(expire).Unix())) + + // step: create an fake upstream endpoint + proxy.upstream = new(fakeUpstreamService) + service := httptest.NewServer(proxy.router) + config.RedirectionURL = service.URL + + // step: we need to update the client config + if proxy.client, proxy.idp, proxy.idpClient, err = newOpenIDClient(config); err != nil { + panic("failed to recreate the openid client, error: " + err.Error()) } - testToken, _ := jose.NewJWT(jose.JOSEHeader{"alg": "RS256"}, *claims) - return testToken + return proxy, auth, service.URL } -func getFakeRealmAccessToken(t *testing.T) jose.JWT { - return newFakeAccessToken(&defaultFakeRealmClaims, 0) +func newFakeHTTPRequest(method, path string) *http.Request { + return &http.Request{ + Method: method, + Header: make(map[string][]string, 0), + Host: "127.0.0.1", + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1", + Path: path, + }, + } } func newFakeKeycloakConfig() *Config { @@ -193,18 +224,13 @@ func newFakeKeycloakConfig() *Config { ClientSecret: fakeSecret, CookieAccessName: "kc-access", CookieRefreshName: "kc-state", - DiscoveryURL: "127.0.0.1:8080", - Listen: "127.0.0.1:443", - ListenHTTP: "127.0.0.1:80", + DiscoveryURL: "127.0.0.1:0", + Listen: "127.0.0.1:0", + ListenHTTP: "127.0.0.1:0", EnableAuthorizationHeader: true, - EnableRefreshTokens: false, EnableLoginHandler: true, EncryptionKey: "AgXa7xRcoClDEU0ZDSH4X0XhL5Qy2Z2j", - LogRequests: true, Scopes: []string{}, - SecureCookie: false, - SkipTokenVerification: false, - Verbose: false, Resources: []*Resource{ { URL: fakeAdminRoleURL, @@ -221,150 +247,21 @@ func newFakeKeycloakConfig() *Config { Methods: []string{"GET"}, Roles: []string{fakeAdminRole, fakeTestRole}, }, - { - URL: fakeTestWhitelistedURL, - WhiteListed: true, - Methods: []string{}, - Roles: []string{}, - }, { URL: fakeAuthAllURL, - Methods: []string{"ANY"}, + Methods: allHTTPMethods, Roles: []string{}, }, { URL: fakeTestWhitelistedURL, WhiteListed: true, - Methods: []string{}, + Methods: allHTTPMethods, Roles: []string{}, }, }, } } -func newTestService() string { - _, _, u := newTestProxyService(nil) - - return u -} - -func newTestServiceWithConfig(cfg *Config) string { - _, _, u := newTestProxyService(cfg) - - return u -} - -func newTestProxyOnlyService() *oauthProxy { - p, _, _ := newTestProxyService(nil) - return p -} - -func newTestProxyService(config *Config) (*oauthProxy, *fakeOAuthServer, string) { - log.SetOutput(ioutil.Discard) - // step: create a fake oauth server - auth := newFakeOAuthServer() - // step: use the default config if required - if config == nil { - config = newFakeKeycloakConfig() - } - // step: set the config - config.DiscoveryURL = auth.getLocation() - config.RevocationEndpoint = auth.getRevocationURL() - - // step: create a proxy - proxy, err := newProxy(config) - if err != nil { - panic("failed to create proxy service, error: " + err.Error()) - } - - // step: create an fake upstream endpoint - proxy.upstream = new(testReverseProxy) - service := httptest.NewServer(proxy.router) - config.RedirectionURL = service.URL - - // step: we need to update the client config - proxy.client, proxy.idp, proxy.idpClient, err = newOpenIDClient(config) - if err != nil { - panic("failed to recreate the openid client, error: " + err.Error()) - } - - return proxy, auth, service.URL -} - -func newTestServiceWithWithResources(t *testing.T, resources []*Resource) (*oauthProxy, string) { - config := newFakeKeycloakConfig() - config.Resources = resources - px, _, url := newTestProxyService(config) - - return px, url -} - -func TestNewKeycloakProxy(t *testing.T) { - cfg := newFakeKeycloakConfig() - cfg.DiscoveryURL = newFakeOAuthServer().getLocation() - - proxy, err := newProxy(cfg) - assert.NoError(t, err) - assert.NotNil(t, proxy) - assert.NotNil(t, proxy.config) - assert.NotNil(t, proxy.router) - assert.NotNil(t, proxy.endpoint) -} - -func newFakeResponse() *fakeResponse { - return &fakeResponse{ - status: http.StatusOK, - headers: make(http.Header, 0), - } -} - -func newFakeGinContext(method, uri string) *gin.Context { - return &gin.Context{ - Request: &http.Request{ - Method: method, - Host: "127.0.0.1", - RequestURI: uri, - URL: &url.URL{ - Scheme: "http", - Host: "127.0.0.1", - Path: uri, - }, - Header: make(http.Header, 0), - RemoteAddr: "127.0.0.1:8989", - }, - Writer: newFakeResponse(), - } -} - -// makeTestOauthLogin performs a fake oauth login into the service, retrieving the access token -func makeTestOauthLogin(location string) (string, error) { - resp, err := makeTestCodeFlowLogin(location) - if err != nil { - return "", err - } - - // step: check the cookie is there - for _, c := range resp.Cookies() { - if c.Name == "kc-access" { - return c.Value, nil - } - } - - return "", errors.New("access cookie not found in response from oauth service") -} - -func newFakeHTTPRequest(method, path string) *http.Request { - return &http.Request{ - Method: method, - Header: make(map[string][]string, 0), - URL: &url.URL{ - Scheme: "http", - Host: "127.0.0.1", - Path: path, - }, - } -} - func makeTestCodeFlowLogin(location string) (*http.Response, error) { u, err := url.Parse(location) if err != nil { @@ -373,7 +270,7 @@ func makeTestCodeFlowLogin(location string) (*http.Response, error) { // step: get the redirect var resp *http.Response for count := 0; count < 4; count++ { - req, err := http.NewRequest("GET", location, nil) + req, err := http.NewRequest(http.MethodGet, location, nil) if err != nil { return nil, err } @@ -390,66 +287,79 @@ func makeTestCodeFlowLogin(location string) (*http.Response, error) { location = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, location) } } - return resp, nil } -func newFakeGinContextWithCookies(method, url string, cookies []*http.Cookie) *gin.Context { - cx := newFakeGinContext(method, url) - for _, x := range cookies { - cx.Request.AddCookie(x) - } +// fakeUpstreamResponse is the response from fake upstream +type fakeUpstreamResponse struct { + URI string `json:"uri"` + Method string `json:"method"` + Address string `json:"address"` + Headers http.Header `json:"headers"` +} + +// fakeUpstreamService acts as a fake upstream service, returns the headers and request +type fakeUpstreamService struct{} - return cx +func (f *fakeUpstreamService) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set(testProxyAccepted, "true") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + content, _ := json.Marshal(&fakeUpstreamResponse{ + URI: r.RequestURI, + Method: r.Method, + Address: r.RemoteAddr, + Headers: r.Header, + }) + w.Write(content) } -// testUpstreamResponse is the response from fake upstream -type testUpstreamResponse struct { - URI string - Method string - Address string - Headers http.Header +type fakeToken struct { + claims jose.Claims } -const testProxyAccepted = "Proxy-Accepted" +func newTestToken(issuer string) *fakeToken { + claims := make(jose.Claims, 0) + for k, v := range defaultTestTokenClaims { + claims[k] = v + } + claims.Add("exp", float64(time.Now().Add(1*time.Hour).Unix())) + claims.Add("iat", float64(time.Now().Unix())) + claims.Add("iss", issuer) -type testReverseProxy struct{} + return &fakeToken{claims: claims} +} -func (r testReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { - resp := testUpstreamResponse{ - URI: req.RequestURI, - Method: req.Method, - Address: req.RemoteAddr, - Headers: req.Header, +// merge is responsible for merging claims into the token +func (t *fakeToken) merge(claims jose.Claims) { + for k, v := range claims { + t.claims.Add(k, v) } - w.WriteHeader(http.StatusOK) - w.Header().Set(testProxyAccepted, "true") - w.Header().Set("Content-Type", "application/json") - // step: encode the response - encoded, _ := json.Marshal(&resp) +} - w.Write(encoded) +// getToken returns a JWT token from the clains +func (t *fakeToken) getToken() jose.JWT { + tk, _ := jose.NewJWT(jose.JOSEHeader{"alg": "RS256"}, t.claims) + return tk +} + +// setExpiration sets the expiration of the token +func (t *fakeToken) setExpiration(tm time.Time) { + t.claims.Add("exp", float64(tm.Unix())) } -type fakeResponse struct { - size int - status int - headers http.Header - body bytes.Buffer - written bool +// addRealmRoles adds realms roles to token +func (t *fakeToken) addRealmRoles(roles []string) { + t.claims.Add("realm_access", map[string]interface{}{ + "roles": roles, + }) } -func (r *fakeResponse) Flush() {} -func (r *fakeResponse) Written() bool { return r.written } -func (r *fakeResponse) WriteHeaderNow() {} -func (r *fakeResponse) Size() int { return r.size } -func (r *fakeResponse) Status() int { return r.status } -func (r *fakeResponse) Header() http.Header { return r.headers } -func (r *fakeResponse) WriteHeader(code int) { - r.status = code - r.written = true +// addClientRoles adds client roles to the token +func (t *fakeToken) addClientRoles(client string, roles []string) { + t.claims.Add("resource_access", map[string]interface{}{ + client: map[string]interface{}{ + "roles": roles, + }, + }) } -func (r *fakeResponse) Write(content []byte) (int, error) { return len(content), nil } -func (r *fakeResponse) WriteString(s string) (int, error) { return len(s), nil } -func (r *fakeResponse) Hijack() (net.Conn, *bufio.ReadWriter, error) { return nil, nil, nil } -func (r *fakeResponse) CloseNotify() <-chan bool { return make(chan bool, 0) } diff --git a/session.go b/session.go index 20d9dd49b..b7e42d298 100644 --- a/session.go +++ b/session.go @@ -26,27 +26,21 @@ import ( // getIdentity retrieves the user identity from a request, either from a session cookie or a bearer token func (r *oauthProxy) getIdentity(req *http.Request) (*userContext, error) { var isBearer bool - // step: check for a bearer token or cookie with jwt token access, isBearer, err := getTokenInRequest(req, r.config.CookieAccessName) if err != nil { return nil, err } - // step: parse the access token token, err := jose.ParseJWT(access) if err != nil { return nil, err } - - // step: parse the access token and extract the user identity user, err := extractIdentity(token) if err != nil { return nil, err } - user.bearerToken = isBearer - // step: add some logging for debug purposed log.WithFields(log.Fields{ "id": user.id, "name": user.name, @@ -102,10 +96,8 @@ func getTokenInBearer(req *http.Request) (string, error) { // getTokenInCookie retrieves the access token from the request cookies func getTokenInCookie(req *http.Request, name string) (string, error) { - cookie := findCookie(name, req.Cookies()) - if cookie == nil { - return "", ErrSessionNotFound + if cookie := findCookie(name, req.Cookies()); cookie != nil { + return cookie.Value, nil } - - return cookie.Value, nil + return "", ErrSessionNotFound } diff --git a/session_test.go b/session_test.go index 71b7cae70..d68433673 100644 --- a/session_test.go +++ b/session_test.go @@ -24,8 +24,8 @@ import ( ) func TestGetIndentity(t *testing.T) { - p, _, _ := newTestProxyService(nil) - token := newFakeAccessToken(nil, 0) + p, idp, _ := newTestProxyService(nil) + token := newTestToken(idp.getLocation()).getToken() encoded := token.Encode() testCases := []struct { @@ -50,7 +50,6 @@ func TestGetIndentity(t *testing.T) { Header: http.Header{}, }, }, - // @TODO need to other checks } for i, c := range testCases { @@ -70,7 +69,7 @@ func TestGetIndentity(t *testing.T) { func TestGetTokenInRequest(t *testing.T) { defaultName := newDefaultConfig().CookieAccessName - token := newFakeAccessToken(nil, 0) + token := newTestToken("test").getToken() cs := []struct { Token string IsBearer bool diff --git a/store_boltdb.go b/store_boltdb.go index f73711ac5..879374cc8 100644 --- a/store_boltdb.go +++ b/store_boltdb.go @@ -63,7 +63,7 @@ func newBoltDBStore(location *url.URL) (storage, error) { } // Set adds a token to the store -func (r boltdbStore) Set(key, value string) error { +func (r *boltdbStore) Set(key, value string) error { log.WithFields(log.Fields{ "key": key, "value": value, @@ -79,7 +79,7 @@ func (r boltdbStore) Set(key, value string) error { } // Get retrieves a token from the store -func (r boltdbStore) Get(key string) (string, error) { +func (r *boltdbStore) Get(key string) (string, error) { log.WithFields(log.Fields{ "key": key, }).Debugf("retrieving the key: %s from store", key) @@ -98,7 +98,7 @@ func (r boltdbStore) Get(key string) (string, error) { } // Delete removes the key from the bucket -func (r boltdbStore) Delete(key string) error { +func (r *boltdbStore) Delete(key string) error { log.WithFields(log.Fields{ "key": key, }).Debugf("deleting the key: %s from store", key) @@ -113,7 +113,7 @@ func (r boltdbStore) Delete(key string) error { } // Close closes of any open resources -func (r boltdbStore) Close() error { +func (r *boltdbStore) Close() error { log.Infof("closing the resourcese for boltdb store") return r.client.Close() } diff --git a/store_boltdb_test.go b/store_boltdb_test.go new file mode 100644 index 000000000..a5d6788b3 --- /dev/null +++ b/store_boltdb_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2017 All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "io/ioutil" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +type fakeBoltDBStore struct { + storedb *os.File + store *boltdbStore +} + +func (f *fakeBoltDBStore) close() { + if f.storedb != nil { + f.storedb.Close() + os.Remove(f.storedb.Name()) + } +} + +func newTestBoldDB(t *testing.T) *fakeBoltDBStore { + tmpfile, err := ioutil.TempFile("/tmp", "keycloak-proxy") + if err != nil { + t.Fatalf("unable to create temporary file, error: %s", err) + } + u, err := url.Parse(fmt.Sprintf("file:///%s", tmpfile.Name())) + if err != nil { + t.Fatalf("unable to parse file url, error: %s", err) + } + s, err := newBoltDBStore(u) + if err != nil { + tmpfile.Close() + os.Remove(tmpfile.Name()) + t.Fatalf("unable to test boltdb, error: %s", err) + } + return &fakeBoltDBStore{tmpfile, s.(*boltdbStore)} +} + +func TestNewBoltDBStore(t *testing.T) { + s := newTestBoldDB(t) + defer s.close() + assert.NotNil(t, s) +} + +func TestBoltSet(t *testing.T) { + s := newTestBoldDB(t) + defer s.close() + err := s.store.Set("test", "value") + assert.NoError(t, err) +} + +func TestBoltGet(t *testing.T) { + s := newTestBoldDB(t) + defer s.close() + v, err := s.store.Get("test") + assert.NoError(t, err) + assert.Empty(t, v) + err = s.store.Set("test", "value") + assert.NoError(t, err) + v, err = s.store.Get("test") + assert.NoError(t, err) + assert.Equal(t, "value", v) +} + +func TestBoltDelete(t *testing.T) { + keyname := "test" + value := "value" + s := newTestBoldDB(t) + defer s.close() + err := s.store.Set(keyname, value) + assert.NoError(t, err) + v, err := s.store.Get(keyname) + assert.NoError(t, err) + assert.Equal(t, value, v) + err = s.store.Delete(keyname) + assert.NoError(t, err) + v, err = s.store.Get(keyname) + assert.NoError(t, err) + assert.Empty(t, v) +} + +func TestBoldClose(t *testing.T) { + s := newTestBoldDB(t) + defer s.close() + err := s.store.Close() + assert.NoError(t, err) +} diff --git a/stores.go b/stores.go index c53ff879f..a5db99730 100644 --- a/stores.go +++ b/stores.go @@ -44,23 +44,17 @@ func createStorage(location string) (storage, error) { return store, err } -// // useStore checks if we are using a store to hold the refresh tokens -// func (r *oauthProxy) useStore() bool { return r.store != nil } -// // StoreRefreshToken the token to the store -// func (r *oauthProxy) StoreRefreshToken(token jose.JWT, value string) error { return r.store.Set(getHashKey(&token), value) } -// // Get retrieves a token from the store, the key we are using here is the access token -// func (r *oauthProxy) GetRefreshToken(token jose.JWT) (string, error) { // step: the key is the access token v, err := r.store.Get(getHashKey(&token)) @@ -74,9 +68,7 @@ func (r *oauthProxy) GetRefreshToken(token jose.JWT) (string, error) { return v, nil } -// // DeleteRefreshToken removes a key from the store -// func (r *oauthProxy) DeleteRefreshToken(token jose.JWT) error { if err := r.store.Delete(getHashKey(&token)); err != nil { log.WithFields(log.Fields{ @@ -89,9 +81,7 @@ func (r *oauthProxy) DeleteRefreshToken(token jose.JWT) error { return nil } -// // Close is used to close off any resources -// func (r *oauthProxy) CloseStore() error { if r.store != nil { return r.store.Close() diff --git a/stores_test.go b/stores_test.go index ea6dc8df6..1fa342183 100644 --- a/stores_test.go +++ b/stores_test.go @@ -32,9 +32,8 @@ func TestCreateStorageBoltDB(t *testing.T) { store, err := createStorage("boltdb:////tmp/bolt") assert.NotNil(t, store) assert.NoError(t, err) - - if err == nil { - os.Remove("/tmp/bold") + if store != nil { + os.Remove("/tmp/bolt") } } diff --git a/user_context.go b/user_context.go index dd2353092..58807e7fa 100644 --- a/user_context.go +++ b/user_context.go @@ -26,12 +26,10 @@ import ( // extractIdentity parse the jwt token and extracts the various elements is order to construct func extractIdentity(token jose.JWT) (*userContext, error) { - // step: decode the claims from the tokens claims, err := token.Claims() if err != nil { return nil, err } - // step: extract the identity identity, err := oidc.IdentityFromClaims(claims) if err != nil { return nil, err @@ -39,10 +37,8 @@ func extractIdentity(token jose.JWT) (*userContext, error) { // step: ensure we have and can extract the preferred name of the user, if not, we set to the ID preferredName, found, err := claims.StringClaim(claimPreferredName) if err != nil || !found { - // choice: set the preferredName to the Email if claim not found preferredName = identity.Email } - // step: retrieve the audience from access token audience, found, err := claims.StringClaim(claimAudience) if err != nil || !found { return nil, ErrNoTokenAudience @@ -57,7 +53,7 @@ func extractIdentity(token jose.JWT) (*userContext, error) { } } - // step: extract the roles from the access token + // step: extract the client roles from the access token if accesses, found := claims[claimResourceAccess].(map[string]interface{}); found { for roleName, roleList := range accesses { scopes := roleList.(map[string]interface{}) @@ -83,7 +79,7 @@ func extractIdentity(token jose.JWT) (*userContext, error) { } // isAudience checks the audience -func (r userContext) isAudience(aud string) bool { +func (r *userContext) isAudience(aud string) bool { if r.audience == aud { return true } @@ -92,26 +88,26 @@ func (r userContext) isAudience(aud string) bool { } // getRoles returns a list of roles -func (r userContext) getRoles() string { +func (r *userContext) getRoles() string { return strings.Join(r.roles, ",") } // isExpired checks if the token has expired -func (r userContext) isExpired() bool { +func (r *userContext) isExpired() bool { return r.expiresAt.Before(time.Now()) } -// isBearerToken checks if the token -func (r userContext) isBearer() bool { +// isBearer checks if the token +func (r *userContext) isBearer() bool { return r.bearerToken } // isCookie checks if it's by a cookie -func (r userContext) isCookie() bool { +func (r *userContext) isCookie() bool { return !r.isBearer() } // String returns a string representation of the user context -func (r userContext) String() string { +func (r *userContext) String() string { return fmt.Sprintf("user: %s, expires: %s, roles: %s", r.preferredName, r.expiresAt.String(), strings.Join(r.roles, ",")) } diff --git a/user_context_test.go b/user_context_test.go index aaacaa1e9..b28c45f50 100644 --- a/user_context_test.go +++ b/user_context_test.go @@ -72,21 +72,25 @@ func TestIsCookie(t *testing.T) { } func TestGetUserContext(t *testing.T) { - roles := []string{"openvpn:dev-vpn"} - - context, err := extractIdentity(newFakeAccessToken(nil, 0)) + realmRoles := []string{"realm:realm"} + clientRoles := []string{"client:client"} + token := newTestToken("test") + token.addRealmRoles(realmRoles) + token.addClientRoles("client", []string{"client"}) + context, err := extractIdentity(token.getToken()) assert.NoError(t, err) assert.NotNil(t, context) assert.Equal(t, "1e11e539-8256-4b3b-bda8-cc0d56cddb48", context.id) assert.Equal(t, "gambol99@gmail.com", context.email) assert.Equal(t, "rjayawardene", context.preferredName) - assert.Equal(t, roles, context.roles) + assert.Equal(t, append(realmRoles, clientRoles...), context.roles) } func TestGetUserRealmRoleContext(t *testing.T) { roles := []string{"dsp-dev-vpn", "vpn-user", "dsp-prod-vpn", "openvpn:dev-vpn"} - - context, err := extractIdentity(getFakeRealmAccessToken(t)) + token := newTestToken("test") + token.addRealmRoles(roles) + context, err := extractIdentity(token.getToken()) assert.NoError(t, err) assert.NotNil(t, context) assert.Equal(t, "1e11e539-8256-4b3b-bda8-cc0d56cddb48", context.id) @@ -96,7 +100,8 @@ func TestGetUserRealmRoleContext(t *testing.T) { } func TestUserContextString(t *testing.T) { - context, err := extractIdentity(newFakeAccessToken(nil, 0)) + token := newTestToken("test") + context, err := extractIdentity(token.getToken()) assert.NoError(t, err) assert.NotNil(t, context) assert.NotEmpty(t, context.String()) diff --git a/utils.go b/utils.go index 70d01b60d..b13189190 100644 --- a/utils.go +++ b/utils.go @@ -44,11 +44,24 @@ import ( log "github.com/Sirupsen/logrus" "github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/oidc" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" "github.com/urfave/cli" "gopkg.in/yaml.v2" ) +var ( + allHTTPMethods = []string{ + echo.DELETE, + echo.GET, + echo.HEAD, + echo.OPTIONS, + echo.PATCH, + echo.POST, + echo.PUT, + echo.TRACE, + } +) + var ( httpMethodRegex = regexp.MustCompile("^(ANY|GET|POST|DELETE|PATCH|HEAD|PUT|TRACE)$") symbolsFilter = regexp.MustCompilePOSIX("[_$><\\[\\].,\\+-/'%^&*()!\\\\]+") @@ -220,7 +233,13 @@ func decodeKeyPairs(list []string) (map[string]string, error) { // isValidHTTPMethod ensure this is a valid http method type func isValidHTTPMethod(method string) bool { - return httpMethodRegex.MatchString(method) + for _, x := range allHTTPMethods { + if method == x { + return true + } + } + + return false } // defaultTo returns the value of the default @@ -338,7 +357,7 @@ func transferBytes(src io.Reader, dest io.Writer, wg *sync.WaitGroup) (int64, er } // tryUpdateConnection attempt to upgrade the connection to a http pdy stream -func tryUpdateConnection(cx *gin.Context, endpoint *url.URL) error { +func tryUpdateConnection(req *http.Request, writer http.ResponseWriter, endpoint *url.URL) error { // step: dial the endpoint tlsConn, err := tryDialEndpoint(endpoint) if err != nil { @@ -347,14 +366,14 @@ func tryUpdateConnection(cx *gin.Context, endpoint *url.URL) error { defer tlsConn.Close() // step: we need to hijack the underlining client connection - clientConn, _, err := cx.Writer.(http.Hijacker).Hijack() + clientConn, _, err := writer.(http.Hijacker).Hijack() if err != nil { return err } defer clientConn.Close() // step: write the request to upstream - if err = cx.Request.Write(tlsConn); err != nil { + if err = req.Write(tlsConn); err != nil { return err } @@ -449,8 +468,13 @@ func loadCA(cert, key string) (*tls.Certificate, error) { // getWithin calculates a duration of x percent of the time period, i.e. something // expires in 1 hours, get me a duration within 80% -func getWithin(expires time.Time, in float64) time.Duration { - seconds := int(float64(expires.Sub(time.Now()).Seconds()) * in) +func getWithin(expires time.Time, within float64) time.Duration { + left := expires.UTC().Sub(time.Now().UTC()).Seconds() + if left <= 0 { + return time.Duration(0) + } + seconds := int(float64(left * within)) + return time.Duration(seconds) * time.Second } diff --git a/utils_test.go b/utils_test.go index 21e4876a4..f4339134b 100644 --- a/utils_test.go +++ b/utils_test.go @@ -273,7 +273,7 @@ func TestIdValidHTTPMethod(t *testing.T) { }{ {Method: "GET", Ok: true}, {Method: "GETT"}, - {Method: "CONNECT"}, + {Method: "CONNECT", Ok: false}, {Method: "PUT", Ok: true}, {Method: "PATCH", Ok: true}, } diff --git a/vendor/github.com/coreos/go-systemd/LICENSE b/vendor/github.com/coreos/go-systemd/LICENSE deleted file mode 100644 index 37ec93a14..000000000 --- a/vendor/github.com/coreos/go-systemd/LICENSE +++ /dev/null @@ -1,191 +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: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -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 -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. diff --git a/vendor/github.com/dgrijalva/jwt-go/.gitignore b/vendor/github.com/dgrijalva/jwt-go/.gitignore new file mode 100644 index 000000000..80bed650e --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +bin + + diff --git a/vendor/github.com/dgrijalva/jwt-go/.travis.yml b/vendor/github.com/dgrijalva/jwt-go/.travis.yml new file mode 100644 index 000000000..1027f56cd --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/.travis.yml @@ -0,0 +1,13 @@ +language: go + +script: + - go vet ./... + - go test -v ./... + +go: + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - tip diff --git a/vendor/github.com/dgrijalva/jwt-go/LICENSE b/vendor/github.com/dgrijalva/jwt-go/LICENSE new file mode 100644 index 000000000..df83a9c2f --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/LICENSE @@ -0,0 +1,8 @@ +Copyright (c) 2012 Dave Grijalva + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md b/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md new file mode 100644 index 000000000..7fc1f793c --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md @@ -0,0 +1,97 @@ +## Migration Guide from v2 -> v3 + +Version 3 adds several new, frequently requested features. To do so, it introduces a few breaking changes. We've worked to keep these as minimal as possible. This guide explains the breaking changes and how you can quickly update your code. + +### `Token.Claims` is now an interface type + +The most requested feature from the 2.0 verison of this library was the ability to provide a custom type to the JSON parser for claims. This was implemented by introducing a new interface, `Claims`, to replace `map[string]interface{}`. We also included two concrete implementations of `Claims`: `MapClaims` and `StandardClaims`. + +`MapClaims` is an alias for `map[string]interface{}` with built in validation behavior. It is the default claims type when using `Parse`. The usage is unchanged except you must type cast the claims property. + +The old example for parsing a token looked like this.. + +```go + if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil { + fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) + } +``` + +is now directly mapped to... + +```go + if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil { + claims := token.Claims.(jwt.MapClaims) + fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"]) + } +``` + +`StandardClaims` is designed to be embedded in your custom type. You can supply a custom claims type with the new `ParseWithClaims` function. Here's an example of using a custom claims type. + +```go + type MyCustomClaims struct { + User string + *StandardClaims + } + + if token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, keyLookupFunc); err == nil { + claims := token.Claims.(*MyCustomClaims) + fmt.Printf("Token for user %v expires %v", claims.User, claims.StandardClaims.ExpiresAt) + } +``` + +### `ParseFromRequest` has been moved + +To keep this library focused on the tokens without becoming overburdened with complex request processing logic, `ParseFromRequest` and its new companion `ParseFromRequestWithClaims` have been moved to a subpackage, `request`. The method signatues have also been augmented to receive a new argument: `Extractor`. + +`Extractors` do the work of picking the token string out of a request. The interface is simple and composable. + +This simple parsing example: + +```go + if token, err := jwt.ParseFromRequest(tokenString, req, keyLookupFunc); err == nil { + fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) + } +``` + +is directly mapped to: + +```go + if token, err := request.ParseFromRequest(req, request.OAuth2Extractor, keyLookupFunc); err == nil { + claims := token.Claims.(jwt.MapClaims) + fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"]) + } +``` + +There are several concrete `Extractor` types provided for your convenience: + +* `HeaderExtractor` will search a list of headers until one contains content. +* `ArgumentExtractor` will search a list of keys in request query and form arguments until one contains content. +* `MultiExtractor` will try a list of `Extractors` in order until one returns content. +* `AuthorizationHeaderExtractor` will look in the `Authorization` header for a `Bearer` token. +* `OAuth2Extractor` searches the places an OAuth2 token would be specified (per the spec): `Authorization` header and `access_token` argument +* `PostExtractionFilter` wraps an `Extractor`, allowing you to process the content before it's parsed. A simple example is stripping the `Bearer ` text from a header + + +### RSA signing methods no longer accept `[]byte` keys + +Due to a [critical vulnerability](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/), we've decided the convenience of accepting `[]byte` instead of `rsa.PublicKey` or `rsa.PrivateKey` isn't worth the risk of misuse. + +To replace this behavior, we've added two helper methods: `ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error)` and `ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error)`. These are just simple helpers for unpacking PEM encoded PKCS1 and PKCS8 keys. If your keys are encoded any other way, all you need to do is convert them to the `crypto/rsa` package's types. + +```go + func keyLookupFunc(*Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + // Look up key + key, err := lookupPublicKey(token.Header["kid"]) + if err != nil { + return nil, err + } + + // Unpack key from PEM encoded PKCS8 + return jwt.ParseRSAPublicKeyFromPEM(key) + } +``` diff --git a/vendor/github.com/dgrijalva/jwt-go/README.md b/vendor/github.com/dgrijalva/jwt-go/README.md new file mode 100644 index 000000000..f48365faf --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/README.md @@ -0,0 +1,85 @@ +A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) + +[![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go) + +**BREAKING CHANGES:*** Version 3.0.0 is here. It includes _a lot_ of changes including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code. + +**NOTICE:** A vulnerability in JWT was [recently published](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). As this library doesn't force users to validate the `alg` is what they expected, it's possible your usage is effected. There will be an update soon to remedy this, and it will likey require backwards-incompatible changes to the API. In the short term, please make sure your implementation verifies the `alg` is what you expect. + + +## What the heck is a JWT? + +JWT.io has [a great introduction](https://jwt.io/introduction) to JSON Web Tokens. + +In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way. + +The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used. + +The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [the RFC](http://self-issued.info/docs/draft-jones-json-web-token.html) for information about reserved keys and the proper way to add your own. + +## What's in the box? + +This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are HMAC SHA, RSA, RSA-PSS, and ECDSA, though hooks are present for adding your own. + +## Examples + +See [the project documentation](https://godoc.org/github.com/dgrijalva/jwt-go) for examples of usage: + +* [Simple example of parsing and validating a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac) +* [Simple example of building and signing a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-New--Hmac) +* [Directory of Examples](https://godoc.org/github.com/dgrijalva/jwt-go#pkg-examples) + +## Extensions + +This library publishes all the necessary components for adding your own signing methods. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod`. + +Here's an example of an extension that integrates with the Google App Engine signing tools: https://github.com/someone1/gcp-jwt-go + +## Compliance + +This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences: + +* In order to protect against accidental use of [Unsecured JWTs](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#UnsecuredJWT), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key. + +## Project Status & Versioning + +This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason). + +This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases). + +While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning. + +## Usage Tips + +### Signing vs Encryption + +A token is simply a JSON object that is signed by its author. this tells you exactly two things about the data: + +* The author of the token was in the possession of the signing secret +* The data has not been modified since it was signed + +It's important to know that JWT does not provide encryption, which means anyone who has access to the token can read its contents. If you need to protect (encrypt) the data, there is a companion spec, `JWE`, that provides this functionality. JWE is currently outside the scope of this library. + +### Choosing a Signing Method + +There are several signing methods available, and you should probably take the time to learn about the various options before choosing one. The principal design decision is most likely going to be symmetric vs asymmetric. + +Symmetric signing methods, such as HSA, use only a single secret. This is probably the simplest signing method to use since any `[]byte` can be used as a valid secret. They are also slightly computationally faster to use, though this rarely is enough to matter. Symmetric signing methods work the best when both producers and consumers of tokens are trusted, or even the same system. Since the same secret is used to both sign and validate tokens, you can't easily distribute the key for validation. + +Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification. + +### JWT and OAuth + +It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication. + +Without going too far down the rabbit hole, here's a description of the interaction of these technologies: + +* OAuth is a protocol for allowing an identity provider to be separate from the service a user is logging in to. For example, whenever you use Facebook to log into a different service (Yelp, Spotify, etc), you are using OAuth. +* OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token. +* Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL. + +## More + +Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go). + +The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in to documentation. diff --git a/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md b/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md new file mode 100644 index 000000000..b605b4509 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md @@ -0,0 +1,105 @@ +## `jwt-go` Version History + +#### 3.0.0 + +* **Compatibility Breaking Changes**: See MIGRATION_GUIDE.md for tips on updating your code + * Dropped support for `[]byte` keys when using RSA signing methods. This convenience feature could contribute to security vulnerabilities involving mismatched key types with signing methods. + * `ParseFromRequest` has been moved to `request` subpackage and usage has changed + * The `Claims` property on `Token` is now type `Claims` instead of `map[string]interface{}`. The default value is type `MapClaims`, which is an alias to `map[string]interface{}`. This makes it possible to use a custom type when decoding claims. +* Other Additions and Changes + * Added `Claims` interface type to allow users to decode the claims into a custom type + * Added `ParseWithClaims`, which takes a third argument of type `Claims`. Use this function instead of `Parse` if you have a custom type you'd like to decode into. + * Dramatically improved the functionality and flexibility of `ParseFromRequest`, which is now in the `request` subpackage + * Added `ParseFromRequestWithClaims` which is the `FromRequest` equivalent of `ParseWithClaims` + * Added new interface type `Extractor`, which is used for extracting JWT strings from http requests. Used with `ParseFromRequest` and `ParseFromRequestWithClaims`. + * Added several new, more specific, validation errors to error type bitmask + * Moved examples from README to executable example files + * Signing method registry is now thread safe + * Added new property to `ValidationError`, which contains the raw error returned by calls made by parse/verify (such as those returned by keyfunc or json parser) + +#### 2.7.0 + +This will likely be the last backwards compatible release before 3.0.0, excluding essential bug fixes. + +* Added new option `-show` to the `jwt` command that will just output the decoded token without verifying +* Error text for expired tokens includes how long it's been expired +* Fixed incorrect error returned from `ParseRSAPublicKeyFromPEM` +* Documentation updates + +#### 2.6.0 + +* Exposed inner error within ValidationError +* Fixed validation errors when using UseJSONNumber flag +* Added several unit tests + +#### 2.5.0 + +* Added support for signing method none. You shouldn't use this. The API tries to make this clear. +* Updated/fixed some documentation +* Added more helpful error message when trying to parse tokens that begin with `BEARER ` + +#### 2.4.0 + +* Added new type, Parser, to allow for configuration of various parsing parameters + * You can now specify a list of valid signing methods. Anything outside this set will be rejected. + * You can now opt to use the `json.Number` type instead of `float64` when parsing token JSON +* Added support for [Travis CI](https://travis-ci.org/dgrijalva/jwt-go) +* Fixed some bugs with ECDSA parsing + +#### 2.3.0 + +* Added support for ECDSA signing methods +* Added support for RSA PSS signing methods (requires go v1.4) + +#### 2.2.0 + +* Gracefully handle a `nil` `Keyfunc` being passed to `Parse`. Result will now be the parsed token and an error, instead of a panic. + +#### 2.1.0 + +Backwards compatible API change that was missed in 2.0.0. + +* The `SignedString` method on `Token` now takes `interface{}` instead of `[]byte` + +#### 2.0.0 + +There were two major reasons for breaking backwards compatibility with this update. The first was a refactor required to expand the width of the RSA and HMAC-SHA signing implementations. There will likely be no required code changes to support this change. + +The second update, while unfortunately requiring a small change in integration, is required to open up this library to other signing methods. Not all keys used for all signing methods have a single standard on-disk representation. Requiring `[]byte` as the type for all keys proved too limiting. Additionally, this implementation allows for pre-parsed tokens to be reused, which might matter in an application that parses a high volume of tokens with a small set of keys. Backwards compatibilty has been maintained for passing `[]byte` to the RSA signing methods, but they will also accept `*rsa.PublicKey` and `*rsa.PrivateKey`. + +It is likely the only integration change required here will be to change `func(t *jwt.Token) ([]byte, error)` to `func(t *jwt.Token) (interface{}, error)` when calling `Parse`. + +* **Compatibility Breaking Changes** + * `SigningMethodHS256` is now `*SigningMethodHMAC` instead of `type struct` + * `SigningMethodRS256` is now `*SigningMethodRSA` instead of `type struct` + * `KeyFunc` now returns `interface{}` instead of `[]byte` + * `SigningMethod.Sign` now takes `interface{}` instead of `[]byte` for the key + * `SigningMethod.Verify` now takes `interface{}` instead of `[]byte` for the key +* Renamed type `SigningMethodHS256` to `SigningMethodHMAC`. Specific sizes are now just instances of this type. + * Added public package global `SigningMethodHS256` + * Added public package global `SigningMethodHS384` + * Added public package global `SigningMethodHS512` +* Renamed type `SigningMethodRS256` to `SigningMethodRSA`. Specific sizes are now just instances of this type. + * Added public package global `SigningMethodRS256` + * Added public package global `SigningMethodRS384` + * Added public package global `SigningMethodRS512` +* Moved sample private key for HMAC tests from an inline value to a file on disk. Value is unchanged. +* Refactored the RSA implementation to be easier to read +* Exposed helper methods `ParseRSAPrivateKeyFromPEM` and `ParseRSAPublicKeyFromPEM` + +#### 1.0.2 + +* Fixed bug in parsing public keys from certificates +* Added more tests around the parsing of keys for RS256 +* Code refactoring in RS256 implementation. No functional changes + +#### 1.0.1 + +* Fixed panic if RS256 signing method was passed an invalid key + +#### 1.0.0 + +* First versioned release +* API stabilized +* Supports creating, signing, parsing, and validating JWT tokens +* Supports RS256 and HS256 signing methods \ No newline at end of file diff --git a/vendor/github.com/dgrijalva/jwt-go/claims.go b/vendor/github.com/dgrijalva/jwt-go/claims.go new file mode 100644 index 000000000..f0228f02e --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/claims.go @@ -0,0 +1,134 @@ +package jwt + +import ( + "crypto/subtle" + "fmt" + "time" +) + +// For a type to be a Claims object, it must just have a Valid method that determines +// if the token is invalid for any supported reason +type Claims interface { + Valid() error +} + +// Structured version of Claims Section, as referenced at +// https://tools.ietf.org/html/rfc7519#section-4.1 +// See examples for how to use this with your own claim types +type StandardClaims struct { + Audience string `json:"aud,omitempty"` + ExpiresAt int64 `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` +} + +// Validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (c StandardClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + // The claims below are optional, by default, so if they are set to the + // default value in Go, let's not fail the verification for them. + if c.VerifyExpiresAt(now, false) == false { + delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) + vErr.Inner = fmt.Errorf("token is expired by %v", delta) + vErr.Errors |= ValidationErrorExpired + } + + if c.VerifyIssuedAt(now, false) == false { + vErr.Inner = fmt.Errorf("Token used before issued") + vErr.Errors |= ValidationErrorIssuedAt + } + + if c.VerifyNotBefore(now, false) == false { + vErr.Inner = fmt.Errorf("token is not valid yet") + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { + return verifyAud(c.Audience, cmp, req) +} + +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { + return verifyExp(c.ExpiresAt, cmp, req) +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { + return verifyIat(c.IssuedAt, cmp, req) +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { + return verifyIss(c.Issuer, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { + return verifyNbf(c.NotBefore, cmp, req) +} + +// ----- helpers + +func verifyAud(aud string, cmp string, required bool) bool { + if aud == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { + return true + } else { + return false + } +} + +func verifyExp(exp int64, now int64, required bool) bool { + if exp == 0 { + return !required + } + return now <= exp +} + +func verifyIat(iat int64, now int64, required bool) bool { + if iat == 0 { + return !required + } + return now >= iat +} + +func verifyIss(iss string, cmp string, required bool) bool { + if iss == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { + return true + } else { + return false + } +} + +func verifyNbf(nbf int64, now int64, required bool) bool { + if nbf == 0 { + return !required + } + return now >= nbf +} diff --git a/vendor/github.com/dgrijalva/jwt-go/doc.go b/vendor/github.com/dgrijalva/jwt-go/doc.go new file mode 100644 index 000000000..a86dc1a3b --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/doc.go @@ -0,0 +1,4 @@ +// Package jwt is a Go implementation of JSON Web Tokens: http://self-issued.info/docs/draft-jones-json-web-token.html +// +// See README.md for more info. +package jwt diff --git a/vendor/github.com/dgrijalva/jwt-go/ecdsa.go b/vendor/github.com/dgrijalva/jwt-go/ecdsa.go new file mode 100644 index 000000000..2f59a2223 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/ecdsa.go @@ -0,0 +1,147 @@ +package jwt + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "errors" + "math/big" +) + +var ( + // Sadly this is missing from crypto/ecdsa compared to crypto/rsa + ErrECDSAVerification = errors.New("crypto/ecdsa: verification error") +) + +// Implements the ECDSA family of signing methods signing methods +type SigningMethodECDSA struct { + Name string + Hash crypto.Hash + KeySize int + CurveBits int +} + +// Specific instances for EC256 and company +var ( + SigningMethodES256 *SigningMethodECDSA + SigningMethodES384 *SigningMethodECDSA + SigningMethodES512 *SigningMethodECDSA +) + +func init() { + // ES256 + SigningMethodES256 = &SigningMethodECDSA{"ES256", crypto.SHA256, 32, 256} + RegisterSigningMethod(SigningMethodES256.Alg(), func() SigningMethod { + return SigningMethodES256 + }) + + // ES384 + SigningMethodES384 = &SigningMethodECDSA{"ES384", crypto.SHA384, 48, 384} + RegisterSigningMethod(SigningMethodES384.Alg(), func() SigningMethod { + return SigningMethodES384 + }) + + // ES512 + SigningMethodES512 = &SigningMethodECDSA{"ES512", crypto.SHA512, 66, 521} + RegisterSigningMethod(SigningMethodES512.Alg(), func() SigningMethod { + return SigningMethodES512 + }) +} + +func (m *SigningMethodECDSA) Alg() string { + return m.Name +} + +// Implements the Verify method from SigningMethod +// For this verify method, key must be an ecdsa.PublicKey struct +func (m *SigningMethodECDSA) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + // Get the key + var ecdsaKey *ecdsa.PublicKey + switch k := key.(type) { + case *ecdsa.PublicKey: + ecdsaKey = k + default: + return ErrInvalidKeyType + } + + if len(sig) != 2*m.KeySize { + return ErrECDSAVerification + } + + r := big.NewInt(0).SetBytes(sig[:m.KeySize]) + s := big.NewInt(0).SetBytes(sig[m.KeySize:]) + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Verify the signature + if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), r, s); verifystatus == true { + return nil + } else { + return ErrECDSAVerification + } +} + +// Implements the Sign method from SigningMethod +// For this signing method, key must be an ecdsa.PrivateKey struct +func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string, error) { + // Get the key + var ecdsaKey *ecdsa.PrivateKey + switch k := key.(type) { + case *ecdsa.PrivateKey: + ecdsaKey = k + default: + return "", ErrInvalidKeyType + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return r, s + if r, s, err := ecdsa.Sign(rand.Reader, ecdsaKey, hasher.Sum(nil)); err == nil { + curveBits := ecdsaKey.Curve.Params().BitSize + + if m.CurveBits != curveBits { + return "", ErrInvalidKey + } + + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes += 1 + } + + // We serialize the outpus (r and s) into big-endian byte arrays and pad + // them with zeros on the left to make sure the sizes work out. Both arrays + // must be keyBytes long, and the output must be 2*keyBytes long. + rBytes := r.Bytes() + rBytesPadded := make([]byte, keyBytes) + copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) + + sBytes := s.Bytes() + sBytesPadded := make([]byte, keyBytes) + copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) + + out := append(rBytesPadded, sBytesPadded...) + + return EncodeSegment(out), nil + } else { + return "", err + } +} diff --git a/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go b/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go new file mode 100644 index 000000000..d19624b72 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/ecdsa_utils.go @@ -0,0 +1,67 @@ +package jwt + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrNotECPublicKey = errors.New("Key is not a valid ECDSA public key") + ErrNotECPrivateKey = errors.New("Key is not a valid ECDSA private key") +) + +// Parse PEM encoded Elliptic Curve Private Key Structure +func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { + return nil, err + } + + var pkey *ecdsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { + return nil, ErrNotECPrivateKey + } + + return pkey, nil +} + +// Parse PEM encoded PKCS1 or PKCS8 public key +func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + var pkey *ecdsa.PublicKey + var ok bool + if pkey, ok = parsedKey.(*ecdsa.PublicKey); !ok { + return nil, ErrNotECPublicKey + } + + return pkey, nil +} diff --git a/vendor/github.com/dgrijalva/jwt-go/errors.go b/vendor/github.com/dgrijalva/jwt-go/errors.go new file mode 100644 index 000000000..1c93024aa --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/errors.go @@ -0,0 +1,59 @@ +package jwt + +import ( + "errors" +) + +// Error constants +var ( + ErrInvalidKey = errors.New("key is invalid") + ErrInvalidKeyType = errors.New("key is of invalid type") + ErrHashUnavailable = errors.New("the requested hash function is unavailable") +) + +// The errors that might occur when parsing and validating a token +const ( + ValidationErrorMalformed uint32 = 1 << iota // Token is malformed + ValidationErrorUnverifiable // Token could not be verified because of signing problems + ValidationErrorSignatureInvalid // Signature validation failed + + // Standard Claim validation errors + ValidationErrorAudience // AUD validation failed + ValidationErrorExpired // EXP validation failed + ValidationErrorIssuedAt // IAT validation failed + ValidationErrorIssuer // ISS validation failed + ValidationErrorNotValidYet // NBF validation failed + ValidationErrorId // JTI validation failed + ValidationErrorClaimsInvalid // Generic claims validation error +) + +// Helper for constructing a ValidationError with a string error message +func NewValidationError(errorText string, errorFlags uint32) *ValidationError { + return &ValidationError{ + text: errorText, + Errors: errorFlags, + } +} + +// The error from Parse if token is not valid +type ValidationError struct { + Inner error // stores the error returned by external dependencies, i.e.: KeyFunc + Errors uint32 // bitfield. see ValidationError... constants + text string // errors that do not have a valid error just have text +} + +// Validation error is an error type +func (e ValidationError) Error() string { + if e.Inner != nil { + return e.Inner.Error() + } else if e.text != "" { + return e.text + } else { + return "token is invalid" + } +} + +// No errors +func (e *ValidationError) valid() bool { + return e.Errors == 0 +} diff --git a/vendor/github.com/dgrijalva/jwt-go/hmac.go b/vendor/github.com/dgrijalva/jwt-go/hmac.go new file mode 100644 index 000000000..c22991925 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/hmac.go @@ -0,0 +1,94 @@ +package jwt + +import ( + "crypto" + "crypto/hmac" + "errors" +) + +// Implements the HMAC-SHA family of signing methods signing methods +type SigningMethodHMAC struct { + Name string + Hash crypto.Hash +} + +// Specific instances for HS256 and company +var ( + SigningMethodHS256 *SigningMethodHMAC + SigningMethodHS384 *SigningMethodHMAC + SigningMethodHS512 *SigningMethodHMAC + ErrSignatureInvalid = errors.New("signature is invalid") +) + +func init() { + // HS256 + SigningMethodHS256 = &SigningMethodHMAC{"HS256", crypto.SHA256} + RegisterSigningMethod(SigningMethodHS256.Alg(), func() SigningMethod { + return SigningMethodHS256 + }) + + // HS384 + SigningMethodHS384 = &SigningMethodHMAC{"HS384", crypto.SHA384} + RegisterSigningMethod(SigningMethodHS384.Alg(), func() SigningMethod { + return SigningMethodHS384 + }) + + // HS512 + SigningMethodHS512 = &SigningMethodHMAC{"HS512", crypto.SHA512} + RegisterSigningMethod(SigningMethodHS512.Alg(), func() SigningMethod { + return SigningMethodHS512 + }) +} + +func (m *SigningMethodHMAC) Alg() string { + return m.Name +} + +// Verify the signature of HSXXX tokens. Returns nil if the signature is valid. +func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error { + // Verify the key is the right type + keyBytes, ok := key.([]byte) + if !ok { + return ErrInvalidKeyType + } + + // Decode signature, for comparison + sig, err := DecodeSegment(signature) + if err != nil { + return err + } + + // Can we use the specified hashing method? + if !m.Hash.Available() { + return ErrHashUnavailable + } + + // This signing method is symmetric, so we validate the signature + // by reproducing the signature from the signing string and key, then + // comparing that against the provided signature. + hasher := hmac.New(m.Hash.New, keyBytes) + hasher.Write([]byte(signingString)) + if !hmac.Equal(sig, hasher.Sum(nil)) { + return ErrSignatureInvalid + } + + // No validation errors. Signature is good. + return nil +} + +// Implements the Sign method from SigningMethod for this signing method. +// Key must be []byte +func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) { + if keyBytes, ok := key.([]byte); ok { + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := hmac.New(m.Hash.New, keyBytes) + hasher.Write([]byte(signingString)) + + return EncodeSegment(hasher.Sum(nil)), nil + } + + return "", ErrInvalidKey +} diff --git a/vendor/github.com/dgrijalva/jwt-go/map_claims.go b/vendor/github.com/dgrijalva/jwt-go/map_claims.go new file mode 100644 index 000000000..291213c46 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/map_claims.go @@ -0,0 +1,94 @@ +package jwt + +import ( + "encoding/json" + "errors" + // "fmt" +) + +// Claims type that uses the map[string]interface{} for JSON decoding +// This is the default claims type if you don't supply one +type MapClaims map[string]interface{} + +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyAudience(cmp string, req bool) bool { + aud, _ := m["aud"].(string) + return verifyAud(aud, cmp, req) +} + +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { + switch exp := m["exp"].(type) { + case float64: + return verifyExp(int64(exp), cmp, req) + case json.Number: + v, _ := exp.Int64() + return verifyExp(v, cmp, req) + } + return req == false +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { + switch iat := m["iat"].(type) { + case float64: + return verifyIat(int64(iat), cmp, req) + case json.Number: + v, _ := iat.Int64() + return verifyIat(v, cmp, req) + } + return req == false +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyIssuer(cmp string, req bool) bool { + iss, _ := m["iss"].(string) + return verifyIss(iss, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { + switch nbf := m["nbf"].(type) { + case float64: + return verifyNbf(int64(nbf), cmp, req) + case json.Number: + v, _ := nbf.Int64() + return verifyNbf(v, cmp, req) + } + return req == false +} + +// Validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (m MapClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if m.VerifyExpiresAt(now, false) == false { + vErr.Inner = errors.New("Token is expired") + vErr.Errors |= ValidationErrorExpired + } + + if m.VerifyIssuedAt(now, false) == false { + vErr.Inner = errors.New("Token used before issued") + vErr.Errors |= ValidationErrorIssuedAt + } + + if m.VerifyNotBefore(now, false) == false { + vErr.Inner = errors.New("Token is not valid yet") + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} diff --git a/vendor/github.com/dgrijalva/jwt-go/none.go b/vendor/github.com/dgrijalva/jwt-go/none.go new file mode 100644 index 000000000..f04d189d0 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/none.go @@ -0,0 +1,52 @@ +package jwt + +// Implements the none signing method. This is required by the spec +// but you probably should never use it. +var SigningMethodNone *signingMethodNone + +const UnsafeAllowNoneSignatureType unsafeNoneMagicConstant = "none signing method allowed" + +var NoneSignatureTypeDisallowedError error + +type signingMethodNone struct{} +type unsafeNoneMagicConstant string + +func init() { + SigningMethodNone = &signingMethodNone{} + NoneSignatureTypeDisallowedError = NewValidationError("'none' signature type is not allowed", ValidationErrorSignatureInvalid) + + RegisterSigningMethod(SigningMethodNone.Alg(), func() SigningMethod { + return SigningMethodNone + }) +} + +func (m *signingMethodNone) Alg() string { + return "none" +} + +// Only allow 'none' alg type if UnsafeAllowNoneSignatureType is specified as the key +func (m *signingMethodNone) Verify(signingString, signature string, key interface{}) (err error) { + // Key must be UnsafeAllowNoneSignatureType to prevent accidentally + // accepting 'none' signing method + if _, ok := key.(unsafeNoneMagicConstant); !ok { + return NoneSignatureTypeDisallowedError + } + // If signing method is none, signature must be an empty string + if signature != "" { + return NewValidationError( + "'none' signing method with non-empty signature", + ValidationErrorSignatureInvalid, + ) + } + + // Accept 'none' signing method. + return nil +} + +// Only allow 'none' signing if UnsafeAllowNoneSignatureType is specified as the key +func (m *signingMethodNone) Sign(signingString string, key interface{}) (string, error) { + if _, ok := key.(unsafeNoneMagicConstant); ok { + return "", nil + } + return "", NoneSignatureTypeDisallowedError +} diff --git a/vendor/github.com/dgrijalva/jwt-go/parser.go b/vendor/github.com/dgrijalva/jwt-go/parser.go new file mode 100644 index 000000000..7bf1c4ea0 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/parser.go @@ -0,0 +1,131 @@ +package jwt + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +type Parser struct { + ValidMethods []string // If populated, only these methods will be considered valid + UseJSONNumber bool // Use JSON Number format in JSON decoder + SkipClaimsValidation bool // Skip claims validation during token parsing +} + +// Parse, validate, and return a token. +// keyFunc will receive the parsed token and should return the key for validating. +// If everything is kosher, err will be nil +func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { + return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc) +} + +func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { + parts := strings.Split(tokenString, ".") + if len(parts) != 3 { + return nil, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed) + } + + var err error + token := &Token{Raw: tokenString} + + // parse Header + var headerBytes []byte + if headerBytes, err = DecodeSegment(parts[0]); err != nil { + if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") { + return token, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed) + } + return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + if err = json.Unmarshal(headerBytes, &token.Header); err != nil { + return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + + // parse Claims + var claimBytes []byte + token.Claims = claims + + if claimBytes, err = DecodeSegment(parts[1]); err != nil { + return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + dec := json.NewDecoder(bytes.NewBuffer(claimBytes)) + if p.UseJSONNumber { + dec.UseNumber() + } + // JSON Decode. Special case for map type to avoid weird pointer behavior + if c, ok := token.Claims.(MapClaims); ok { + err = dec.Decode(&c) + } else { + err = dec.Decode(&claims) + } + // Handle decode error + if err != nil { + return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + + // Lookup signature method + if method, ok := token.Header["alg"].(string); ok { + if token.Method = GetSigningMethod(method); token.Method == nil { + return token, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable) + } + } else { + return token, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable) + } + + // Verify signing method is in the required set + if p.ValidMethods != nil { + var signingMethodValid = false + var alg = token.Method.Alg() + for _, m := range p.ValidMethods { + if m == alg { + signingMethodValid = true + break + } + } + if !signingMethodValid { + // signing method is not in the listed set + return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid) + } + } + + // Lookup key + var key interface{} + if keyFunc == nil { + // keyFunc was not provided. short circuiting validation + return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable) + } + if key, err = keyFunc(token); err != nil { + // keyFunc returned an error + return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable} + } + + vErr := &ValidationError{} + + // Validate Claims + if !p.SkipClaimsValidation { + if err := token.Claims.Valid(); err != nil { + + // If the Claims Valid returned an error, check if it is a validation error, + // If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set + if e, ok := err.(*ValidationError); !ok { + vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid} + } else { + vErr = e + } + } + } + + // Perform validation + token.Signature = parts[2] + if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil { + vErr.Inner = err + vErr.Errors |= ValidationErrorSignatureInvalid + } + + if vErr.valid() { + token.Valid = true + return token, nil + } + + return token, vErr +} diff --git a/vendor/github.com/dgrijalva/jwt-go/rsa.go b/vendor/github.com/dgrijalva/jwt-go/rsa.go new file mode 100644 index 000000000..0ae0b1984 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/rsa.go @@ -0,0 +1,100 @@ +package jwt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" +) + +// Implements the RSA family of signing methods signing methods +type SigningMethodRSA struct { + Name string + Hash crypto.Hash +} + +// Specific instances for RS256 and company +var ( + SigningMethodRS256 *SigningMethodRSA + SigningMethodRS384 *SigningMethodRSA + SigningMethodRS512 *SigningMethodRSA +) + +func init() { + // RS256 + SigningMethodRS256 = &SigningMethodRSA{"RS256", crypto.SHA256} + RegisterSigningMethod(SigningMethodRS256.Alg(), func() SigningMethod { + return SigningMethodRS256 + }) + + // RS384 + SigningMethodRS384 = &SigningMethodRSA{"RS384", crypto.SHA384} + RegisterSigningMethod(SigningMethodRS384.Alg(), func() SigningMethod { + return SigningMethodRS384 + }) + + // RS512 + SigningMethodRS512 = &SigningMethodRSA{"RS512", crypto.SHA512} + RegisterSigningMethod(SigningMethodRS512.Alg(), func() SigningMethod { + return SigningMethodRS512 + }) +} + +func (m *SigningMethodRSA) Alg() string { + return m.Name +} + +// Implements the Verify method from SigningMethod +// For this signing method, must be an rsa.PublicKey structure. +func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + var rsaKey *rsa.PublicKey + var ok bool + + if rsaKey, ok = key.(*rsa.PublicKey); !ok { + return ErrInvalidKeyType + } + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Verify the signature + return rsa.VerifyPKCS1v15(rsaKey, m.Hash, hasher.Sum(nil), sig) +} + +// Implements the Sign method from SigningMethod +// For this signing method, must be an rsa.PrivateKey structure. +func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) { + var rsaKey *rsa.PrivateKey + var ok bool + + // Validate type of key + if rsaKey, ok = key.(*rsa.PrivateKey); !ok { + return "", ErrInvalidKey + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return the encoded bytes + if sigBytes, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil)); err == nil { + return EncodeSegment(sigBytes), nil + } else { + return "", err + } +} diff --git a/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go b/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go new file mode 100644 index 000000000..10ee9db8a --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/rsa_pss.go @@ -0,0 +1,126 @@ +// +build go1.4 + +package jwt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" +) + +// Implements the RSAPSS family of signing methods signing methods +type SigningMethodRSAPSS struct { + *SigningMethodRSA + Options *rsa.PSSOptions +} + +// Specific instances for RS/PS and company +var ( + SigningMethodPS256 *SigningMethodRSAPSS + SigningMethodPS384 *SigningMethodRSAPSS + SigningMethodPS512 *SigningMethodRSAPSS +) + +func init() { + // PS256 + SigningMethodPS256 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS256", + Hash: crypto.SHA256, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA256, + }, + } + RegisterSigningMethod(SigningMethodPS256.Alg(), func() SigningMethod { + return SigningMethodPS256 + }) + + // PS384 + SigningMethodPS384 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS384", + Hash: crypto.SHA384, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA384, + }, + } + RegisterSigningMethod(SigningMethodPS384.Alg(), func() SigningMethod { + return SigningMethodPS384 + }) + + // PS512 + SigningMethodPS512 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS512", + Hash: crypto.SHA512, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA512, + }, + } + RegisterSigningMethod(SigningMethodPS512.Alg(), func() SigningMethod { + return SigningMethodPS512 + }) +} + +// Implements the Verify method from SigningMethod +// For this verify method, key must be an rsa.PublicKey struct +func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + var rsaKey *rsa.PublicKey + switch k := key.(type) { + case *rsa.PublicKey: + rsaKey = k + default: + return ErrInvalidKey + } + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + return rsa.VerifyPSS(rsaKey, m.Hash, hasher.Sum(nil), sig, m.Options) +} + +// Implements the Sign method from SigningMethod +// For this signing method, key must be an rsa.PrivateKey struct +func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (string, error) { + var rsaKey *rsa.PrivateKey + + switch k := key.(type) { + case *rsa.PrivateKey: + rsaKey = k + default: + return "", ErrInvalidKeyType + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return the encoded bytes + if sigBytes, err := rsa.SignPSS(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil), m.Options); err == nil { + return EncodeSegment(sigBytes), nil + } else { + return "", err + } +} diff --git a/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go b/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go new file mode 100644 index 000000000..213a90dbb --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/rsa_utils.go @@ -0,0 +1,69 @@ +package jwt + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key") + ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key") + ErrNotRSAPublicKey = errors.New("Key is not a valid RSA public key") +) + +// Parse PEM encoded PKCS1 or PKCS8 private key +func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + var parsedKey interface{} + if parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { + return nil, err + } + } + + var pkey *rsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, ErrNotRSAPrivateKey + } + + return pkey, nil +} + +// Parse PEM encoded PKCS1 or PKCS8 public key +func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + var pkey *rsa.PublicKey + var ok bool + if pkey, ok = parsedKey.(*rsa.PublicKey); !ok { + return nil, ErrNotRSAPublicKey + } + + return pkey, nil +} diff --git a/vendor/github.com/dgrijalva/jwt-go/signing_method.go b/vendor/github.com/dgrijalva/jwt-go/signing_method.go new file mode 100644 index 000000000..ed1f212b2 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/signing_method.go @@ -0,0 +1,35 @@ +package jwt + +import ( + "sync" +) + +var signingMethods = map[string]func() SigningMethod{} +var signingMethodLock = new(sync.RWMutex) + +// Implement SigningMethod to add new methods for signing or verifying tokens. +type SigningMethod interface { + Verify(signingString, signature string, key interface{}) error // Returns nil if signature is valid + Sign(signingString string, key interface{}) (string, error) // Returns encoded signature or error + Alg() string // returns the alg identifier for this method (example: 'HS256') +} + +// Register the "alg" name and a factory function for signing method. +// This is typically done during init() in the method's implementation +func RegisterSigningMethod(alg string, f func() SigningMethod) { + signingMethodLock.Lock() + defer signingMethodLock.Unlock() + + signingMethods[alg] = f +} + +// Get a signing method from an "alg" string +func GetSigningMethod(alg string) (method SigningMethod) { + signingMethodLock.RLock() + defer signingMethodLock.RUnlock() + + if methodF, ok := signingMethods[alg]; ok { + method = methodF() + } + return +} diff --git a/vendor/github.com/dgrijalva/jwt-go/token.go b/vendor/github.com/dgrijalva/jwt-go/token.go new file mode 100644 index 000000000..d637e0867 --- /dev/null +++ b/vendor/github.com/dgrijalva/jwt-go/token.go @@ -0,0 +1,108 @@ +package jwt + +import ( + "encoding/base64" + "encoding/json" + "strings" + "time" +) + +// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time). +// You can override it to use another time value. This is useful for testing or if your +// server uses a different time zone than your tokens. +var TimeFunc = time.Now + +// Parse methods use this callback function to supply +// the key for verification. The function receives the parsed, +// but unverified Token. This allows you to use properties in the +// Header of the token (such as `kid`) to identify which key to use. +type Keyfunc func(*Token) (interface{}, error) + +// A JWT Token. Different fields will be used depending on whether you're +// creating or parsing/verifying a token. +type Token struct { + Raw string // The raw token. Populated when you Parse a token + Method SigningMethod // The signing method used or to be used + Header map[string]interface{} // The first segment of the token + Claims Claims // The second segment of the token + Signature string // The third segment of the token. Populated when you Parse a token + Valid bool // Is the token valid? Populated when you Parse/Verify a token +} + +// Create a new Token. Takes a signing method +func New(method SigningMethod) *Token { + return NewWithClaims(method, MapClaims{}) +} + +func NewWithClaims(method SigningMethod, claims Claims) *Token { + return &Token{ + Header: map[string]interface{}{ + "typ": "JWT", + "alg": method.Alg(), + }, + Claims: claims, + Method: method, + } +} + +// Get the complete, signed token +func (t *Token) SignedString(key interface{}) (string, error) { + var sig, sstr string + var err error + if sstr, err = t.SigningString(); err != nil { + return "", err + } + if sig, err = t.Method.Sign(sstr, key); err != nil { + return "", err + } + return strings.Join([]string{sstr, sig}, "."), nil +} + +// Generate the signing string. This is the +// most expensive part of the whole deal. Unless you +// need this for something special, just go straight for +// the SignedString. +func (t *Token) SigningString() (string, error) { + var err error + parts := make([]string, 2) + for i, _ := range parts { + var jsonValue []byte + if i == 0 { + if jsonValue, err = json.Marshal(t.Header); err != nil { + return "", err + } + } else { + if jsonValue, err = json.Marshal(t.Claims); err != nil { + return "", err + } + } + + parts[i] = EncodeSegment(jsonValue) + } + return strings.Join(parts, "."), nil +} + +// Parse, validate, and return a token. +// keyFunc will receive the parsed token and should return the key for validating. +// If everything is kosher, err will be nil +func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { + return new(Parser).Parse(tokenString, keyFunc) +} + +func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { + return new(Parser).ParseWithClaims(tokenString, claims, keyFunc) +} + +// Encode JWT specific base64url encoding with padding stripped +func EncodeSegment(seg []byte) string { + return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=") +} + +// Decode JWT specific base64url encoding with padding stripped +func DecodeSegment(seg string) ([]byte, error) { + if l := len(seg) % 4; l > 0 { + seg += strings.Repeat("=", 4-l) + } + + return base64.URLEncoding.DecodeString(seg) +} diff --git a/vendor/github.com/gin-gonic/gin/.gitignore b/vendor/github.com/gin-gonic/gin/.gitignore deleted file mode 100644 index 9f48f1425..000000000 --- a/vendor/github.com/gin-gonic/gin/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -Godeps/* -!Godeps/Godeps.json -coverage.out -count.out diff --git a/vendor/github.com/gin-gonic/gin/.travis.yml b/vendor/github.com/gin-gonic/gin/.travis.yml deleted file mode 100644 index 53f436f4c..000000000 --- a/vendor/github.com/gin-gonic/gin/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: go -sudo: false -go: - - 1.4 - - 1.5.4 - - 1.6.4 - - 1.7.4 - - tip - -script: - - go test -v -covermode=count -coverprofile=coverage.out - -after_success: - - bash <(curl -s https://codecov.io/bash) - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/7f95bf605c4d356372f4 - on_success: change # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: false # default: false diff --git a/vendor/github.com/gin-gonic/gin/AUTHORS.md b/vendor/github.com/gin-gonic/gin/AUTHORS.md deleted file mode 100644 index 2feaf4677..000000000 --- a/vendor/github.com/gin-gonic/gin/AUTHORS.md +++ /dev/null @@ -1,229 +0,0 @@ -List of all the awesome people working to make Gin the best Web Framework in Go. - - - -##gin 0.x series authors - -**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho) - -People and companies, who have contributed, in alphabetical order. - -**@858806258 (杰哥)** -- Fix typo in example - - -**@achedeuzot (Klemen Sever)** -- Fix newline debug printing - - -**@adammck (Adam Mckaig)** -- Add MIT license - - -**@AlexanderChen1989 (Alexander)** -- Typos in README - - -**@alexanderdidenko (Aleksandr Didenko)** -- Add support multipart/form-data - - -**@alexandernyquist (Alexander Nyquist)** -- Using template.Must to fix multiple return issue -- ★ Added support for OPTIONS verb -- ★ Setting response headers before calling WriteHeader -- Improved documentation for model binding -- ★ Added Content.Redirect() -- ★ Added tons of Unit tests - - -**@austinheap (Austin Heap)** -- Added travis CI integration - - -**@andredublin (Andre Dublin)** -- Fix typo in comment - - -**@bredov (Ludwig Valda Vasquez)** -- Fix html templating in debug mode - - -**@bluele (Jun Kimura)** -- Fixes code examples in README - - -**@chad-russell** -- ★ Support for serializing gin.H into XML - - -**@dickeyxxx (Jeff Dickey)** -- Typos in README -- Add example about serving static files - - -**@donileo (Adonis)** -- Add NoMethod handler - - -**@dutchcoders (DutchCoders)** -- ★ Fix security bug that allows client to spoof ip -- Fix typo. r.HTMLTemplates -> SetHTMLTemplate - - -**@el3ctro- (Joshua Loper)** -- Fix typo in example - - -**@ethankan (Ethan Kan)** -- Unsigned integers in binding - - -**(Evgeny Persienko)** -- Validate sub structures - - -**@frankbille (Frank Bille)** -- Add support for HTTP Realm Auth - - -**@fmd (Fareed Dudhia)** -- Fix typo. SetHTTPTemplate -> SetHTMLTemplate - - -**@ironiridis (Christopher Harrington)** -- Remove old reference - - -**@jammie-stackhouse (Jamie Stackhouse)** -- Add more shortcuts for router methods - - -**@jasonrhansen** -- Fix spelling and grammar errors in documentation - - -**@JasonSoft (Jason Lee)** -- Fix typo in comment - - -**@joiggama (Ignacio Galindo)** -- Add utf-8 charset header on renders - - -**@julienschmidt (Julien Schmidt)** -- gofmt the code examples - - -**@kelcecil (Kel Cecil)** -- Fix readme typo - - -**@kyledinh (Kyle Dinh)** -- Adds RunTLS() - - -**@LinusU (Linus Unnebäck)** -- Small fixes in README - - -**@loongmxbt (Saint Asky)** -- Fix typo in example - - -**@lucas-clemente (Lucas Clemente)** -- ★ work around path.Join removing trailing slashes from routes - - -**@mattn (Yasuhiro Matsumoto)** -- Improve color logger - - -**@mdigger (Dmitry Sedykh)** -- Fixes Form binding when content-type is x-www-form-urlencoded -- No repeat call c.Writer.Status() in gin.Logger -- Fixes Content-Type for json render - - -**@mirzac (Mirza Ceric)** -- Fix debug printing - - -**@mopemope (Yutaka Matsubara)** -- ★ Adds Godep support (Dependencies Manager) -- Fix variadic parameter in the flexible render API -- Fix Corrupted plain render -- Add Pluggable View Renderer Example - - -**@msemenistyi (Mykyta Semenistyi)** -- update Readme.md. Add code to String method - - -**@msoedov (Sasha Myasoedov)** -- ★ Adds tons of unit tests. - - -**@ngerakines (Nick Gerakines)** -- ★ Improves API, c.GET() doesn't panic -- Adds MustGet() method - - -**@r8k (Rajiv Kilaparti)** -- Fix Port usage in README. - - -**@rayrod2030 (Ray Rodriguez)** -- Fix typo in example - - -**@rns** -- Fix typo in example - - -**@RobAWilkinson (Robert Wilkinson)** -- Add example of forms and params - - -**@rogierlommers (Rogier Lommers)** -- Add updated static serve example - - -**@se77en (Damon Zhao)** -- Improve color logging - - -**@silasb (Silas Baronda)** -- Fixing quotes in README - - -**@SkuliOskarsson (Skuli Oskarsson)** -- Fixes some texts in README II - - -**@slimmy (Jimmy Pettersson)** -- Added messages for required bindings - - -**@smira (Andrey Smirnov)** -- Add support for ignored/unexported fields in binding - - -**@superalsrk (SRK.Lyu)** -- Update httprouter godeps - - -**@tebeka (Miki Tebeka)** -- Use net/http constants instead of numeric values - - -**@techjanitor** -- Update context.go reserved IPs - - -**@yosssi (Keiji Yoshida)** -- Fix link in README - - -**@yuyabee** -- Fixed README \ No newline at end of file diff --git a/vendor/github.com/gin-gonic/gin/BENCHMARKS.md b/vendor/github.com/gin-gonic/gin/BENCHMARKS.md deleted file mode 100644 index 181f75b36..000000000 --- a/vendor/github.com/gin-gonic/gin/BENCHMARKS.md +++ /dev/null @@ -1,298 +0,0 @@ -**Machine:** intel i7 ivy bridge quad-core. 8GB RAM. -**Date:** June 4th, 2015 -[https://github.com/gin-gonic/go-http-routing-benchmark](https://github.com/gin-gonic/go-http-routing-benchmark) - -``` -BenchmarkAce_Param 5000000 372 ns/op 32 B/op 1 allocs/op -BenchmarkBear_Param 1000000 1165 ns/op 424 B/op 5 allocs/op -BenchmarkBeego_Param 1000000 2440 ns/op 720 B/op 10 allocs/op -BenchmarkBone_Param 1000000 1067 ns/op 384 B/op 3 allocs/op -BenchmarkDenco_Param 5000000 240 ns/op 32 B/op 1 allocs/op -BenchmarkEcho_Param 10000000 130 ns/op 0 B/op 0 allocs/op -BenchmarkGin_Param 10000000 133 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_Param 1000000 1826 ns/op 656 B/op 9 allocs/op -BenchmarkGoji_Param 2000000 957 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_Param 1000000 2021 ns/op 657 B/op 14 allocs/op -BenchmarkGoRestful_Param 200000 8825 ns/op 2496 B/op 31 allocs/op -BenchmarkGorillaMux_Param 500000 3340 ns/op 784 B/op 9 allocs/op -BenchmarkHttpRouter_Param 10000000 152 ns/op 32 B/op 1 allocs/op -BenchmarkHttpTreeMux_Param 2000000 717 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_Param 3000000 423 ns/op 56 B/op 3 allocs/op -BenchmarkMacaron_Param 1000000 3410 ns/op 1104 B/op 11 allocs/op -BenchmarkMartini_Param 200000 7101 ns/op 1152 B/op 12 allocs/op -BenchmarkPat_Param 1000000 2040 ns/op 656 B/op 14 allocs/op -BenchmarkPossum_Param 1000000 2048 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_Param 1000000 1144 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_Param 200000 6725 ns/op 1672 B/op 28 allocs/op -BenchmarkRivet_Param 1000000 1121 ns/op 464 B/op 5 allocs/op -BenchmarkTango_Param 1000000 1479 ns/op 256 B/op 10 allocs/op -BenchmarkTigerTonic_Param 1000000 3393 ns/op 992 B/op 19 allocs/op -BenchmarkTraffic_Param 300000 5525 ns/op 1984 B/op 23 allocs/op -BenchmarkVulcan_Param 2000000 924 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_Param 1000000 1084 ns/op 368 B/op 3 allocs/op -BenchmarkAce_Param5 3000000 614 ns/op 160 B/op 1 allocs/op -BenchmarkBear_Param5 1000000 1617 ns/op 469 B/op 5 allocs/op -BenchmarkBeego_Param5 1000000 3373 ns/op 992 B/op 13 allocs/op -BenchmarkBone_Param5 1000000 1478 ns/op 432 B/op 3 allocs/op -BenchmarkDenco_Param5 3000000 570 ns/op 160 B/op 1 allocs/op -BenchmarkEcho_Param5 5000000 256 ns/op 0 B/op 0 allocs/op -BenchmarkGin_Param5 10000000 222 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_Param5 1000000 2789 ns/op 928 B/op 12 allocs/op -BenchmarkGoji_Param5 1000000 1287 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_Param5 1000000 3670 ns/op 1105 B/op 17 allocs/op -BenchmarkGoRestful_Param5 200000 10756 ns/op 2672 B/op 31 allocs/op -BenchmarkGorillaMux_Param5 300000 5543 ns/op 912 B/op 9 allocs/op -BenchmarkHttpRouter_Param5 5000000 403 ns/op 160 B/op 1 allocs/op -BenchmarkHttpTreeMux_Param5 1000000 1089 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_Param5 1000000 1682 ns/op 440 B/op 10 allocs/op -BenchmarkMacaron_Param5 300000 4596 ns/op 1376 B/op 14 allocs/op -BenchmarkMartini_Param5 100000 15703 ns/op 1280 B/op 12 allocs/op -BenchmarkPat_Param5 300000 5320 ns/op 1008 B/op 42 allocs/op -BenchmarkPossum_Param5 1000000 2155 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_Param5 1000000 1559 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_Param5 200000 8184 ns/op 2024 B/op 35 allocs/op -BenchmarkRivet_Param5 1000000 1914 ns/op 528 B/op 9 allocs/op -BenchmarkTango_Param5 1000000 3280 ns/op 944 B/op 18 allocs/op -BenchmarkTigerTonic_Param5 200000 11638 ns/op 2519 B/op 53 allocs/op -BenchmarkTraffic_Param5 200000 8941 ns/op 2280 B/op 31 allocs/op -BenchmarkVulcan_Param5 1000000 1279 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_Param5 1000000 1574 ns/op 416 B/op 3 allocs/op -BenchmarkAce_Param20 1000000 1528 ns/op 640 B/op 1 allocs/op -BenchmarkBear_Param20 300000 4906 ns/op 1633 B/op 5 allocs/op -BenchmarkBeego_Param20 200000 10529 ns/op 3868 B/op 17 allocs/op -BenchmarkBone_Param20 300000 7362 ns/op 2539 B/op 5 allocs/op -BenchmarkDenco_Param20 1000000 1884 ns/op 640 B/op 1 allocs/op -BenchmarkEcho_Param20 2000000 689 ns/op 0 B/op 0 allocs/op -BenchmarkGin_Param20 3000000 545 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_Param20 200000 9437 ns/op 3804 B/op 16 allocs/op -BenchmarkGoji_Param20 500000 3987 ns/op 1246 B/op 2 allocs/op -BenchmarkGoJsonRest_Param20 100000 12799 ns/op 4492 B/op 21 allocs/op -BenchmarkGoRestful_Param20 100000 19451 ns/op 5244 B/op 33 allocs/op -BenchmarkGorillaMux_Param20 100000 12456 ns/op 3275 B/op 11 allocs/op -BenchmarkHttpRouter_Param20 1000000 1333 ns/op 640 B/op 1 allocs/op -BenchmarkHttpTreeMux_Param20 300000 6490 ns/op 2187 B/op 4 allocs/op -BenchmarkKocha_Param20 300000 5335 ns/op 1808 B/op 27 allocs/op -BenchmarkMacaron_Param20 200000 11325 ns/op 4252 B/op 18 allocs/op -BenchmarkMartini_Param20 20000 64419 ns/op 3644 B/op 14 allocs/op -BenchmarkPat_Param20 50000 24672 ns/op 4888 B/op 151 allocs/op -BenchmarkPossum_Param20 1000000 2085 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_Param20 300000 6809 ns/op 2283 B/op 8 allocs/op -BenchmarkRevel_Param20 100000 16600 ns/op 5551 B/op 54 allocs/op -BenchmarkRivet_Param20 200000 8428 ns/op 2620 B/op 26 allocs/op -BenchmarkTango_Param20 100000 16302 ns/op 8224 B/op 48 allocs/op -BenchmarkTigerTonic_Param20 30000 46828 ns/op 10538 B/op 178 allocs/op -BenchmarkTraffic_Param20 50000 28871 ns/op 7998 B/op 66 allocs/op -BenchmarkVulcan_Param20 1000000 2267 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_Param20 300000 6828 ns/op 2507 B/op 5 allocs/op -BenchmarkAce_ParamWrite 3000000 502 ns/op 40 B/op 2 allocs/op -BenchmarkBear_ParamWrite 1000000 1303 ns/op 424 B/op 5 allocs/op -BenchmarkBeego_ParamWrite 1000000 2489 ns/op 728 B/op 11 allocs/op -BenchmarkBone_ParamWrite 1000000 1181 ns/op 384 B/op 3 allocs/op -BenchmarkDenco_ParamWrite 5000000 315 ns/op 32 B/op 1 allocs/op -BenchmarkEcho_ParamWrite 10000000 237 ns/op 8 B/op 1 allocs/op -BenchmarkGin_ParamWrite 5000000 336 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_ParamWrite 1000000 2079 ns/op 664 B/op 10 allocs/op -BenchmarkGoji_ParamWrite 1000000 1092 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_ParamWrite 1000000 3329 ns/op 1136 B/op 19 allocs/op -BenchmarkGoRestful_ParamWrite 200000 9273 ns/op 2504 B/op 32 allocs/op -BenchmarkGorillaMux_ParamWrite 500000 3919 ns/op 792 B/op 10 allocs/op -BenchmarkHttpRouter_ParamWrite 10000000 223 ns/op 32 B/op 1 allocs/op -BenchmarkHttpTreeMux_ParamWrite 2000000 788 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_ParamWrite 3000000 549 ns/op 56 B/op 3 allocs/op -BenchmarkMacaron_ParamWrite 500000 4558 ns/op 1216 B/op 16 allocs/op -BenchmarkMartini_ParamWrite 200000 8850 ns/op 1256 B/op 16 allocs/op -BenchmarkPat_ParamWrite 500000 3679 ns/op 1088 B/op 19 allocs/op -BenchmarkPossum_ParamWrite 1000000 2114 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_ParamWrite 1000000 1320 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_ParamWrite 200000 8048 ns/op 2128 B/op 33 allocs/op -BenchmarkRivet_ParamWrite 1000000 1393 ns/op 472 B/op 6 allocs/op -BenchmarkTango_ParamWrite 2000000 819 ns/op 136 B/op 5 allocs/op -BenchmarkTigerTonic_ParamWrite 300000 5860 ns/op 1440 B/op 25 allocs/op -BenchmarkTraffic_ParamWrite 200000 7429 ns/op 2400 B/op 27 allocs/op -BenchmarkVulcan_ParamWrite 2000000 972 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_ParamWrite 1000000 1226 ns/op 368 B/op 3 allocs/op -BenchmarkAce_GithubStatic 5000000 294 ns/op 0 B/op 0 allocs/op -BenchmarkBear_GithubStatic 3000000 575 ns/op 88 B/op 3 allocs/op -BenchmarkBeego_GithubStatic 1000000 1561 ns/op 368 B/op 7 allocs/op -BenchmarkBone_GithubStatic 200000 12301 ns/op 2880 B/op 60 allocs/op -BenchmarkDenco_GithubStatic 20000000 74.6 ns/op 0 B/op 0 allocs/op -BenchmarkEcho_GithubStatic 10000000 176 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GithubStatic 10000000 159 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GithubStatic 1000000 1116 ns/op 304 B/op 6 allocs/op -BenchmarkGoji_GithubStatic 5000000 413 ns/op 0 B/op 0 allocs/op -BenchmarkGoRestful_GithubStatic 30000 55200 ns/op 3520 B/op 36 allocs/op -BenchmarkGoJsonRest_GithubStatic 1000000 1504 ns/op 337 B/op 12 allocs/op -BenchmarkGorillaMux_GithubStatic 100000 23620 ns/op 464 B/op 8 allocs/op -BenchmarkHttpRouter_GithubStatic 20000000 78.3 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_GithubStatic 20000000 84.9 ns/op 0 B/op 0 allocs/op -BenchmarkKocha_GithubStatic 20000000 111 ns/op 0 B/op 0 allocs/op -BenchmarkMacaron_GithubStatic 1000000 2686 ns/op 752 B/op 8 allocs/op -BenchmarkMartini_GithubStatic 100000 22244 ns/op 832 B/op 11 allocs/op -BenchmarkPat_GithubStatic 100000 13278 ns/op 3648 B/op 76 allocs/op -BenchmarkPossum_GithubStatic 1000000 1429 ns/op 480 B/op 4 allocs/op -BenchmarkR2router_GithubStatic 2000000 726 ns/op 144 B/op 5 allocs/op -BenchmarkRevel_GithubStatic 300000 6271 ns/op 1288 B/op 25 allocs/op -BenchmarkRivet_GithubStatic 3000000 474 ns/op 112 B/op 2 allocs/op -BenchmarkTango_GithubStatic 1000000 1842 ns/op 256 B/op 10 allocs/op -BenchmarkTigerTonic_GithubStatic 5000000 361 ns/op 48 B/op 1 allocs/op -BenchmarkTraffic_GithubStatic 30000 47197 ns/op 18920 B/op 149 allocs/op -BenchmarkVulcan_GithubStatic 1000000 1415 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GithubStatic 1000000 2522 ns/op 512 B/op 11 allocs/op -BenchmarkAce_GithubParam 3000000 578 ns/op 96 B/op 1 allocs/op -BenchmarkBear_GithubParam 1000000 1592 ns/op 464 B/op 5 allocs/op -BenchmarkBeego_GithubParam 1000000 2891 ns/op 784 B/op 11 allocs/op -BenchmarkBone_GithubParam 300000 6440 ns/op 1456 B/op 16 allocs/op -BenchmarkDenco_GithubParam 3000000 514 ns/op 128 B/op 1 allocs/op -BenchmarkEcho_GithubParam 5000000 292 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GithubParam 10000000 242 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GithubParam 1000000 2343 ns/op 720 B/op 10 allocs/op -BenchmarkGoji_GithubParam 1000000 1566 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_GithubParam 1000000 2828 ns/op 721 B/op 15 allocs/op -BenchmarkGoRestful_GithubParam 10000 177711 ns/op 2816 B/op 35 allocs/op -BenchmarkGorillaMux_GithubParam 100000 13591 ns/op 816 B/op 9 allocs/op -BenchmarkHttpRouter_GithubParam 5000000 352 ns/op 96 B/op 1 allocs/op -BenchmarkHttpTreeMux_GithubParam 2000000 973 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_GithubParam 2000000 889 ns/op 128 B/op 5 allocs/op -BenchmarkMacaron_GithubParam 500000 4047 ns/op 1168 B/op 12 allocs/op -BenchmarkMartini_GithubParam 50000 28982 ns/op 1184 B/op 12 allocs/op -BenchmarkPat_GithubParam 200000 8747 ns/op 2480 B/op 56 allocs/op -BenchmarkPossum_GithubParam 1000000 2158 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_GithubParam 1000000 1352 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_GithubParam 200000 7673 ns/op 1784 B/op 30 allocs/op -BenchmarkRivet_GithubParam 1000000 1573 ns/op 480 B/op 6 allocs/op -BenchmarkTango_GithubParam 1000000 2418 ns/op 480 B/op 13 allocs/op -BenchmarkTigerTonic_GithubParam 300000 6048 ns/op 1440 B/op 28 allocs/op -BenchmarkTraffic_GithubParam 100000 20143 ns/op 6024 B/op 55 allocs/op -BenchmarkVulcan_GithubParam 1000000 2224 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GithubParam 500000 4156 ns/op 1312 B/op 12 allocs/op -BenchmarkAce_GithubAll 10000 109482 ns/op 13792 B/op 167 allocs/op -BenchmarkBear_GithubAll 10000 287490 ns/op 79952 B/op 943 allocs/op -BenchmarkBeego_GithubAll 3000 562184 ns/op 146272 B/op 2092 allocs/op -BenchmarkBone_GithubAll 500 2578716 ns/op 648016 B/op 8119 allocs/op -BenchmarkDenco_GithubAll 20000 94955 ns/op 20224 B/op 167 allocs/op -BenchmarkEcho_GithubAll 30000 58705 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GithubAll 30000 50991 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GithubAll 5000 449648 ns/op 133280 B/op 1889 allocs/op -BenchmarkGoji_GithubAll 2000 689748 ns/op 56113 B/op 334 allocs/op -BenchmarkGoJsonRest_GithubAll 5000 537769 ns/op 135995 B/op 2940 allocs/op -BenchmarkGoRestful_GithubAll 100 18410628 ns/op 797236 B/op 7725 allocs/op -BenchmarkGorillaMux_GithubAll 200 8036360 ns/op 153137 B/op 1791 allocs/op -BenchmarkHttpRouter_GithubAll 20000 63506 ns/op 13792 B/op 167 allocs/op -BenchmarkHttpTreeMux_GithubAll 10000 165927 ns/op 56112 B/op 334 allocs/op -BenchmarkKocha_GithubAll 10000 171362 ns/op 23304 B/op 843 allocs/op -BenchmarkMacaron_GithubAll 2000 817008 ns/op 224960 B/op 2315 allocs/op -BenchmarkMartini_GithubAll 100 12609209 ns/op 237952 B/op 2686 allocs/op -BenchmarkPat_GithubAll 300 4830398 ns/op 1504101 B/op 32222 allocs/op -BenchmarkPossum_GithubAll 10000 301716 ns/op 97440 B/op 812 allocs/op -BenchmarkR2router_GithubAll 10000 270691 ns/op 77328 B/op 1182 allocs/op -BenchmarkRevel_GithubAll 1000 1491919 ns/op 345553 B/op 5918 allocs/op -BenchmarkRivet_GithubAll 10000 283860 ns/op 84272 B/op 1079 allocs/op -BenchmarkTango_GithubAll 5000 473821 ns/op 87078 B/op 2470 allocs/op -BenchmarkTigerTonic_GithubAll 2000 1120131 ns/op 241088 B/op 6052 allocs/op -BenchmarkTraffic_GithubAll 200 8708979 ns/op 2664762 B/op 22390 allocs/op -BenchmarkVulcan_GithubAll 5000 353392 ns/op 19894 B/op 609 allocs/op -BenchmarkZeus_GithubAll 2000 944234 ns/op 300688 B/op 2648 allocs/op -BenchmarkAce_GPlusStatic 5000000 251 ns/op 0 B/op 0 allocs/op -BenchmarkBear_GPlusStatic 3000000 415 ns/op 72 B/op 3 allocs/op -BenchmarkBeego_GPlusStatic 1000000 1416 ns/op 352 B/op 7 allocs/op -BenchmarkBone_GPlusStatic 10000000 192 ns/op 32 B/op 1 allocs/op -BenchmarkDenco_GPlusStatic 30000000 47.6 ns/op 0 B/op 0 allocs/op -BenchmarkEcho_GPlusStatic 10000000 131 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GPlusStatic 10000000 131 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GPlusStatic 1000000 1035 ns/op 288 B/op 6 allocs/op -BenchmarkGoji_GPlusStatic 5000000 304 ns/op 0 B/op 0 allocs/op -BenchmarkGoJsonRest_GPlusStatic 1000000 1286 ns/op 337 B/op 12 allocs/op -BenchmarkGoRestful_GPlusStatic 200000 9649 ns/op 2160 B/op 30 allocs/op -BenchmarkGorillaMux_GPlusStatic 1000000 2346 ns/op 464 B/op 8 allocs/op -BenchmarkHttpRouter_GPlusStatic 30000000 42.7 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_GPlusStatic 30000000 49.5 ns/op 0 B/op 0 allocs/op -BenchmarkKocha_GPlusStatic 20000000 74.8 ns/op 0 B/op 0 allocs/op -BenchmarkMacaron_GPlusStatic 1000000 2520 ns/op 736 B/op 8 allocs/op -BenchmarkMartini_GPlusStatic 300000 5310 ns/op 832 B/op 11 allocs/op -BenchmarkPat_GPlusStatic 5000000 398 ns/op 96 B/op 2 allocs/op -BenchmarkPossum_GPlusStatic 1000000 1434 ns/op 480 B/op 4 allocs/op -BenchmarkR2router_GPlusStatic 2000000 646 ns/op 144 B/op 5 allocs/op -BenchmarkRevel_GPlusStatic 300000 6172 ns/op 1272 B/op 25 allocs/op -BenchmarkRivet_GPlusStatic 3000000 444 ns/op 112 B/op 2 allocs/op -BenchmarkTango_GPlusStatic 1000000 1400 ns/op 208 B/op 10 allocs/op -BenchmarkTigerTonic_GPlusStatic 10000000 213 ns/op 32 B/op 1 allocs/op -BenchmarkTraffic_GPlusStatic 1000000 3091 ns/op 1208 B/op 16 allocs/op -BenchmarkVulcan_GPlusStatic 2000000 863 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GPlusStatic 10000000 237 ns/op 16 B/op 1 allocs/op -BenchmarkAce_GPlusParam 3000000 435 ns/op 64 B/op 1 allocs/op -BenchmarkBear_GPlusParam 1000000 1205 ns/op 448 B/op 5 allocs/op -BenchmarkBeego_GPlusParam 1000000 2494 ns/op 720 B/op 10 allocs/op -BenchmarkBone_GPlusParam 1000000 1126 ns/op 384 B/op 3 allocs/op -BenchmarkDenco_GPlusParam 5000000 325 ns/op 64 B/op 1 allocs/op -BenchmarkEcho_GPlusParam 10000000 168 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GPlusParam 10000000 170 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GPlusParam 1000000 1895 ns/op 656 B/op 9 allocs/op -BenchmarkGoji_GPlusParam 1000000 1071 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_GPlusParam 1000000 2282 ns/op 657 B/op 14 allocs/op -BenchmarkGoRestful_GPlusParam 100000 19400 ns/op 2560 B/op 33 allocs/op -BenchmarkGorillaMux_GPlusParam 500000 5001 ns/op 784 B/op 9 allocs/op -BenchmarkHttpRouter_GPlusParam 10000000 240 ns/op 64 B/op 1 allocs/op -BenchmarkHttpTreeMux_GPlusParam 2000000 797 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_GPlusParam 3000000 505 ns/op 56 B/op 3 allocs/op -BenchmarkMacaron_GPlusParam 1000000 3668 ns/op 1104 B/op 11 allocs/op -BenchmarkMartini_GPlusParam 200000 10672 ns/op 1152 B/op 12 allocs/op -BenchmarkPat_GPlusParam 1000000 2376 ns/op 704 B/op 14 allocs/op -BenchmarkPossum_GPlusParam 1000000 2090 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_GPlusParam 1000000 1233 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_GPlusParam 200000 6778 ns/op 1704 B/op 28 allocs/op -BenchmarkRivet_GPlusParam 1000000 1279 ns/op 464 B/op 5 allocs/op -BenchmarkTango_GPlusParam 1000000 1981 ns/op 272 B/op 10 allocs/op -BenchmarkTigerTonic_GPlusParam 500000 3893 ns/op 1064 B/op 19 allocs/op -BenchmarkTraffic_GPlusParam 200000 6585 ns/op 2000 B/op 23 allocs/op -BenchmarkVulcan_GPlusParam 1000000 1233 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GPlusParam 1000000 1350 ns/op 368 B/op 3 allocs/op -BenchmarkAce_GPlus2Params 3000000 512 ns/op 64 B/op 1 allocs/op -BenchmarkBear_GPlus2Params 1000000 1564 ns/op 464 B/op 5 allocs/op -BenchmarkBeego_GPlus2Params 1000000 3043 ns/op 784 B/op 11 allocs/op -BenchmarkBone_GPlus2Params 1000000 3152 ns/op 736 B/op 7 allocs/op -BenchmarkDenco_GPlus2Params 3000000 431 ns/op 64 B/op 1 allocs/op -BenchmarkEcho_GPlus2Params 5000000 247 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GPlus2Params 10000000 219 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GPlus2Params 1000000 2363 ns/op 720 B/op 10 allocs/op -BenchmarkGoji_GPlus2Params 1000000 1540 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_GPlus2Params 1000000 2872 ns/op 721 B/op 15 allocs/op -BenchmarkGoRestful_GPlus2Params 100000 23030 ns/op 2720 B/op 35 allocs/op -BenchmarkGorillaMux_GPlus2Params 200000 10516 ns/op 816 B/op 9 allocs/op -BenchmarkHttpRouter_GPlus2Params 5000000 273 ns/op 64 B/op 1 allocs/op -BenchmarkHttpTreeMux_GPlus2Params 2000000 939 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_GPlus2Params 2000000 844 ns/op 128 B/op 5 allocs/op -BenchmarkMacaron_GPlus2Params 500000 3914 ns/op 1168 B/op 12 allocs/op -BenchmarkMartini_GPlus2Params 50000 35759 ns/op 1280 B/op 16 allocs/op -BenchmarkPat_GPlus2Params 200000 7089 ns/op 2304 B/op 41 allocs/op -BenchmarkPossum_GPlus2Params 1000000 2093 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_GPlus2Params 1000000 1320 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_GPlus2Params 200000 7351 ns/op 1800 B/op 30 allocs/op -BenchmarkRivet_GPlus2Params 1000000 1485 ns/op 480 B/op 6 allocs/op -BenchmarkTango_GPlus2Params 1000000 2111 ns/op 448 B/op 12 allocs/op -BenchmarkTigerTonic_GPlus2Params 300000 6271 ns/op 1528 B/op 28 allocs/op -BenchmarkTraffic_GPlus2Params 100000 14886 ns/op 3312 B/op 34 allocs/op -BenchmarkVulcan_GPlus2Params 1000000 1883 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GPlus2Params 1000000 2686 ns/op 784 B/op 6 allocs/op -BenchmarkAce_GPlusAll 300000 5912 ns/op 640 B/op 11 allocs/op -BenchmarkBear_GPlusAll 100000 16448 ns/op 5072 B/op 61 allocs/op -BenchmarkBeego_GPlusAll 50000 32916 ns/op 8976 B/op 129 allocs/op -BenchmarkBone_GPlusAll 50000 25836 ns/op 6992 B/op 76 allocs/op -BenchmarkDenco_GPlusAll 500000 4462 ns/op 672 B/op 11 allocs/op -BenchmarkEcho_GPlusAll 500000 2806 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GPlusAll 500000 2579 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GPlusAll 50000 25223 ns/op 8144 B/op 116 allocs/op -BenchmarkGoji_GPlusAll 100000 14237 ns/op 3696 B/op 22 allocs/op -BenchmarkGoJsonRest_GPlusAll 50000 29227 ns/op 8221 B/op 183 allocs/op -BenchmarkGoRestful_GPlusAll 10000 203144 ns/op 36064 B/op 441 allocs/op -BenchmarkGorillaMux_GPlusAll 20000 80906 ns/op 9712 B/op 115 allocs/op -BenchmarkHttpRouter_GPlusAll 500000 3040 ns/op 640 B/op 11 allocs/op -BenchmarkHttpTreeMux_GPlusAll 200000 9627 ns/op 3696 B/op 22 allocs/op -BenchmarkKocha_GPlusAll 200000 8108 ns/op 976 B/op 43 allocs/op -BenchmarkMacaron_GPlusAll 30000 48083 ns/op 13968 B/op 142 allocs/op -BenchmarkMartini_GPlusAll 10000 196978 ns/op 15072 B/op 178 allocs/op -BenchmarkPat_GPlusAll 30000 58865 ns/op 16880 B/op 343 allocs/op -BenchmarkPossum_GPlusAll 100000 19685 ns/op 6240 B/op 52 allocs/op -BenchmarkR2router_GPlusAll 100000 16251 ns/op 5040 B/op 76 allocs/op -BenchmarkRevel_GPlusAll 20000 93489 ns/op 21656 B/op 368 allocs/op -BenchmarkRivet_GPlusAll 100000 16907 ns/op 5408 B/op 64 allocs/op -``` \ No newline at end of file diff --git a/vendor/github.com/gin-gonic/gin/CHANGELOG.md b/vendor/github.com/gin-gonic/gin/CHANGELOG.md deleted file mode 100644 index 82f1bead8..000000000 --- a/vendor/github.com/gin-gonic/gin/CHANGELOG.md +++ /dev/null @@ -1,150 +0,0 @@ -#CHANGELOG - -###Gin 1.0rc2 (...) - -- [PERFORMANCE] Fast path for writing Content-Type. -- [PERFORMANCE] Much faster 404 routing -- [PERFORMANCE] Allocation optimizations -- [PERFORMANCE] Faster root tree lookup -- [PERFORMANCE] Zero overhead, String() and JSON() rendering. -- [PERFORMANCE] Faster ClientIP parsing -- [PERFORMANCE] Much faster SSE implementation -- [NEW] Benchmarks suite -- [NEW] Bind validation can be disabled and replaced with custom validators. -- [NEW] More flexible HTML render -- [NEW] Multipart and PostForm bindings -- [NEW] Adds method to return all the registered routes -- [NEW] Context.HandlerName() returns the main handler's name -- [NEW] Adds Error.IsType() helper -- [FIX] Binding multipart form -- [FIX] Integration tests -- [FIX] Crash when binding non struct object in Context. -- [FIX] RunTLS() implementation -- [FIX] Logger() unit tests -- [FIX] Adds SetHTMLTemplate() warning -- [FIX] Context.IsAborted() -- [FIX] More unit tests -- [FIX] JSON, XML, HTML renders accept custom content-types -- [FIX] gin.AbortIndex is unexported -- [FIX] Better approach to avoid directory listing in StaticFS() -- [FIX] Context.ClientIP() always returns the IP with trimmed spaces. -- [FIX] Better warning when running in debug mode. -- [FIX] Google App Engine integration. debugPrint does not use os.Stdout -- [FIX] Fixes integer overflow in error type -- [FIX] Error implements the json.Marshaller interface -- [FIX] MIT license in every file - - -###Gin 1.0rc1 (May 22, 2015) - -- [PERFORMANCE] Zero allocation router -- [PERFORMANCE] Faster JSON, XML and text rendering -- [PERFORMANCE] Custom hand optimized HttpRouter for Gin -- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations -- [NEW] Built-in support for golang.org/x/net/context -- [NEW] Any(path, handler). Create a route that matches any path -- [NEW] Refactored rendering pipeline (faster and static typeded) -- [NEW] Refactored errors API -- [NEW] IndentedJSON() prints pretty JSON -- [NEW] Added gin.DefaultWriter -- [NEW] UNIX socket support -- [NEW] RouterGroup.BasePath is exposed -- [NEW] JSON validation using go-validate-yourself (very powerful options) -- [NEW] Completed suite of unit tests -- [NEW] HTTP streaming with c.Stream() -- [NEW] StaticFile() creates a router for serving just one file. -- [NEW] StaticFS() has an option to disable directory listing. -- [NEW] StaticFS() for serving static files through virtual filesystems -- [NEW] Server-Sent Events native support -- [NEW] WrapF() and WrapH() helpers for wrapping http.HandlerFunc and http.Handler -- [NEW] Added LoggerWithWriter() middleware -- [NEW] Added RecoveryWithWriter() middleware -- [NEW] Added DefaultPostFormValue() -- [NEW] Added DefaultFormValue() -- [NEW] Added DefaultParamValue() -- [FIX] BasicAuth() when using custom realm -- [FIX] Bug when serving static files in nested routing group -- [FIX] Redirect using built-in http.Redirect() -- [FIX] Logger when printing the requested path -- [FIX] Documentation typos -- [FIX] Context.Engine renamed to Context.engine -- [FIX] Better debugging messages -- [FIX] ErrorLogger -- [FIX] Debug HTTP render -- [FIX] Refactored binding and render modules -- [FIX] Refactored Context initialization -- [FIX] Refactored BasicAuth() -- [FIX] NoMethod/NoRoute handlers -- [FIX] Hijacking http -- [FIX] Better support for Google App Engine (using log instead of fmt) - - -###Gin 0.6 (Mar 9, 2015) - -- [NEW] Support multipart/form-data -- [NEW] NoMethod handler -- [NEW] Validate sub structures -- [NEW] Support for HTTP Realm Auth -- [FIX] Unsigned integers in binding -- [FIX] Improve color logger - - -###Gin 0.5 (Feb 7, 2015) - -- [NEW] Content Negotiation -- [FIX] Solved security bug that allow a client to spoof ip -- [FIX] Fix unexported/ignored fields in binding - - -###Gin 0.4 (Aug 21, 2014) - -- [NEW] Development mode -- [NEW] Unit tests -- [NEW] Add Content.Redirect() -- [FIX] Deferring WriteHeader() -- [FIX] Improved documentation for model binding - - -###Gin 0.3 (Jul 18, 2014) - -- [PERFORMANCE] Normal log and error log are printed in the same call. -- [PERFORMANCE] Improve performance of NoRouter() -- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults. -- [NEW] Flexible rendering API -- [NEW] Add Context.File() -- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS -- [FIX] Rename NotFound404() to NoRoute() -- [FIX] Errors in context are purged -- [FIX] Adds HEAD method in Static file serving -- [FIX] Refactors Static() file serving -- [FIX] Using keyed initialization to fix app-engine integration -- [FIX] Can't unmarshal JSON array, #63 -- [FIX] Renaming Context.Req to Context.Request -- [FIX] Check application/x-www-form-urlencoded when parsing form - - -###Gin 0.2b (Jul 08, 2014) -- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead -- [NEW] Travis CI integration -- [NEW] Completely new logger -- [NEW] New API for serving static files. gin.Static() -- [NEW] gin.H() can be serialized into XML -- [NEW] Typed errors. Errors can be typed. Internet/external/custom. -- [NEW] Support for Godeps -- [NEW] Travis/Godocs badges in README -- [NEW] New Bind() and BindWith() methods for parsing request body. -- [NEW] Add Content.Copy() -- [NEW] Add context.LastError() -- [NEW] Add shorcut for OPTIONS HTTP method -- [FIX] Tons of README fixes -- [FIX] Header is written before body -- [FIX] BasicAuth() and changes API a little bit -- [FIX] Recovery() middleware only prints panics -- [FIX] Context.Get() does not panic anymore. Use MustGet() instead. -- [FIX] Multiple http.WriteHeader() in NotFound handlers -- [FIX] Engine.Run() panics if http server can't be setted up -- [FIX] Crash when route path doesn't start with '/' -- [FIX] Do not update header when status code is negative -- [FIX] Setting response headers before calling WriteHeader in context.String() -- [FIX] Add MIT license -- [FIX] Changes behaviour of ErrorLogger() and Logger() diff --git a/vendor/github.com/gin-gonic/gin/README.md b/vendor/github.com/gin-gonic/gin/README.md deleted file mode 100644 index 82ea6a58d..000000000 --- a/vendor/github.com/gin-gonic/gin/README.md +++ /dev/null @@ -1,733 +0,0 @@ - -#Gin Web Framework - - -[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) -[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) -[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) -[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) -[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. - -![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) - -```sh -$ cat test.go -``` -```go -package main - -import "gopkg.in/gin-gonic/gin.v1" - -func main() { - r := gin.Default() - r.GET("/ping", func(c *gin.Context) { - c.JSON(200, gin.H{ - "message": "pong", - }) - }) - r.Run() // listen and serve on 0.0.0.0:8080 -} -``` - -## Benchmarks - -Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) - -[See all benchmarks](/BENCHMARKS.md) - - -Benchmark name | (1) | (2) | (3) | (4) ---------------------------------|----------:|----------:|----------:|------: -BenchmarkAce_GithubAll | 10000 | 109482 | 13792 | 167 -BenchmarkBear_GithubAll | 10000 | 287490 | 79952 | 943 -BenchmarkBeego_GithubAll | 3000 | 562184 | 146272 | 2092 -BenchmarkBone_GithubAll | 500 | 2578716 | 648016 | 8119 -BenchmarkDenco_GithubAll | 20000 | 94955 | 20224 | 167 -BenchmarkEcho_GithubAll | 30000 | 58705 | 0 | 0 -**BenchmarkGin_GithubAll** | **30000** | **50991** | **0** | **0** -BenchmarkGocraftWeb_GithubAll | 5000 | 449648 | 133280 | 1889 -BenchmarkGoji_GithubAll | 2000 | 689748 | 56113 | 334 -BenchmarkGoJsonRest_GithubAll | 5000 | 537769 | 135995 | 2940 -BenchmarkGoRestful_GithubAll | 100 | 18410628 | 797236 | 7725 -BenchmarkGorillaMux_GithubAll | 200 | 8036360 | 153137 | 1791 -BenchmarkHttpRouter_GithubAll | 20000 | 63506 | 13792 | 167 -BenchmarkHttpTreeMux_GithubAll | 10000 | 165927 | 56112 | 334 -BenchmarkKocha_GithubAll | 10000 | 171362 | 23304 | 843 -BenchmarkMacaron_GithubAll | 2000 | 817008 | 224960 | 2315 -BenchmarkMartini_GithubAll | 100 | 12609209 | 237952 | 2686 -BenchmarkPat_GithubAll | 300 | 4830398 | 1504101 | 32222 -BenchmarkPossum_GithubAll | 10000 | 301716 | 97440 | 812 -BenchmarkR2router_GithubAll | 10000 | 270691 | 77328 | 1182 -BenchmarkRevel_GithubAll | 1000 | 1491919 | 345553 | 5918 -BenchmarkRivet_GithubAll | 10000 | 283860 | 84272 | 1079 -BenchmarkTango_GithubAll | 5000 | 473821 | 87078 | 2470 -BenchmarkTigerTonic_GithubAll | 2000 | 1120131 | 241088 | 6052 -BenchmarkTraffic_GithubAll | 200 | 8708979 | 2664762 | 22390 -BenchmarkVulcan_GithubAll | 5000 | 353392 | 19894 | 609 -BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 - -(1): Total Repetitions -(2): Single Repetition Duration (ns/op) -(3): Heap Memory (B/op) -(4): Average Allocations per Repetition (allocs/op) - -## Gin v1. stable - -- [x] Zero allocation router. -- [x] Still the fastest http router and framework. From routing to writing. -- [x] Complete suite of unit tests -- [x] Battle tested -- [x] API frozen, new releases will not break your code. - - -## Start using it - -1. Download and install it: - - ```sh - $ go get gopkg.in/gin-gonic/gin.v1 - ``` - -2. Import it in your code: - - ```go - import "gopkg.in/gin-gonic/gin.v1" - ``` - -3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`. - - ```go - import "net/http" - ``` - -## API Examples - -#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS - -```go -func main() { - // Creates a gin router with default middleware: - // logger and recovery (crash-free) middleware - router := gin.Default() - - router.GET("/someGet", getting) - router.POST("/somePost", posting) - router.PUT("/somePut", putting) - router.DELETE("/someDelete", deleting) - router.PATCH("/somePatch", patching) - router.HEAD("/someHead", head) - router.OPTIONS("/someOptions", options) - - // By default it serves on :8080 unless a - // PORT environment variable was defined. - router.Run() - // router.Run(":3000") for a hard coded port -} -``` - -#### Parameters in path - -```go -func main() { - router := gin.Default() - - // This handler will match /user/john but will not match neither /user/ or /user - router.GET("/user/:name", func(c *gin.Context) { - name := c.Param("name") - c.String(http.StatusOK, "Hello %s", name) - }) - - // However, this one will match /user/john/ and also /user/john/send - // If no other routers match /user/john, it will redirect to /user/john/ - router.GET("/user/:name/*action", func(c *gin.Context) { - name := c.Param("name") - action := c.Param("action") - message := name + " is " + action - c.String(http.StatusOK, message) - }) - - router.Run(":8080") -} -``` - -#### Querystring parameters -```go -func main() { - router := gin.Default() - - // Query string parameters are parsed using the existing underlying request object. - // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe - router.GET("/welcome", func(c *gin.Context) { - firstname := c.DefaultQuery("firstname", "Guest") - lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname") - - c.String(http.StatusOK, "Hello %s %s", firstname, lastname) - }) - router.Run(":8080") -} -``` - -### Multipart/Urlencoded Form - -```go -func main() { - router := gin.Default() - - router.POST("/form_post", func(c *gin.Context) { - message := c.PostForm("message") - nick := c.DefaultPostForm("nick", "anonymous") - - c.JSON(200, gin.H{ - "status": "posted", - "message": message, - "nick": nick, - }) - }) - router.Run(":8080") -} -``` - -### Another example: query + post form - -``` -POST /post?id=1234&page=1 HTTP/1.1 -Content-Type: application/x-www-form-urlencoded - -name=manu&message=this_is_great -``` - -```go -func main() { - router := gin.Default() - - router.POST("/post", func(c *gin.Context) { - - id := c.Query("id") - page := c.DefaultQuery("page", "0") - name := c.PostForm("name") - message := c.PostForm("message") - - fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message) - }) - router.Run(":8080") -} -``` - -``` -id: 1234; page: 1; name: manu; message: this_is_great -``` - -### Another example: upload file - -References issue [#548](https://github.com/gin-gonic/gin/issues/548). - -```go -func main() { - router := gin.Default() - - router.POST("/upload", func(c *gin.Context) { - - file, header , err := c.Request.FormFile("upload") - filename := header.Filename - fmt.Println(header.Filename) - out, err := os.Create("./tmp/"+filename+".png") - if err != nil { - log.Fatal(err) - } - defer out.Close() - _, err = io.Copy(out, file) - if err != nil { - log.Fatal(err) - } - }) - router.Run(":8080") -} -``` - -#### Grouping routes -```go -func main() { - router := gin.Default() - - // Simple group: v1 - v1 := router.Group("/v1") - { - v1.POST("/login", loginEndpoint) - v1.POST("/submit", submitEndpoint) - v1.POST("/read", readEndpoint) - } - - // Simple group: v2 - v2 := router.Group("/v2") - { - v2.POST("/login", loginEndpoint) - v2.POST("/submit", submitEndpoint) - v2.POST("/read", readEndpoint) - } - - router.Run(":8080") -} -``` - - -#### Blank Gin without middleware by default - -Use - -```go -r := gin.New() -``` -instead of - -```go -r := gin.Default() -``` - - -#### Using middleware -```go -func main() { - // Creates a router without any middleware by default - r := gin.New() - - // Global middleware - r.Use(gin.Logger()) - r.Use(gin.Recovery()) - - // Per route middleware, you can add as many as you desire. - r.GET("/benchmark", MyBenchLogger(), benchEndpoint) - - // Authorization group - // authorized := r.Group("/", AuthRequired()) - // exactly the same as: - authorized := r.Group("/") - // per group middleware! in this case we use the custom created - // AuthRequired() middleware just in the "authorized" group. - authorized.Use(AuthRequired()) - { - authorized.POST("/login", loginEndpoint) - authorized.POST("/submit", submitEndpoint) - authorized.POST("/read", readEndpoint) - - // nested group - testing := authorized.Group("testing") - testing.GET("/analytics", analyticsEndpoint) - } - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -#### Model binding and validation - -To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz). - -Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. - -When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use BindWith. - -You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, the current request will fail with an error. - -```go -// Binding from JSON -type Login struct { - User string `form:"user" json:"user" binding:"required"` - Password string `form:"password" json:"password" binding:"required"` -} - -func main() { - router := gin.Default() - - // Example for binding JSON ({"user": "manu", "password": "123"}) - router.POST("/loginJSON", func(c *gin.Context) { - var json Login - if c.BindJSON(&json) == nil { - if json.User == "manu" && json.Password == "123" { - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - } - } - }) - - // Example for binding a HTML form (user=manu&password=123) - router.POST("/loginForm", func(c *gin.Context) { - var form Login - // This will infer what binder to use depending on the content-type header. - if c.Bind(&form) == nil { - if form.User == "manu" && form.Password == "123" { - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - } - } - }) - - // Listen and serve on 0.0.0.0:8080 - router.Run(":8080") -} -``` - - -###Multipart/Urlencoded binding -```go -package main - -import ( - "gopkg.in/gin-gonic/gin.v1" -) - -type LoginForm struct { - User string `form:"user" binding:"required"` - Password string `form:"password" binding:"required"` -} - -func main() { - router := gin.Default() - router.POST("/login", func(c *gin.Context) { - // you can bind multipart form with explicit binding declaration: - // c.BindWith(&form, binding.Form) - // or you can simply use autobinding with Bind method: - var form LoginForm - // in this case proper binding will be automatically selected - if c.Bind(&form) == nil { - if form.User == "user" && form.Password == "password" { - c.JSON(200, gin.H{"status": "you are logged in"}) - } else { - c.JSON(401, gin.H{"status": "unauthorized"}) - } - } - }) - router.Run(":8080") -} -``` - -Test it with: -```sh -$ curl -v --form user=user --form password=password http://localhost:8080/login -``` - - -#### XML, JSON and YAML rendering - -```go -func main() { - r := gin.Default() - - // gin.H is a shortcut for map[string]interface{} - r.GET("/someJSON", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) - }) - - r.GET("/moreJSON", func(c *gin.Context) { - // You also can use a struct - var msg struct { - Name string `json:"user"` - Message string - Number int - } - msg.Name = "Lena" - msg.Message = "hey" - msg.Number = 123 - // Note that msg.Name becomes "user" in the JSON - // Will output : {"user": "Lena", "Message": "hey", "Number": 123} - c.JSON(http.StatusOK, msg) - }) - - r.GET("/someXML", func(c *gin.Context) { - c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) - }) - - r.GET("/someYAML", func(c *gin.Context) { - c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -####Serving static files - -```go -func main() { - router := gin.Default() - router.Static("/assets", "./assets") - router.StaticFS("/more_static", http.Dir("my_file_system")) - router.StaticFile("/favicon.ico", "./resources/favicon.ico") - - // Listen and serve on 0.0.0.0:8080 - router.Run(":8080") -} -``` - -####HTML rendering - -Using LoadHTMLTemplates() - -```go -func main() { - router := gin.Default() - router.LoadHTMLGlob("templates/*") - //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") - router.GET("/index", func(c *gin.Context) { - c.HTML(http.StatusOK, "index.tmpl", gin.H{ - "title": "Main website", - }) - }) - router.Run(":8080") -} -``` -templates/index.tmpl -```html - -

- {{ .title }} -

- -``` - -Using templates with same name in different directories - -```go -func main() { - router := gin.Default() - router.LoadHTMLGlob("templates/**/*") - router.GET("/posts/index", func(c *gin.Context) { - c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{ - "title": "Posts", - }) - }) - router.GET("/users/index", func(c *gin.Context) { - c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ - "title": "Users", - }) - }) - router.Run(":8080") -} -``` -templates/posts/index.tmpl -```html -{{ define "posts/index.tmpl" }} -

- {{ .title }} -

-

Using posts/index.tmpl

- -{{ end }} -``` -templates/users/index.tmpl -```html -{{ define "users/index.tmpl" }} -

- {{ .title }} -

-

Using users/index.tmpl

- -{{ end }} -``` - -You can also use your own html template render - -```go -import "html/template" - -func main() { - router := gin.Default() - html := template.Must(template.ParseFiles("file1", "file2")) - router.SetHTMLTemplate(html) - router.Run(":8080") -} -``` - - -#### Redirects - -Issuing a HTTP redirect is easy: - -```go -r.GET("/test", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, "http://www.google.com/") -}) -``` -Both internal and external locations are supported. - - -#### Custom Middleware - -```go -func Logger() gin.HandlerFunc { - return func(c *gin.Context) { - t := time.Now() - - // Set example variable - c.Set("example", "12345") - - // before request - - c.Next() - - // after request - latency := time.Since(t) - log.Print(latency) - - // access the status we are sending - status := c.Writer.Status() - log.Println(status) - } -} - -func main() { - r := gin.New() - r.Use(Logger()) - - r.GET("/test", func(c *gin.Context) { - example := c.MustGet("example").(string) - - // it would print: "12345" - log.Println(example) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -#### Using BasicAuth() middleware -```go -// simulate some private data -var secrets = gin.H{ - "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, - "austin": gin.H{"email": "austin@example.com", "phone": "666"}, - "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"}, -} - -func main() { - r := gin.Default() - - // Group using gin.BasicAuth() middleware - // gin.Accounts is a shortcut for map[string]string - authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ - "foo": "bar", - "austin": "1234", - "lena": "hello2", - "manu": "4321", - })) - - // /admin/secrets endpoint - // hit "localhost:8080/admin/secrets - authorized.GET("/secrets", func(c *gin.Context) { - // get user, it was set by the BasicAuth middleware - user := c.MustGet(gin.AuthUserKey).(string) - if secret, ok := secrets[user]; ok { - c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret}) - } else { - c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("}) - } - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - - -#### Goroutines inside a middleware -When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. - -```go -func main() { - r := gin.Default() - - r.GET("/long_async", func(c *gin.Context) { - // create copy to be used inside the goroutine - cCp := c.Copy() - go func() { - // simulate a long task with time.Sleep(). 5 seconds - time.Sleep(5 * time.Second) - - // note that you are using the copied context "cCp", IMPORTANT - log.Println("Done! in path " + cCp.Request.URL.Path) - }() - }) - - r.GET("/long_sync", func(c *gin.Context) { - // simulate a long task with time.Sleep(). 5 seconds - time.Sleep(5 * time.Second) - - // since we are NOT using a goroutine, we do not have to copy the context - log.Println("Done! in path " + c.Request.URL.Path) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -#### Custom HTTP configuration - -Use `http.ListenAndServe()` directly, like this: - -```go -func main() { - router := gin.Default() - http.ListenAndServe(":8080", router) -} -``` -or - -```go -func main() { - router := gin.Default() - - s := &http.Server{ - Addr: ":8080", - Handler: router, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - s.ListenAndServe() -} -``` - -#### Graceful restart or stop - -Do you want to graceful restart or stop your web server? -There are some ways this can be done. - -We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details. - -```go -router := gin.Default() -router.GET("/", handler) -// [...] -endless.ListenAndServe(":4242", router) -``` - -An alternative to endless: - -* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully. - -## Contributing - -- With issues: - - Use the search tool before opening a new issue. - - Please provide source code and commit sha if you found a bug. - - Review existing issues and provide feedback or react to them. -- With pull requests: - - Open your pull request against develop - - Your pull request should have no more than two commits, if not you should squash them. - - It should pass all tests in the available continuous integrations systems such as TravisCI. - - You should add/modify tests to cover your proposed code changes. - - If your pull request contains a new feature, please document it on the README. - -## Example - -Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. - -* [drone](https://github.com/drone/drone): Drone is a Continuous Delivery platform built on Docker, written in Go -* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go. diff --git a/vendor/github.com/gin-gonic/gin/auth.go b/vendor/github.com/gin-gonic/gin/auth.go deleted file mode 100644 index 125e659f2..000000000 --- a/vendor/github.com/gin-gonic/gin/auth.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "crypto/subtle" - "encoding/base64" - "strconv" -) - -const AuthUserKey = "user" - -type ( - Accounts map[string]string - authPair struct { - Value string - User string - } - authPairs []authPair -) - -func (a authPairs) searchCredential(authValue string) (string, bool) { - if len(authValue) == 0 { - return "", false - } - for _, pair := range a { - if pair.Value == authValue { - return pair.User, true - } - } - return "", false -} - -// BasicAuthForRealm returns a Basic HTTP Authorization middleware. It takes as arguments a map[string]string where -// the key is the user name and the value is the password, as well as the name of the Realm. -// If the realm is empty, "Authorization Required" will be used by default. -// (see http://tools.ietf.org/html/rfc2617#section-1.2) -func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { - if realm == "" { - realm = "Authorization Required" - } - realm = "Basic realm=" + strconv.Quote(realm) - pairs := processAccounts(accounts) - return func(c *Context) { - // Search user in the slice of allowed credentials - user, found := pairs.searchCredential(c.Request.Header.Get("Authorization")) - if !found { - // Credentials doesn't match, we return 401 and abort handlers chain. - c.Header("WWW-Authenticate", realm) - c.AbortWithStatus(401) - } else { - // The user credentials was found, set user's id to key AuthUserKey in this context, the userId can be read later using - // c.MustGet(gin.AuthUserKey) - c.Set(AuthUserKey, user) - } - } -} - -// BasicAuth returns a Basic HTTP Authorization middleware. It takes as argument a map[string]string where -// the key is the user name and the value is the password. -func BasicAuth(accounts Accounts) HandlerFunc { - return BasicAuthForRealm(accounts, "") -} - -func processAccounts(accounts Accounts) authPairs { - assert1(len(accounts) > 0, "Empty list of authorized credentials") - pairs := make(authPairs, 0, len(accounts)) - for user, password := range accounts { - assert1(len(user) > 0, "User can not be empty") - value := authorizationHeader(user, password) - pairs = append(pairs, authPair{ - Value: value, - User: user, - }) - } - return pairs -} - -func authorizationHeader(user, password string) string { - base := user + ":" + password - return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) -} - -func secureCompare(given, actual string) bool { - if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { - return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1 - } - /* Securely compare actual to itself to keep constant time, but always return false */ - return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false -} diff --git a/vendor/github.com/gin-gonic/gin/binding/binding.go b/vendor/github.com/gin-gonic/gin/binding/binding.go deleted file mode 100644 index dc7397f1c..000000000 --- a/vendor/github.com/gin-gonic/gin/binding/binding.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import "net/http" - -const ( - MIMEJSON = "application/json" - MIMEHTML = "text/html" - MIMEXML = "application/xml" - MIMEXML2 = "text/xml" - MIMEPlain = "text/plain" - MIMEPOSTForm = "application/x-www-form-urlencoded" - MIMEMultipartPOSTForm = "multipart/form-data" - MIMEPROTOBUF = "application/x-protobuf" -) - -type Binding interface { - Name() string - Bind(*http.Request, interface{}) error -} - -type StructValidator interface { - // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. - // If the received type is not a struct, any validation should be skipped and nil must be returned. - // If the received type is a struct or pointer to a struct, the validation should be performed. - // If the struct is not valid or the validation itself fails, a descriptive error should be returned. - // Otherwise nil must be returned. - ValidateStruct(interface{}) error -} - -var Validator StructValidator = &defaultValidator{} - -var ( - JSON = jsonBinding{} - XML = xmlBinding{} - Form = formBinding{} - FormPost = formPostBinding{} - FormMultipart = formMultipartBinding{} - ProtoBuf = protobufBinding{} -) - -func Default(method, contentType string) Binding { - if method == "GET" { - return Form - } else { - switch contentType { - case MIMEJSON: - return JSON - case MIMEXML, MIMEXML2: - return XML - case MIMEPROTOBUF: - return ProtoBuf - default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: - return Form - } - } -} - -func validate(obj interface{}) error { - if Validator == nil { - return nil - } - return Validator.ValidateStruct(obj) -} diff --git a/vendor/github.com/gin-gonic/gin/binding/default_validator.go b/vendor/github.com/gin-gonic/gin/binding/default_validator.go deleted file mode 100644 index 760728bbe..000000000 --- a/vendor/github.com/gin-gonic/gin/binding/default_validator.go +++ /dev/null @@ -1,41 +0,0 @@ -package binding - -import ( - "reflect" - "sync" - - "gopkg.in/go-playground/validator.v8" -) - -type defaultValidator struct { - once sync.Once - validate *validator.Validate -} - -var _ StructValidator = &defaultValidator{} - -func (v *defaultValidator) ValidateStruct(obj interface{}) error { - if kindOfData(obj) == reflect.Struct { - v.lazyinit() - if err := v.validate.Struct(obj); err != nil { - return error(err) - } - } - return nil -} - -func (v *defaultValidator) lazyinit() { - v.once.Do(func() { - config := &validator.Config{TagName: "binding"} - v.validate = validator.New(config) - }) -} - -func kindOfData(data interface{}) reflect.Kind { - value := reflect.ValueOf(data) - valueType := value.Kind() - if valueType == reflect.Ptr { - valueType = value.Elem().Kind() - } - return valueType -} diff --git a/vendor/github.com/gin-gonic/gin/binding/form.go b/vendor/github.com/gin-gonic/gin/binding/form.go deleted file mode 100644 index 557333e6f..000000000 --- a/vendor/github.com/gin-gonic/gin/binding/form.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import "net/http" - -type formBinding struct{} -type formPostBinding struct{} -type formMultipartBinding struct{} - -func (formBinding) Name() string { - return "form" -} - -func (formBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return err - } - req.ParseMultipartForm(32 << 10) // 32 MB - if err := mapForm(obj, req.Form); err != nil { - return err - } - return validate(obj) -} - -func (formPostBinding) Name() string { - return "form-urlencoded" -} - -func (formPostBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return err - } - if err := mapForm(obj, req.PostForm); err != nil { - return err - } - return validate(obj) -} - -func (formMultipartBinding) Name() string { - return "multipart/form-data" -} - -func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseMultipartForm(32 << 10); err != nil { - return err - } - if err := mapForm(obj, req.MultipartForm.Value); err != nil { - return err - } - return validate(obj) -} diff --git a/vendor/github.com/gin-gonic/gin/binding/form_mapping.go b/vendor/github.com/gin-gonic/gin/binding/form_mapping.go deleted file mode 100644 index 07c837512..000000000 --- a/vendor/github.com/gin-gonic/gin/binding/form_mapping.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import ( - "errors" - "reflect" - "strconv" -) - -func mapForm(ptr interface{}, form map[string][]string) error { - typ := reflect.TypeOf(ptr).Elem() - val := reflect.ValueOf(ptr).Elem() - for i := 0; i < typ.NumField(); i++ { - typeField := typ.Field(i) - structField := val.Field(i) - if !structField.CanSet() { - continue - } - - structFieldKind := structField.Kind() - inputFieldName := typeField.Tag.Get("form") - if inputFieldName == "" { - inputFieldName = typeField.Name - - // if "form" tag is nil, we inspect if the field is a struct. - // this would not make sense for JSON parsing but it does for a form - // since data is flatten - if structFieldKind == reflect.Struct { - err := mapForm(structField.Addr().Interface(), form) - if err != nil { - return err - } - continue - } - } - inputValue, exists := form[inputFieldName] - if !exists { - continue - } - - numElems := len(inputValue) - if structFieldKind == reflect.Slice && numElems > 0 { - sliceOf := structField.Type().Elem().Kind() - slice := reflect.MakeSlice(structField.Type(), numElems, numElems) - for i := 0; i < numElems; i++ { - if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { - return err - } - } - val.Field(i).Set(slice) - } else { - if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { - return err - } - } - } - return nil -} - -func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { - switch valueKind { - case reflect.Int: - return setIntField(val, 0, structField) - case reflect.Int8: - return setIntField(val, 8, structField) - case reflect.Int16: - return setIntField(val, 16, structField) - case reflect.Int32: - return setIntField(val, 32, structField) - case reflect.Int64: - return setIntField(val, 64, structField) - case reflect.Uint: - return setUintField(val, 0, structField) - case reflect.Uint8: - return setUintField(val, 8, structField) - case reflect.Uint16: - return setUintField(val, 16, structField) - case reflect.Uint32: - return setUintField(val, 32, structField) - case reflect.Uint64: - return setUintField(val, 64, structField) - case reflect.Bool: - return setBoolField(val, structField) - case reflect.Float32: - return setFloatField(val, 32, structField) - case reflect.Float64: - return setFloatField(val, 64, structField) - case reflect.String: - structField.SetString(val) - default: - return errors.New("Unknown type") - } - return nil -} - -func setIntField(val string, bitSize int, field reflect.Value) error { - if val == "" { - val = "0" - } - intVal, err := strconv.ParseInt(val, 10, bitSize) - if err == nil { - field.SetInt(intVal) - } - return err -} - -func setUintField(val string, bitSize int, field reflect.Value) error { - if val == "" { - val = "0" - } - uintVal, err := strconv.ParseUint(val, 10, bitSize) - if err == nil { - field.SetUint(uintVal) - } - return err -} - -func setBoolField(val string, field reflect.Value) error { - if val == "" { - val = "false" - } - boolVal, err := strconv.ParseBool(val) - if err == nil { - field.SetBool(boolVal) - } - return nil -} - -func setFloatField(val string, bitSize int, field reflect.Value) error { - if val == "" { - val = "0.0" - } - floatVal, err := strconv.ParseFloat(val, bitSize) - if err == nil { - field.SetFloat(floatVal) - } - return err -} - -// Don't pass in pointers to bind to. Can lead to bugs. See: -// https://github.com/codegangsta/martini-contrib/issues/40 -// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 -func ensureNotPointer(obj interface{}) { - if reflect.TypeOf(obj).Kind() == reflect.Ptr { - panic("Pointers are not accepted as binding models") - } -} diff --git a/vendor/github.com/gin-gonic/gin/binding/json.go b/vendor/github.com/gin-gonic/gin/binding/json.go deleted file mode 100644 index 6e5324433..000000000 --- a/vendor/github.com/gin-gonic/gin/binding/json.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import ( - "encoding/json" - - "net/http" -) - -type jsonBinding struct{} - -func (jsonBinding) Name() string { - return "json" -} - -func (jsonBinding) Bind(req *http.Request, obj interface{}) error { - decoder := json.NewDecoder(req.Body) - if err := decoder.Decode(obj); err != nil { - return err - } - return validate(obj) -} diff --git a/vendor/github.com/gin-gonic/gin/binding/protobuf.go b/vendor/github.com/gin-gonic/gin/binding/protobuf.go deleted file mode 100644 index 9f9562283..000000000 --- a/vendor/github.com/gin-gonic/gin/binding/protobuf.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import ( - "github.com/golang/protobuf/proto" - - "io/ioutil" - "net/http" -) - -type protobufBinding struct{} - -func (protobufBinding) Name() string { - return "protobuf" -} - -func (protobufBinding) Bind(req *http.Request, obj interface{}) error { - - buf, err := ioutil.ReadAll(req.Body) - if err != nil { - return err - } - - if err = proto.Unmarshal(buf, obj.(proto.Message)); err != nil { - return err - } - - //Here it's same to return validate(obj), but util now we cann't add `binding:""` to the struct - //which automatically generate by gen-proto - return nil - //return validate(obj) -} diff --git a/vendor/github.com/gin-gonic/gin/binding/xml.go b/vendor/github.com/gin-gonic/gin/binding/xml.go deleted file mode 100644 index f84a6b7f1..000000000 --- a/vendor/github.com/gin-gonic/gin/binding/xml.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import ( - "encoding/xml" - "net/http" -) - -type xmlBinding struct{} - -func (xmlBinding) Name() string { - return "xml" -} - -func (xmlBinding) Bind(req *http.Request, obj interface{}) error { - decoder := xml.NewDecoder(req.Body) - if err := decoder.Decode(obj); err != nil { - return err - } - return validate(obj) -} diff --git a/vendor/github.com/gin-gonic/gin/codecov.yml b/vendor/github.com/gin-gonic/gin/codecov.yml deleted file mode 100644 index c9c9a522d..000000000 --- a/vendor/github.com/gin-gonic/gin/codecov.yml +++ /dev/null @@ -1,5 +0,0 @@ -coverage: - notify: - gitter: - default: - url: https://webhooks.gitter.im/e/d90dcdeeab2f1e357165 diff --git a/vendor/github.com/gin-gonic/gin/context.go b/vendor/github.com/gin-gonic/gin/context.go deleted file mode 100644 index df001a403..000000000 --- a/vendor/github.com/gin-gonic/gin/context.go +++ /dev/null @@ -1,601 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "errors" - "io" - "math" - "net" - "net/http" - "net/url" - "strings" - "time" - - "github.com/gin-gonic/gin/binding" - "github.com/gin-gonic/gin/render" - "github.com/manucorporat/sse" - "golang.org/x/net/context" -) - -// Content-Type MIME of the most common data formats -const ( - MIMEJSON = binding.MIMEJSON - MIMEHTML = binding.MIMEHTML - MIMEXML = binding.MIMEXML - MIMEXML2 = binding.MIMEXML2 - MIMEPlain = binding.MIMEPlain - MIMEPOSTForm = binding.MIMEPOSTForm - MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm -) - -const abortIndex int8 = math.MaxInt8 / 2 - -// Context is the most important part of gin. It allows us to pass variables between middleware, -// manage the flow, validate the JSON of a request and render a JSON response for example. -type Context struct { - writermem responseWriter - Request *http.Request - Writer ResponseWriter - - Params Params - handlers HandlersChain - index int8 - - engine *Engine - Keys map[string]interface{} - Errors errorMsgs - Accepted []string -} - -var _ context.Context = &Context{} - -/************************************/ -/********** CONTEXT CREATION ********/ -/************************************/ - -func (c *Context) reset() { - c.Writer = &c.writermem - c.Params = c.Params[0:0] - c.handlers = nil - c.index = -1 - c.Keys = nil - c.Errors = c.Errors[0:0] - c.Accepted = nil -} - -// Copy returns a copy of the current context that can be safely used outside the request's scope. -// This have to be used then the context has to be passed to a goroutine. -func (c *Context) Copy() *Context { - var cp = *c - cp.writermem.ResponseWriter = nil - cp.Writer = &cp.writermem - cp.index = abortIndex - cp.handlers = nil - return &cp -} - -// HandlerName returns the main handler's name. For example if the handler is "handleGetUsers()", this -// function will return "main.handleGetUsers" -func (c *Context) HandlerName() string { - return nameOfFunction(c.handlers.Last()) -} - -/************************************/ -/*********** FLOW CONTROL ***********/ -/************************************/ - -// Next should be used only inside middleware. -// It executes the pending handlers in the chain inside the calling handler. -// See example in github. -func (c *Context) Next() { - c.index++ - s := int8(len(c.handlers)) - for ; c.index < s; c.index++ { - c.handlers[c.index](c) - } -} - -// IsAborted returns true if the current context was aborted. -func (c *Context) IsAborted() bool { - return c.index >= abortIndex -} - -// Abort prevents pending handlers from being called. Note that this will not stop the current handler. -// Let's say you have an authorization middleware that validates that the current request is authorized. If the -// authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers -// for this request are not called. -func (c *Context) Abort() { - c.index = abortIndex -} - -// AbortWithStatus calls `Abort()` and writes the headers with the specified status code. -// For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401). -func (c *Context) AbortWithStatus(code int) { - c.Status(code) - c.Writer.WriteHeaderNow() - c.Abort() -} - -// AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and -// pushes the specified error to `c.Errors`. -// See Context.Error() for more details. -func (c *Context) AbortWithError(code int, err error) *Error { - c.AbortWithStatus(code) - return c.Error(err) -} - -/************************************/ -/********* ERROR MANAGEMENT *********/ -/************************************/ - -// Attaches an error to the current context. The error is pushed to a list of errors. -// It's a good idea to call Error for each error that occurred during the resolution of a request. -// A middleware can be used to collect all the errors -// and push them to a database together, print a log, or append it in the HTTP response. -func (c *Context) Error(err error) *Error { - var parsedError *Error - switch err.(type) { - case *Error: - parsedError = err.(*Error) - default: - parsedError = &Error{ - Err: err, - Type: ErrorTypePrivate, - } - } - c.Errors = append(c.Errors, parsedError) - return parsedError -} - -/************************************/ -/******** METADATA MANAGEMENT********/ -/************************************/ - -// Set is used to store a new key/value pair exclusivelly for this context. -// It also lazy initializes c.Keys if it was not used previously. -func (c *Context) Set(key string, value interface{}) { - if c.Keys == nil { - c.Keys = make(map[string]interface{}) - } - c.Keys[key] = value -} - -// Get returns the value for the given key, ie: (value, true). -// If the value does not exists it returns (nil, false) -func (c *Context) Get(key string) (value interface{}, exists bool) { - if c.Keys != nil { - value, exists = c.Keys[key] - } - return -} - -// MustGet returns the value for the given key if it exists, otherwise it panics. -func (c *Context) MustGet(key string) interface{} { - if value, exists := c.Get(key); exists { - return value - } - panic("Key \"" + key + "\" does not exist") -} - -/************************************/ -/************ INPUT DATA ************/ -/************************************/ - -// Param returns the value of the URL param. -// It is a shortcut for c.Params.ByName(key) -// router.GET("/user/:id", func(c *gin.Context) { -// // a GET request to /user/john -// id := c.Param("id") // id == "john" -// }) -func (c *Context) Param(key string) string { - return c.Params.ByName(key) -} - -// Query returns the keyed url query value if it exists, -// othewise it returns an empty string `("")`. -// It is shortcut for `c.Request.URL.Query().Get(key)` -// GET /path?id=1234&name=Manu&value= -// c.Query("id") == "1234" -// c.Query("name") == "Manu" -// c.Query("value") == "" -// c.Query("wtf") == "" -func (c *Context) Query(key string) string { - value, _ := c.GetQuery(key) - return value -} - -// DefaultQuery returns the keyed url query value if it exists, -// othewise it returns the specified defaultValue string. -// See: Query() and GetQuery() for further information. -// GET /?name=Manu&lastname= -// c.DefaultQuery("name", "unknown") == "Manu" -// c.DefaultQuery("id", "none") == "none" -// c.DefaultQuery("lastname", "none") == "" -func (c *Context) DefaultQuery(key, defaultValue string) string { - if value, ok := c.GetQuery(key); ok { - return value - } - return defaultValue -} - -// GetQuery is like Query(), it returns the keyed url query value -// if it exists `(value, true)` (even when the value is an empty string), -// othewise it returns `("", false)`. -// It is shortcut for `c.Request.URL.Query().Get(key)` -// GET /?name=Manu&lastname= -// ("Manu", true) == c.GetQuery("name") -// ("", false) == c.GetQuery("id") -// ("", true) == c.GetQuery("lastname") -func (c *Context) GetQuery(key string) (string, bool) { - if values, ok := c.GetQueryArray(key); ok { - return values[0], ok - } - return "", false -} - -// QueryArray returns a slice of strings for a given query key. -// The length of the slice depends on the number of params with the given key. -func (c *Context) QueryArray(key string) []string { - values, _ := c.GetQueryArray(key) - return values -} - -// GetQueryArray returns a slice of strings for a given query key, plus -// a boolean value whether at least one value exists for the given key. -func (c *Context) GetQueryArray(key string) ([]string, bool) { - req := c.Request - if values, ok := req.URL.Query()[key]; ok && len(values) > 0 { - return values, true - } - return []string{}, false -} - -// PostForm returns the specified key from a POST urlencoded form or multipart form -// when it exists, otherwise it returns an empty string `("")`. -func (c *Context) PostForm(key string) string { - value, _ := c.GetPostForm(key) - return value -} - -// DefaultPostForm returns the specified key from a POST urlencoded form or multipart form -// when it exists, otherwise it returns the specified defaultValue string. -// See: PostForm() and GetPostForm() for further information. -func (c *Context) DefaultPostForm(key, defaultValue string) string { - if value, ok := c.GetPostForm(key); ok { - return value - } - return defaultValue -} - -// GetPostForm is like PostForm(key). It returns the specified key from a POST urlencoded -// form or multipart form when it exists `(value, true)` (even when the value is an empty string), -// otherwise it returns ("", false). -// For example, during a PATCH request to update the user's email: -// email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com" -// email= --> ("", true) := GetPostForm("email") // set email to "" -// --> ("", false) := GetPostForm("email") // do nothing with email -func (c *Context) GetPostForm(key string) (string, bool) { - if values, ok := c.GetPostFormArray(key); ok { - return values[0], ok - } - return "", false -} - -// PostFormArray returns a slice of strings for a given form key. -// The length of the slice depends on the number of params with the given key. -func (c *Context) PostFormArray(key string) []string { - values, _ := c.GetPostFormArray(key) - return values -} - -// GetPostFormArray returns a slice of strings for a given form key, plus -// a boolean value whether at least one value exists for the given key. -func (c *Context) GetPostFormArray(key string) ([]string, bool) { - req := c.Request - req.ParseForm() - req.ParseMultipartForm(32 << 20) // 32 MB - if values := req.PostForm[key]; len(values) > 0 { - return values, true - } - if req.MultipartForm != nil && req.MultipartForm.File != nil { - if values := req.MultipartForm.Value[key]; len(values) > 0 { - return values, true - } - } - return []string{}, false -} - -// Bind checks the Content-Type to select a binding engine automatically, -// Depending the "Content-Type" header different bindings are used: -// "application/json" --> JSON binding -// "application/xml" --> XML binding -// otherwise --> returns an error -// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. -// It decodes the json payload into the struct specified as a pointer. -// Like ParseBody() but this method also writes a 400 error if the json is not valid. -func (c *Context) Bind(obj interface{}) error { - b := binding.Default(c.Request.Method, c.ContentType()) - return c.BindWith(obj, b) -} - -// BindJSON is a shortcut for c.BindWith(obj, binding.JSON) -func (c *Context) BindJSON(obj interface{}) error { - return c.BindWith(obj, binding.JSON) -} - -// BindWith binds the passed struct pointer using the specified binding engine. -// See the binding package. -func (c *Context) BindWith(obj interface{}, b binding.Binding) error { - if err := b.Bind(c.Request, obj); err != nil { - c.AbortWithError(400, err).SetType(ErrorTypeBind) - return err - } - return nil -} - -// ClientIP implements a best effort algorithm to return the real client IP, it parses -// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. -func (c *Context) ClientIP() string { - if c.engine.ForwardedByClientIP { - clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip")) - if len(clientIP) > 0 { - return clientIP - } - clientIP = c.requestHeader("X-Forwarded-For") - if index := strings.IndexByte(clientIP, ','); index >= 0 { - clientIP = clientIP[0:index] - } - clientIP = strings.TrimSpace(clientIP) - if len(clientIP) > 0 { - return clientIP - } - } - if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil { - return ip - } - return "" -} - -// ContentType returns the Content-Type header of the request. -func (c *Context) ContentType() string { - return filterFlags(c.requestHeader("Content-Type")) -} - -func (c *Context) requestHeader(key string) string { - if values, _ := c.Request.Header[key]; len(values) > 0 { - return values[0] - } - return "" -} - -/************************************/ -/******** RESPONSE RENDERING ********/ -/************************************/ - -func (c *Context) Status(code int) { - c.writermem.WriteHeader(code) -} - -// Header is a intelligent shortcut for c.Writer.Header().Set(key, value) -// It writes a header in the response. -// If value == "", this method removes the header `c.Writer.Header().Del(key)` -func (c *Context) Header(key, value string) { - if len(value) == 0 { - c.Writer.Header().Del(key) - } else { - c.Writer.Header().Set(key, value) - } -} - -func (c *Context) SetCookie( - name string, - value string, - maxAge int, - path string, - domain string, - secure bool, - httpOnly bool, -) { - if path == "" { - path = "/" - } - http.SetCookie(c.Writer, &http.Cookie{ - Name: name, - Value: url.QueryEscape(value), - MaxAge: maxAge, - Path: path, - Domain: domain, - Secure: secure, - HttpOnly: httpOnly, - }) -} - -func (c *Context) Cookie(name string) (string, error) { - cookie, err := c.Request.Cookie(name) - if err != nil { - return "", err - } - val, _ := url.QueryUnescape(cookie.Value) - return val, nil -} - -func (c *Context) Render(code int, r render.Render) { - c.Status(code) - if err := r.Render(c.Writer); err != nil { - panic(err) - } -} - -// HTML renders the HTTP template specified by its file name. -// It also updates the HTTP code and sets the Content-Type as "text/html". -// See http://golang.org/doc/articles/wiki/ -func (c *Context) HTML(code int, name string, obj interface{}) { - instance := c.engine.HTMLRender.Instance(name, obj) - c.Render(code, instance) -} - -// IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body. -// It also sets the Content-Type as "application/json". -// WARNING: we recommend to use this only for development propuses since printing pretty JSON is -// more CPU and bandwidth consuming. Use Context.JSON() instead. -func (c *Context) IndentedJSON(code int, obj interface{}) { - c.Render(code, render.IndentedJSON{Data: obj}) -} - -// JSON serializes the given struct as JSON into the response body. -// It also sets the Content-Type as "application/json". -func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) - if err := render.WriteJSON(c.Writer, obj); err != nil { - panic(err) - } -} - -// XML serializes the given struct as XML into the response body. -// It also sets the Content-Type as "application/xml". -func (c *Context) XML(code int, obj interface{}) { - c.Render(code, render.XML{Data: obj}) -} - -// YAML serializes the given struct as YAML into the response body. -func (c *Context) YAML(code int, obj interface{}) { - c.Render(code, render.YAML{Data: obj}) -} - -// String writes the given string into the response body. -func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) - render.WriteString(c.Writer, format, values) -} - -// Redirect returns a HTTP redirect to the specific location. -func (c *Context) Redirect(code int, location string) { - c.Render(-1, render.Redirect{ - Code: code, - Location: location, - Request: c.Request, - }) -} - -// Data writes some data into the body stream and updates the HTTP code. -func (c *Context) Data(code int, contentType string, data []byte) { - c.Render(code, render.Data{ - ContentType: contentType, - Data: data, - }) -} - -// File writes the specified file into the body stream in a efficient way. -func (c *Context) File(filepath string) { - http.ServeFile(c.Writer, c.Request, filepath) -} - -// SSEvent writes a Server-Sent Event into the body stream. -func (c *Context) SSEvent(name string, message interface{}) { - c.Render(-1, sse.Event{ - Event: name, - Data: message, - }) -} - -func (c *Context) Stream(step func(w io.Writer) bool) { - w := c.Writer - clientGone := w.CloseNotify() - for { - select { - case <-clientGone: - return - default: - keepOpen := step(w) - w.Flush() - if !keepOpen { - return - } - } - } -} - -/************************************/ -/******** CONTENT NEGOTIATION *******/ -/************************************/ - -type Negotiate struct { - Offered []string - HTMLName string - HTMLData interface{} - JSONData interface{} - XMLData interface{} - Data interface{} -} - -func (c *Context) Negotiate(code int, config Negotiate) { - switch c.NegotiateFormat(config.Offered...) { - case binding.MIMEJSON: - data := chooseData(config.JSONData, config.Data) - c.JSON(code, data) - - case binding.MIMEHTML: - data := chooseData(config.HTMLData, config.Data) - c.HTML(code, config.HTMLName, data) - - case binding.MIMEXML: - data := chooseData(config.XMLData, config.Data) - c.XML(code, data) - - default: - c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) - } -} - -func (c *Context) NegotiateFormat(offered ...string) string { - assert1(len(offered) > 0, "you must provide at least one offer") - - if c.Accepted == nil { - c.Accepted = parseAccept(c.requestHeader("Accept")) - } - if len(c.Accepted) == 0 { - return offered[0] - } - for _, accepted := range c.Accepted { - for _, offert := range offered { - if accepted == offert { - return offert - } - } - } - return "" -} - -func (c *Context) SetAccepted(formats ...string) { - c.Accepted = formats -} - -/************************************/ -/***** GOLANG.ORG/X/NET/CONTEXT *****/ -/************************************/ - -func (c *Context) Deadline() (deadline time.Time, ok bool) { - return -} - -func (c *Context) Done() <-chan struct{} { - return nil -} - -func (c *Context) Err() error { - return nil -} - -func (c *Context) Value(key interface{}) interface{} { - if key == 0 { - return c.Request - } - if keyAsString, ok := key.(string); ok { - val, _ := c.Get(keyAsString) - return val - } - return nil -} diff --git a/vendor/github.com/gin-gonic/gin/debug.go b/vendor/github.com/gin-gonic/gin/debug.go deleted file mode 100644 index a121591a8..000000000 --- a/vendor/github.com/gin-gonic/gin/debug.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "bytes" - "html/template" - "log" -) - -func init() { - log.SetFlags(0) -} - -// IsDebugging returns true if the framework is running in debug mode. -// Use SetMode(gin.Release) to switch to disable the debug mode. -func IsDebugging() bool { - return ginMode == debugCode -} - -func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { - if IsDebugging() { - nuHandlers := len(handlers) - handlerName := nameOfFunction(handlers.Last()) - debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) - } -} - -func debugPrintLoadTemplate(tmpl *template.Template) { - if IsDebugging() { - var buf bytes.Buffer - for _, tmpl := range tmpl.Templates() { - buf.WriteString("\t- ") - buf.WriteString(tmpl.Name()) - buf.WriteString("\n") - } - debugPrint("Loaded HTML Templates (%d): \n%s\n", len(tmpl.Templates()), buf.String()) - } -} - -func debugPrint(format string, values ...interface{}) { - if IsDebugging() { - log.Printf("[GIN-debug] "+format, values...) - } -} - -func debugPrintWARNINGNew() { - debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production. - - using env: export GIN_MODE=release - - using code: gin.SetMode(gin.ReleaseMode) - -`) -} - -func debugPrintWARNINGSetHTMLTemplate() { - debugPrint(`[WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called -at initialization. ie. before any route is registered or the router is listening in a socket: - - router := gin.Default() - router.SetHTMLTemplate(template) // << good place - -`) -} - -func debugPrintError(err error) { - if err != nil { - debugPrint("[ERROR] %v\n", err) - } -} diff --git a/vendor/github.com/gin-gonic/gin/deprecated.go b/vendor/github.com/gin-gonic/gin/deprecated.go deleted file mode 100644 index 0488a9b01..000000000 --- a/vendor/github.com/gin-gonic/gin/deprecated.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import "log" - -func (c *Context) GetCookie(name string) (string, error) { - log.Println("GetCookie() method is deprecated. Use Cookie() instead.") - return c.Cookie(name) -} diff --git a/vendor/github.com/gin-gonic/gin/errors.go b/vendor/github.com/gin-gonic/gin/errors.go deleted file mode 100644 index 7716bfaea..000000000 --- a/vendor/github.com/gin-gonic/gin/errors.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "bytes" - "encoding/json" - "fmt" - "reflect" -) - -type ErrorType uint64 - -const ( - ErrorTypeBind ErrorType = 1 << 63 // used when c.Bind() fails - ErrorTypeRender ErrorType = 1 << 62 // used when c.Render() fails - ErrorTypePrivate ErrorType = 1 << 0 - ErrorTypePublic ErrorType = 1 << 1 - - ErrorTypeAny ErrorType = 1<<64 - 1 - ErrorTypeNu = 2 -) - -type ( - Error struct { - Err error - Type ErrorType - Meta interface{} - } - - errorMsgs []*Error -) - -var _ error = &Error{} - -func (msg *Error) SetType(flags ErrorType) *Error { - msg.Type = flags - return msg -} - -func (msg *Error) SetMeta(data interface{}) *Error { - msg.Meta = data - return msg -} - -func (msg *Error) JSON() interface{} { - json := H{} - if msg.Meta != nil { - value := reflect.ValueOf(msg.Meta) - switch value.Kind() { - case reflect.Struct: - return msg.Meta - case reflect.Map: - for _, key := range value.MapKeys() { - json[key.String()] = value.MapIndex(key).Interface() - } - default: - json["meta"] = msg.Meta - } - } - if _, ok := json["error"]; !ok { - json["error"] = msg.Error() - } - return json -} - -// MarshalJSON implements the json.Marshaller interface -func (msg *Error) MarshalJSON() ([]byte, error) { - return json.Marshal(msg.JSON()) -} - -// Implements the error interface -func (msg *Error) Error() string { - return msg.Err.Error() -} - -func (msg *Error) IsType(flags ErrorType) bool { - return (msg.Type & flags) > 0 -} - -// Returns a readonly copy filterd the byte. -// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic -func (a errorMsgs) ByType(typ ErrorType) errorMsgs { - if len(a) == 0 { - return nil - } - if typ == ErrorTypeAny { - return a - } - var result errorMsgs - for _, msg := range a { - if msg.IsType(typ) { - result = append(result, msg) - } - } - return result -} - -// Returns the last error in the slice. It returns nil if the array is empty. -// Shortcut for errors[len(errors)-1] -func (a errorMsgs) Last() *Error { - length := len(a) - if length > 0 { - return a[length-1] - } - return nil -} - -// Returns an array will all the error messages. -// Example: -// c.Error(errors.New("first")) -// c.Error(errors.New("second")) -// c.Error(errors.New("third")) -// c.Errors.Errors() // == []string{"first", "second", "third"} -func (a errorMsgs) Errors() []string { - if len(a) == 0 { - return nil - } - errorStrings := make([]string, len(a)) - for i, err := range a { - errorStrings[i] = err.Error() - } - return errorStrings -} - -func (a errorMsgs) JSON() interface{} { - switch len(a) { - case 0: - return nil - case 1: - return a.Last().JSON() - default: - json := make([]interface{}, len(a)) - for i, err := range a { - json[i] = err.JSON() - } - return json - } -} - -func (a errorMsgs) MarshalJSON() ([]byte, error) { - return json.Marshal(a.JSON()) -} - -func (a errorMsgs) String() string { - if len(a) == 0 { - return "" - } - var buffer bytes.Buffer - for i, msg := range a { - fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err) - if msg.Meta != nil { - fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta) - } - } - return buffer.String() -} diff --git a/vendor/github.com/gin-gonic/gin/fs.go b/vendor/github.com/gin-gonic/gin/fs.go deleted file mode 100644 index 6af3ded55..000000000 --- a/vendor/github.com/gin-gonic/gin/fs.go +++ /dev/null @@ -1,42 +0,0 @@ -package gin - -import ( - "net/http" - "os" -) - -type ( - onlyfilesFS struct { - fs http.FileSystem - } - neuteredReaddirFile struct { - http.File - } -) - -// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used interally -// in router.Static(). -// if listDirectory == true, then it works the same as http.Dir() otherwise it returns -// a filesystem that prevents http.FileServer() to list the directory files. -func Dir(root string, listDirectory bool) http.FileSystem { - fs := http.Dir(root) - if listDirectory { - return fs - } - return &onlyfilesFS{fs} -} - -// Conforms to http.Filesystem -func (fs onlyfilesFS) Open(name string) (http.File, error) { - f, err := fs.fs.Open(name) - if err != nil { - return nil, err - } - return neuteredReaddirFile{f}, nil -} - -// Overrides the http.File default implementation -func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) { - // this disables directory listing - return nil, nil -} diff --git a/vendor/github.com/gin-gonic/gin/gin.go b/vendor/github.com/gin-gonic/gin/gin.go deleted file mode 100644 index 60f4ab292..000000000 --- a/vendor/github.com/gin-gonic/gin/gin.go +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "html/template" - "net" - "net/http" - "os" - "sync" - - "github.com/gin-gonic/gin/render" -) - -// Version is Framework's version -const Version = "v1.0rc2" - -var default404Body = []byte("404 page not found") -var default405Body = []byte("405 method not allowed") - -type HandlerFunc func(*Context) -type HandlersChain []HandlerFunc - -// Last returns the last handler in the chain. ie. the last handler is the main own. -func (c HandlersChain) Last() HandlerFunc { - length := len(c) - if length > 0 { - return c[length-1] - } - return nil -} - -type ( - RoutesInfo []RouteInfo - RouteInfo struct { - Method string - Path string - Handler string - } - - // Engine is the framework's instance, it contains the muxer, middleware and configuration settings. - // Create an instance of Engine, by using New() or Default() - Engine struct { - RouterGroup - HTMLRender render.HTMLRender - allNoRoute HandlersChain - allNoMethod HandlersChain - noRoute HandlersChain - noMethod HandlersChain - pool sync.Pool - trees methodTrees - - // Enables automatic redirection if the current route can't be matched but a - // handler for the path with (without) the trailing slash exists. - // For example if /foo/ is requested but a route only exists for /foo, the - // client is redirected to /foo with http status code 301 for GET requests - // and 307 for all other request methods. - RedirectTrailingSlash bool - - // If enabled, the router tries to fix the current request path, if no - // handle is registered for it. - // First superfluous path elements like ../ or // are removed. - // Afterwards the router does a case-insensitive lookup of the cleaned path. - // If a handle can be found for this route, the router makes a redirection - // to the corrected path with status code 301 for GET requests and 307 for - // all other request methods. - // For example /FOO and /..//Foo could be redirected to /foo. - // RedirectTrailingSlash is independent of this option. - RedirectFixedPath bool - - // If enabled, the router checks if another method is allowed for the - // current route, if the current request can not be routed. - // If this is the case, the request is answered with 'Method Not Allowed' - // and HTTP status code 405. - // If no other Method is allowed, the request is delegated to the NotFound - // handler. - HandleMethodNotAllowed bool - ForwardedByClientIP bool - } -) - -var _ IRouter = &Engine{} - -// New returns a new blank Engine instance without any middleware attached. -// By default the configuration is: -// - RedirectTrailingSlash: true -// - RedirectFixedPath: false -// - HandleMethodNotAllowed: false -// - ForwardedByClientIP: true -func New() *Engine { - debugPrintWARNINGNew() - engine := &Engine{ - RouterGroup: RouterGroup{ - Handlers: nil, - basePath: "/", - root: true, - }, - RedirectTrailingSlash: true, - RedirectFixedPath: false, - HandleMethodNotAllowed: false, - ForwardedByClientIP: true, - trees: make(methodTrees, 0, 9), - } - engine.RouterGroup.engine = engine - engine.pool.New = func() interface{} { - return engine.allocateContext() - } - return engine -} - -// Default returns an Engine instance with the Logger and Recovery middleware already attached. -func Default() *Engine { - engine := New() - engine.Use(Logger(), Recovery()) - return engine -} - -func (engine *Engine) allocateContext() *Context { - return &Context{engine: engine} -} - -func (engine *Engine) LoadHTMLGlob(pattern string) { - if IsDebugging() { - debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern))) - engine.HTMLRender = render.HTMLDebug{Glob: pattern} - } else { - templ := template.Must(template.ParseGlob(pattern)) - engine.SetHTMLTemplate(templ) - } -} - -func (engine *Engine) LoadHTMLFiles(files ...string) { - if IsDebugging() { - engine.HTMLRender = render.HTMLDebug{Files: files} - } else { - templ := template.Must(template.ParseFiles(files...)) - engine.SetHTMLTemplate(templ) - } -} - -func (engine *Engine) SetHTMLTemplate(templ *template.Template) { - if len(engine.trees) > 0 { - debugPrintWARNINGSetHTMLTemplate() - } - engine.HTMLRender = render.HTMLProduction{Template: templ} -} - -// NoRoute adds handlers for NoRoute. It return a 404 code by default. -func (engine *Engine) NoRoute(handlers ...HandlerFunc) { - engine.noRoute = handlers - engine.rebuild404Handlers() -} - -// NoMethod sets the handlers called when... TODO -func (engine *Engine) NoMethod(handlers ...HandlerFunc) { - engine.noMethod = handlers - engine.rebuild405Handlers() -} - -// Use attachs a global middleware to the router. ie. the middleware attached though Use() will be -// included in the handlers chain for every single request. Even 404, 405, static files... -// For example, this is the right place for a logger or error management middleware. -func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { - engine.RouterGroup.Use(middleware...) - engine.rebuild404Handlers() - engine.rebuild405Handlers() - return engine -} - -func (engine *Engine) rebuild404Handlers() { - engine.allNoRoute = engine.combineHandlers(engine.noRoute) -} - -func (engine *Engine) rebuild405Handlers() { - engine.allNoMethod = engine.combineHandlers(engine.noMethod) -} - -func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { - assert1(path[0] == '/', "path must begin with '/'") - assert1(len(method) > 0, "HTTP method can not be empty") - assert1(len(handlers) > 0, "there must be at least one handler") - - debugPrintRoute(method, path, handlers) - root := engine.trees.get(method) - if root == nil { - root = new(node) - engine.trees = append(engine.trees, methodTree{method: method, root: root}) - } - root.addRoute(path, handlers) -} - -// Routes returns a slice of registered routes, including some useful information, such as: -// the http method, path and the handler name. -func (engine *Engine) Routes() (routes RoutesInfo) { - for _, tree := range engine.trees { - routes = iterate("", tree.method, routes, tree.root) - } - return routes -} - -func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo { - path += root.path - if len(root.handlers) > 0 { - routes = append(routes, RouteInfo{ - Method: method, - Path: path, - Handler: nameOfFunction(root.handlers.Last()), - }) - } - for _, child := range root.children { - routes = iterate(path, method, routes, child) - } - return routes -} - -// Run attaches the router to a http.Server and starts listening and serving HTTP requests. -// It is a shortcut for http.ListenAndServe(addr, router) -// Note: this method will block the calling goroutine indefinitely unless an error happens. -func (engine *Engine) Run(addr ...string) (err error) { - defer func() { debugPrintError(err) }() - - address := resolveAddress(addr) - debugPrint("Listening and serving HTTP on %s\n", address) - err = http.ListenAndServe(address, engine) - return -} - -// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests. -// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) -// Note: this method will block the calling goroutine indefinitely unless an error happens. -func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err error) { - debugPrint("Listening and serving HTTPS on %s\n", addr) - defer func() { debugPrintError(err) }() - - err = http.ListenAndServeTLS(addr, certFile, keyFile, engine) - return -} - -// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests -// through the specified unix socket (ie. a file). -// Note: this method will block the calling goroutine indefinitely unless an error happens. -func (engine *Engine) RunUnix(file string) (err error) { - debugPrint("Listening and serving HTTP on unix:/%s", file) - defer func() { debugPrintError(err) }() - - os.Remove(file) - listener, err := net.Listen("unix", file) - if err != nil { - return - } - defer listener.Close() - err = http.Serve(listener, engine) - return -} - -// Conforms to the http.Handler interface. -func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { - c := engine.pool.Get().(*Context) - c.writermem.reset(w) - c.Request = req - c.reset() - - engine.handleHTTPRequest(c) - - engine.pool.Put(c) -} - -func (engine *Engine) handleHTTPRequest(context *Context) { - httpMethod := context.Request.Method - path := context.Request.URL.Path - - // Find root of the tree for the given HTTP method - t := engine.trees - for i, tl := 0, len(t); i < tl; i++ { - if t[i].method == httpMethod { - root := t[i].root - // Find route in tree - handlers, params, tsr := root.getValue(path, context.Params) - if handlers != nil { - context.handlers = handlers - context.Params = params - context.Next() - context.writermem.WriteHeaderNow() - return - - } else if httpMethod != "CONNECT" && path != "/" { - if tsr && engine.RedirectTrailingSlash { - redirectTrailingSlash(context) - return - } - if engine.RedirectFixedPath && redirectFixedPath(context, root, engine.RedirectFixedPath) { - return - } - } - break - } - } - - // TODO: unit test - if engine.HandleMethodNotAllowed { - for _, tree := range engine.trees { - if tree.method != httpMethod { - if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil { - context.handlers = engine.allNoMethod - serveError(context, 405, default405Body) - return - } - } - } - } - context.handlers = engine.allNoRoute - serveError(context, 404, default404Body) -} - -var mimePlain = []string{MIMEPlain} - -func serveError(c *Context, code int, defaultMessage []byte) { - c.writermem.status = code - c.Next() - if !c.writermem.Written() { - if c.writermem.Status() == code { - c.writermem.Header()["Content-Type"] = mimePlain - c.Writer.Write(defaultMessage) - } else { - c.writermem.WriteHeaderNow() - } - } -} - -func redirectTrailingSlash(c *Context) { - req := c.Request - path := req.URL.Path - code := 301 // Permanent redirect, request with GET method - if req.Method != "GET" { - code = 307 - } - - if len(path) > 1 && path[len(path)-1] == '/' { - req.URL.Path = path[:len(path)-1] - } else { - req.URL.Path = path + "/" - } - debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) - http.Redirect(c.Writer, req, req.URL.String(), code) - c.writermem.WriteHeaderNow() -} - -func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { - req := c.Request - path := req.URL.Path - - fixedPath, found := root.findCaseInsensitivePath( - cleanPath(path), - trailingSlash, - ) - if found { - code := 301 // Permanent redirect, request with GET method - if req.Method != "GET" { - code = 307 - } - req.URL.Path = string(fixedPath) - debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) - http.Redirect(c.Writer, req, req.URL.String(), code) - c.writermem.WriteHeaderNow() - return true - } - return false -} diff --git a/vendor/github.com/gin-gonic/gin/logger.go b/vendor/github.com/gin-gonic/gin/logger.go deleted file mode 100644 index 7c2a72be1..000000000 --- a/vendor/github.com/gin-gonic/gin/logger.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "fmt" - "io" - "os" - "time" - - "github.com/mattn/go-isatty" -) - -var ( - green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) - white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) - yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) - red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) - blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) - magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) - cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) - reset = string([]byte{27, 91, 48, 109}) -) - -func ErrorLogger() HandlerFunc { - return ErrorLoggerT(ErrorTypeAny) -} - -func ErrorLoggerT(typ ErrorType) HandlerFunc { - return func(c *Context) { - c.Next() - errors := c.Errors.ByType(typ) - if len(errors) > 0 { - c.JSON(-1, errors) - } - } -} - -// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter -// By default gin.DefaultWriter = os.Stdout -func Logger() HandlerFunc { - return LoggerWithWriter(DefaultWriter) -} - -// LoggerWithWriter instance a Logger middleware with the specified writter buffer. -// Example: os.Stdout, a file opened in write mode, a socket... -func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { - isTerm := true - - if w, ok := out.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) { - isTerm = false - } - - var skip map[string]struct{} - - if length := len(notlogged); length > 0 { - skip = make(map[string]struct{}, length) - - for _, path := range notlogged { - skip[path] = struct{}{} - } - } - - return func(c *Context) { - // Start timer - start := time.Now() - path := c.Request.URL.Path - - // Process request - c.Next() - - // Log only when path is not being skipped - if _, ok := skip[path]; !ok { - // Stop timer - end := time.Now() - latency := end.Sub(start) - - clientIP := c.ClientIP() - method := c.Request.Method - statusCode := c.Writer.Status() - var statusColor, methodColor string - if isTerm { - statusColor = colorForStatus(statusCode) - methodColor = colorForMethod(method) - } - comment := c.Errors.ByType(ErrorTypePrivate).String() - - fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s", - end.Format("2006/01/02 - 15:04:05"), - statusColor, statusCode, reset, - latency, - clientIP, - methodColor, reset, method, - path, - comment, - ) - } - } -} - -func colorForStatus(code int) string { - switch { - case code >= 200 && code < 300: - return green - case code >= 300 && code < 400: - return white - case code >= 400 && code < 500: - return yellow - default: - return red - } -} - -func colorForMethod(method string) string { - switch method { - case "GET": - return blue - case "POST": - return cyan - case "PUT": - return yellow - case "DELETE": - return red - case "PATCH": - return green - case "HEAD": - return magenta - case "OPTIONS": - return white - default: - return reset - } -} diff --git a/vendor/github.com/gin-gonic/gin/logo.jpg b/vendor/github.com/gin-gonic/gin/logo.jpg deleted file mode 100644 index bb51852e4..000000000 Binary files a/vendor/github.com/gin-gonic/gin/logo.jpg and /dev/null differ diff --git a/vendor/github.com/gin-gonic/gin/mode.go b/vendor/github.com/gin-gonic/gin/mode.go deleted file mode 100644 index c600b7b5b..000000000 --- a/vendor/github.com/gin-gonic/gin/mode.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "io" - "os" - - "github.com/gin-gonic/gin/binding" -) - -const ENV_GIN_MODE = "GIN_MODE" - -const ( - DebugMode string = "debug" - ReleaseMode string = "release" - TestMode string = "test" -) -const ( - debugCode = iota - releaseCode - testCode -) - -// DefaultWriter is the default io.Writer used the Gin for debug output and -// middleware output like Logger() or Recovery(). -// Note that both Logger and Recovery provides custom ways to configure their -// output io.Writer. -// To support coloring in Windows use: -// import "github.com/mattn/go-colorable" -// gin.DefaultWriter = colorable.NewColorableStdout() -var DefaultWriter io.Writer = os.Stdout -var DefaultErrorWriter io.Writer = os.Stderr - -var ginMode = debugCode -var modeName = DebugMode - -func init() { - mode := os.Getenv(ENV_GIN_MODE) - if len(mode) == 0 { - SetMode(DebugMode) - } else { - SetMode(mode) - } -} - -func SetMode(value string) { - switch value { - case DebugMode: - ginMode = debugCode - case ReleaseMode: - ginMode = releaseCode - case TestMode: - ginMode = testCode - default: - panic("gin mode unknown: " + value) - } - modeName = value -} - -func DisableBindValidation() { - binding.Validator = nil -} - -func Mode() string { - return modeName -} diff --git a/vendor/github.com/gin-gonic/gin/path.go b/vendor/github.com/gin-gonic/gin/path.go deleted file mode 100644 index 43cdd0472..000000000 --- a/vendor/github.com/gin-gonic/gin/path.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Based on the path package, Copyright 2009 The Go Authors. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package gin - -// CleanPath is the URL version of path.Clean, it returns a canonical URL path -// for p, eliminating . and .. elements. -// -// The following rules are applied iteratively until no further processing can -// be done: -// 1. Replace multiple slashes with a single slash. -// 2. Eliminate each . path name element (the current directory). -// 3. Eliminate each inner .. path name element (the parent directory) -// along with the non-.. element that precedes it. -// 4. Eliminate .. elements that begin a rooted path: -// that is, replace "/.." by "/" at the beginning of a path. -// -// If the result of this process is an empty string, "/" is returned -func cleanPath(p string) string { - // Turn empty string into "/" - if p == "" { - return "/" - } - - n := len(p) - var buf []byte - - // Invariants: - // reading from path; r is index of next byte to process. - // writing to buf; w is index of next byte to write. - - // path must start with '/' - r := 1 - w := 1 - - if p[0] != '/' { - r = 0 - buf = make([]byte, n+1) - buf[0] = '/' - } - - trailing := n > 2 && p[n-1] == '/' - - // A bit more clunky without a 'lazybuf' like the path package, but the loop - // gets completely inlined (bufApp). So in contrast to the path package this - // loop has no expensive function calls (except 1x make) - - for r < n { - switch { - case p[r] == '/': - // empty path element, trailing slash is added after the end - r++ - - case p[r] == '.' && r+1 == n: - trailing = true - r++ - - case p[r] == '.' && p[r+1] == '/': - // . element - r++ - - case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): - // .. element: remove to last / - r += 2 - - if w > 1 { - // can backtrack - w-- - - if buf == nil { - for w > 1 && p[w] != '/' { - w-- - } - } else { - for w > 1 && buf[w] != '/' { - w-- - } - } - } - - default: - // real path element. - // add slash if needed - if w > 1 { - bufApp(&buf, p, w, '/') - w++ - } - - // copy element - for r < n && p[r] != '/' { - bufApp(&buf, p, w, p[r]) - w++ - r++ - } - } - } - - // re-append trailing slash - if trailing && w > 1 { - bufApp(&buf, p, w, '/') - w++ - } - - if buf == nil { - return p[:w] - } - return string(buf[:w]) -} - -// internal helper to lazily create a buffer if necessary -func bufApp(buf *[]byte, s string, w int, c byte) { - if *buf == nil { - if s[w] == c { - return - } - - *buf = make([]byte, len(s)) - copy(*buf, s[:w]) - } - (*buf)[w] = c -} diff --git a/vendor/github.com/gin-gonic/gin/recovery.go b/vendor/github.com/gin-gonic/gin/recovery.go deleted file mode 100644 index c502f3553..000000000 --- a/vendor/github.com/gin-gonic/gin/recovery.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "log" - "net/http/httputil" - "runtime" -) - -var ( - dunno = []byte("???") - centerDot = []byte("·") - dot = []byte(".") - slash = []byte("/") -) - -// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. -func Recovery() HandlerFunc { - return RecoveryWithWriter(DefaultErrorWriter) -} - -func RecoveryWithWriter(out io.Writer) HandlerFunc { - var logger *log.Logger - if out != nil { - logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags) - } - return func(c *Context) { - defer func() { - if err := recover(); err != nil { - if logger != nil { - stack := stack(3) - httprequest, _ := httputil.DumpRequest(c.Request, false) - logger.Printf("[Recovery] panic recovered:\n%s\n%s\n%s%s", string(httprequest), err, stack, reset) - } - c.AbortWithStatus(500) - } - }() - c.Next() - } -} - -// stack returns a nicely formated stack frame, skipping skip frames -func stack(skip int) []byte { - buf := new(bytes.Buffer) // the returned data - // As we loop, we open files and read them. These variables record the currently - // loaded file. - var lines [][]byte - var lastFile string - for i := skip; ; i++ { // Skip the expected number of frames - pc, file, line, ok := runtime.Caller(i) - if !ok { - break - } - // Print this much at least. If we can't find the source, it won't show. - fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) - if file != lastFile { - data, err := ioutil.ReadFile(file) - if err != nil { - continue - } - lines = bytes.Split(data, []byte{'\n'}) - lastFile = file - } - fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) - } - return buf.Bytes() -} - -// source returns a space-trimmed slice of the n'th line. -func source(lines [][]byte, n int) []byte { - n-- // in stack trace, lines are 1-indexed but our array is 0-indexed - if n < 0 || n >= len(lines) { - return dunno - } - return bytes.TrimSpace(lines[n]) -} - -// function returns, if possible, the name of the function containing the PC. -func function(pc uintptr) []byte { - fn := runtime.FuncForPC(pc) - if fn == nil { - return dunno - } - name := []byte(fn.Name()) - // The name includes the path name to the package, which is unnecessary - // since the file name is already included. Plus, it has center dots. - // That is, we see - // runtime/debug.*T·ptrmethod - // and want - // *T.ptrmethod - // Also the package path might contains dot (e.g. code.google.com/...), - // so first eliminate the path prefix - if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { - name = name[lastslash+1:] - } - if period := bytes.Index(name, dot); period >= 0 { - name = name[period+1:] - } - name = bytes.Replace(name, centerDot, dot, -1) - return name -} diff --git a/vendor/github.com/gin-gonic/gin/render/data.go b/vendor/github.com/gin-gonic/gin/render/data.go deleted file mode 100644 index efa75d559..000000000 --- a/vendor/github.com/gin-gonic/gin/render/data.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import "net/http" - -type Data struct { - ContentType string - Data []byte -} - -func (r Data) Render(w http.ResponseWriter) error { - if len(r.ContentType) > 0 { - w.Header()["Content-Type"] = []string{r.ContentType} - } - w.Write(r.Data) - return nil -} diff --git a/vendor/github.com/gin-gonic/gin/render/html.go b/vendor/github.com/gin-gonic/gin/render/html.go deleted file mode 100644 index 8bfb23ac8..000000000 --- a/vendor/github.com/gin-gonic/gin/render/html.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "html/template" - "net/http" -) - -type ( - HTMLRender interface { - Instance(string, interface{}) Render - } - - HTMLProduction struct { - Template *template.Template - } - - HTMLDebug struct { - Files []string - Glob string - } - - HTML struct { - Template *template.Template - Name string - Data interface{} - } -) - -var htmlContentType = []string{"text/html; charset=utf-8"} - -func (r HTMLProduction) Instance(name string, data interface{}) Render { - return HTML{ - Template: r.Template, - Name: name, - Data: data, - } -} - -func (r HTMLDebug) Instance(name string, data interface{}) Render { - return HTML{ - Template: r.loadTemplate(), - Name: name, - Data: data, - } -} -func (r HTMLDebug) loadTemplate() *template.Template { - if len(r.Files) > 0 { - return template.Must(template.ParseFiles(r.Files...)) - } - if len(r.Glob) > 0 { - return template.Must(template.ParseGlob(r.Glob)) - } - panic("the HTML debug render was created without files or glob pattern") -} - -func (r HTML) Render(w http.ResponseWriter) error { - writeContentType(w, htmlContentType) - if len(r.Name) == 0 { - return r.Template.Execute(w, r.Data) - } - return r.Template.ExecuteTemplate(w, r.Name, r.Data) -} diff --git a/vendor/github.com/gin-gonic/gin/render/json.go b/vendor/github.com/gin-gonic/gin/render/json.go deleted file mode 100644 index 32e6058db..000000000 --- a/vendor/github.com/gin-gonic/gin/render/json.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "encoding/json" - "net/http" -) - -type ( - JSON struct { - Data interface{} - } - - IndentedJSON struct { - Data interface{} - } -) - -var jsonContentType = []string{"application/json; charset=utf-8"} - -func (r JSON) Render(w http.ResponseWriter) error { - return WriteJSON(w, r.Data) -} - -func (r IndentedJSON) Render(w http.ResponseWriter) error { - writeContentType(w, jsonContentType) - jsonBytes, err := json.MarshalIndent(r.Data, "", " ") - if err != nil { - return err - } - w.Write(jsonBytes) - return nil -} - -func WriteJSON(w http.ResponseWriter, obj interface{}) error { - writeContentType(w, jsonContentType) - return json.NewEncoder(w).Encode(obj) -} diff --git a/vendor/github.com/gin-gonic/gin/render/redirect.go b/vendor/github.com/gin-gonic/gin/render/redirect.go deleted file mode 100644 index bd48d7d83..000000000 --- a/vendor/github.com/gin-gonic/gin/render/redirect.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "fmt" - "net/http" -) - -type Redirect struct { - Code int - Request *http.Request - Location string -} - -func (r Redirect) Render(w http.ResponseWriter) error { - if (r.Code < 300 || r.Code > 308) && r.Code != 201 { - panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code)) - } - http.Redirect(w, r.Request, r.Location, r.Code) - return nil -} diff --git a/vendor/github.com/gin-gonic/gin/render/render.go b/vendor/github.com/gin-gonic/gin/render/render.go deleted file mode 100644 index 3808666ac..000000000 --- a/vendor/github.com/gin-gonic/gin/render/render.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import "net/http" - -type Render interface { - Render(http.ResponseWriter) error -} - -var ( - _ Render = JSON{} - _ Render = IndentedJSON{} - _ Render = XML{} - _ Render = String{} - _ Render = Redirect{} - _ Render = Data{} - _ Render = HTML{} - _ HTMLRender = HTMLDebug{} - _ HTMLRender = HTMLProduction{} - _ Render = YAML{} -) - -func writeContentType(w http.ResponseWriter, value []string) { - header := w.Header() - if val := header["Content-Type"]; len(val) == 0 { - header["Content-Type"] = value - } -} diff --git a/vendor/github.com/gin-gonic/gin/render/text.go b/vendor/github.com/gin-gonic/gin/render/text.go deleted file mode 100644 index 5a9e280bd..000000000 --- a/vendor/github.com/gin-gonic/gin/render/text.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "fmt" - "io" - "net/http" -) - -type String struct { - Format string - Data []interface{} -} - -var plainContentType = []string{"text/plain; charset=utf-8"} - -func (r String) Render(w http.ResponseWriter) error { - WriteString(w, r.Format, r.Data) - return nil -} - -func WriteString(w http.ResponseWriter, format string, data []interface{}) { - writeContentType(w, plainContentType) - - if len(data) > 0 { - fmt.Fprintf(w, format, data...) - } else { - io.WriteString(w, format) - } -} diff --git a/vendor/github.com/gin-gonic/gin/render/xml.go b/vendor/github.com/gin-gonic/gin/render/xml.go deleted file mode 100644 index be22e6f21..000000000 --- a/vendor/github.com/gin-gonic/gin/render/xml.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "encoding/xml" - "net/http" -) - -type XML struct { - Data interface{} -} - -var xmlContentType = []string{"application/xml; charset=utf-8"} - -func (r XML) Render(w http.ResponseWriter) error { - writeContentType(w, xmlContentType) - return xml.NewEncoder(w).Encode(r.Data) -} diff --git a/vendor/github.com/gin-gonic/gin/render/yaml.go b/vendor/github.com/gin-gonic/gin/render/yaml.go deleted file mode 100644 index 46937d888..000000000 --- a/vendor/github.com/gin-gonic/gin/render/yaml.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "net/http" - - "gopkg.in/yaml.v2" -) - -type YAML struct { - Data interface{} -} - -var yamlContentType = []string{"application/x-yaml; charset=utf-8"} - -func (r YAML) Render(w http.ResponseWriter) error { - writeContentType(w, yamlContentType) - - bytes, err := yaml.Marshal(r.Data) - if err != nil { - return err - } - - w.Write(bytes) - return nil -} diff --git a/vendor/github.com/gin-gonic/gin/response_writer.go b/vendor/github.com/gin-gonic/gin/response_writer.go deleted file mode 100644 index fcbe230d0..000000000 --- a/vendor/github.com/gin-gonic/gin/response_writer.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "bufio" - "io" - "net" - "net/http" -) - -const ( - noWritten = -1 - defaultStatus = 200 -) - -type ( - ResponseWriter interface { - http.ResponseWriter - http.Hijacker - http.Flusher - http.CloseNotifier - - // Returns the HTTP response status code of the current request. - Status() int - - // Returns the number of bytes already written into the response http body. - // See Written() - Size() int - - // Writes the string into the response body. - WriteString(string) (int, error) - - // Returns true if the response body was already written. - Written() bool - - // Forces to write the http header (status code + headers). - WriteHeaderNow() - } - - responseWriter struct { - http.ResponseWriter - size int - status int - } -) - -var _ ResponseWriter = &responseWriter{} - -func (w *responseWriter) reset(writer http.ResponseWriter) { - w.ResponseWriter = writer - w.size = noWritten - w.status = defaultStatus -} - -func (w *responseWriter) WriteHeader(code int) { - if code > 0 && w.status != code { - if w.Written() { - debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code) - } - w.status = code - } -} - -func (w *responseWriter) WriteHeaderNow() { - if !w.Written() { - w.size = 0 - w.ResponseWriter.WriteHeader(w.status) - } -} - -func (w *responseWriter) Write(data []byte) (n int, err error) { - w.WriteHeaderNow() - n, err = w.ResponseWriter.Write(data) - w.size += n - return -} - -func (w *responseWriter) WriteString(s string) (n int, err error) { - w.WriteHeaderNow() - n, err = io.WriteString(w.ResponseWriter, s) - w.size += n - return -} - -func (w *responseWriter) Status() int { - return w.status -} - -func (w *responseWriter) Size() int { - return w.size -} - -func (w *responseWriter) Written() bool { - return w.size != noWritten -} - -// Implements the http.Hijacker interface -func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - if w.size < 0 { - w.size = 0 - } - return w.ResponseWriter.(http.Hijacker).Hijack() -} - -// Implements the http.CloseNotify interface -func (w *responseWriter) CloseNotify() <-chan bool { - return w.ResponseWriter.(http.CloseNotifier).CloseNotify() -} - -// Implements the http.Flush interface -func (w *responseWriter) Flush() { - w.ResponseWriter.(http.Flusher).Flush() -} diff --git a/vendor/github.com/gin-gonic/gin/routergroup.go b/vendor/github.com/gin-gonic/gin/routergroup.go deleted file mode 100644 index f22729bbd..000000000 --- a/vendor/github.com/gin-gonic/gin/routergroup.go +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "net/http" - "path" - "regexp" - "strings" -) - -type ( - IRouter interface { - IRoutes - Group(string, ...HandlerFunc) *RouterGroup - } - - IRoutes interface { - Use(...HandlerFunc) IRoutes - - Handle(string, string, ...HandlerFunc) IRoutes - Any(string, ...HandlerFunc) IRoutes - GET(string, ...HandlerFunc) IRoutes - POST(string, ...HandlerFunc) IRoutes - DELETE(string, ...HandlerFunc) IRoutes - PATCH(string, ...HandlerFunc) IRoutes - PUT(string, ...HandlerFunc) IRoutes - OPTIONS(string, ...HandlerFunc) IRoutes - HEAD(string, ...HandlerFunc) IRoutes - - StaticFile(string, string) IRoutes - Static(string, string) IRoutes - StaticFS(string, http.FileSystem) IRoutes - } - - // RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix - // and an array of handlers (middleware) - RouterGroup struct { - Handlers HandlersChain - basePath string - engine *Engine - root bool - } -) - -var _ IRouter = &RouterGroup{} - -// Use adds middleware to the group, see example code in github. -func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { - group.Handlers = append(group.Handlers, middleware...) - return group.returnObj() -} - -// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix. -// For example, all the routes that use a common middlware for authorization could be grouped. -func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { - return &RouterGroup{ - Handlers: group.combineHandlers(handlers), - basePath: group.calculateAbsolutePath(relativePath), - engine: group.engine, - } -} - -func (group *RouterGroup) BasePath() string { - return group.basePath -} - -func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { - absolutePath := group.calculateAbsolutePath(relativePath) - handlers = group.combineHandlers(handlers) - group.engine.addRoute(httpMethod, absolutePath, handlers) - return group.returnObj() -} - -// Handle registers a new request handle and middleware with the given path and method. -// The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes. -// See the example code in github. -// -// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut -// functions can be used. -// -// This function is intended for bulk loading and to allow the usage of less -// frequently used, non-standardized or custom methods (e.g. for internal -// communication with a proxy). -func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { - if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { - panic("http method " + httpMethod + " is not valid") - } - return group.handle(httpMethod, relativePath, handlers) -} - -// POST is a shortcut for router.Handle("POST", path, handle) -func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("POST", relativePath, handlers) -} - -// GET is a shortcut for router.Handle("GET", path, handle) -func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("GET", relativePath, handlers) -} - -// DELETE is a shortcut for router.Handle("DELETE", path, handle) -func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("DELETE", relativePath, handlers) -} - -// PATCH is a shortcut for router.Handle("PATCH", path, handle) -func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("PATCH", relativePath, handlers) -} - -// PUT is a shortcut for router.Handle("PUT", path, handle) -func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("PUT", relativePath, handlers) -} - -// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) -func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("OPTIONS", relativePath, handlers) -} - -// HEAD is a shortcut for router.Handle("HEAD", path, handle) -func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("HEAD", relativePath, handlers) -} - -// Any registers a route that matches all the HTTP methods. -// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE -func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes { - group.handle("GET", relativePath, handlers) - group.handle("POST", relativePath, handlers) - group.handle("PUT", relativePath, handlers) - group.handle("PATCH", relativePath, handlers) - group.handle("HEAD", relativePath, handlers) - group.handle("OPTIONS", relativePath, handlers) - group.handle("DELETE", relativePath, handlers) - group.handle("CONNECT", relativePath, handlers) - group.handle("TRACE", relativePath, handlers) - return group.returnObj() -} - -// StaticFile registers a single route in order to server a single file of the local filesystem. -// router.StaticFile("favicon.ico", "./resources/favicon.ico") -func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { - if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { - panic("URL parameters can not be used when serving a static file") - } - handler := func(c *Context) { - c.File(filepath) - } - group.GET(relativePath, handler) - group.HEAD(relativePath, handler) - return group.returnObj() -} - -// Static serves files from the given file system root. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use : -// router.Static("/static", "/var/www") -func (group *RouterGroup) Static(relativePath, root string) IRoutes { - return group.StaticFS(relativePath, Dir(root, false)) -} - -// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead. -// Gin by default user: gin.Dir() -func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes { - if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { - panic("URL parameters can not be used when serving a static folder") - } - handler := group.createStaticHandler(relativePath, fs) - urlPattern := path.Join(relativePath, "/*filepath") - - // Register GET and HEAD handlers - group.GET(urlPattern, handler) - group.HEAD(urlPattern, handler) - return group.returnObj() -} - -func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc { - absolutePath := group.calculateAbsolutePath(relativePath) - fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) - _, nolisting := fs.(*onlyfilesFS) - return func(c *Context) { - if nolisting { - c.Writer.WriteHeader(404) - } - fileServer.ServeHTTP(c.Writer, c.Request) - } -} - -func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { - finalSize := len(group.Handlers) + len(handlers) - if finalSize >= int(abortIndex) { - panic("too many handlers") - } - mergedHandlers := make(HandlersChain, finalSize) - copy(mergedHandlers, group.Handlers) - copy(mergedHandlers[len(group.Handlers):], handlers) - return mergedHandlers -} - -func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { - return joinPaths(group.basePath, relativePath) -} - -func (group *RouterGroup) returnObj() IRoutes { - if group.root { - return group.engine - } - return group -} diff --git a/vendor/github.com/gin-gonic/gin/tree.go b/vendor/github.com/gin-gonic/gin/tree.go deleted file mode 100644 index 4f1da271f..000000000 --- a/vendor/github.com/gin-gonic/gin/tree.go +++ /dev/null @@ -1,605 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package gin - -import ( - "strings" - "unicode" -) - -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// Get returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) Get(name string) (string, bool) { - for _, entry := range ps { - if entry.Key == name { - return entry.Value, true - } - } - return "", false -} - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) (va string) { - va, _ = ps.Get(name) - return -} - -type methodTree struct { - method string - root *node -} - -type methodTrees []methodTree - -func (trees methodTrees) get(method string) *node { - for _, tree := range trees { - if tree.method == method { - return tree.root - } - } - return nil -} - -func min(a, b int) int { - if a <= b { - return a - } - return b -} - -func countParams(path string) uint8 { - var n uint - for i := 0; i < len(path); i++ { - if path[i] != ':' && path[i] != '*' { - continue - } - n++ - } - if n >= 255 { - return 255 - } - return uint8(n) -} - -type nodeType uint8 - -const ( - static nodeType = iota // default - root - param - catchAll -) - -type node struct { - path string - wildChild bool - nType nodeType - maxParams uint8 - indices string - children []*node - handlers HandlersChain - priority uint32 -} - -// increments priority of the given child and reorders if necessary -func (n *node) incrementChildPrio(pos int) int { - n.children[pos].priority++ - prio := n.children[pos].priority - - // adjust position (move to front) - newPos := pos - for newPos > 0 && n.children[newPos-1].priority < prio { - // swap node positions - tmpN := n.children[newPos-1] - n.children[newPos-1] = n.children[newPos] - n.children[newPos] = tmpN - - newPos-- - } - - // build new index char string - if newPos != pos { - n.indices = n.indices[:newPos] + // unchanged prefix, might be empty - n.indices[pos:pos+1] + // the index char we move - n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' - } - - return newPos -} - -// addRoute adds a node with the given handle to the path. -// Not concurrency-safe! -func (n *node) addRoute(path string, handlers HandlersChain) { - fullPath := path - n.priority++ - numParams := countParams(path) - - // non-empty tree - if len(n.path) > 0 || len(n.children) > 0 { - walk: - for { - // Update maxParams of the current node - if numParams > n.maxParams { - n.maxParams = numParams - } - - // Find the longest common prefix. - // This also implies that the common prefix contains no ':' or '*' - // since the existing key can't contain those chars. - i := 0 - max := min(len(path), len(n.path)) - for i < max && path[i] == n.path[i] { - i++ - } - - // Split edge - if i < len(n.path) { - child := node{ - path: n.path[i:], - wildChild: n.wildChild, - indices: n.indices, - children: n.children, - handlers: n.handlers, - priority: n.priority - 1, - } - - // Update maxParams (max of all children) - for i := range child.children { - if child.children[i].maxParams > child.maxParams { - child.maxParams = child.children[i].maxParams - } - } - - n.children = []*node{&child} - // []byte for proper unicode char conversion, see #65 - n.indices = string([]byte{n.path[i]}) - n.path = path[:i] - n.handlers = nil - n.wildChild = false - } - - // Make new node a child of this node - if i < len(path) { - path = path[i:] - - if n.wildChild { - n = n.children[0] - n.priority++ - - // Update maxParams of the child node - if numParams > n.maxParams { - n.maxParams = numParams - } - numParams-- - - // Check if the wildcard matches - if len(path) >= len(n.path) && n.path == path[:len(n.path)] { - // check for longer wildcard, e.g. :name and :names - if len(n.path) >= len(path) || path[len(n.path)] == '/' { - continue walk - } - } - - panic("path segment '" + path + - "' conflicts with existing wildcard '" + n.path + - "' in path '" + fullPath + "'") - } - - c := path[0] - - // slash after param - if n.nType == param && c == '/' && len(n.children) == 1 { - n = n.children[0] - n.priority++ - continue walk - } - - // Check if a child with the next path byte exists - for i := 0; i < len(n.indices); i++ { - if c == n.indices[i] { - i = n.incrementChildPrio(i) - n = n.children[i] - continue walk - } - } - - // Otherwise insert it - if c != ':' && c != '*' { - // []byte for proper unicode char conversion, see #65 - n.indices += string([]byte{c}) - child := &node{ - maxParams: numParams, - } - n.children = append(n.children, child) - n.incrementChildPrio(len(n.indices) - 1) - n = child - } - n.insertChild(numParams, path, fullPath, handlers) - return - - } else if i == len(path) { // Make node a (in-path) leaf - if n.handlers != nil { - panic("handlers are already registered for path ''" + fullPath + "'") - } - n.handlers = handlers - } - return - } - } else { // Empty tree - n.insertChild(numParams, path, fullPath, handlers) - n.nType = root - } -} - -func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { - var offset int // already handled bytes of the path - - // find prefix until first wildcard (beginning with ':'' or '*'') - for i, max := 0, len(path); numParams > 0; i++ { - c := path[i] - if c != ':' && c != '*' { - continue - } - - // find wildcard end (either '/' or path end) - end := i + 1 - for end < max && path[end] != '/' { - switch path[end] { - // the wildcard name must not contain ':' and '*' - case ':', '*': - panic("only one wildcard per path segment is allowed, has: '" + - path[i:] + "' in path '" + fullPath + "'") - default: - end++ - } - } - - // check if this Node existing children which would be - // unreachable if we insert the wildcard here - if len(n.children) > 0 { - panic("wildcard route '" + path[i:end] + - "' conflicts with existing children in path '" + fullPath + "'") - } - - // check if the wildcard has a name - if end-i < 2 { - panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") - } - - if c == ':' { // param - // split path at the beginning of the wildcard - if i > 0 { - n.path = path[offset:i] - offset = i - } - - child := &node{ - nType: param, - maxParams: numParams, - } - n.children = []*node{child} - n.wildChild = true - n = child - n.priority++ - numParams-- - - // if the path doesn't end with the wildcard, then there - // will be another non-wildcard subpath starting with '/' - if end < max { - n.path = path[offset:end] - offset = end - - child := &node{ - maxParams: numParams, - priority: 1, - } - n.children = []*node{child} - n = child - } - - } else { // catchAll - if end != max || numParams > 1 { - panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") - } - - if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") - } - - // currently fixed width 1 for '/' - i-- - if path[i] != '/' { - panic("no / before catch-all in path '" + fullPath + "'") - } - - n.path = path[offset:i] - - // first node: catchAll node with empty path - child := &node{ - wildChild: true, - nType: catchAll, - maxParams: 1, - } - n.children = []*node{child} - n.indices = string(path[i]) - n = child - n.priority++ - - // second node: node holding the variable - child = &node{ - path: path[i:], - nType: catchAll, - maxParams: 1, - handlers: handlers, - priority: 1, - } - n.children = []*node{child} - - return - } - } - - // insert remaining path part and handle to the leaf - n.path = path[offset:] - n.handlers = handlers -} - -// Returns the handle registered with the given path (key). The values of -// wildcards are saved to a map. -// If no handle can be found, a TSR (trailing slash redirect) recommendation is -// made if a handle exists with an extra (without the) trailing slash for the -// given path. -func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) { - p = po -walk: // Outer loop for walking the tree - for { - if len(path) > len(n.path) { - if path[:len(n.path)] == n.path { - path = path[len(n.path):] - // If this node does not have a wildcard (param or catchAll) - // child, we can just look up the next child node and continue - // to walk down the tree - if !n.wildChild { - c := path[0] - for i := 0; i < len(n.indices); i++ { - if c == n.indices[i] { - n = n.children[i] - continue walk - } - } - - // Nothing found. - // We can recommend to redirect to the same URL without a - // trailing slash if a leaf exists for that path. - tsr = (path == "/" && n.handlers != nil) - return - } - - // handle wildcard child - n = n.children[0] - switch n.nType { - case param: - // find param end (either '/' or path end) - end := 0 - for end < len(path) && path[end] != '/' { - end++ - } - - // save param value - if cap(p) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) - } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[1:] - p[i].Value = path[:end] - - // we need to go deeper! - if end < len(path) { - if len(n.children) > 0 { - path = path[end:] - n = n.children[0] - continue walk - } - - // ... but we can't - tsr = (len(path) == end+1) - return - } - - if handlers = n.handlers; handlers != nil { - return - } else if len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists for TSR recommendation - n = n.children[0] - tsr = (n.path == "/" && n.handlers != nil) - } - - return - - case catchAll: - // save param value - if cap(p) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) - } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[2:] - p[i].Value = path - - handlers = n.handlers - return - - default: - panic("invalid node type") - } - } - } else if path == n.path { - // We should have reached the node containing the handle. - // Check if this node has a handle registered. - if handlers = n.handlers; handlers != nil { - return - } - - if path == "/" && n.wildChild && n.nType != root { - tsr = true - return - } - - // No handle found. Check if a handle for this path + a - // trailing slash exists for trailing slash recommendation - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == '/' { - n = n.children[i] - tsr = (len(n.path) == 1 && n.handlers != nil) || - (n.nType == catchAll && n.children[0].handlers != nil) - return - } - } - - return - } - - // Nothing found. We can recommend to redirect to the same URL with an - // extra trailing slash if a leaf exists for that path - tsr = (path == "/") || - (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && - path == n.path[:len(n.path)-1] && n.handlers != nil) - return - } -} - -// Makes a case-insensitive lookup of the given path and tries to find a handler. -// It can optionally also fix trailing slashes. -// It returns the case-corrected path and a bool indicating whether the lookup -// was successful. -func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { - ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory - - // Outer loop for walking the tree - for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { - path = path[len(n.path):] - ciPath = append(ciPath, n.path...) - - if len(path) > 0 { - // If this node does not have a wildcard (param or catchAll) child, - // we can just look up the next child node and continue to walk down - // the tree - if !n.wildChild { - r := unicode.ToLower(rune(path[0])) - for i, index := range n.indices { - // must use recursive approach since both index and - // ToLower(index) could exist. We must check both. - if r == unicode.ToLower(index) { - out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) - if found { - return append(ciPath, out...), true - } - } - } - - // Nothing found. We can recommend to redirect to the same URL - // without a trailing slash if a leaf exists for that path - found = (fixTrailingSlash && path == "/" && n.handlers != nil) - return - } - - n = n.children[0] - switch n.nType { - case param: - // find param end (either '/' or path end) - k := 0 - for k < len(path) && path[k] != '/' { - k++ - } - - // add param value to case insensitive path - ciPath = append(ciPath, path[:k]...) - - // we need to go deeper! - if k < len(path) { - if len(n.children) > 0 { - path = path[k:] - n = n.children[0] - continue - } - - // ... but we can't - if fixTrailingSlash && len(path) == k+1 { - return ciPath, true - } - return - } - - if n.handlers != nil { - return ciPath, true - } else if fixTrailingSlash && len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists - n = n.children[0] - if n.path == "/" && n.handlers != nil { - return append(ciPath, '/'), true - } - } - return - - case catchAll: - return append(ciPath, path...), true - - default: - panic("invalid node type") - } - } else { - // We should have reached the node containing the handle. - // Check if this node has a handle registered. - if n.handlers != nil { - return ciPath, true - } - - // No handle found. - // Try to fix the path by adding a trailing slash - if fixTrailingSlash { - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == '/' { - n = n.children[i] - if (len(n.path) == 1 && n.handlers != nil) || - (n.nType == catchAll && n.children[0].handlers != nil) { - return append(ciPath, '/'), true - } - return - } - } - } - return - } - } - - // Nothing found. - // Try to fix the path by adding / removing a trailing slash - if fixTrailingSlash { - if path == "/" { - return ciPath, true - } - if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && - strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && - n.handlers != nil { - return append(ciPath, n.path...), true - } - } - return -} diff --git a/vendor/github.com/gin-gonic/gin/utils.go b/vendor/github.com/gin-gonic/gin/utils.go deleted file mode 100644 index 18064fb53..000000000 --- a/vendor/github.com/gin-gonic/gin/utils.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "encoding/xml" - "net/http" - "os" - "path" - "reflect" - "runtime" - "strings" -) - -const BindKey = "_gin-gonic/gin/bindkey" - -func Bind(val interface{}) HandlerFunc { - value := reflect.ValueOf(val) - if value.Kind() == reflect.Ptr { - panic(`Bind struct can not be a pointer. Example: - Use: gin.Bind(Struct{}) instead of gin.Bind(&Struct{}) -`) - } - typ := value.Type() - - return func(c *Context) { - obj := reflect.New(typ).Interface() - if c.Bind(obj) == nil { - c.Set(BindKey, obj) - } - } -} - -func WrapF(f http.HandlerFunc) HandlerFunc { - return func(c *Context) { - f(c.Writer, c.Request) - } -} - -func WrapH(h http.Handler) HandlerFunc { - return func(c *Context) { - h.ServeHTTP(c.Writer, c.Request) - } -} - -type H map[string]interface{} - -// MarshalXML allows type H to be used with xml.Marshal -func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - start.Name = xml.Name{ - Space: "", - Local: "map", - } - if err := e.EncodeToken(start); err != nil { - return err - } - for key, value := range h { - elem := xml.StartElement{ - Name: xml.Name{Space: "", Local: key}, - Attr: []xml.Attr{}, - } - if err := e.EncodeElement(value, elem); err != nil { - return err - } - } - if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { - return err - } - return nil -} - -func assert1(guard bool, text string) { - if !guard { - panic(text) - } -} - -func filterFlags(content string) string { - for i, char := range content { - if char == ' ' || char == ';' { - return content[:i] - } - } - return content -} - -func chooseData(custom, wildcard interface{}) interface{} { - if custom == nil { - if wildcard == nil { - panic("negotiation config is invalid") - } - return wildcard - } - return custom -} - -func parseAccept(acceptHeader string) []string { - parts := strings.Split(acceptHeader, ",") - out := make([]string, 0, len(parts)) - for _, part := range parts { - index := strings.IndexByte(part, ';') - if index >= 0 { - part = part[0:index] - } - part = strings.TrimSpace(part) - if len(part) > 0 { - out = append(out, part) - } - } - return out -} - -func lastChar(str string) uint8 { - size := len(str) - if size == 0 { - panic("The length of the string can't be 0") - } - return str[size-1] -} - -func nameOfFunction(f interface{}) string { - return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() -} - -func joinPaths(absolutePath, relativePath string) string { - if len(relativePath) == 0 { - return absolutePath - } - - finalPath := path.Join(absolutePath, relativePath) - appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/' - if appendSlash { - return finalPath + "/" - } - return finalPath -} - -func resolveAddress(addr []string) string { - switch len(addr) { - case 0: - if port := os.Getenv("PORT"); len(port) > 0 { - debugPrint("Environment variable PORT=\"%s\"", port) - return ":" + port - } - debugPrint("Environment variable PORT is undefined. Using port :8080 by default") - return ":8080" - case 1: - return addr[0] - default: - panic("too much parameters") - } -} diff --git a/vendor/github.com/gin-gonic/gin/wercker.yml b/vendor/github.com/gin-gonic/gin/wercker.yml deleted file mode 100644 index 3ab8084cc..000000000 --- a/vendor/github.com/gin-gonic/gin/wercker.yml +++ /dev/null @@ -1 +0,0 @@ -box: wercker/default \ No newline at end of file diff --git a/vendor/github.com/golang/protobuf/AUTHORS b/vendor/github.com/golang/protobuf/AUTHORS new file mode 100644 index 000000000..15167cd74 --- /dev/null +++ b/vendor/github.com/golang/protobuf/AUTHORS @@ -0,0 +1,3 @@ +# This source code refers to The Go Authors for copyright purposes. +# The master list of authors is in the main Go distribution, +# visible at http://tip.golang.org/AUTHORS. diff --git a/vendor/github.com/golang/protobuf/CONTRIBUTORS b/vendor/github.com/golang/protobuf/CONTRIBUTORS new file mode 100644 index 000000000..1c4577e96 --- /dev/null +++ b/vendor/github.com/golang/protobuf/CONTRIBUTORS @@ -0,0 +1,3 @@ +# This source code was written by the Go contributors. +# The master list of contributors is in the main Go distribution, +# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/vendor/github.com/labstack/echo/.editorconfig b/vendor/github.com/labstack/echo/.editorconfig new file mode 100644 index 000000000..17ae50dd0 --- /dev/null +++ b/vendor/github.com/labstack/echo/.editorconfig @@ -0,0 +1,25 @@ +# EditorConfig coding styles definitions. For more information about the +# properties used in this file, please see the EditorConfig documentation: +# http://editorconfig.org/ + +# indicate this is the root of the project +root = true + +[*] +charset = utf-8 + +end_of_line = LF +insert_final_newline = true +trim_trailing_whitespace = true + +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false + +[*.go] +indent_style = tab diff --git a/vendor/github.com/labstack/echo/.gitattributes b/vendor/github.com/labstack/echo/.gitattributes new file mode 100644 index 000000000..49b63e526 --- /dev/null +++ b/vendor/github.com/labstack/echo/.gitattributes @@ -0,0 +1,20 @@ +# Automatically normalize line endings for all text-based files +# http://git-scm.com/docs/gitattributes#_end_of_line_conversion +* text=auto + +# For the following file types, normalize line endings to LF on checking and +# prevent conversion to CRLF when they are checked out (this is required in +# order to prevent newline related issues) +.* text eol=lf +*.go text eol=lf +*.yml text eol=lf +*.html text eol=lf +*.css text eol=lf +*.js text eol=lf +*.json text eol=lf +LICENSE text eol=lf + +# Exclude `website` and `cookbook` from GitHub's language statistics +# https://github.com/github/linguist#using-gitattributes +cookbook/* linguist-documentation +website/* linguist-documentation diff --git a/vendor/github.com/labstack/echo/.gitignore b/vendor/github.com/labstack/echo/.gitignore new file mode 100644 index 000000000..36331f0cd --- /dev/null +++ b/vendor/github.com/labstack/echo/.gitignore @@ -0,0 +1,8 @@ +# Website +website/public + +# Glide +vendor + +.DS_Store +_test diff --git a/vendor/github.com/labstack/echo/.travis.yml b/vendor/github.com/labstack/echo/.travis.yml new file mode 100644 index 000000000..689ab3565 --- /dev/null +++ b/vendor/github.com/labstack/echo/.travis.yml @@ -0,0 +1,19 @@ +language: go +go: + - 1.7 + - 1.8 + - tip +install: + - go get golang.org/x/tools/cmd/cover + - go get github.com/Masterminds/glide + - go get github.com/mattn/goveralls + - go get github.com/modocache/gover + - glide install +script: + - go test -coverprofile=echo.coverprofile + - go test -coverprofile=middleware.coverprofile ./middleware + - gover + - goveralls -coverprofile=gover.coverprofile -service=travis-ci +matrix: + allow_failures: + - go: tip diff --git a/vendor/github.com/gin-gonic/gin/LICENSE b/vendor/github.com/labstack/echo/LICENSE similarity index 87% rename from vendor/github.com/gin-gonic/gin/LICENSE rename to vendor/github.com/labstack/echo/LICENSE index 1ff7f3706..b5b006b4e 100644 --- a/vendor/github.com/gin-gonic/gin/LICENSE +++ b/vendor/github.com/labstack/echo/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Manuel Martínez-Almeida +Copyright (c) 2017 LabStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/labstack/echo/README.md b/vendor/github.com/labstack/echo/README.md new file mode 100644 index 000000000..3c0fd6372 --- /dev/null +++ b/vendor/github.com/labstack/echo/README.md @@ -0,0 +1,54 @@ +# [Echo] (https://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo) [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack) + +## Feature Overview + +- Optimized HTTP router which smartly prioritize routes +- Build robust and scalable RESTful APIs +- Group APIs +- Extensible middleware framework +- Define middleware at root, group or route level +- Data binding for JSON, XML and form payload +- Handy functions to send variety of HTTP responses +- Centralized HTTP error handling +- Template rendering with any template engine +- Define your format for the logger +- Highly customizable +- Automatic TLS via Let’s Encrypt +- HTTP/2 support + +## Performance + +![Performance](https://i.imgur.com/F2V7TfO.png) + +## [Get Started](https://echo.labstack.com/guide) + +## Support Us + +- :star: the project +- [Donate](https://echo.labstack.com/support-echo) +- :earth_americas: spread the word +- [Contribute](#contribute) to the project + +## Contribute + +**Use issues for everything** + +- For a small change, just send a PR. +- For bigger changes open an issue for discussion before sending a PR. +- PR should have: + - Test case + - Documentation + - Example (If it makes sense) +- You can also contribute by: + - Reporting issues + - Suggesting new features or enhancements + - Improve/fix documentation + +## Credits +- [Vishal Rana](https://github.com/vishr) - Author +- [Nitin Rana](https://github.com/nr17) - Consultant +- [Contributors](https://github.com/labstack/echo/graphs/contributors) + +## License + +[MIT](https://github.com/labstack/echo/blob/master/LICENSE) diff --git a/vendor/github.com/labstack/echo/bind.go b/vendor/github.com/labstack/echo/bind.go new file mode 100644 index 000000000..274378116 --- /dev/null +++ b/vendor/github.com/labstack/echo/bind.go @@ -0,0 +1,259 @@ +package echo + +import ( + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "net/http" + "reflect" + "strconv" + "strings" +) + +type ( + // Binder is the interface that wraps the Bind method. + Binder interface { + Bind(i interface{}, c Context) error + } + + // DefaultBinder is the default implementation of the Binder interface. + DefaultBinder struct{} + + // BindUnmarshaler is the interface used to wrap the UnmarshalParam method. + BindUnmarshaler interface { + // UnmarshalParam decodes and assigns a value from an form or query param. + UnmarshalParam(param string) error + } +) + +// Bind implements the `Binder#Bind` function. +func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) { + req := c.Request() + if req.ContentLength == 0 { + if req.Method == GET { + if err = b.bindData(i, c.QueryParams(), "query"); err != nil { + return NewHTTPError(http.StatusBadRequest, err.Error()) + } + return + } + return NewHTTPError(http.StatusBadRequest, "Request body can't be empty") + } + ctype := req.Header.Get(HeaderContentType) + switch { + case strings.HasPrefix(ctype, MIMEApplicationJSON): + if err = json.NewDecoder(req.Body).Decode(i); err != nil { + if ute, ok := err.(*json.UnmarshalTypeError); ok { + return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, offset=%v", ute.Type, ute.Value, ute.Offset)) + } else if se, ok := err.(*json.SyntaxError); ok { + return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())) + } else { + return NewHTTPError(http.StatusBadRequest, err.Error()) + } + } + case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML): + if err = xml.NewDecoder(req.Body).Decode(i); err != nil { + if ute, ok := err.(*xml.UnsupportedTypeError); ok { + return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())) + } else if se, ok := err.(*xml.SyntaxError); ok { + return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: line=%v, error=%v", se.Line, se.Error())) + } else { + return NewHTTPError(http.StatusBadRequest, err.Error()) + } + } + case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm): + params, err := c.FormParams() + if err != nil { + return NewHTTPError(http.StatusBadRequest, err.Error()) + } + if err = b.bindData(i, params, "form"); err != nil { + return NewHTTPError(http.StatusBadRequest, err.Error()) + } + default: + return ErrUnsupportedMediaType + } + return +} + +func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error { + typ := reflect.TypeOf(ptr).Elem() + val := reflect.ValueOf(ptr).Elem() + + if typ.Kind() != reflect.Struct { + return errors.New("Binding element must be a struct") + } + + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := val.Field(i) + if !structField.CanSet() { + continue + } + structFieldKind := structField.Kind() + inputFieldName := typeField.Tag.Get(tag) + + if inputFieldName == "" { + inputFieldName = typeField.Name + // If tag is nil, we inspect if the field is a struct. + if _, ok := bindUnmarshaler(structField); !ok && structFieldKind == reflect.Struct { + err := b.bindData(structField.Addr().Interface(), data, tag) + if err != nil { + return err + } + continue + } + } + inputValue, exists := data[inputFieldName] + if !exists { + continue + } + + // Call this first, in case we're dealing with an alias to an array type + if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok { + if err != nil { + return err + } + continue + } + + numElems := len(inputValue) + if structFieldKind == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for j := 0; j < numElems; j++ { + if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil { + return err + } + } + val.Field(i).Set(slice) + } else { + if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { + return err + } + } + } + return nil +} + +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { + // But also call it here, in case we're dealing with an array of BindUnmarshalers + if ok, err := unmarshalField(valueKind, val, structField); ok { + return err + } + + switch valueKind { + case reflect.Int: + return setIntField(val, 0, structField) + case reflect.Int8: + return setIntField(val, 8, structField) + case reflect.Int16: + return setIntField(val, 16, structField) + case reflect.Int32: + return setIntField(val, 32, structField) + case reflect.Int64: + return setIntField(val, 64, structField) + case reflect.Uint: + return setUintField(val, 0, structField) + case reflect.Uint8: + return setUintField(val, 8, structField) + case reflect.Uint16: + return setUintField(val, 16, structField) + case reflect.Uint32: + return setUintField(val, 32, structField) + case reflect.Uint64: + return setUintField(val, 64, structField) + case reflect.Bool: + return setBoolField(val, structField) + case reflect.Float32: + return setFloatField(val, 32, structField) + case reflect.Float64: + return setFloatField(val, 64, structField) + case reflect.String: + structField.SetString(val) + default: + return errors.New("unknown type") + } + return nil +} + +func unmarshalField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) { + switch valueKind { + case reflect.Ptr: + return unmarshalFieldPtr(val, field) + default: + return unmarshalFieldNonPtr(val, field) + } +} + +// bindUnmarshaler attempts to unmarshal a reflect.Value into a BindUnmarshaler +func bindUnmarshaler(field reflect.Value) (BindUnmarshaler, bool) { + ptr := reflect.New(field.Type()) + if ptr.CanInterface() { + iface := ptr.Interface() + if unmarshaler, ok := iface.(BindUnmarshaler); ok { + return unmarshaler, ok + } + } + return nil, false +} + +func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) { + if unmarshaler, ok := bindUnmarshaler(field); ok { + err := unmarshaler.UnmarshalParam(value) + field.Set(reflect.ValueOf(unmarshaler).Elem()) + return true, err + } + return false, nil +} + +func unmarshalFieldPtr(value string, field reflect.Value) (bool, error) { + if field.IsNil() { + // Initialize the pointer to a nil value + field.Set(reflect.New(field.Type().Elem())) + } + return unmarshalFieldNonPtr(value, field.Elem()) +} + +func setIntField(value string, bitSize int, field reflect.Value) error { + if value == "" { + value = "0" + } + intVal, err := strconv.ParseInt(value, 10, bitSize) + if err == nil { + field.SetInt(intVal) + } + return err +} + +func setUintField(value string, bitSize int, field reflect.Value) error { + if value == "" { + value = "0" + } + uintVal, err := strconv.ParseUint(value, 10, bitSize) + if err == nil { + field.SetUint(uintVal) + } + return err +} + +func setBoolField(value string, field reflect.Value) error { + if value == "" { + value = "false" + } + boolVal, err := strconv.ParseBool(value) + if err == nil { + field.SetBool(boolVal) + } + return err +} + +func setFloatField(value string, bitSize int, field reflect.Value) error { + if value == "" { + value = "0.0" + } + floatVal, err := strconv.ParseFloat(value, bitSize) + if err == nil { + field.SetFloat(floatVal) + } + return err +} diff --git a/vendor/github.com/labstack/echo/context.go b/vendor/github.com/labstack/echo/context.go new file mode 100644 index 000000000..c1442010f --- /dev/null +++ b/vendor/github.com/labstack/echo/context.go @@ -0,0 +1,556 @@ +package echo + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "mime/multipart" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +type ( + // Context represents the context of the current HTTP request. It holds request and + // response objects, path, path parameters, data and registered handler. + Context interface { + // Request returns `*http.Request`. + Request() *http.Request + + // SetRequest sets `*http.Request`. + SetRequest(r *http.Request) + + // Response returns `*Response`. + Response() *Response + + // IsTLS returns true if HTTP connection is TLS otherwise false. + IsTLS() bool + + // Scheme returns the HTTP protocol scheme, `http` or `https`. + Scheme() string + + // RealIP returns the client's network address based on `X-Forwarded-For` + // or `X-Real-IP` request header. + RealIP() string + + // Path returns the registered path for the handler. + Path() string + + // SetPath sets the registered path for the handler. + SetPath(p string) + + // Param returns path parameter by name. + Param(name string) string + + // ParamNames returns path parameter names. + ParamNames() []string + + // SetParamNames sets path parameter names. + SetParamNames(names ...string) + + // ParamValues returns path parameter values. + ParamValues() []string + + // SetParamValues sets path parameter values. + SetParamValues(values ...string) + + // QueryParam returns the query param for the provided name. + QueryParam(name string) string + + // QueryParams returns the query parameters as `url.Values`. + QueryParams() url.Values + + // QueryString returns the URL query string. + QueryString() string + + // FormValue returns the form field value for the provided name. + FormValue(name string) string + + // FormParams returns the form parameters as `url.Values`. + FormParams() (url.Values, error) + + // FormFile returns the multipart form file for the provided name. + FormFile(name string) (*multipart.FileHeader, error) + + // MultipartForm returns the multipart form. + MultipartForm() (*multipart.Form, error) + + // Cookie returns the named cookie provided in the request. + Cookie(name string) (*http.Cookie, error) + + // SetCookie adds a `Set-Cookie` header in HTTP response. + SetCookie(cookie *http.Cookie) + + // Cookies returns the HTTP cookies sent with the request. + Cookies() []*http.Cookie + + // Get retrieves data from the context. + Get(key string) interface{} + + // Set saves data in the context. + Set(key string, val interface{}) + + // Bind binds the request body into provided type `i`. The default binder + // does it based on Content-Type header. + Bind(i interface{}) error + + // Validate validates provided `i`. It is usually called after `Context#Bind()`. + // Validator must be registered using `Echo#Validator`. + Validate(i interface{}) error + + // Render renders a template with data and sends a text/html response with status + // code. Renderer must be registered using `Echo.Renderer`. + Render(code int, name string, data interface{}) error + + // HTML sends an HTTP response with status code. + HTML(code int, html string) error + + // HTMLBlob sends an HTTP blob response with status code. + HTMLBlob(code int, b []byte) error + + // String sends a string response with status code. + String(code int, s string) error + + // JSON sends a JSON response with status code. + JSON(code int, i interface{}) error + + // JSONPretty sends a pretty-print JSON with status code. + JSONPretty(code int, i interface{}, indent string) error + + // JSONBlob sends a JSON blob response with status code. + JSONBlob(code int, b []byte) error + + // JSONP sends a JSONP response with status code. It uses `callback` to construct + // the JSONP payload. + JSONP(code int, callback string, i interface{}) error + + // JSONPBlob sends a JSONP blob response with status code. It uses `callback` + // to construct the JSONP payload. + JSONPBlob(code int, callback string, b []byte) error + + // XML sends an XML response with status code. + XML(code int, i interface{}) error + + // XMLPretty sends a pretty-print XML with status code. + XMLPretty(code int, i interface{}, indent string) error + + // XMLBlob sends an XML blob response with status code. + XMLBlob(code int, b []byte) error + + // Blob sends a blob response with status code and content type. + Blob(code int, contentType string, b []byte) error + + // Stream sends a streaming response with status code and content type. + Stream(code int, contentType string, r io.Reader) error + + // File sends a response with the content of the file. + File(file string) error + + // Attachment sends a response as attachment, prompting client to save the + // file. + Attachment(file string, name string) error + + // Inline sends a response as inline, opening the file in the browser. + Inline(file string, name string) error + + // NoContent sends a response with no body and a status code. + NoContent(code int) error + + // Redirect redirects the request to a provided URL with status code. + Redirect(code int, url string) error + + // Error invokes the registered HTTP error handler. Generally used by middleware. + Error(err error) + + // Handler returns the matched handler by router. + Handler() HandlerFunc + + // SetHandler sets the matched handler by router. + SetHandler(h HandlerFunc) + + // Logger returns the `Logger` instance. + Logger() Logger + + // Echo returns the `Echo` instance. + Echo() *Echo + + // Reset resets the context after request completes. It must be called along + // with `Echo#AcquireContext()` and `Echo#ReleaseContext()`. + // See `Echo#ServeHTTP()` + Reset(r *http.Request, w http.ResponseWriter) + } + + context struct { + request *http.Request + response *Response + path string + pnames []string + pvalues []string + query url.Values + handler HandlerFunc + store Map + echo *Echo + } +) + +const ( + defaultMemory = 32 << 20 // 32 MB + indexPage = "index.html" +) + +func (c *context) Request() *http.Request { + return c.request +} + +func (c *context) SetRequest(r *http.Request) { + c.request = r +} + +func (c *context) Response() *Response { + return c.response +} + +func (c *context) IsTLS() bool { + return c.request.TLS != nil +} + +func (c *context) Scheme() string { + // Can't use `r.Request.URL.Scheme` + // See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0 + if c.IsTLS() { + return "https" + } + return "http" +} + +func (c *context) RealIP() string { + ra := c.request.RemoteAddr + if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" { + ra = ip + } else if ip := c.request.Header.Get(HeaderXRealIP); ip != "" { + ra = ip + } else { + ra, _, _ = net.SplitHostPort(ra) + } + return ra +} + +func (c *context) Path() string { + return c.path +} + +func (c *context) SetPath(p string) { + c.path = p +} + +func (c *context) Param(name string) string { + for i, n := range c.pnames { + if i < len(c.pvalues) { + if n == name { + return c.pvalues[i] + } + + // Param name with aliases + for _, p := range strings.Split(n, ",") { + if p == name { + return c.pvalues[i] + } + } + } + } + return "" +} + +func (c *context) ParamNames() []string { + return c.pnames +} + +func (c *context) SetParamNames(names ...string) { + c.pnames = names +} + +func (c *context) ParamValues() []string { + return c.pvalues +} + +func (c *context) SetParamValues(values ...string) { + c.pvalues = values +} + +func (c *context) QueryParam(name string) string { + if c.query == nil { + c.query = c.request.URL.Query() + } + return c.query.Get(name) +} + +func (c *context) QueryParams() url.Values { + if c.query == nil { + c.query = c.request.URL.Query() + } + return c.query +} + +func (c *context) QueryString() string { + return c.request.URL.RawQuery +} + +func (c *context) FormValue(name string) string { + return c.request.FormValue(name) +} + +func (c *context) FormParams() (url.Values, error) { + if strings.HasPrefix(c.request.Header.Get(HeaderContentType), MIMEMultipartForm) { + if err := c.request.ParseMultipartForm(defaultMemory); err != nil { + return nil, err + } + } else { + if err := c.request.ParseForm(); err != nil { + return nil, err + } + } + return c.request.Form, nil +} + +func (c *context) FormFile(name string) (*multipart.FileHeader, error) { + _, fh, err := c.request.FormFile(name) + return fh, err +} + +func (c *context) MultipartForm() (*multipart.Form, error) { + err := c.request.ParseMultipartForm(defaultMemory) + return c.request.MultipartForm, err +} + +func (c *context) Cookie(name string) (*http.Cookie, error) { + return c.request.Cookie(name) +} + +func (c *context) SetCookie(cookie *http.Cookie) { + http.SetCookie(c.Response(), cookie) +} + +func (c *context) Cookies() []*http.Cookie { + return c.request.Cookies() +} + +func (c *context) Get(key string) interface{} { + return c.store[key] +} + +func (c *context) Set(key string, val interface{}) { + if c.store == nil { + c.store = make(Map) + } + c.store[key] = val +} + +func (c *context) Bind(i interface{}) error { + return c.echo.Binder.Bind(i, c) +} + +func (c *context) Validate(i interface{}) error { + if c.echo.Validator == nil { + return ErrValidatorNotRegistered + } + return c.echo.Validator.Validate(i) +} + +func (c *context) Render(code int, name string, data interface{}) (err error) { + if c.echo.Renderer == nil { + return ErrRendererNotRegistered + } + buf := new(bytes.Buffer) + if err = c.echo.Renderer.Render(buf, name, data, c); err != nil { + return + } + return c.HTMLBlob(code, buf.Bytes()) +} + +func (c *context) HTML(code int, html string) (err error) { + return c.HTMLBlob(code, []byte(html)) +} + +func (c *context) HTMLBlob(code int, b []byte) (err error) { + return c.Blob(code, MIMETextHTMLCharsetUTF8, b) +} + +func (c *context) String(code int, s string) (err error) { + return c.Blob(code, MIMETextPlainCharsetUTF8, []byte(s)) +} + +func (c *context) JSON(code int, i interface{}) (err error) { + if c.echo.Debug { + return c.JSONPretty(code, i, " ") + } + b, err := json.Marshal(i) + if err != nil { + return + } + return c.JSONBlob(code, b) +} + +func (c *context) JSONPretty(code int, i interface{}, indent string) (err error) { + b, err := json.MarshalIndent(i, "", indent) + if err != nil { + return + } + return c.JSONBlob(code, b) +} + +func (c *context) JSONBlob(code int, b []byte) (err error) { + return c.Blob(code, MIMEApplicationJSONCharsetUTF8, b) +} + +func (c *context) JSONP(code int, callback string, i interface{}) (err error) { + b, err := json.Marshal(i) + if err != nil { + return + } + return c.JSONPBlob(code, callback, b) +} + +func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) { + c.response.Header().Set(HeaderContentType, MIMEApplicationJavaScriptCharsetUTF8) + c.response.WriteHeader(code) + if _, err = c.response.Write([]byte(callback + "(")); err != nil { + return + } + if _, err = c.response.Write(b); err != nil { + return + } + _, err = c.response.Write([]byte(");")) + return +} + +func (c *context) XML(code int, i interface{}) (err error) { + if c.echo.Debug { + return c.XMLPretty(code, i, " ") + } + b, err := xml.Marshal(i) + if err != nil { + return + } + return c.XMLBlob(code, b) +} + +func (c *context) XMLPretty(code int, i interface{}, indent string) (err error) { + b, err := xml.MarshalIndent(i, "", indent) + if err != nil { + return + } + return c.XMLBlob(code, b) +} + +func (c *context) XMLBlob(code int, b []byte) (err error) { + c.response.Header().Set(HeaderContentType, MIMEApplicationXMLCharsetUTF8) + c.response.WriteHeader(code) + if _, err = c.response.Write([]byte(xml.Header)); err != nil { + return + } + _, err = c.response.Write(b) + return +} + +func (c *context) Blob(code int, contentType string, b []byte) (err error) { + c.response.Header().Set(HeaderContentType, contentType) + c.response.WriteHeader(code) + _, err = c.response.Write(b) + return +} + +func (c *context) Stream(code int, contentType string, r io.Reader) (err error) { + c.response.Header().Set(HeaderContentType, contentType) + c.response.WriteHeader(code) + _, err = io.Copy(c.response, r) + return +} + +func (c *context) File(file string) (err error) { + file, err = url.QueryUnescape(file) // Issue #839 + if err != nil { + return + } + + f, err := os.Open(file) + if err != nil { + return ErrNotFound + } + defer f.Close() + + fi, _ := f.Stat() + if fi.IsDir() { + file = filepath.Join(file, indexPage) + f, err = os.Open(file) + if err != nil { + return ErrNotFound + } + defer f.Close() + if fi, err = f.Stat(); err != nil { + return + } + } + http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f) + return +} + +func (c *context) Attachment(file, name string) (err error) { + return c.contentDisposition(file, name, "attachment") +} + +func (c *context) Inline(file, name string) (err error) { + return c.contentDisposition(file, name, "inline") +} + +func (c *context) contentDisposition(file, name, dispositionType string) (err error) { + c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%s", dispositionType, name)) + c.File(file) + return +} + +func (c *context) NoContent(code int) error { + c.response.WriteHeader(code) + return nil +} + +func (c *context) Redirect(code int, url string) error { + if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect { + return ErrInvalidRedirectCode + } + c.response.Header().Set(HeaderLocation, url) + c.response.WriteHeader(code) + return nil +} + +func (c *context) Error(err error) { + c.echo.HTTPErrorHandler(err, c) +} + +func (c *context) Echo() *Echo { + return c.echo +} + +func (c *context) Handler() HandlerFunc { + return c.handler +} + +func (c *context) SetHandler(h HandlerFunc) { + c.handler = h +} + +func (c *context) Logger() Logger { + return c.echo.Logger +} + +func (c *context) Reset(r *http.Request, w http.ResponseWriter) { + c.request = r + c.response.reset(w) + c.query = nil + c.handler = NotFoundHandler + c.store = nil +} diff --git a/vendor/github.com/labstack/echo/echo.go b/vendor/github.com/labstack/echo/echo.go new file mode 100644 index 000000000..5b43fff0c --- /dev/null +++ b/vendor/github.com/labstack/echo/echo.go @@ -0,0 +1,678 @@ +/* +Package echo implements high performance, minimalist Go web framework. + +Example: + + package main + + import ( + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/middleware" + ) + + // Handler + func hello(c echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + } + + func main() { + // Echo instance + e := echo.New() + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Routes + e.GET("/", hello) + + // Start server + e.Logger.Fatal(e.Start(":1323")) + } + +Learn more at https://echo.labstack.com +*/ +package echo + +import ( + "bytes" + "crypto/tls" + "errors" + "fmt" + "io" + stdLog "log" + "net" + "net/http" + "path" + "path/filepath" + "reflect" + "runtime" + "sync" + "time" + + "github.com/labstack/gommon/color" + "github.com/labstack/gommon/log" + "golang.org/x/crypto/acme/autocert" +) + +type ( + // Echo is the top-level framework instance. + Echo struct { + stdLogger *stdLog.Logger + colorer *color.Color + premiddleware []MiddlewareFunc + middleware []MiddlewareFunc + maxParam *int + router *Router + notFoundHandler HandlerFunc + pool sync.Pool + Server *http.Server + TLSServer *http.Server + Listener net.Listener + TLSListener net.Listener + DisableHTTP2 bool + Debug bool + HTTPErrorHandler HTTPErrorHandler + Binder Binder + Validator Validator + Renderer Renderer + AutoTLSManager autocert.Manager + Mutex sync.RWMutex + Logger Logger + } + + // Route contains a handler and information for matching against requests. + Route struct { + Method string + Path string + Handler string + } + + // HTTPError represents an error that occurred while handling a request. + HTTPError struct { + Code int + Message interface{} + } + + // MiddlewareFunc defines a function to process middleware. + MiddlewareFunc func(HandlerFunc) HandlerFunc + + // HandlerFunc defines a function to server HTTP requests. + HandlerFunc func(Context) error + + // HTTPErrorHandler is a centralized HTTP error handler. + HTTPErrorHandler func(error, Context) + + // Validator is the interface that wraps the Validate function. + Validator interface { + Validate(i interface{}) error + } + + // Renderer is the interface that wraps the Render function. + Renderer interface { + Render(io.Writer, string, interface{}, Context) error + } + + // Map defines a generic map of type `map[string]interface{}`. + Map map[string]interface{} + + // i is the interface for Echo and Group. + i interface { + GET(string, HandlerFunc, ...MiddlewareFunc) + } +) + +// HTTP methods +const ( + CONNECT = "CONNECT" + DELETE = "DELETE" + GET = "GET" + HEAD = "HEAD" + OPTIONS = "OPTIONS" + PATCH = "PATCH" + POST = "POST" + PUT = "PUT" + TRACE = "TRACE" +) + +// MIME types +const ( + MIMEApplicationJSON = "application/json" + MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8 + MIMEApplicationJavaScript = "application/javascript" + MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8 + MIMEApplicationXML = "application/xml" + MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + charsetUTF8 + MIMETextXML = "text/xml" + MIMETextXMLCharsetUTF8 = MIMETextXML + "; " + charsetUTF8 + MIMEApplicationForm = "application/x-www-form-urlencoded" + MIMEApplicationProtobuf = "application/protobuf" + MIMEApplicationMsgpack = "application/msgpack" + MIMETextHTML = "text/html" + MIMETextHTMLCharsetUTF8 = MIMETextHTML + "; " + charsetUTF8 + MIMETextPlain = "text/plain" + MIMETextPlainCharsetUTF8 = MIMETextPlain + "; " + charsetUTF8 + MIMEMultipartForm = "multipart/form-data" + MIMEOctetStream = "application/octet-stream" +) + +const ( + charsetUTF8 = "charset=UTF-8" +) + +// Headers +const ( + HeaderAcceptEncoding = "Accept-Encoding" + HeaderAllow = "Allow" + HeaderAuthorization = "Authorization" + HeaderContentDisposition = "Content-Disposition" + HeaderContentEncoding = "Content-Encoding" + HeaderContentLength = "Content-Length" + HeaderContentType = "Content-Type" + HeaderCookie = "Cookie" + HeaderSetCookie = "Set-Cookie" + HeaderIfModifiedSince = "If-Modified-Since" + HeaderLastModified = "Last-Modified" + HeaderLocation = "Location" + HeaderUpgrade = "Upgrade" + HeaderVary = "Vary" + HeaderWWWAuthenticate = "WWW-Authenticate" + HeaderXForwardedProto = "X-Forwarded-Proto" + HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" + HeaderXForwardedFor = "X-Forwarded-For" + HeaderXRealIP = "X-Real-IP" + HeaderXRequestID = "X-Request-ID" + HeaderServer = "Server" + HeaderOrigin = "Origin" + HeaderAccessControlRequestMethod = "Access-Control-Request-Method" + HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" + HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" + HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderAccessControlMaxAge = "Access-Control-Max-Age" + + // Security + HeaderStrictTransportSecurity = "Strict-Transport-Security" + HeaderXContentTypeOptions = "X-Content-Type-Options" + HeaderXXSSProtection = "X-XSS-Protection" + HeaderXFrameOptions = "X-Frame-Options" + HeaderContentSecurityPolicy = "Content-Security-Policy" + HeaderXCSRFToken = "X-CSRF-Token" +) + +var ( + methods = [...]string{ + CONNECT, + DELETE, + GET, + HEAD, + OPTIONS, + PATCH, + POST, + PUT, + TRACE, + } +) + +// Errors +var ( + ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType) + ErrNotFound = NewHTTPError(http.StatusNotFound) + ErrUnauthorized = NewHTTPError(http.StatusUnauthorized) + ErrForbidden = NewHTTPError(http.StatusForbidden) + ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed) + ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge) + ErrValidatorNotRegistered = errors.New("Validator not registered") + ErrRendererNotRegistered = errors.New("Renderer not registered") + ErrInvalidRedirectCode = errors.New("Invalid redirect status code") + ErrCookieNotFound = errors.New("Cookie not found") +) + +// Error handlers +var ( + NotFoundHandler = func(c Context) error { + return ErrNotFound + } + + MethodNotAllowedHandler = func(c Context) error { + return ErrMethodNotAllowed + } +) + +// New creates an instance of Echo. +func New() (e *Echo) { + e = &Echo{ + Server: new(http.Server), + TLSServer: new(http.Server), + AutoTLSManager: autocert.Manager{ + Prompt: autocert.AcceptTOS, + }, + Logger: log.New("echo"), + colorer: color.New(), + maxParam: new(int), + } + e.Server.Handler = e + e.TLSServer.Handler = e + e.HTTPErrorHandler = e.DefaultHTTPErrorHandler + e.Binder = &DefaultBinder{} + e.Logger.SetLevel(log.OFF) + e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) + e.pool.New = func() interface{} { + return e.NewContext(nil, nil) + } + e.router = NewRouter(e) + return +} + +// NewContext returns a Context instance. +func (e *Echo) NewContext(r *http.Request, w http.ResponseWriter) Context { + return &context{ + request: r, + response: NewResponse(w, e), + store: make(Map), + echo: e, + pvalues: make([]string, *e.maxParam), + handler: NotFoundHandler, + } +} + +// Router returns router. +func (e *Echo) Router() *Router { + return e.router +} + +// DefaultHTTPErrorHandler is the default HTTP error handler. It sends a JSON response +// with status code. +func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) { + var ( + code = http.StatusInternalServerError + msg interface{} + ) + + if he, ok := err.(*HTTPError); ok { + code = he.Code + msg = he.Message + } else if e.Debug { + msg = err.Error() + } else { + msg = http.StatusText(code) + } + if _, ok := msg.(string); ok { + msg = Map{"message": msg} + } + + if !c.Response().Committed { + if c.Request().Method == HEAD { // Issue #608 + if err := c.NoContent(code); err != nil { + goto ERROR + } + } else { + if err := c.JSON(code, msg); err != nil { + goto ERROR + } + } + } +ERROR: + e.Logger.Error(err) +} + +// Pre adds middleware to the chain which is run before router. +func (e *Echo) Pre(middleware ...MiddlewareFunc) { + e.premiddleware = append(e.premiddleware, middleware...) +} + +// Use adds middleware to the chain which is run after router. +func (e *Echo) Use(middleware ...MiddlewareFunc) { + e.middleware = append(e.middleware, middleware...) +} + +// CONNECT registers a new CONNECT route for a path with matching handler in the +// router with optional route-level middleware. +func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(CONNECT, path, h, m...) +} + +// DELETE registers a new DELETE route for a path with matching handler in the router +// with optional route-level middleware. +func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(DELETE, path, h, m...) +} + +// GET registers a new GET route for a path with matching handler in the router +// with optional route-level middleware. +func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(GET, path, h, m...) +} + +// HEAD registers a new HEAD route for a path with matching handler in the +// router with optional route-level middleware. +func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(HEAD, path, h, m...) +} + +// OPTIONS registers a new OPTIONS route for a path with matching handler in the +// router with optional route-level middleware. +func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(OPTIONS, path, h, m...) +} + +// PATCH registers a new PATCH route for a path with matching handler in the +// router with optional route-level middleware. +func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(PATCH, path, h, m...) +} + +// POST registers a new POST route for a path with matching handler in the +// router with optional route-level middleware. +func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(POST, path, h, m...) +} + +// PUT registers a new PUT route for a path with matching handler in the +// router with optional route-level middleware. +func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(PUT, path, h, m...) +} + +// TRACE registers a new TRACE route for a path with matching handler in the +// router with optional route-level middleware. +func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { + e.add(TRACE, path, h, m...) +} + +// Any registers a new route for all HTTP methods and path with matching handler +// in the router with optional route-level middleware. +func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) { + for _, m := range methods { + e.add(m, path, handler, middleware...) + } +} + +// Match registers a new route for multiple HTTP methods and path with matching +// handler in the router with optional route-level middleware. +func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { + for _, m := range methods { + e.add(m, path, handler, middleware...) + } +} + +// Static registers a new route with path prefix to serve static files from the +// provided root directory. +func (e *Echo) Static(prefix, root string) { + if root == "" { + root = "." // For security we want to restrict to CWD. + } + static(e, prefix, root) +} + +func static(i i, prefix, root string) { + h := func(c Context) error { + name := filepath.Join(root, path.Clean("/"+c.Param("*"))) // "/"+ for security + return c.File(name) + } + i.GET(prefix, h) + if prefix == "/" { + i.GET(prefix+"*", h) + } else { + i.GET(prefix+"/*", h) + } +} + +// File registers a new route with path to serve a static file. +func (e *Echo) File(path, file string) { + e.GET(path, func(c Context) error { + return c.File(file) + }) +} + +func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { + name := handlerName(handler) + e.router.Add(method, path, func(c Context) error { + h := handler + // Chain middleware + for i := len(middleware) - 1; i >= 0; i-- { + h = middleware[i](h) + } + return h(c) + }) + r := Route{ + Method: method, + Path: path, + Handler: name, + } + e.router.routes[method+path] = r +} + +// Group creates a new router group with prefix and optional group-level middleware. +func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) { + g = &Group{prefix: prefix, echo: e} + g.Use(m...) + return +} + +// URI generates a URI from handler. +func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string { + uri := new(bytes.Buffer) + ln := len(params) + n := 0 + name := handlerName(handler) + for _, r := range e.router.routes { + if r.Handler == name { + for i, l := 0, len(r.Path); i < l; i++ { + if r.Path[i] == ':' && n < ln { + for ; i < l && r.Path[i] != '/'; i++ { + } + uri.WriteString(fmt.Sprintf("%v", params[n])) + n++ + } + if i < l { + uri.WriteByte(r.Path[i]) + } + } + break + } + } + return uri.String() +} + +// URL is an alias for `URI` function. +func (e *Echo) URL(h HandlerFunc, params ...interface{}) string { + return e.URI(h, params...) +} + +// Routes returns the registered routes. +func (e *Echo) Routes() []Route { + routes := []Route{} + for _, v := range e.router.routes { + routes = append(routes, v) + } + return routes +} + +// AcquireContext returns an empty `Context` instance from the pool. +// You must return the context by calling `ReleaseContext()`. +func (e *Echo) AcquireContext() Context { + return e.pool.Get().(Context) +} + +// ReleaseContext returns the `Context` instance back to the pool. +// You must call it after `AcquireContext()`. +func (e *Echo) ReleaseContext(c Context) { + e.pool.Put(c) +} + +// ServeHTTP implements `http.Handler` interface, which serves HTTP requests. +func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Acquire lock + e.Mutex.RLock() + defer e.Mutex.RUnlock() + + // Acquire context + c := e.pool.Get().(*context) + defer e.pool.Put(c) + c.Reset(r, w) + + // Middleware + h := func(c Context) error { + method := r.Method + path := r.URL.RawPath + if path == "" { + path = r.URL.Path + } + e.router.Find(method, path, c) + h := c.Handler() + for i := len(e.middleware) - 1; i >= 0; i-- { + h = e.middleware[i](h) + } + return h(c) + } + + // Premiddleware + for i := len(e.premiddleware) - 1; i >= 0; i-- { + h = e.premiddleware[i](h) + } + + // Execute chain + if err := h(c); err != nil { + e.HTTPErrorHandler(err, c) + } +} + +// Start starts an HTTP server. +func (e *Echo) Start(address string) error { + e.Server.Addr = address + return e.StartServer(e.Server) +} + +// StartTLS starts an HTTPS server. +func (e *Echo) StartTLS(address string, certFile, keyFile string) (err error) { + if certFile == "" || keyFile == "" { + return errors.New("invalid tls configuration") + } + s := e.TLSServer + s.TLSConfig = new(tls.Config) + s.TLSConfig.Certificates = make([]tls.Certificate, 1) + s.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return + } + return e.startTLS(address) +} + +// StartAutoTLS starts an HTTPS server using certificates automatically installed from https://letsencrypt.org. +func (e *Echo) StartAutoTLS(address string) error { + s := e.TLSServer + s.TLSConfig = new(tls.Config) + s.TLSConfig.GetCertificate = e.AutoTLSManager.GetCertificate + return e.startTLS(address) +} + +func (e *Echo) startTLS(address string) error { + s := e.TLSServer + s.Addr = address + if !e.DisableHTTP2 { + s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "h2") + } + return e.StartServer(e.TLSServer) +} + +// StartServer starts a custom http server. +func (e *Echo) StartServer(s *http.Server) (err error) { + // Setup + e.colorer.SetOutput(e.Logger.Output()) + s.Handler = e + s.ErrorLog = e.stdLogger + + if s.TLSConfig == nil { + if e.Listener == nil { + e.Listener, err = newListener(s.Addr) + if err != nil { + return err + } + } + e.colorer.Printf("⇛ http server started on %s\n", e.colorer.Green(e.Listener.Addr())) + return s.Serve(e.Listener) + } + if e.TLSListener == nil { + l, err := newListener(s.Addr) + if err != nil { + return err + } + e.TLSListener = tls.NewListener(l, s.TLSConfig) + } + e.colorer.Printf("⇛ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr())) + return s.Serve(e.TLSListener) +} + +// NewHTTPError creates a new HTTPError instance. +func NewHTTPError(code int, message ...interface{}) *HTTPError { + he := &HTTPError{Code: code, Message: http.StatusText(code)} + if len(message) > 0 { + he.Message = message[0] + } + return he +} + +// Error makes it compatible with `error` interface. +func (he *HTTPError) Error() string { + return fmt.Sprintf("code=%d, message=%s", he.Code, he.Message) +} + +// WrapHandler wraps `http.Handler` into `echo.HandlerFunc`. +func WrapHandler(h http.Handler) HandlerFunc { + return func(c Context) error { + h.ServeHTTP(c.Response(), c.Request()) + return nil + } +} + +// WrapMiddleware wraps `func(http.Handler) http.Handler` into `echo.MiddlewareFunc` +func WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc { + return func(next HandlerFunc) HandlerFunc { + return func(c Context) (err error) { + m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.SetRequest(r) + err = next(c) + })).ServeHTTP(c.Response(), c.Request()) + return + } + } +} + +func handlerName(h HandlerFunc) string { + t := reflect.ValueOf(h).Type() + if t.Kind() == reflect.Func { + return runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name() + } + return t.String() +} + +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. It's used by ListenAndServe and ListenAndServeTLS so +// dead TCP connections (e.g. closing laptop mid-download) eventually +// go away. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + +func newListener(address string) (*tcpKeepAliveListener, error) { + l, err := net.Listen("tcp", address) + if err != nil { + return nil, err + } + return &tcpKeepAliveListener{l.(*net.TCPListener)}, nil +} diff --git a/vendor/github.com/labstack/echo/echo_go1.8.go b/vendor/github.com/labstack/echo/echo_go1.8.go new file mode 100644 index 000000000..340bed705 --- /dev/null +++ b/vendor/github.com/labstack/echo/echo_go1.8.go @@ -0,0 +1,25 @@ +// +build go1.8 + +package echo + +import ( + stdContext "context" +) + +// Close immediately stops the server. +// It internally calls `http.Server#Close()`. +func (e *Echo) Close() error { + if err := e.TLSServer.Close(); err != nil { + return err + } + return e.Server.Close() +} + +// Shutdown stops server the gracefully. +// It internally calls `http.Server#Shutdown()`. +func (e *Echo) Shutdown(ctx stdContext.Context) error { + if err := e.TLSServer.Shutdown(ctx); err != nil { + return err + } + return e.Server.Shutdown(ctx) +} diff --git a/vendor/github.com/labstack/echo/glide.lock b/vendor/github.com/labstack/echo/glide.lock new file mode 100644 index 000000000..c51f7aac0 --- /dev/null +++ b/vendor/github.com/labstack/echo/glide.lock @@ -0,0 +1,92 @@ +hash: 3de2a96bbdc145cce325de2a482111b0524cc330f60a4fbc781a08ed3b879e58 +updated: 2017-01-28T10:22:00.230111692-08:00 +imports: +- name: github.com/daaku/go.zipexe + version: a5fe2436ffcb3236e175e5149162b41cd28bd27d +- name: github.com/dgrijalva/jwt-go + version: a601269ab70c205d26370c16f7c81e9017c14e04 +- name: github.com/facebookgo/clock + version: 600d898af40aa09a7a93ecb9265d87b0504b6f03 +- name: github.com/facebookgo/grace + version: 5729e484473f52048578af1b80d0008c7024089b + subpackages: + - gracehttp + - gracenet +- name: github.com/facebookgo/httpdown + version: a3b1354551a26449fbe05f5d855937f6e7acbd71 +- name: github.com/facebookgo/stats + version: 1b76add642e42c6ffba7211ad7b3939ce654526e +- name: github.com/GeertJohan/go.rice + version: 4bbccbfa39e784796e483270451217d3369ecfbe + subpackages: + - embedded +- name: github.com/golang/protobuf + version: 8ee79997227bf9b34611aee7946ae64735e6fd93 + subpackages: + - proto +- name: github.com/gorilla/websocket + version: c36f2fe5c330f0ac404b616b96c438b8616b1aaf +- name: github.com/kardianos/osext + version: c2c54e542fb797ad986b31721e1baedf214ca413 +- name: github.com/labstack/gommon + version: f72d3c883f8ea180da8f085dd320804c41332ad1 + subpackages: + - bytes + - color + - log + - random +- name: github.com/mattn/go-colorable + version: d228849504861217f796da67fae4f6e347643f15 +- name: github.com/mattn/go-isatty + version: 30a891c33c7cde7b02a981314b4228ec99380cca +- name: github.com/tylerb/graceful + version: 0e9129e9c6d47da90dc0c188b26bd7bb1dab53cd +- name: github.com/valyala/bytebufferpool + version: e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7 +- name: github.com/valyala/fasttemplate + version: d090d65668a286d9a180d43a19dfdc5dcad8fe88 +- name: golang.org/x/crypto + version: 9477e0b78b9ac3d0b03822fd95422e2fe07627cd + subpackages: + - acme + - acme/autocert +- name: golang.org/x/net + version: f2499483f923065a842d38eb4c7f1927e6fc6e6d + subpackages: + - context + - context/ctxhttp + - websocket +- name: golang.org/x/sys + version: d75a52659825e75fff6158388dddc6a5b04f9ba5 + subpackages: + - unix +- name: google.golang.org/appengine + version: a2c54d2174c17540446e0ced57d9d459af61bc1c + subpackages: + - internal + - internal/app_identity + - internal/base + - internal/datastore + - internal/log + - internal/modules + - internal/remote_api +- name: gopkg.in/mgo.v2 + version: 3f83fa5005286a7fe593b055f0d7771a7dce4655 + subpackages: + - bson + - internal/json + - internal/sasl + - internal/scram +testImports: +- name: github.com/davecgh/go-spew + version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9 + subpackages: + - spew +- name: github.com/pmezard/go-difflib + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + subpackages: + - difflib +- name: github.com/stretchr/testify + version: 2402e8e7a02fc811447d11f881aa9746cdc57983 + subpackages: + - assert diff --git a/vendor/github.com/labstack/echo/glide.yaml b/vendor/github.com/labstack/echo/glide.yaml new file mode 100644 index 000000000..9ac9d1c82 --- /dev/null +++ b/vendor/github.com/labstack/echo/glide.yaml @@ -0,0 +1,30 @@ +package: github.com/labstack/echo +import: +- package: github.com/GeertJohan/go.rice +- package: github.com/dgrijalva/jwt-go +- package: github.com/facebookgo/grace + subpackages: + - gracehttp +- package: github.com/gorilla/websocket +- package: github.com/labstack/gommon + subpackages: + - bytes + - color + - log + - random +- package: github.com/tylerb/graceful +- package: github.com/valyala/fasttemplate +- package: golang.org/x/crypto + subpackages: + - acme/autocert +- package: golang.org/x/net + subpackages: + - websocket +- package: google.golang.org/appengine +- package: gopkg.in/mgo.v2 + subpackages: + - bson +testImport: +- package: github.com/stretchr/testify + subpackages: + - assert diff --git a/vendor/github.com/labstack/echo/group.go b/vendor/github.com/labstack/echo/group.go new file mode 100644 index 000000000..799a8f90b --- /dev/null +++ b/vendor/github.com/labstack/echo/group.go @@ -0,0 +1,113 @@ +package echo + +import ( + "path" +) + +type ( + // Group is a set of sub-routes for a specified route. It can be used for inner + // routes that share a common middleware or functionality that should be separate + // from the parent echo instance while still inheriting from it. + Group struct { + prefix string + middleware []MiddlewareFunc + echo *Echo + } +) + +// Use implements `Echo#Use()` for sub-routes within the Group. +func (g *Group) Use(middleware ...MiddlewareFunc) { + g.middleware = append(g.middleware, middleware...) + // Allow all requests to reach the group as they might get dropped if router + // doesn't find a match, making none of the group middleware process. + g.echo.Any(path.Clean(g.prefix+"/*"), func(c Context) error { + return ErrNotFound + }, g.middleware...) +} + +// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. +func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(CONNECT, path, h, m...) +} + +// DELETE implements `Echo#DELETE()` for sub-routes within the Group. +func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(DELETE, path, h, m...) +} + +// GET implements `Echo#GET()` for sub-routes within the Group. +func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(GET, path, h, m...) +} + +// HEAD implements `Echo#HEAD()` for sub-routes within the Group. +func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(HEAD, path, h, m...) +} + +// OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group. +func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(OPTIONS, path, h, m...) +} + +// PATCH implements `Echo#PATCH()` for sub-routes within the Group. +func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(PATCH, path, h, m...) +} + +// POST implements `Echo#POST()` for sub-routes within the Group. +func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(POST, path, h, m...) +} + +// PUT implements `Echo#PUT()` for sub-routes within the Group. +func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(PUT, path, h, m...) +} + +// TRACE implements `Echo#TRACE()` for sub-routes within the Group. +func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { + g.add(TRACE, path, h, m...) +} + +// Any implements `Echo#Any()` for sub-routes within the Group. +func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) { + for _, m := range methods { + g.add(m, path, handler, middleware...) + } +} + +// Match implements `Echo#Match()` for sub-routes within the Group. +func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { + for _, m := range methods { + g.add(m, path, handler, middleware...) + } +} + +// Group creates a new sub-group with prefix and optional sub-group-level middleware. +func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) *Group { + m := []MiddlewareFunc{} + m = append(m, g.middleware...) + m = append(m, middleware...) + return g.echo.Group(g.prefix+prefix, m...) +} + +// Static implements `Echo#Static()` for sub-routes within the Group. +func (g *Group) Static(prefix, root string) { + static(g, prefix, root) +} + +// File implements `Echo#File()` for sub-routes within the Group. +func (g *Group) File(path, file string) { + g.echo.File(g.prefix+path, file) +} + +func (g *Group) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { + // Combine into a new slice to avoid accidentally passing the same slice for + // multiple routes, which would lead to later add() calls overwriting the + // middleware from earlier calls. + m := []MiddlewareFunc{} + m = append(m, g.middleware...) + m = append(m, middleware...) + g.echo.add(method, g.prefix+path, handler, m...) +} diff --git a/vendor/github.com/labstack/echo/log.go b/vendor/github.com/labstack/echo/log.go new file mode 100644 index 000000000..b194c39c4 --- /dev/null +++ b/vendor/github.com/labstack/echo/log.go @@ -0,0 +1,40 @@ +package echo + +import ( + "io" + + "github.com/labstack/gommon/log" +) + +type ( + // Logger defines the logging interface. + Logger interface { + Output() io.Writer + SetOutput(w io.Writer) + Prefix() string + SetPrefix(p string) + Level() log.Lvl + SetLevel(v log.Lvl) + Print(i ...interface{}) + Printf(format string, args ...interface{}) + Printj(j log.JSON) + Debug(i ...interface{}) + Debugf(format string, args ...interface{}) + Debugj(j log.JSON) + Info(i ...interface{}) + Infof(format string, args ...interface{}) + Infoj(j log.JSON) + Warn(i ...interface{}) + Warnf(format string, args ...interface{}) + Warnj(j log.JSON) + Error(i ...interface{}) + Errorf(format string, args ...interface{}) + Errorj(j log.JSON) + Fatal(i ...interface{}) + Fatalj(j log.JSON) + Fatalf(format string, args ...interface{}) + Panic(i ...interface{}) + Panicj(j log.JSON) + Panicf(format string, args ...interface{}) + } +) diff --git a/vendor/github.com/labstack/echo/middleware/basic_auth.go b/vendor/github.com/labstack/echo/middleware/basic_auth.go new file mode 100644 index 000000000..be321fd5c --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/basic_auth.go @@ -0,0 +1,103 @@ +package middleware + +import ( + "encoding/base64" + "strconv" + + "github.com/labstack/echo" +) + +type ( + // BasicAuthConfig defines the config for BasicAuth middleware. + BasicAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Validator is a function to validate BasicAuth credentials. + // Required. + Validator BasicAuthValidator + + // Realm is a string to define realm attribute of BasicAuth. + // Default value "Restricted". + Realm string + } + + // BasicAuthValidator defines a function to validate BasicAuth credentials. + BasicAuthValidator func(string, string, echo.Context) bool +) + +const ( + basic = "Basic" + defaultRealm = "Restricted" +) + +var ( + // DefaultBasicAuthConfig is the default BasicAuth middleware config. + DefaultBasicAuthConfig = BasicAuthConfig{ + Skipper: DefaultSkipper, + Realm: defaultRealm, + } +) + +// BasicAuth returns an BasicAuth middleware. +// +// For valid credentials it calls the next handler. +// For missing or invalid credentials, it sends "401 - Unauthorized" response. +func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc { + c := DefaultBasicAuthConfig + c.Validator = fn + return BasicAuthWithConfig(c) +} + +// BasicAuthWithConfig returns an BasicAuth middleware with config. +// See `BasicAuth()`. +func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc { + // Defaults + if config.Validator == nil { + panic("basic-auth middleware requires a validator function") + } + if config.Skipper == nil { + config.Skipper = DefaultBasicAuthConfig.Skipper + } + if config.Realm == "" { + config.Realm = defaultRealm + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + auth := c.Request().Header.Get(echo.HeaderAuthorization) + l := len(basic) + + if len(auth) > l+1 && auth[:l] == basic { + b, err := base64.StdEncoding.DecodeString(auth[l+1:]) + if err != nil { + return err + } + cred := string(b) + for i := 0; i < len(cred); i++ { + if cred[i] == ':' { + // Verify credentials + if config.Validator(cred[:i], cred[i+1:], c) { + return next(c) + } + } + } + } + + realm := "" + if config.Realm == defaultRealm { + realm = defaultRealm + } else { + realm = strconv.Quote(config.Realm) + } + + // Need to return `401` for browsers to pop-up login box. + c.Response().Header().Set(echo.HeaderWWWAuthenticate, basic+" realm="+realm) + return echo.ErrUnauthorized + } + } +} diff --git a/vendor/github.com/labstack/echo/middleware/body_limit.go b/vendor/github.com/labstack/echo/middleware/body_limit.go new file mode 100644 index 000000000..a2ff8d629 --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/body_limit.go @@ -0,0 +1,116 @@ +package middleware + +import ( + "fmt" + "io" + "sync" + + "github.com/labstack/echo" + "github.com/labstack/gommon/bytes" +) + +type ( + // BodyLimitConfig defines the config for BodyLimit middleware. + BodyLimitConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Maximum allowed size for a request body, it can be specified + // as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P. + Limit string `json:"limit"` + limit int64 + } + + limitedReader struct { + BodyLimitConfig + reader io.ReadCloser + read int64 + context echo.Context + } +) + +var ( + // DefaultBodyLimitConfig is the default Gzip middleware config. + DefaultBodyLimitConfig = BodyLimitConfig{ + Skipper: DefaultSkipper, + } +) + +// BodyLimit returns a BodyLimit middleware. +// +// BodyLimit middleware sets the maximum allowed size for a request body, if the +// size exceeds the configured limit, it sends "413 - Request Entity Too Large" +// response. The BodyLimit is determined based on both `Content-Length` request +// header and actual content read, which makes it super secure. +// Limit can be specified as `4x` or `4xB`, where x is one of the multiple from K, M, +// G, T or P. +func BodyLimit(limit string) echo.MiddlewareFunc { + c := DefaultBodyLimitConfig + c.Limit = limit + return BodyLimitWithConfig(c) +} + +// BodyLimitWithConfig returns a BodyLimit middleware with config. +// See: `BodyLimit()`. +func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultBodyLimitConfig.Skipper + } + + limit, err := bytes.Parse(config.Limit) + if err != nil { + panic(fmt.Errorf("invalid body-limit=%s", config.Limit)) + } + config.limit = limit + pool := limitedReaderPool(config) + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + + // Based on content length + if req.ContentLength > config.limit { + return echo.ErrStatusRequestEntityTooLarge + } + + // Based on content read + r := pool.Get().(*limitedReader) + r.Reset(req.Body, c) + defer pool.Put(r) + req.Body = r + + return next(c) + } + } +} + +func (r *limitedReader) Read(b []byte) (n int, err error) { + n, err = r.reader.Read(b) + r.read += int64(n) + if r.read > r.limit { + return n, echo.ErrStatusRequestEntityTooLarge + } + return +} + +func (r *limitedReader) Close() error { + return r.reader.Close() +} + +func (r *limitedReader) Reset(reader io.ReadCloser, context echo.Context) { + r.reader = reader + r.context = context +} + +func limitedReaderPool(c BodyLimitConfig) sync.Pool { + return sync.Pool{ + New: func() interface{} { + return &limitedReader{BodyLimitConfig: c} + }, + } +} diff --git a/vendor/github.com/labstack/echo/middleware/compress.go b/vendor/github.com/labstack/echo/middleware/compress.go new file mode 100644 index 000000000..cffadbd1c --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/compress.go @@ -0,0 +1,121 @@ +package middleware + +import ( + "bufio" + "compress/gzip" + "io" + "io/ioutil" + "net" + "net/http" + "strings" + + "github.com/labstack/echo" +) + +type ( + // GzipConfig defines the config for Gzip middleware. + GzipConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Gzip compression level. + // Optional. Default value -1. + Level int `json:"level"` + } + + gzipResponseWriter struct { + io.Writer + http.ResponseWriter + } +) + +const ( + gzipScheme = "gzip" +) + +var ( + // DefaultGzipConfig is the default Gzip middleware config. + DefaultGzipConfig = GzipConfig{ + Skipper: DefaultSkipper, + Level: -1, + } +) + +// Gzip returns a middleware which compresses HTTP response using gzip compression +// scheme. +func Gzip() echo.MiddlewareFunc { + return GzipWithConfig(DefaultGzipConfig) +} + +// GzipWithConfig return Gzip middleware with config. +// See: `Gzip()`. +func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultGzipConfig.Skipper + } + if config.Level == 0 { + config.Level = DefaultGzipConfig.Level + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + res := c.Response() + res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding) + if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) { + res.Header().Add(echo.HeaderContentEncoding, gzipScheme) // Issue #806 + rw := res.Writer + w, err := gzip.NewWriterLevel(rw, config.Level) + if err != nil { + return err + } + defer func() { + if res.Size == 0 { + if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme { + res.Header().Del(echo.HeaderContentEncoding) + } + // We have to reset response to it's pristine state when + // nothing is written to body or error is returned. + // See issue #424, #407. + res.Writer = rw + w.Reset(ioutil.Discard) + } + w.Close() + }() + grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw} + res.Writer = grw + } + return next(c) + } + } +} + +func (w *gzipResponseWriter) WriteHeader(code int) { + if code == http.StatusNoContent { // Issue #489 + w.ResponseWriter.Header().Del(echo.HeaderContentEncoding) + } + w.ResponseWriter.WriteHeader(code) +} + +func (w *gzipResponseWriter) Write(b []byte) (int, error) { + if w.Header().Get(echo.HeaderContentType) == "" { + w.Header().Set(echo.HeaderContentType, http.DetectContentType(b)) + } + return w.Writer.Write(b) +} + +func (w *gzipResponseWriter) Flush() { + w.Writer.(*gzip.Writer).Flush() +} + +func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return w.ResponseWriter.(http.Hijacker).Hijack() +} + +func (w *gzipResponseWriter) CloseNotify() <-chan bool { + return w.ResponseWriter.(http.CloseNotifier).CloseNotify() +} diff --git a/vendor/github.com/labstack/echo/middleware/cors.go b/vendor/github.com/labstack/echo/middleware/cors.go new file mode 100644 index 000000000..c35fc36ce --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/cors.go @@ -0,0 +1,139 @@ +package middleware + +import ( + "net/http" + "strconv" + "strings" + + "github.com/labstack/echo" +) + +type ( + // CORSConfig defines the config for CORS middleware. + CORSConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // AllowOrigin defines a list of origins that may access the resource. + // Optional. Default value []string{"*"}. + AllowOrigins []string `json:"allow_origins"` + + // AllowMethods defines a list methods allowed when accessing the resource. + // This is used in response to a preflight request. + // Optional. Default value DefaultCORSConfig.AllowMethods. + AllowMethods []string `json:"allow_methods"` + + // AllowHeaders defines a list of request headers that can be used when + // making the actual request. This in response to a preflight request. + // Optional. Default value []string{}. + AllowHeaders []string `json:"allow_headers"` + + // AllowCredentials indicates whether or not the response to the request + // can be exposed when the credentials flag is true. When used as part of + // a response to a preflight request, this indicates whether or not the + // actual request can be made using credentials. + // Optional. Default value false. + AllowCredentials bool `json:"allow_credentials"` + + // ExposeHeaders defines a whitelist headers that clients are allowed to + // access. + // Optional. Default value []string{}. + ExposeHeaders []string `json:"expose_headers"` + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached. + // Optional. Default value 0. + MaxAge int `json:"max_age"` + } +) + +var ( + // DefaultCORSConfig is the default CORS middleware config. + DefaultCORSConfig = CORSConfig{ + Skipper: DefaultSkipper, + AllowOrigins: []string{"*"}, + AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE}, + } +) + +// CORS returns a Cross-Origin Resource Sharing (CORS) middleware. +// See: https://developer.mozilla.org/en/docs/Web/HTTP/Access_control_CORS +func CORS() echo.MiddlewareFunc { + return CORSWithConfig(DefaultCORSConfig) +} + +// CORSWithConfig returns a CORS middleware with config. +// See: `CORS()`. +func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultCORSConfig.Skipper + } + if len(config.AllowOrigins) == 0 { + config.AllowOrigins = DefaultCORSConfig.AllowOrigins + } + if len(config.AllowMethods) == 0 { + config.AllowMethods = DefaultCORSConfig.AllowMethods + } + + allowMethods := strings.Join(config.AllowMethods, ",") + allowHeaders := strings.Join(config.AllowHeaders, ",") + exposeHeaders := strings.Join(config.ExposeHeaders, ",") + maxAge := strconv.Itoa(config.MaxAge) + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + res := c.Response() + origin := req.Header.Get(echo.HeaderOrigin) + allowOrigin := "" + + // Check allowed origins + for _, o := range config.AllowOrigins { + if o == "*" || o == origin { + allowOrigin = o + break + } + } + + // Simple request + if req.Method != echo.OPTIONS { + res.Header().Add(echo.HeaderVary, echo.HeaderOrigin) + res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin) + if config.AllowCredentials { + res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true") + } + if exposeHeaders != "" { + res.Header().Set(echo.HeaderAccessControlExposeHeaders, exposeHeaders) + } + return next(c) + } + + // Preflight request + res.Header().Add(echo.HeaderVary, echo.HeaderOrigin) + res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod) + res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders) + res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin) + res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods) + if config.AllowCredentials { + res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true") + } + if allowHeaders != "" { + res.Header().Set(echo.HeaderAccessControlAllowHeaders, allowHeaders) + } else { + h := req.Header.Get(echo.HeaderAccessControlRequestHeaders) + if h != "" { + res.Header().Set(echo.HeaderAccessControlAllowHeaders, h) + } + } + if config.MaxAge > 0 { + res.Header().Set(echo.HeaderAccessControlMaxAge, maxAge) + } + return c.NoContent(http.StatusNoContent) + } + } +} diff --git a/vendor/github.com/labstack/echo/middleware/csrf.go b/vendor/github.com/labstack/echo/middleware/csrf.go new file mode 100644 index 000000000..5bbeecb4a --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/csrf.go @@ -0,0 +1,210 @@ +package middleware + +import ( + "crypto/subtle" + "errors" + "net/http" + "strings" + "time" + + "github.com/labstack/echo" + "github.com/labstack/gommon/random" +) + +type ( + // CSRFConfig defines the config for CSRF middleware. + CSRFConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // TokenLength is the length of the generated token. + TokenLength uint8 `json:"token_length"` + // Optional. Default value 32. + + // TokenLookup is a string in the form of ":" that is used + // to extract token from the request. + // Optional. Default value "header:X-CSRF-Token". + // Possible values: + // - "header:" + // - "form:" + // - "query:" + TokenLookup string `json:"token_lookup"` + + // Context key to store generated CSRF token into context. + // Optional. Default value "csrf". + ContextKey string `json:"context_key"` + + // Name of the CSRF cookie. This cookie will store CSRF token. + // Optional. Default value "csrf". + CookieName string `json:"cookie_name"` + + // Domain of the CSRF cookie. + // Optional. Default value none. + CookieDomain string `json:"cookie_domain"` + + // Path of the CSRF cookie. + // Optional. Default value none. + CookiePath string `json:"cookie_path"` + + // Max age (in seconds) of the CSRF cookie. + // Optional. Default value 86400 (24hr). + CookieMaxAge int `json:"cookie_max_age"` + + // Indicates if CSRF cookie is secure. + // Optional. Default value false. + CookieSecure bool `json:"cookie_secure"` + + // Indicates if CSRF cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool `json:"cookie_http_only"` + } + + // csrfTokenExtractor defines a function that takes `echo.Context` and returns + // either a token or an error. + csrfTokenExtractor func(echo.Context) (string, error) +) + +var ( + // DefaultCSRFConfig is the default CSRF middleware config. + DefaultCSRFConfig = CSRFConfig{ + Skipper: DefaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, + } +) + +// CSRF returns a Cross-Site Request Forgery (CSRF) middleware. +// See: https://en.wikipedia.org/wiki/Cross-site_request_forgery +func CSRF() echo.MiddlewareFunc { + c := DefaultCSRFConfig + return CSRFWithConfig(c) +} + +// CSRFWithConfig returns a CSRF middleware with config. +// See `CSRF()`. +func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultCSRFConfig.Skipper + } + if config.TokenLength == 0 { + config.TokenLength = DefaultCSRFConfig.TokenLength + } + if config.TokenLookup == "" { + config.TokenLookup = DefaultCSRFConfig.TokenLookup + } + if config.ContextKey == "" { + config.ContextKey = DefaultCSRFConfig.ContextKey + } + if config.CookieName == "" { + config.CookieName = DefaultCSRFConfig.CookieName + } + if config.CookieMaxAge == 0 { + config.CookieMaxAge = DefaultCSRFConfig.CookieMaxAge + } + + // Initialize + parts := strings.Split(config.TokenLookup, ":") + extractor := csrfTokenFromHeader(parts[1]) + switch parts[0] { + case "form": + extractor = csrfTokenFromForm(parts[1]) + case "query": + extractor = csrfTokenFromQuery(parts[1]) + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + k, err := c.Cookie(config.CookieName) + token := "" + + if err != nil { + // Generate token + token = random.String(config.TokenLength) + } else { + // Reuse token + token = k.Value + } + + switch req.Method { + case echo.GET, echo.HEAD, echo.OPTIONS, echo.TRACE: + default: + // Validate token only for requests which are not defined as 'safe' by RFC7231 + clientToken, err := extractor(c) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if !validateCSRFToken(token, clientToken) { + return echo.NewHTTPError(http.StatusForbidden, "Invalid csrf token") + } + } + + // Set CSRF cookie + cookie := new(http.Cookie) + cookie.Name = config.CookieName + cookie.Value = token + if config.CookiePath != "" { + cookie.Path = config.CookiePath + } + if config.CookieDomain != "" { + cookie.Domain = config.CookieDomain + } + cookie.Expires = time.Now().Add(time.Duration(config.CookieMaxAge) * time.Second) + cookie.Secure = config.CookieSecure + cookie.HttpOnly = config.CookieHTTPOnly + c.SetCookie(cookie) + + // Store token in the context + c.Set(config.ContextKey, token) + + // Protect clients from caching the response + c.Response().Header().Add(echo.HeaderVary, echo.HeaderCookie) + + return next(c) + } + } +} + +// csrfTokenFromForm returns a `csrfTokenExtractor` that extracts token from the +// provided request header. +func csrfTokenFromHeader(header string) csrfTokenExtractor { + return func(c echo.Context) (string, error) { + return c.Request().Header.Get(header), nil + } +} + +// csrfTokenFromForm returns a `csrfTokenExtractor` that extracts token from the +// provided form parameter. +func csrfTokenFromForm(param string) csrfTokenExtractor { + return func(c echo.Context) (string, error) { + token := c.FormValue(param) + if token == "" { + return "", errors.New("Missing csrf token in the form parameter") + } + return token, nil + } +} + +// csrfTokenFromQuery returns a `csrfTokenExtractor` that extracts token from the +// provided query parameter. +func csrfTokenFromQuery(param string) csrfTokenExtractor { + return func(c echo.Context) (string, error) { + token := c.QueryParam(param) + if token == "" { + return "", errors.New("Missing csrf token in the query string") + } + return token, nil + } +} + +func validateCSRFToken(token, clientToken string) bool { + return subtle.ConstantTimeCompare([]byte(token), []byte(clientToken)) == 1 +} diff --git a/vendor/github.com/labstack/echo/middleware/jwt.go b/vendor/github.com/labstack/echo/middleware/jwt.go new file mode 100644 index 000000000..b26587391 --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/jwt.go @@ -0,0 +1,189 @@ +package middleware + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/dgrijalva/jwt-go" + "github.com/labstack/echo" +) + +type ( + // JWTConfig defines the config for JWT middleware. + JWTConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Signing key to validate token. + // Required. + SigningKey interface{} + + // Signing method, used to check token signing method. + // Optional. Default value HS256. + SigningMethod string + + // Context key to store user information from the token into context. + // Optional. Default value "user". + ContextKey string + + // Claims are extendable claims data defining token content. + // Optional. Default value jwt.MapClaims + Claims jwt.Claims + + // TokenLookup is a string in the form of ":" that is used + // to extract token from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" + // - "query:" + // - "cookie:" + TokenLookup string + + // AuthScheme to be used in the Authorization header. + // Optional. Default value "Bearer". + AuthScheme string + + keyFunc jwt.Keyfunc + } + + jwtExtractor func(echo.Context) (string, error) +) + +// Algorithms +const ( + AlgorithmHS256 = "HS256" +) + +var ( + // DefaultJWTConfig is the default JWT auth middleware config. + DefaultJWTConfig = JWTConfig{ + Skipper: DefaultSkipper, + SigningMethod: AlgorithmHS256, + ContextKey: "user", + TokenLookup: "header:" + echo.HeaderAuthorization, + AuthScheme: "Bearer", + Claims: jwt.MapClaims{}, + } +) + +// JWT returns a JSON Web Token (JWT) auth middleware. +// +// For valid token, it sets the user in context and calls next handler. +// For invalid token, it returns "401 - Unauthorized" error. +// For missing token, it returns "400 - Bad Request" error. +// +// See: https://jwt.io/introduction +// See `JWTConfig.TokenLookup` +func JWT(key []byte) echo.MiddlewareFunc { + c := DefaultJWTConfig + c.SigningKey = key + return JWTWithConfig(c) +} + +// JWTWithConfig returns a JWT auth middleware with config. +// See: `JWT()`. +func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultJWTConfig.Skipper + } + if config.SigningKey == nil { + panic("jwt middleware requires signing key") + } + if config.SigningMethod == "" { + config.SigningMethod = DefaultJWTConfig.SigningMethod + } + if config.ContextKey == "" { + config.ContextKey = DefaultJWTConfig.ContextKey + } + if config.Claims == nil { + config.Claims = DefaultJWTConfig.Claims + } + if config.TokenLookup == "" { + config.TokenLookup = DefaultJWTConfig.TokenLookup + } + if config.AuthScheme == "" { + config.AuthScheme = DefaultJWTConfig.AuthScheme + } + config.keyFunc = func(t *jwt.Token) (interface{}, error) { + // Check the signing method + if t.Method.Alg() != config.SigningMethod { + return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"]) + } + return config.SigningKey, nil + } + + // Initialize + parts := strings.Split(config.TokenLookup, ":") + extractor := jwtFromHeader(parts[1], config.AuthScheme) + switch parts[0] { + case "query": + extractor = jwtFromQuery(parts[1]) + case "cookie": + extractor = jwtFromCookie(parts[1]) + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + auth, err := extractor(c) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + token := new(jwt.Token) + // Issue #647, #656 + if _, ok := config.Claims.(jwt.MapClaims); ok { + token, err = jwt.Parse(auth, config.keyFunc) + } else { + claims := reflect.ValueOf(config.Claims).Interface().(jwt.Claims) + token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc) + } + if err == nil && token.Valid { + // Store user information from token into context. + c.Set(config.ContextKey, token) + return next(c) + } + return echo.ErrUnauthorized + } + } +} + +// jwtFromHeader returns a `jwtExtractor` that extracts token from the request header. +func jwtFromHeader(header string, authScheme string) jwtExtractor { + return func(c echo.Context) (string, error) { + auth := c.Request().Header.Get(header) + l := len(authScheme) + if len(auth) > l+1 && auth[:l] == authScheme { + return auth[l+1:], nil + } + return "", errors.New("Missing or invalid jwt in the request header") + } +} + +// jwtFromQuery returns a `jwtExtractor` that extracts token from the query string. +func jwtFromQuery(param string) jwtExtractor { + return func(c echo.Context) (string, error) { + token := c.QueryParam(param) + if token == "" { + return "", errors.New("Missing jwt in the query string") + } + return token, nil + } +} + +// jwtFromCookie returns a `jwtExtractor` that extracts token from the named cookie. +func jwtFromCookie(name string) jwtExtractor { + return func(c echo.Context) (string, error) { + cookie, err := c.Cookie(name) + if err != nil { + return "", errors.New("Missing jwt in the cookie") + } + return cookie.Value, nil + } +} diff --git a/vendor/github.com/labstack/echo/middleware/key_auth.go b/vendor/github.com/labstack/echo/middleware/key_auth.go new file mode 100644 index 000000000..4d4cb9404 --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/key_auth.go @@ -0,0 +1,133 @@ +package middleware + +import ( + "errors" + "net/http" + "strings" + + "github.com/labstack/echo" +) + +type ( + // KeyAuthConfig defines the config for KeyAuth middleware. + KeyAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // KeyLookup is a string in the form of ":" that is used + // to extract key from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" + // - "query:" + KeyLookup string `json:"key_lookup"` + + // AuthScheme to be used in the Authorization header. + // Optional. Default value "Bearer". + AuthScheme string + + // Validator is a function to validate key. + // Required. + Validator KeyAuthValidator + } + + // KeyAuthValidator defines a function to validate KeyAuth credentials. + KeyAuthValidator func(string, echo.Context) bool + + keyExtractor func(echo.Context) (string, error) +) + +var ( + // DefaultKeyAuthConfig is the default KeyAuth middleware config. + DefaultKeyAuthConfig = KeyAuthConfig{ + Skipper: DefaultSkipper, + KeyLookup: "header:" + echo.HeaderAuthorization, + AuthScheme: "Bearer", + } +) + +// KeyAuth returns an KeyAuth middleware. +// +// For valid key it calls the next handler. +// For invalid key, it sends "401 - Unauthorized" response. +// For missing key, it sends "400 - Bad Request" response. +func KeyAuth(fn KeyAuthValidator) echo.MiddlewareFunc { + c := DefaultKeyAuthConfig + c.Validator = fn + return KeyAuthWithConfig(c) +} + +// KeyAuthWithConfig returns an KeyAuth middleware with config. +// See `KeyAuth()`. +func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultKeyAuthConfig.Skipper + } + // Defaults + if config.AuthScheme == "" { + config.AuthScheme = DefaultKeyAuthConfig.AuthScheme + } + if config.KeyLookup == "" { + config.KeyLookup = DefaultKeyAuthConfig.KeyLookup + } + if config.Validator == nil { + panic("key-auth middleware requires a validator function") + } + + // Initialize + parts := strings.Split(config.KeyLookup, ":") + extractor := keyFromHeader(parts[1], config.AuthScheme) + switch parts[0] { + case "query": + extractor = keyFromQuery(parts[1]) + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + // Extract and verify key + key, err := extractor(c) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if config.Validator(key, c) { + return next(c) + } + + return echo.ErrUnauthorized + } + } +} + +// keyFromHeader returns a `keyExtractor` that extracts key from the request header. +func keyFromHeader(header string, authScheme string) keyExtractor { + return func(c echo.Context) (string, error) { + auth := c.Request().Header.Get(header) + if auth == "" { + return "", errors.New("Missing key in request header") + } + if header == echo.HeaderAuthorization { + l := len(authScheme) + if len(auth) > l+1 && auth[:l] == authScheme { + return auth[l+1:], nil + } + return "", errors.New("Invalid key in the request header") + } + return auth, nil + } +} + +// keyFromQuery returns a `keyExtractor` that extracts key from the query string. +func keyFromQuery(param string) keyExtractor { + return func(c echo.Context) (string, error) { + key := c.QueryParam(param) + if key == "" { + return "", errors.New("Missing key in the query string") + } + return key, nil + } +} diff --git a/vendor/github.com/labstack/echo/middleware/logger.go b/vendor/github.com/labstack/echo/middleware/logger.go new file mode 100644 index 000000000..071d0f672 --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/logger.go @@ -0,0 +1,197 @@ +package middleware + +import ( + "bytes" + "io" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/labstack/echo" + "github.com/labstack/gommon/color" + "github.com/valyala/fasttemplate" +) + +type ( + // LoggerConfig defines the config for Logger middleware. + LoggerConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Tags to constructed the logger format. + // + // - time_unix + // - time_unix_nano + // - time_rfc3339 + // - time_rfc3339_nano + // - id (Request ID) + // - remote_ip + // - uri + // - host + // - method + // - path + // - referer + // - user_agent + // - status + // - latency (In nanoseconds) + // - latency_human (Human readable) + // - bytes_in (Bytes received) + // - bytes_out (Bytes sent) + // - header: + // - query: + // - form: + // + // Example "${remote_ip} ${status}" + // + // Optional. Default value DefaultLoggerConfig.Format. + Format string `json:"format"` + + // Output is a writer where logs in JSON format are written. + // Optional. Default value os.Stdout. + Output io.Writer + + template *fasttemplate.Template + colorer *color.Color + pool *sync.Pool + } +) + +var ( + // DefaultLoggerConfig is the default Logger middleware config. + DefaultLoggerConfig = LoggerConfig{ + Skipper: DefaultSkipper, + Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}",` + + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + + `"latency_human":"${latency_human}","bytes_in":${bytes_in},` + + `"bytes_out":${bytes_out}}` + "\n", + Output: os.Stdout, + colorer: color.New(), + } +) + +// Logger returns a middleware that logs HTTP requests. +func Logger() echo.MiddlewareFunc { + return LoggerWithConfig(DefaultLoggerConfig) +} + +// LoggerWithConfig returns a Logger middleware with config. +// See: `Logger()`. +func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultLoggerConfig.Skipper + } + if config.Format == "" { + config.Format = DefaultLoggerConfig.Format + } + if config.Output == nil { + config.Output = DefaultLoggerConfig.Output + } + + config.template = fasttemplate.New(config.Format, "${", "}") + config.colorer = color.New() + config.colorer.SetOutput(config.Output) + config.pool = &sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 256)) + }, + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) (err error) { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + res := c.Response() + start := time.Now() + if err = next(c); err != nil { + c.Error(err) + } + stop := time.Now() + buf := config.pool.Get().(*bytes.Buffer) + buf.Reset() + defer config.pool.Put(buf) + + if _, err = config.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) { + switch tag { + case "time_unix": + return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10)) + case "time_unix_nano": + return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10)) + case "time_rfc3339": + return buf.WriteString(time.Now().Format(time.RFC3339)) + case "time_rfc3339_nano": + return buf.WriteString(time.Now().Format(time.RFC3339Nano)) + case "id": + id := req.Header.Get(echo.HeaderXRequestID) + if id == "" { + id = res.Header().Get(echo.HeaderXRequestID) + } + return buf.WriteString(id) + case "remote_ip": + return buf.WriteString(c.RealIP()) + case "host": + return buf.WriteString(req.Host) + case "uri": + return buf.WriteString(req.RequestURI) + case "method": + return buf.WriteString(req.Method) + case "path": + p := req.URL.Path + if p == "" { + p = "/" + } + return buf.WriteString(p) + case "referer": + return buf.WriteString(req.Referer()) + case "user_agent": + return buf.WriteString(req.UserAgent()) + case "status": + n := res.Status + s := config.colorer.Green(n) + switch { + case n >= 500: + s = config.colorer.Red(n) + case n >= 400: + s = config.colorer.Yellow(n) + case n >= 300: + s = config.colorer.Cyan(n) + } + return buf.WriteString(s) + case "latency": + l := stop.Sub(start) + return buf.WriteString(strconv.FormatInt(int64(l), 10)) + case "latency_human": + return buf.WriteString(stop.Sub(start).String()) + case "bytes_in": + cl := req.Header.Get(echo.HeaderContentLength) + if cl == "" { + cl = "0" + } + return buf.WriteString(cl) + case "bytes_out": + return buf.WriteString(strconv.FormatInt(res.Size, 10)) + default: + switch { + case strings.HasPrefix(tag, "header:"): + return buf.Write([]byte(c.Request().Header.Get(tag[7:]))) + case strings.HasPrefix(tag, "query:"): + return buf.Write([]byte(c.QueryParam(tag[6:]))) + case strings.HasPrefix(tag, "form:"): + return buf.Write([]byte(c.FormValue(tag[5:]))) + } + } + return 0, nil + }); err != nil { + return + } + + _, err = config.Output.Write(buf.Bytes()) + return + } + } +} diff --git a/vendor/github.com/labstack/echo/middleware/method_override.go b/vendor/github.com/labstack/echo/middleware/method_override.go new file mode 100644 index 000000000..955fd11ed --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/method_override.go @@ -0,0 +1,88 @@ +package middleware + +import "github.com/labstack/echo" + +type ( + // MethodOverrideConfig defines the config for MethodOverride middleware. + MethodOverrideConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Getter is a function that gets overridden method from the request. + // Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride). + Getter MethodOverrideGetter + } + + // MethodOverrideGetter is a function that gets overridden method from the request + MethodOverrideGetter func(echo.Context) string +) + +var ( + // DefaultMethodOverrideConfig is the default MethodOverride middleware config. + DefaultMethodOverrideConfig = MethodOverrideConfig{ + Skipper: DefaultSkipper, + Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), + } +) + +// MethodOverride returns a MethodOverride middleware. +// MethodOverride middleware checks for the overridden method from the request and +// uses it instead of the original method. +// +// For security reasons, only `POST` method can be overridden. +func MethodOverride() echo.MiddlewareFunc { + return MethodOverrideWithConfig(DefaultMethodOverrideConfig) +} + +// MethodOverrideWithConfig returns a MethodOverride middleware with config. +// See: `MethodOverride()`. +func MethodOverrideWithConfig(config MethodOverrideConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultMethodOverrideConfig.Skipper + } + if config.Getter == nil { + config.Getter = DefaultMethodOverrideConfig.Getter + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + if req.Method == echo.POST { + m := config.Getter(c) + if m != "" { + req.Method = m + } + } + return next(c) + } + } +} + +// MethodFromHeader is a `MethodOverrideGetter` that gets overridden method from +// the request header. +func MethodFromHeader(header string) MethodOverrideGetter { + return func(c echo.Context) string { + return c.Request().Header.Get(header) + } +} + +// MethodFromForm is a `MethodOverrideGetter` that gets overridden method from the +// form parameter. +func MethodFromForm(param string) MethodOverrideGetter { + return func(c echo.Context) string { + return c.FormValue(param) + } +} + +// MethodFromQuery is a `MethodOverrideGetter` that gets overridden method from +// the query parameter. +func MethodFromQuery(param string) MethodOverrideGetter { + return func(c echo.Context) string { + return c.QueryParam(param) + } +} diff --git a/vendor/github.com/labstack/echo/middleware/middleware.go b/vendor/github.com/labstack/echo/middleware/middleware.go new file mode 100644 index 000000000..efcbab913 --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/middleware.go @@ -0,0 +1,14 @@ +package middleware + +import "github.com/labstack/echo" + +type ( + // Skipper defines a function to skip middleware. Returning true skips processing + // the middleware. + Skipper func(c echo.Context) bool +) + +// DefaultSkipper returns false which processes the middleware. +func DefaultSkipper(echo.Context) bool { + return false +} diff --git a/vendor/github.com/labstack/echo/middleware/recover.go b/vendor/github.com/labstack/echo/middleware/recover.go new file mode 100644 index 000000000..96fa62c90 --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/recover.go @@ -0,0 +1,85 @@ +package middleware + +import ( + "fmt" + "runtime" + + "github.com/labstack/echo" + "github.com/labstack/gommon/color" +) + +type ( + // RecoverConfig defines the config for Recover middleware. + RecoverConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Size of the stack to be printed. + // Optional. Default value 4KB. + StackSize int `json:"stack_size"` + + // DisableStackAll disables formatting stack traces of all other goroutines + // into buffer after the trace for the current goroutine. + // Optional. Default value false. + DisableStackAll bool `json:"disable_stack_all"` + + // DisablePrintStack disables printing stack trace. + // Optional. Default value as false. + DisablePrintStack bool `json:"disable_print_stack"` + } +) + +var ( + // DefaultRecoverConfig is the default Recover middleware config. + DefaultRecoverConfig = RecoverConfig{ + Skipper: DefaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, + } +) + +// Recover returns a middleware which recovers from panics anywhere in the chain +// and handles the control to the centralized HTTPErrorHandler. +func Recover() echo.MiddlewareFunc { + return RecoverWithConfig(DefaultRecoverConfig) +} + +// RecoverWithConfig returns a Recover middleware with config. +// See: `Recover()`. +func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultRecoverConfig.Skipper + } + if config.StackSize == 0 { + config.StackSize = DefaultRecoverConfig.StackSize + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + defer func() { + if r := recover(); r != nil { + var err error + switch r := r.(type) { + case error: + err = r + default: + err = fmt.Errorf("%v", r) + } + stack := make([]byte, config.StackSize) + length := runtime.Stack(stack, !config.DisableStackAll) + if !config.DisablePrintStack { + c.Logger().Printf("[%s] %s %s\n", color.Red("PANIC RECOVER"), err, stack[:length]) + } + c.Error(err) + } + }() + return next(c) + } + } +} diff --git a/vendor/github.com/labstack/echo/middleware/redirect.go b/vendor/github.com/labstack/echo/middleware/redirect.go new file mode 100644 index 000000000..b87dab091 --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/redirect.go @@ -0,0 +1,215 @@ +package middleware + +import ( + "net/http" + + "github.com/labstack/echo" +) + +type ( + // RedirectConfig defines the config for Redirect middleware. + RedirectConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Status code to be used when redirecting the request. + // Optional. Default value http.StatusMovedPermanently. + Code int `json:"code"` + } +) + +const ( + www = "www" +) + +var ( + // DefaultRedirectConfig is the default Redirect middleware config. + DefaultRedirectConfig = RedirectConfig{ + Skipper: DefaultSkipper, + Code: http.StatusMovedPermanently, + } +) + +// HTTPSRedirect redirects http requests to https. +// For example, http://labstack.com will be redirect to https://labstack.com. +// +// Usage `Echo#Pre(HTTPSRedirect())` +func HTTPSRedirect() echo.MiddlewareFunc { + return HTTPSRedirectWithConfig(DefaultRedirectConfig) +} + +// HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config. +// See `HTTPSRedirect()`. +func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultTrailingSlashConfig.Skipper + } + if config.Code == 0 { + config.Code = DefaultRedirectConfig.Code + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + host := req.Host + uri := req.RequestURI + if !c.IsTLS() { + return c.Redirect(config.Code, "https://"+host+uri) + } + return next(c) + } + } +} + +// HTTPSWWWRedirect redirects http requests to https www. +// For example, http://labstack.com will be redirect to https://www.labstack.com. +// +// Usage `Echo#Pre(HTTPSWWWRedirect())` +func HTTPSWWWRedirect() echo.MiddlewareFunc { + return HTTPSWWWRedirectWithConfig(DefaultRedirectConfig) +} + +// HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. +// See `HTTPSWWWRedirect()`. +func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultTrailingSlashConfig.Skipper + } + if config.Code == 0 { + config.Code = DefaultRedirectConfig.Code + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + host := req.Host + uri := req.RequestURI + if !c.IsTLS() && host[:3] != www { + return c.Redirect(config.Code, "https://www."+host+uri) + } + return next(c) + } + } +} + +// HTTPSNonWWWRedirect redirects http requests to https non www. +// For example, http://www.labstack.com will be redirect to https://labstack.com. +// +// Usage `Echo#Pre(HTTPSNonWWWRedirect())` +func HTTPSNonWWWRedirect() echo.MiddlewareFunc { + return HTTPSNonWWWRedirectWithConfig(DefaultRedirectConfig) +} + +// HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. +// See `HTTPSNonWWWRedirect()`. +func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultTrailingSlashConfig.Skipper + } + if config.Code == 0 { + config.Code = DefaultRedirectConfig.Code + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + host := req.Host + uri := req.RequestURI + if !c.IsTLS() { + if host[:3] == www { + return c.Redirect(config.Code, "https://"+host[4:]+uri) + } + return c.Redirect(config.Code, "https://"+host+uri) + } + return next(c) + } + } +} + +// WWWRedirect redirects non www requests to www. +// For example, http://labstack.com will be redirect to http://www.labstack.com. +// +// Usage `Echo#Pre(WWWRedirect())` +func WWWRedirect() echo.MiddlewareFunc { + return WWWRedirectWithConfig(DefaultRedirectConfig) +} + +// WWWRedirectWithConfig returns an HTTPSRedirect middleware with config. +// See `WWWRedirect()`. +func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultTrailingSlashConfig.Skipper + } + if config.Code == 0 { + config.Code = DefaultRedirectConfig.Code + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + scheme := c.Scheme() + host := req.Host + if host[:3] != www { + uri := req.RequestURI + return c.Redirect(config.Code, scheme+"://www."+host+uri) + } + return next(c) + } + } +} + +// NonWWWRedirect redirects www requests to non www. +// For example, http://www.labstack.com will be redirect to http://labstack.com. +// +// Usage `Echo#Pre(NonWWWRedirect())` +func NonWWWRedirect() echo.MiddlewareFunc { + return NonWWWRedirectWithConfig(DefaultRedirectConfig) +} + +// NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. +// See `NonWWWRedirect()`. +func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { + if config.Skipper == nil { + config.Skipper = DefaultTrailingSlashConfig.Skipper + } + if config.Code == 0 { + config.Code = DefaultRedirectConfig.Code + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + scheme := c.Scheme() + host := req.Host + if host[:3] == www { + uri := req.RequestURI + return c.Redirect(config.Code, scheme+"://"+host[4:]+uri) + } + return next(c) + } + } +} diff --git a/vendor/github.com/labstack/echo/middleware/request_id.go b/vendor/github.com/labstack/echo/middleware/request_id.go new file mode 100644 index 000000000..f376c296d --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/request_id.go @@ -0,0 +1,64 @@ +package middleware + +import ( + "github.com/labstack/echo" + "github.com/labstack/gommon/random" +) + +type ( + // RequestIDConfig defines the config for RequestID middleware. + RequestIDConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Generator defines a function to generate an ID. + // Optional. Default value random.String(32). + Generator func() string + } +) + +var ( + // DefaultRequestIDConfig is the default RequestID middleware config. + DefaultRequestIDConfig = RequestIDConfig{ + Skipper: DefaultSkipper, + Generator: generator, + } +) + +// RequestID returns a X-Request-ID middleware. +func RequestID() echo.MiddlewareFunc { + return RequestIDWithConfig(DefaultRequestIDConfig) +} + +// RequestIDWithConfig returns a X-Request-ID middleware with config. +func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultRequestIDConfig.Skipper + } + if config.Generator == nil { + config.Generator = generator + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + res := c.Response() + rid := req.Header.Get(echo.HeaderXRequestID) + if rid == "" { + rid = config.Generator() + } + res.Header().Set(echo.HeaderXRequestID, rid) + + return next(c) + } + } +} + +func generator() string { + return random.String(32) +} diff --git a/vendor/github.com/labstack/echo/middleware/secure.go b/vendor/github.com/labstack/echo/middleware/secure.go new file mode 100644 index 000000000..0125e74a7 --- /dev/null +++ b/vendor/github.com/labstack/echo/middleware/secure.go @@ -0,0 +1,116 @@ +package middleware + +import ( + "fmt" + + "github.com/labstack/echo" +) + +type ( + // SecureConfig defines the config for Secure middleware. + SecureConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // XSSProtection provides protection against cross-site scripting attack (XSS) + // by setting the `X-XSS-Protection` header. + // Optional. Default value "1; mode=block". + XSSProtection string `json:"xss_protection"` + + // ContentTypeNosniff provides protection against overriding Content-Type + // header by setting the `X-Content-Type-Options` header. + // Optional. Default value "nosniff". + ContentTypeNosniff string `json:"content_type_nosniff"` + + // XFrameOptions can be used to indicate whether or not a browser should + // be allowed to render a page in a ,