Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md23
-rw-r--r--acceptance_test.go364
-rw-r--r--app.go79
-rw-r--r--app_config.go6
-rw-r--r--helpers_test.go38
-rw-r--r--internal/artifact/artifact.go3
-rw-r--r--internal/auth/auth.go516
-rw-r--r--internal/auth/auth_test.go295
-rw-r--r--internal/domain/domain.go139
-rw-r--r--internal/domain/domain_config.go16
-rw-r--r--internal/domain/domain_test.go85
-rw-r--r--internal/domain/map.go11
-rw-r--r--internal/domain/map_test.go2
-rw-r--r--internal/httperrors/httperrors.go12
-rw-r--r--internal/httperrors/httperrors_test.go12
-rw-r--r--internal/httptransport/transport.go (renamed from internal/artifact/transport.go)5
-rw-r--r--main.go51
-rw-r--r--server.go3
-rw-r--r--shared/pages/group.auth/group.auth.gitlab-example.com/public/index.html1
-rw-r--r--shared/pages/group.auth/group.auth.gitlab-example.com/public/private.project/index.html1
-rw-r--r--shared/pages/group.auth/private.project.1/config.json1
-rw-r--r--shared/pages/group.auth/private.project.1/public/index.html1
-rw-r--r--shared/pages/group.auth/private.project.2/config.json1
-rw-r--r--shared/pages/group.auth/private.project.2/public/index.html1
-rw-r--r--shared/pages/group.auth/private.project/config.json10
-rw-r--r--shared/pages/group.auth/private.project/public/index.html1
-rw-r--r--shared/pages/group/group.gitlab-example.com/public/project/index.html1
-rw-r--r--vendor/github.com/gorilla/context/LICENSE27
-rw-r--r--vendor/github.com/gorilla/context/README.md10
-rw-r--r--vendor/github.com/gorilla/context/context.go143
-rw-r--r--vendor/github.com/gorilla/context/doc.go88
-rw-r--r--vendor/github.com/gorilla/securecookie/LICENSE27
-rw-r--r--vendor/github.com/gorilla/securecookie/README.md80
-rw-r--r--vendor/github.com/gorilla/securecookie/doc.go61
-rw-r--r--vendor/github.com/gorilla/securecookie/fuzz.go25
-rw-r--r--vendor/github.com/gorilla/securecookie/securecookie.go646
-rw-r--r--vendor/github.com/gorilla/sessions/LICENSE27
-rw-r--r--vendor/github.com/gorilla/sessions/README.md92
-rw-r--r--vendor/github.com/gorilla/sessions/doc.go198
-rw-r--r--vendor/github.com/gorilla/sessions/go.mod6
-rw-r--r--vendor/github.com/gorilla/sessions/lex.go102
-rw-r--r--vendor/github.com/gorilla/sessions/sessions.go243
-rw-r--r--vendor/github.com/gorilla/sessions/store.go295
-rw-r--r--vendor/vendor.json18
44 files changed, 3693 insertions, 73 deletions
diff --git a/README.md b/README.md
index 7e2c3cbf..3040e302 100644
--- a/README.md
+++ b/README.md
@@ -160,6 +160,29 @@ $ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pa
This is most useful in dual-stack environments (IPv4+IPv6) where both Gitlab
Pages and another HTTP server have to co-exist on the same server.
+### GitLab access control
+
+GitLab access control is configured with properties `auth-client-id`, `auth-client-secret`, `auth-redirect-uri`, `auth-server` and `auth-secret`. Client ID, secret and redirect uri are configured in the GitLab and should match. `auth-server` points to a GitLab instance used for authentication. `auth-redirect-uri` should be `http(s)://pages-domain/auth`. Note that if the pages-domain is not handled by GitLab pages, then the `auth-redirect-uri` should use some reserved namespace prefix (such as `http(s)://projects.pages-domain/auth`). Using HTTPS is _strongly_ encouraged. `auth-secret` is used to encrypt the session cookie, and it should be strong enough.
+
+Example:
+```
+$ make
+$ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pages-root path/to/gitlab/shared/pages -pages-domain example.com -auth-client-id <id> -auth-client-secret <secret> -auth-redirect-uri https://projects.example.com/auth -auth-secret something-very-secret -auth-server https://gitlab.com
+```
+
+#### How it works
+
+1. GitLab pages looks for `access_control` and `id` fields in `config.json` files
+ in `pages-root/group/project` directories.
+2. For projects that have `access_control` set to `true` pages will require user to authenticate.
+3. When user accesses a project that requires authentication, user will be redirected
+ to GitLab to log in and grant access for GitLab pages.
+4. When user grant's access to GitLab pages, pages will use the OAuth2 `code` to get an access
+ token which is stored in the user session cookie.
+5. Pages will now check user's access to a project with a access token stored in the user
+ session cookie. This is done via a request to GitLab API with the user's access token.
+6. If token is invalidated, user will be redirected again to GitLab to authorize pages again.
+
### Enable Prometheus Metrics
For monitoring purposes, you can pass the `-metrics-address` flag when starting.
diff --git a/acceptance_test.go b/acceptance_test.go
index 361cba68..17eb7cb4 100644
--- a/acceptance_test.go
+++ b/acceptance_test.go
@@ -8,6 +8,7 @@ import (
"net"
"net/http"
"net/http/httptest"
+ "net/url"
"os"
"testing"
"time"
@@ -297,7 +298,7 @@ func TestPrometheusMetricsCanBeScraped(t *testing.T) {
body, _ := ioutil.ReadAll(resp.Body)
assert.Contains(t, string(body), "gitlab_pages_http_sessions_active 0")
- assert.Contains(t, string(body), "gitlab_pages_domains_served_total 11")
+ assert.Contains(t, string(body), "gitlab_pages_domains_served_total 13")
}
}
@@ -469,6 +470,7 @@ func TestArtifactProxyRequest(t *testing.T) {
t.Log("Artifact server URL", artifactServerURL)
for _, c := range cases {
+
t.Run(fmt.Sprintf("Proxy Request Test: %s", c.Description), func(t *testing.T) {
teardown := RunPagesProcessWithSSLCertFile(
t,
@@ -573,3 +575,363 @@ func TestKnownHostInReverseProxySetupReturns200(t *testing.T) {
assert.Equal(t, http.StatusOK, rsp.StatusCode)
}
}
+
+func TestWhenAuthIsDisabledPrivateIsNotAccessible(t *testing.T) {
+ skipUnlessEnabled(t)
+ teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "")
+ defer teardown()
+
+ rsp, err := GetPageFromListener(t, httpListener, "group.auth.gitlab-example.com", "private.project/")
+
+ require.NoError(t, err)
+ rsp.Body.Close()
+ assert.Equal(t, http.StatusInternalServerError, rsp.StatusCode)
+}
+
+func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) {
+ skipUnlessEnabled(t)
+ teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "")
+ defer teardown()
+
+ rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/")
+
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+
+ assert.Equal(t, http.StatusFound, rsp.StatusCode)
+ assert.Equal(t, 1, len(rsp.Header["Location"]))
+ url, err := url.Parse(rsp.Header.Get("Location"))
+ require.NoError(t, err)
+ rsp, err = GetRedirectPage(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery)
+
+ assert.Equal(t, http.StatusFound, rsp.StatusCode)
+ assert.Equal(t, 1, len(rsp.Header["Location"]))
+
+ url, err = url.Parse(rsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ assert.Equal(t, "https", url.Scheme)
+ assert.Equal(t, "gitlab-auth.com", url.Host)
+ assert.Equal(t, "/oauth/authorize", url.Path)
+ assert.Equal(t, "1", url.Query().Get("client_id"))
+ assert.Equal(t, "https://projects.gitlab-example.com/auth", url.Query().Get("redirect_uri"))
+ assert.NotEqual(t, "", url.Query().Get("state"))
+}
+
+func TestWhenAuthDeniedWillCauseUnauthorized(t *testing.T) {
+ skipUnlessEnabled(t)
+ teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "")
+ defer teardown()
+
+ rsp, err := GetPageFromListener(t, httpsListener, "projects.gitlab-example.com", "/auth?error=access_denied")
+
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+
+ assert.Equal(t, http.StatusUnauthorized, rsp.StatusCode)
+}
+func TestWhenLoginCallbackWithWrongStateShouldFail(t *testing.T) {
+ skipUnlessEnabled(t)
+ teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "")
+ defer teardown()
+
+ rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/")
+
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+
+ // Go to auth page with wrong state will cause failure
+ authrsp, err := GetPageFromListener(t, httpsListener, "projects.gitlab-example.com", "/auth?code=0&state=0")
+
+ require.NoError(t, err)
+ defer authrsp.Body.Close()
+
+ assert.Equal(t, http.StatusUnauthorized, authrsp.StatusCode)
+}
+
+func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) {
+ skipUnlessEnabled(t)
+ teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "")
+ defer teardown()
+
+ rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/")
+
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+
+ cookie := rsp.Header.Get("Set-Cookie")
+
+ url, err := url.Parse(rsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ // Go to auth page with correct state will cause fetching the token
+ authrsp, err := GetPageFromListenerWithCookie(t, httpsListener, "projects.gitlab-example.com", "/auth?code=1&state="+
+ url.Query().Get("state"), cookie)
+
+ require.NoError(t, err)
+ defer authrsp.Body.Close()
+
+ // Will cause 503 because token endpoint is not available
+ assert.Equal(t, http.StatusServiceUnavailable, authrsp.StatusCode)
+}
+
+func TestAccessControlUnderCustomDomain(t *testing.T) {
+ skipUnlessEnabled(t, "not-inplace-chroot")
+
+ testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/oauth/token":
+ assert.Equal(t, "POST", r.Method)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, "{\"access_token\":\"abc\"}")
+ case "/api/v4/projects/1000/pages_access":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusOK)
+ default:
+ t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+ testServer.Start()
+ defer testServer.Close()
+
+ teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL)
+ defer teardown()
+
+ rsp, err := GetRedirectPage(t, httpListener, "private.domain.com", "/")
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+
+ cookie := rsp.Header.Get("Set-Cookie")
+
+ url, err := url.Parse(rsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ state := url.Query().Get("state")
+ assert.Equal(t, url.Query().Get("domain"), "http://private.domain.com")
+
+ pagesrsp, err := GetRedirectPage(t, httpListener, url.Host, url.Path+"?"+url.RawQuery)
+ require.NoError(t, err)
+ defer pagesrsp.Body.Close()
+
+ pagescookie := pagesrsp.Header.Get("Set-Cookie")
+
+ // Go to auth page with correct state will cause fetching the token
+ authrsp, err := GetRedirectPageWithCookie(t, httpListener, "projects.gitlab-example.com", "/auth?code=1&state="+
+ state, pagescookie)
+
+ require.NoError(t, err)
+ defer authrsp.Body.Close()
+
+ url, err = url.Parse(authrsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ // Will redirect to custom domain
+ assert.Equal(t, "private.domain.com", url.Host)
+ assert.Equal(t, "1", url.Query().Get("code"))
+ assert.Equal(t, state, url.Query().Get("state"))
+
+ // Run auth callback in custom domain
+ authrsp, err = GetRedirectPageWithCookie(t, httpListener, "private.domain.com", "/auth?code=1&state="+
+ state, cookie)
+
+ require.NoError(t, err)
+ defer authrsp.Body.Close()
+
+ // Will redirect to the page
+ cookie = authrsp.Header.Get("Set-Cookie")
+ assert.Equal(t, http.StatusFound, authrsp.StatusCode)
+
+ url, err = url.Parse(authrsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ // Will redirect to custom domain
+ assert.Equal(t, "http://private.domain.com/", url.String())
+
+ // Fetch page in custom domain
+ authrsp, err = GetRedirectPageWithCookie(t, httpListener, "private.domain.com", "/", cookie)
+ assert.Equal(t, http.StatusOK, authrsp.StatusCode)
+}
+
+func TestAccessControlGroupDomain404RedirectsAuth(t *testing.T) {
+ skipUnlessEnabled(t)
+ teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "")
+ defer teardown()
+
+ rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "/nonexistent/")
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+ assert.Equal(t, http.StatusFound, rsp.StatusCode)
+ // Redirects to the projects under gitlab pages domain for authentication flow
+ url, err := url.Parse(rsp.Header.Get("Location"))
+ require.NoError(t, err)
+ assert.Equal(t, "projects.gitlab-example.com", url.Host)
+ assert.Equal(t, "/auth", url.Path)
+}
+func TestAccessControlProject404DoesNotRedirect(t *testing.T) {
+ skipUnlessEnabled(t)
+ teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "")
+ defer teardown()
+
+ rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "/project/nonexistent/")
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+ assert.Equal(t, http.StatusNotFound, rsp.StatusCode)
+}
+func TestAccessControl(t *testing.T) {
+ skipUnlessEnabled(t, "not-inplace-chroot")
+
+ transport := (TestHTTPSClient.Transport).(*http.Transport)
+ defer func(t time.Duration) {
+ transport.ResponseHeaderTimeout = t
+ }(transport.ResponseHeaderTimeout)
+ transport.ResponseHeaderTimeout = 5 * time.Second
+
+ testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/oauth/token":
+ assert.Equal(t, "POST", r.Method)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, "{\"access_token\":\"abc\"}")
+ case "/api/v4/user":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusOK)
+ case "/api/v4/projects/1000/pages_access":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusOK)
+ case "/api/v4/projects/2000/pages_access":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusUnauthorized)
+ case "/api/v4/projects/3000/pages_access":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprint(w, "{\"error\":\"invalid_token\"}")
+ default:
+ t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+
+ keyFile, certFile := CreateHTTPSFixtureFiles(t)
+ cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+ require.NoError(t, err)
+ defer os.Remove(keyFile)
+ defer os.Remove(certFile)
+
+ testServer.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
+ testServer.StartTLS()
+ defer testServer.Close()
+
+ cases := []struct {
+ Host string
+ Path string
+ Status int
+ RedirectBack bool
+ Description string
+ }{
+ {
+ "group.auth.gitlab-example.com",
+ "/private.project/",
+ http.StatusOK,
+ false,
+ "project with access",
+ },
+ {
+ "group.auth.gitlab-example.com",
+ "/private.project.1/",
+ http.StatusNotFound, // Do not expose project existed
+ false,
+ "project without access",
+ },
+ {
+ "group.auth.gitlab-example.com",
+ "/private.project.2/",
+ http.StatusFound,
+ true,
+ "invalid token test should redirect back",
+ },
+ {
+ "group.auth.gitlab-example.com",
+ "/nonexistent/",
+ http.StatusNotFound,
+ false,
+ "no project should redirect to login and then return 404",
+ },
+ {
+ "nonexistent.gitlab-example.com",
+ "/nonexistent/",
+ http.StatusNotFound,
+ false,
+ "no project should redirect to login and then return 404",
+ },
+ }
+
+ for _, c := range cases {
+
+ t.Run(fmt.Sprintf("Access Control Test: %s", c.Description), func(t *testing.T) {
+ teardown := RunPagesProcessWithAuthServerWithSSL(t, *pagesBinary, listeners, "", certFile, testServer.URL)
+ defer teardown()
+
+ rsp, err := GetRedirectPage(t, httpsListener, c.Host, c.Path)
+
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+
+ assert.Equal(t, http.StatusFound, rsp.StatusCode)
+ cookie := rsp.Header.Get("Set-Cookie")
+
+ // Redirects to the projects under gitlab pages domain for authentication flow
+ url, err := url.Parse(rsp.Header.Get("Location"))
+ require.NoError(t, err)
+ assert.Equal(t, "projects.gitlab-example.com", url.Host)
+ assert.Equal(t, "/auth", url.Path)
+ state := url.Query().Get("state")
+
+ rsp, err = GetRedirectPage(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery)
+
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+
+ assert.Equal(t, http.StatusFound, rsp.StatusCode)
+ pagesDomainCookie := rsp.Header.Get("Set-Cookie")
+
+ // Go to auth page with correct state will cause fetching the token
+ authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "projects.gitlab-example.com", "/auth?code=1&state="+
+ state, pagesDomainCookie)
+
+ require.NoError(t, err)
+ defer authrsp.Body.Close()
+
+ // Will redirect auth callback to correct host
+ url, err = url.Parse(authrsp.Header.Get("Location"))
+ require.NoError(t, err)
+ assert.Equal(t, c.Host, url.Host)
+ assert.Equal(t, "/auth", url.Path)
+
+ // Request auth callback in project domain
+ authrsp, err = GetRedirectPageWithCookie(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery, cookie)
+
+ // server returns the ticket, user will be redirected to the project page
+ assert.Equal(t, http.StatusFound, authrsp.StatusCode)
+ cookie = authrsp.Header.Get("Set-Cookie")
+ rsp, err = GetRedirectPageWithCookie(t, httpsListener, c.Host, c.Path, cookie)
+
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+
+ assert.Equal(t, c.Status, rsp.StatusCode)
+ assert.Equal(t, "", rsp.Header.Get("Cache-Control"))
+
+ if c.RedirectBack {
+ url, err = url.Parse(rsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ assert.Equal(t, "https", url.Scheme)
+ assert.Equal(t, c.Host, url.Host)
+ assert.Equal(t, c.Path, url.Path)
+ }
+ })
+ }
+}
diff --git a/app.go b/app.go
index 0a7a8268..862a1894 100644
--- a/app.go
+++ b/app.go
@@ -19,6 +19,7 @@ import (
"gitlab.com/gitlab-org/gitlab-pages/internal/admin"
"gitlab.com/gitlab-org/gitlab-pages/internal/artifact"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/auth"
"gitlab.com/gitlab-org/gitlab-pages/internal/domain"
"gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
"gitlab.com/gitlab-org/gitlab-pages/metrics"
@@ -39,6 +40,7 @@ type theApp struct {
dm domain.Map
lock sync.RWMutex
Artifact *artifact.Artifact
+ Auth *auth.Auth
}
func (a *theApp) isReady() bool {
@@ -92,6 +94,32 @@ func (a *theApp) getHostAndDomain(r *http.Request) (host string, domain *domain.
return host, a.domain(host)
}
+func (a *theApp) checkAuthenticationIfNotExists(domain *domain.D, w http.ResponseWriter, r *http.Request) bool {
+ if domain == nil || !domain.HasProject(r) {
+
+ // Only if auth is supported
+ if a.Auth.IsAuthSupported() {
+
+ // To avoid user knowing if pages exist, we will force user to login and authorize pages
+ if a.Auth.CheckAuthenticationWithoutProject(w, r) {
+ return true
+ }
+
+ // User is authenticated, show the 404
+ httperrors.Serve404(w)
+ return true
+ }
+ }
+
+ // Without auth, fall back to 404
+ if domain == nil {
+ httperrors.Serve404(w)
+ return true
+ }
+
+ return false
+}
+
func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, https bool, host string, domain *domain.D) bool {
// short circuit content serving to check for a status page
if r.RequestURI == a.appConfig.StatusPath {
@@ -116,8 +144,7 @@ func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, ht
return true
}
- if domain == nil {
- httperrors.Serve404(w)
+ if a.checkAuthenticationIfNotExists(domain, w, r) {
return true
}
@@ -138,20 +165,59 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo
host, domain := a.getHostAndDomain(r)
+ if a.Auth.TryAuthenticate(&w, r, a.dm, &a.lock) {
+ return
+ }
+
if a.tryAuxiliaryHandlers(&w, r, https, host, domain) {
return
}
+ // Only for projects that have access control enabled
+ if domain.IsAccessControlEnabled(r) {
+ log.WithFields(log.Fields{
+ "host": r.Host,
+ "path": r.RequestURI,
+ }).Debug("Authenticate request")
+
+ if a.Auth.CheckAuthentication(&w, r, domain.GetID(r)) {
+ return
+ }
+ }
+
// Serve static file, applying CORS headers if necessary
if a.DisableCrossOriginRequests {
- domain.ServeHTTP(&w, r)
+ a.serveFileOrNotFound(domain)(&w, r)
} else {
- corsHandler.ServeHTTP(&w, r, domain.ServeHTTP)
+ corsHandler.ServeHTTP(&w, r, a.serveFileOrNotFound(domain))
}
metrics.ProcessedRequests.WithLabelValues(strconv.Itoa(w.status), r.Method).Inc()
}
+func (a *theApp) serveFileOrNotFound(domain *domain.D) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ fileServed := domain.ServeFileHTTP(w, r)
+
+ if !fileServed {
+ // We need to trigger authentication flow here if file does not exist to prevent exposing possibly private project existence,
+ // because the projects override the paths of the namespace project and they might be private even though
+ // namespace project is public.
+ if domain.IsNamespaceProject(r) {
+
+ if a.Auth.CheckAuthenticationWithoutProject(w, r) {
+ return
+ }
+
+ httperrors.Serve404(w)
+ return
+ }
+
+ domain.ServeNotFoundHTTP(w, r)
+ }
+ }
+}
+
func (a *theApp) ServeHTTP(ww http.ResponseWriter, r *http.Request) {
https := r.TLS != nil
a.serveContent(ww, r, https)
@@ -291,6 +357,11 @@ func runApp(config appConfig) {
a.Artifact = artifact.New(config.ArtifactsServer, config.ArtifactsServerTimeout, config.Domain)
}
+ if config.ClientID != "" {
+ a.Auth = auth.New(config.Domain, config.StoreSecret, config.ClientID, config.ClientSecret,
+ config.RedirectURI, config.GitLabServer)
+ }
+
configureLogging(config.LogFormat, config.LogVerbose)
if err := mimedb.LoadTypes(); err != nil {
diff --git a/app_config.go b/app_config.go
index f2aa90cd..ab8cc264 100644
--- a/app_config.go
+++ b/app_config.go
@@ -25,4 +25,10 @@ type appConfig struct {
LogFormat string
LogVerbose bool
+
+ StoreSecret string
+ GitLabServer string
+ ClientID string
+ ClientSecret string
+ RedirectURI string
}
diff --git a/helpers_test.go b/helpers_test.go
index ccbbb6e3..83107488 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -144,6 +144,30 @@ func RunPagesProcessWithSSLCertFile(t *testing.T, pagesPath string, listeners []
return runPagesProcess(t, true, pagesPath, listeners, promPort, []string{"SSL_CERT_FILE=" + sslCertFile}, extraArgs...)
}
+func RunPagesProcessWithAuth(t *testing.T, pagesPath string, listeners []ListenSpec, promPort string) (teardown func()) {
+ return runPagesProcess(t, true, pagesPath, listeners, promPort, nil, "-auth-client-id=1",
+ "-auth-client-secret=1",
+ "-auth-server=https://gitlab-auth.com",
+ "-auth-redirect-uri=https://projects.gitlab-example.com/auth",
+ "-auth-secret=something-very-secret")
+}
+
+func RunPagesProcessWithAuthServer(t *testing.T, pagesPath string, listeners []ListenSpec, promPort string, authServer string) (teardown func()) {
+ return runPagesProcess(t, true, pagesPath, listeners, promPort, nil, "-auth-client-id=1",
+ "-auth-client-secret=1",
+ "-auth-server="+authServer,
+ "-auth-redirect-uri=https://projects.gitlab-example.com/auth",
+ "-auth-secret=something-very-secret")
+}
+
+func RunPagesProcessWithAuthServerWithSSL(t *testing.T, pagesPath string, listeners []ListenSpec, promPort string, sslCertFile string, authServer string) (teardown func()) {
+ return runPagesProcess(t, true, pagesPath, listeners, promPort, []string{"SSL_CERT_FILE=" + sslCertFile}, "-auth-client-id=1",
+ "-auth-client-secret=1",
+ "-auth-server="+authServer,
+ "-auth-redirect-uri=https://projects.gitlab-example.com/auth",
+ "-auth-secret=something-very-secret")
+}
+
func runPagesProcess(t *testing.T, wait bool, pagesPath string, listeners []ListenSpec, promPort string, extraEnv []string, extraArgs ...string) (teardown func()) {
_, err := os.Stat(pagesPath)
require.NoError(t, err)
@@ -248,11 +272,18 @@ func getPagesDaemonArgs(t *testing.T) []string {
// Does a HTTP(S) GET against the listener specified, setting a fake
// Host: and constructing the URL from the listener and the URL suffix.
func GetPageFromListener(t *testing.T, spec ListenSpec, host, urlsuffix string) (*http.Response, error) {
+ return GetPageFromListenerWithCookie(t, spec, host, urlsuffix, "")
+}
+
+func GetPageFromListenerWithCookie(t *testing.T, spec ListenSpec, host, urlsuffix string, cookie string) (*http.Response, error) {
url := spec.URL(urlsuffix)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
+ if cookie != "" {
+ req.Header.Set("Cookie", cookie)
+ }
req.Host = host
@@ -279,11 +310,18 @@ func DoPagesRequest(t *testing.T, req *http.Request) (*http.Response, error) {
}
func GetRedirectPage(t *testing.T, spec ListenSpec, host, urlsuffix string) (*http.Response, error) {
+ return GetRedirectPageWithCookie(t, spec, host, urlsuffix, "")
+}
+
+func GetRedirectPageWithCookie(t *testing.T, spec ListenSpec, host, urlsuffix string, cookie string) (*http.Response, error) {
url := spec.URL(urlsuffix)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
+ if cookie != "" {
+ req.Header.Set("Cookie", cookie)
+ }
req.Host = host
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index 9a23e269..5050b426 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -12,6 +12,7 @@ import (
"time"
"gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport"
)
const (
@@ -43,7 +44,7 @@ func New(server string, timeoutSeconds int, pagesDomain string) *Artifact {
suffix: "." + strings.ToLower(pagesDomain),
client: &http.Client{
Timeout: time.Second * time.Duration(timeoutSeconds),
- Transport: transport,
+ Transport: httptransport.Transport,
},
}
}
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
new file mode 100644
index 00000000..c9f10961
--- /dev/null
+++ b/internal/auth/auth.go
@@ -0,0 +1,516 @@
+package auth
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gorilla/securecookie"
+ "github.com/gorilla/sessions"
+ log "github.com/sirupsen/logrus"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/domain"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport"
+)
+
+const (
+ apiURLUserTemplate = "%s/api/v4/user"
+ apiURLProjectTemplate = "%s/api/v4/projects/%d/pages_access"
+ authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s"
+ tokenURLTemplate = "%s/oauth/token"
+ tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s"
+ callbackPath = "/auth"
+ authorizeProxyTemplate = "%s?domain=%s&state=%s"
+)
+
+// Auth handles authenticating users with GitLab API
+type Auth struct {
+ pagesDomain string
+ clientID string
+ clientSecret string
+ redirectURI string
+ gitLabServer string
+ apiClient *http.Client
+ store sessions.Store
+}
+
+type tokenResponse struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+}
+
+type errorResponse struct {
+ Error string `json:"error"`
+ ErrorDescription string `json:"error_description"`
+}
+
+func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) {
+ session, err := a.store.Get(r, "gitlab-pages")
+
+ if session != nil {
+ // Cookie just for this domain
+ session.Options = &sessions.Options{
+ Path: "/",
+ HttpOnly: true,
+ }
+ }
+
+ return session, err
+}
+
+func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) {
+
+ // Create or get session
+ session, errsession := a.getSessionFromStore(r)
+
+ if errsession != nil {
+ // Save cookie again
+ errsave := session.Save(r, w)
+ if errsave != nil {
+ logRequest(r).WithError(errsave).Error("Failed to save the session")
+ httperrors.Serve500(w)
+ return nil, errsave
+ }
+
+ http.Redirect(w, r, getRequestAddress(r), 302)
+ return nil, errsession
+ }
+
+ return session, nil
+}
+
+// TryAuthenticate tries to authenticate user and fetch access token if request is a callback to auth
+func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain.Map, lock *sync.RWMutex) bool {
+
+ if a == nil {
+ return false
+ }
+
+ session, err := a.checkSession(w, r)
+ if err != nil {
+ return true
+ }
+
+ // Request is for auth
+ if r.URL.Path != callbackPath {
+ return false
+ }
+
+ logRequest(r).Debug("Authentication callback")
+
+ if a.handleProxyingAuth(session, w, r, dm, lock) {
+ return true
+ }
+
+ // If callback is not successful
+ errorParam := r.URL.Query().Get("error")
+ if errorParam != "" {
+ logRequest(r).WithField("error", errorParam).Debug("OAuth endpoint returned error")
+
+ httperrors.Serve401(w)
+ return true
+ }
+
+ if verifyCodeAndStateGiven(r) {
+ a.checkAuthenticationResponse(session, w, r)
+ return true
+ }
+
+ return false
+}
+
+func (a *Auth) checkAuthenticationResponse(session *sessions.Session, w http.ResponseWriter, r *http.Request) {
+
+ if !validateState(r, session) {
+ // State is NOT ok
+ logRequest(r).Debug("Authentication state did not match expected")
+
+ httperrors.Serve401(w)
+ return
+ }
+
+ // Fetch access token with authorization code
+ token, err := a.fetchAccessToken(r.URL.Query().Get("code"))
+
+ // Fetching token not OK
+ if err != nil {
+ logRequest(r).WithError(err).Debug("Fetching access token failed")
+
+ httperrors.Serve503(w)
+ return
+ }
+
+ // Store access token
+ session.Values["access_token"] = token.AccessToken
+ err = session.Save(r, w)
+ if err != nil {
+ logRequest(r).WithError(err).Error("Failed to save the session")
+ httperrors.Serve500(w)
+ return
+ }
+
+ // Redirect back to requested URI
+ logRequest(r).Debug("Authentication was successful, redirecting user back to requested page")
+
+ http.Redirect(w, r, session.Values["uri"].(string), 302)
+}
+
+func (a *Auth) domainAllowed(domain string, dm domain.Map, lock *sync.RWMutex) bool {
+ lock.RLock()
+ defer lock.RUnlock()
+ domain = strings.ToLower(domain)
+ _, present := dm[domain]
+ return domain == a.pagesDomain || strings.HasSuffix("."+domain, a.pagesDomain) || present
+}
+
+func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWriter, r *http.Request, dm domain.Map, lock *sync.RWMutex) bool {
+ // If request is for authenticating via custom domain
+ if shouldProxyAuth(r) {
+ domain := r.URL.Query().Get("domain")
+ state := r.URL.Query().Get("state")
+
+ proxyurl, err := url.Parse(domain)
+ if err != nil {
+ logRequest(r).WithField("domain", domain).Error("Failed to parse domain query parameter")
+ httperrors.Serve500(w)
+ return true
+ }
+ host, _, err := net.SplitHostPort(proxyurl.Host)
+ if err != nil {
+ host = proxyurl.Host
+ }
+
+ if !a.domainAllowed(host, dm, lock) {
+ logRequest(r).WithField("domain", host).Debug("Domain is not configured")
+ httperrors.Serve401(w)
+ return true
+ }
+
+ logRequest(r).WithField("domain", domain).Debug("User is authenticating via domain")
+
+ session.Values["proxy_auth_domain"] = domain
+
+ err = session.Save(r, w)
+ if err != nil {
+ logRequest(r).WithError(err).Error("Failed to save the session")
+ httperrors.Serve500(w)
+ return true
+ }
+
+ url := fmt.Sprintf(authorizeURLTemplate, a.gitLabServer, a.clientID, a.redirectURI, state)
+ http.Redirect(w, r, url, 302)
+
+ return true
+ }
+
+ // If auth request callback should be proxied to custom domain
+ if shouldProxyCallbackToCustomDomain(r, session) {
+ // Auth request is from custom domain, proxy callback there
+ logRequest(r).Debug("Redirecting auth callback to custom domain")
+
+ // Store access token
+ proxyDomain := session.Values["proxy_auth_domain"].(string)
+
+ // Clear proxying from session
+ delete(session.Values, "proxy_auth_domain")
+ err := session.Save(r, w)
+ if err != nil {
+ logRequest(r).WithError(err).Error("Failed to save the session")
+ httperrors.Serve500(w)
+ return true
+ }
+
+ // Redirect pages under custom domain
+ http.Redirect(w, r, proxyDomain+r.URL.Path+"?"+r.URL.RawQuery, 302)
+
+ return true
+ }
+
+ return false
+}
+
+func getRequestAddress(r *http.Request) string {
+ if r.TLS != nil {
+ return "https://" + r.Host + r.RequestURI
+ }
+ return "http://" + r.Host + r.RequestURI
+}
+
+func getRequestDomain(r *http.Request) string {
+ if r.TLS != nil {
+ return "https://" + r.Host
+ }
+ return "http://" + r.Host
+}
+
+func shouldProxyAuth(r *http.Request) bool {
+ return r.URL.Query().Get("domain") != "" && r.URL.Query().Get("state") != ""
+}
+
+func shouldProxyCallbackToCustomDomain(r *http.Request, session *sessions.Session) bool {
+ return session.Values["proxy_auth_domain"] != nil
+}
+
+func validateState(r *http.Request, session *sessions.Session) bool {
+ state := r.URL.Query().Get("state")
+ if state == "" {
+ // No state param
+ return false
+ }
+
+ // Check state
+ if session.Values["state"] == nil || session.Values["state"].(string) != state {
+ // State does not match
+ return false
+ }
+
+ // State ok
+ return true
+}
+
+func verifyCodeAndStateGiven(r *http.Request) bool {
+ return r.URL.Query().Get("code") != "" && r.URL.Query().Get("state") != ""
+}
+
+func (a *Auth) fetchAccessToken(code string) (tokenResponse, error) {
+ token := tokenResponse{}
+
+ // Prepare request
+ url := fmt.Sprintf(tokenURLTemplate, a.gitLabServer)
+ content := fmt.Sprintf(tokenContentTemplate, a.clientID, a.clientSecret, code, a.redirectURI)
+ req, err := http.NewRequest("POST", url, strings.NewReader(content))
+
+ if err != nil {
+ return token, err
+ }
+
+ // Request token
+ resp, err := a.apiClient.Do(req)
+
+ if err != nil {
+ return token, err
+ }
+
+ if resp.StatusCode != 200 {
+ return token, errors.New("response was not OK")
+ }
+
+ // Parse response
+ defer resp.Body.Close()
+ err = json.NewDecoder(resp.Body).Decode(&token)
+ if err != nil {
+ return token, err
+ }
+
+ return token, nil
+}
+
+func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter, r *http.Request) bool {
+ // If no access token redirect to OAuth login page
+ if session.Values["access_token"] == nil {
+ logRequest(r).Debug("No access token exists, redirecting user to OAuth2 login")
+
+ // Generate state hash and store requested address
+ state := base64.URLEncoding.EncodeToString(securecookie.GenerateRandomKey(16))
+ session.Values["state"] = state
+ session.Values["uri"] = getRequestAddress(r)
+
+ // Clear possible proxying
+ delete(session.Values, "proxy_auth_domain")
+
+ err := session.Save(r, w)
+ if err != nil {
+ logRequest(r).WithError(err).Error("Failed to save the session")
+ httperrors.Serve500(w)
+ return true
+ }
+
+ // Because the pages domain might be in public suffix list, we have to
+ // redirect to pages domain to trigger authorization flow
+ http.Redirect(w, r, a.getProxyAddress(r, state), 302)
+
+ return true
+ }
+ return false
+}
+
+func (a *Auth) getProxyAddress(r *http.Request, state string) string {
+ return fmt.Sprintf(authorizeProxyTemplate, a.redirectURI, getRequestDomain(r), state)
+}
+
+func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Request) {
+ logRequest(r).Debug("Destroying session")
+
+ // Invalidate access token and redirect back for refreshing and re-authenticating
+ delete(session.Values, "access_token")
+ err := session.Save(r, w)
+ if err != nil {
+ logRequest(r).WithError(err).Error("Failed to save the session")
+ httperrors.Serve500(w)
+ return
+ }
+
+ http.Redirect(w, r, getRequestAddress(r), 302)
+}
+
+// IsAuthSupported checks if pages is running with the authentication support
+func (a *Auth) IsAuthSupported() bool {
+ if a == nil {
+ return false
+ }
+ return true
+}
+
+// CheckAuthenticationWithoutProject checks if user is authenticated and has a valid token
+func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http.Request) bool {
+
+ if a == nil {
+ // No auth supported
+ return false
+ }
+
+ session, err := a.checkSession(w, r)
+ if err != nil {
+ return true
+ }
+
+ if a.checkTokenExists(session, w, r) {
+ return true
+ }
+
+ // Access token exists, authorize request
+ url := fmt.Sprintf(apiURLUserTemplate, a.gitLabServer)
+ req, err := http.NewRequest("GET", url, nil)
+
+ if err != nil {
+ logRequest(r).WithError(err).Debug("Failed to authenticate request")
+
+ httperrors.Serve500(w)
+ return true
+ }
+
+ req.Header.Add("Authorization", "Bearer "+session.Values["access_token"].(string))
+ resp, err := a.apiClient.Do(req)
+
+ if checkResponseForInvalidToken(resp, err) {
+ logRequest(r).Debug("Access token was invalid, destroying session")
+
+ destroySession(session, w, r)
+ return true
+ }
+
+ if err != nil || resp.StatusCode != 200 {
+ // We return 404 if for some reason token is not valid to avoid (not) existence leak
+ if err != nil {
+ logRequest(r).WithError(err).Debug("Failed to retrieve info with token")
+ }
+
+ httperrors.Serve404(w)
+ return true
+ }
+
+ return false
+}
+
+// CheckAuthentication checks if user is authenticated and has access to the project
+func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool {
+
+ if a == nil {
+ logRequest(r).Debug("Authentication is not configured")
+ httperrors.Serve500(w)
+ return true
+ }
+
+ session, err := a.checkSession(w, r)
+ if err != nil {
+ return true
+ }
+
+ if a.checkTokenExists(session, w, r) {
+ return true
+ }
+
+ // Access token exists, authorize request
+ url := fmt.Sprintf(apiURLProjectTemplate, a.gitLabServer, projectID)
+ req, err := http.NewRequest("GET", url, nil)
+
+ if err != nil {
+ httperrors.Serve500(w)
+ return true
+ }
+
+ req.Header.Add("Authorization", "Bearer "+session.Values["access_token"].(string))
+ resp, err := a.apiClient.Do(req)
+
+ if checkResponseForInvalidToken(resp, err) {
+ logRequest(r).Debug("Access token was invalid, destroying session")
+
+ destroySession(session, w, r)
+ return true
+ }
+
+ if err != nil || resp.StatusCode != 200 {
+ if err != nil {
+ logRequest(r).WithError(err).Debug("Failed to retrieve info with token")
+ }
+
+ // We return 404 if user has no access to avoid user knowing if the pages really existed or not
+ httperrors.Serve404(w)
+ return true
+ }
+
+ return false
+}
+
+func checkResponseForInvalidToken(resp *http.Response, err error) bool {
+ if err == nil && resp.StatusCode == 401 {
+ errResp := errorResponse{}
+
+ // Parse response
+ defer resp.Body.Close()
+ err := json.NewDecoder(resp.Body).Decode(&errResp)
+ if err != nil {
+ return false
+ }
+
+ if errResp.Error == "invalid_token" {
+ // Token is invalid
+ return true
+ }
+ }
+
+ return false
+}
+
+func logRequest(r *http.Request) *log.Entry {
+ return log.WithFields(log.Fields{
+ "host": r.Host,
+ "path": r.RequestURI,
+ })
+}
+
+// New when authentication supported this will be used to create authentication handler
+func New(pagesDomain string, storeSecret string, clientID string, clientSecret string,
+ redirectURI string, gitLabServer string) *Auth {
+ return &Auth{
+ pagesDomain: pagesDomain,
+ clientID: clientID,
+ clientSecret: clientSecret,
+ redirectURI: redirectURI,
+ gitLabServer: strings.TrimRight(gitLabServer, "/"),
+ apiClient: &http.Client{
+ Timeout: 5 * time.Second,
+ Transport: httptransport.Transport,
+ },
+ store: sessions.NewCookieStore([]byte(storeSecret)),
+ }
+}
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go
new file mode 100644
index 00000000..4973ce01
--- /dev/null
+++ b/internal/auth/auth_test.go
@@ -0,0 +1,295 @@
+package auth_test
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "sync"
+ "testing"
+
+ "github.com/gorilla/sessions"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/auth"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/domain"
+)
+
+func createAuth(t *testing.T) *auth.Auth {
+ return auth.New("pages.gitlab-example.com",
+ "something-very-secret",
+ "id",
+ "secret",
+ "http://pages.gitlab-example.com/auth",
+ "http://gitlab-example.com")
+}
+
+func TestTryAuthenticate(t *testing.T) {
+ auth := createAuth(t)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/something/else")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ assert.Equal(t, false, auth.TryAuthenticate(result, r, make(domain.Map), &sync.RWMutex{}))
+}
+
+func TestTryAuthenticateWithError(t *testing.T) {
+ auth := createAuth(t)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/auth?error=access_denied")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ assert.Equal(t, true, auth.TryAuthenticate(result, r, make(domain.Map), &sync.RWMutex{}))
+ assert.Equal(t, 401, result.Code)
+}
+
+func TestTryAuthenticateWithCodeButInvalidState(t *testing.T) {
+ store := sessions.NewCookieStore([]byte("something-very-secret"))
+ auth := createAuth(t)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/auth?code=1&state=invalid")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ session, _ := store.Get(r, "gitlab-pages")
+ session.Values["state"] = "state"
+ session.Save(r, result)
+
+ assert.Equal(t, true, auth.TryAuthenticate(result, r, make(domain.Map), &sync.RWMutex{}))
+ assert.Equal(t, 401, result.Code)
+}
+
+func TestTryAuthenticateWithCodeAndState(t *testing.T) {
+ apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/oauth/token":
+ assert.Equal(t, "POST", r.Method)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, "{\"access_token\":\"abc\"}")
+ case "/api/v4/projects/1000/pages_access":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusOK)
+ default:
+ t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+
+ apiServer.Start()
+ defer apiServer.Close()
+
+ store := sessions.NewCookieStore([]byte("something-very-secret"))
+ auth := auth.New("pages.gitlab-example.com",
+ "something-very-secret",
+ "id",
+ "secret",
+ "http://pages.gitlab-example.com/auth",
+ apiServer.URL)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/auth?code=1&state=state")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ session, _ := store.Get(r, "gitlab-pages")
+ session.Values["uri"] = "http://pages.gitlab-example.com/project/"
+ session.Values["state"] = "state"
+ session.Save(r, result)
+
+ assert.Equal(t, true, auth.TryAuthenticate(result, r, make(domain.Map), &sync.RWMutex{}))
+ assert.Equal(t, 302, result.Code)
+ assert.Equal(t, "http://pages.gitlab-example.com/project/", result.Header().Get("Location"))
+}
+
+func TestCheckAuthenticationWhenAccess(t *testing.T) {
+ apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v4/projects/1000/pages_access":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusOK)
+ default:
+ t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+
+ apiServer.Start()
+ defer apiServer.Close()
+
+ store := sessions.NewCookieStore([]byte("something-very-secret"))
+ auth := auth.New("pages.gitlab-example.com",
+ "something-very-secret",
+ "id",
+ "secret",
+ "http://pages.gitlab-example.com/auth",
+ apiServer.URL)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/auth?code=1&state=state")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ session, _ := store.Get(r, "gitlab-pages")
+ session.Values["access_token"] = "abc"
+ session.Save(r, result)
+
+ assert.Equal(t, false, auth.CheckAuthentication(result, r, 1000))
+ assert.Equal(t, 200, result.Code)
+}
+
+func TestCheckAuthenticationWhenNoAccess(t *testing.T) {
+ apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v4/projects/1000/pages_access":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusUnauthorized)
+ default:
+ t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+
+ apiServer.Start()
+ defer apiServer.Close()
+
+ store := sessions.NewCookieStore([]byte("something-very-secret"))
+ auth := auth.New("pages.gitlab-example.com",
+ "something-very-secret",
+ "id",
+ "secret",
+ "http://pages.gitlab-example.com/auth",
+ apiServer.URL)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/auth?code=1&state=state")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ session, _ := store.Get(r, "gitlab-pages")
+ session.Values["access_token"] = "abc"
+ session.Save(r, result)
+
+ assert.Equal(t, true, auth.CheckAuthentication(result, r, 1000))
+ assert.Equal(t, 404, result.Code)
+}
+
+func TestCheckAuthenticationWhenInvalidToken(t *testing.T) {
+ apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v4/projects/1000/pages_access":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprint(w, "{\"error\":\"invalid_token\"}")
+ default:
+ t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+
+ apiServer.Start()
+ defer apiServer.Close()
+
+ store := sessions.NewCookieStore([]byte("something-very-secret"))
+ auth := auth.New("pages.gitlab-example.com",
+ "something-very-secret",
+ "id",
+ "secret",
+ "http://pages.gitlab-example.com/auth",
+ apiServer.URL)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/auth?code=1&state=state")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ session, _ := store.Get(r, "gitlab-pages")
+ session.Values["access_token"] = "abc"
+ session.Save(r, result)
+
+ assert.Equal(t, true, auth.CheckAuthentication(result, r, 1000))
+ assert.Equal(t, 302, result.Code)
+}
+
+func TestCheckAuthenticationWithoutProject(t *testing.T) {
+ apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v4/user":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusOK)
+ default:
+ t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+
+ apiServer.Start()
+ defer apiServer.Close()
+
+ store := sessions.NewCookieStore([]byte("something-very-secret"))
+ auth := auth.New("pages.gitlab-example.com",
+ "something-very-secret",
+ "id",
+ "secret",
+ "http://pages.gitlab-example.com/auth",
+ apiServer.URL)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/auth?code=1&state=state")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ session, _ := store.Get(r, "gitlab-pages")
+ session.Values["access_token"] = "abc"
+ session.Save(r, result)
+
+ assert.Equal(t, false, auth.CheckAuthenticationWithoutProject(result, r))
+ assert.Equal(t, 200, result.Code)
+}
+
+func TestCheckAuthenticationWithoutProjectWhenInvalidToken(t *testing.T) {
+ apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v4/user":
+ assert.Equal(t, "Bearer abc", r.Header.Get("Authorization"))
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprint(w, "{\"error\":\"invalid_token\"}")
+ default:
+ t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+
+ apiServer.Start()
+ defer apiServer.Close()
+
+ store := sessions.NewCookieStore([]byte("something-very-secret"))
+ auth := auth.New("pages.gitlab-example.com",
+ "something-very-secret",
+ "id",
+ "secret",
+ "http://pages.gitlab-example.com/auth",
+ apiServer.URL)
+
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse("/auth?code=1&state=state")
+ require.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+
+ session, _ := store.Get(r, "gitlab-pages")
+ session.Values["access_token"] = "abc"
+ session.Save(r, result)
+
+ assert.Equal(t, true, auth.CheckAuthenticationWithoutProject(result, r))
+ assert.Equal(t, 302, result.Code)
+}
diff --git a/internal/domain/domain.go b/internal/domain/domain.go
index 44703472..c9bda506 100644
--- a/internal/domain/domain.go
+++ b/internal/domain/domain.go
@@ -25,7 +25,10 @@ type locationDirectoryError struct {
}
type project struct {
- HTTPSOnly bool
+ NamespaceProject bool
+ HTTPSOnly bool
+ AccessControl bool
+ ID uint64
}
type projects map[string]*project
@@ -151,6 +154,79 @@ func (d *D) IsHTTPSOnly(r *http.Request) bool {
return false
}
+// IsAccessControlEnabled figures out if the request is to a project that has access control enabled
+func (d *D) IsAccessControlEnabled(r *http.Request) bool {
+ if d == nil {
+ return false
+ }
+
+ // Check custom domain config (e.g. http://example.com)
+ if d.config != nil {
+ return d.config.AccessControl
+ }
+
+ // Check projects served under the group domain, including the default one
+ if project, _, _ := d.getProjectWithSubpath(r); project != nil {
+ return project.AccessControl
+ }
+
+ return false
+}
+
+// IsNamespaceProject figures out if the request is to a namespace project
+func (d *D) IsNamespaceProject(r *http.Request) bool {
+ if d == nil {
+ return false
+ }
+
+ // If request is to a custom domain, we do not handle it as a namespace project
+ // as there can't be multiple projects under the same custom domain
+ if d.config != nil {
+ return false
+ }
+
+ // Check projects served under the group domain, including the default one
+ if project, _, _ := d.getProjectWithSubpath(r); project != nil {
+ return project.NamespaceProject
+ }
+
+ return false
+}
+
+// GetID figures out what is the ID of the project user tries to access
+func (d *D) GetID(r *http.Request) uint64 {
+ if d == nil {
+ return 0
+ }
+
+ if d.config != nil {
+ return d.config.ID
+ }
+
+ if project, _, _ := d.getProjectWithSubpath(r); project != nil {
+ return project.ID
+ }
+
+ return 0
+}
+
+// HasProject figures out if the project exists that the user tries to access
+func (d *D) HasProject(r *http.Request) bool {
+ if d == nil {
+ return false
+ }
+
+ if d.config != nil {
+ return true
+ }
+
+ if project, _, _ := d.getProjectWithSubpath(r); project != nil {
+ return true
+ }
+
+ return false
+}
+
func (d *D) serveFile(w http.ResponseWriter, r *http.Request, origPath string) error {
fullPath := handleGZip(w, r, origPath)
@@ -165,9 +241,11 @@ func (d *D) serveFile(w http.ResponseWriter, r *http.Request, origPath string) e
return err
}
- // Set caching headers
- w.Header().Set("Cache-Control", "max-age=600")
- w.Header().Set("Expires", time.Now().Add(10*time.Minute).Format(time.RFC1123))
+ if !d.IsAccessControlEnabled(r) {
+ // Set caching headers
+ w.Header().Set("Cache-Control", "max-age=600")
+ w.Header().Set("Expires", time.Now().Add(10*time.Minute).Format(time.RFC1123))
+ }
// ServeContent sets Content-Type for us
http.ServeContent(w, r, origPath, fi.ModTime(), file)
@@ -279,20 +357,26 @@ func (d *D) tryFile(w http.ResponseWriter, r *http.Request, projectName string,
return d.serveFile(w, r, fullPath)
}
-func (d *D) serveFromGroup(w http.ResponseWriter, r *http.Request) {
+func (d *D) serveFileFromGroup(w http.ResponseWriter, r *http.Request) bool {
project, projectName, subPath := d.getProjectWithSubpath(r)
if project == nil {
httperrors.Serve404(w)
- return
+ return true
}
if d.tryFile(w, r, projectName, subPath) == nil {
- return
+ return true
}
- // FIXME: In the public namespace project case, since we only serve these
- // 404s if the project does not exist, they will reveal the existence of
- // private projects once access control is implemented.
+ return false
+}
+
+func (d *D) serveNotFoundFromGroup(w http.ResponseWriter, r *http.Request) {
+ project, projectName, _ := d.getProjectWithSubpath(r)
+ if project == nil {
+ httperrors.Serve404(w)
+ return
+ }
// Try serving custom not-found page
if d.tryNotFound(w, r, projectName) == nil {
@@ -303,12 +387,16 @@ func (d *D) serveFromGroup(w http.ResponseWriter, r *http.Request) {
httperrors.Serve404(w)
}
-func (d *D) serveFromConfig(w http.ResponseWriter, r *http.Request) {
+func (d *D) serveFileFromConfig(w http.ResponseWriter, r *http.Request) bool {
// Try to serve file for http://host/... => /group/project/...
if d.tryFile(w, r, d.projectName, r.URL.Path) == nil {
- return
+ return true
}
+ return false
+}
+
+func (d *D) serveNotFoundFromConfig(w http.ResponseWriter, r *http.Request) {
// Try serving not found page for http://host/ => /group/project/404.html
if d.tryNotFound(w, r, d.projectName) == nil {
return
@@ -335,12 +423,31 @@ func (d *D) EnsureCertificate() (*tls.Certificate, error) {
return d.certificate, d.certificateError
}
-// ServeHTTP implements http.Handler.
-func (d *D) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+// ServeFileHTTP implements http.Handler. Returns true if something was served, false if not.
+func (d *D) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool {
+ if d == nil {
+ httperrors.Serve404(w)
+ return true
+ }
+
+ if d.config != nil {
+ return d.serveFileFromConfig(w, r)
+ }
+
+ return d.serveFileFromGroup(w, r)
+}
+
+// ServeNotFoundHTTP implements http.Handler. Serves the not found pages from the projects.
+func (d *D) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) {
+ if d == nil {
+ httperrors.Serve404(w)
+ return
+ }
+
if d.config != nil {
- d.serveFromConfig(w, r)
+ d.serveNotFoundFromConfig(w, r)
} else {
- d.serveFromGroup(w, r)
+ d.serveNotFoundFromGroup(w, r)
}
}
diff --git a/internal/domain/domain_config.go b/internal/domain/domain_config.go
index ed7e0820..2ab2ce6c 100644
--- a/internal/domain/domain_config.go
+++ b/internal/domain/domain_config.go
@@ -8,15 +8,19 @@ import (
)
type domainConfig struct {
- Domain string
- Certificate string
- Key string
- HTTPSOnly bool `json:"https_only"`
+ Domain string
+ Certificate string
+ Key string
+ HTTPSOnly bool `json:"https_only"`
+ ID uint64 `json:"id"`
+ AccessControl bool `json:"access_control"`
}
type domainsConfig struct {
- Domains []domainConfig
- HTTPSOnly bool `json:"https_only"`
+ Domains []domainConfig
+ HTTPSOnly bool `json:"https_only"`
+ ID uint64 `json:"id"`
+ AccessControl bool `json:"access_control"`
}
func (c *domainConfig) Valid(rootDomain string) bool {
diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go
index 38a4bfed..de780197 100644
--- a/internal/domain/domain_test.go
+++ b/internal/domain/domain_test.go
@@ -17,6 +17,14 @@ import (
"gitlab.com/gitlab-org/gitlab-pages/internal/fixture"
)
+func serveFileOrNotFound(domain *D) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if !domain.ServeFileHTTP(w, r) {
+ domain.ServeNotFoundHTTP(w, r)
+ }
+ }
+}
+
func TestGroupServeHTTP(t *testing.T) {
setUpTests()
@@ -31,26 +39,27 @@ func TestGroupServeHTTP(t *testing.T) {
},
}
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/", nil, "main-dir")
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/index.html", nil, "main-dir")
- assert.HTTPRedirect(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project", nil)
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project", nil,
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/", nil, "main-dir")
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/index.html", nil, "main-dir")
+ assert.HTTPRedirect(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project", nil)
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project", nil,
`<a href="//group.test.io/project/">Found</a>`)
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/", nil, "project-subdir")
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/index.html", nil, "project-subdir")
- assert.HTTPRedirect(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/subdir", nil)
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/subdir", nil,
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/", nil, "project-subdir")
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/index.html", nil, "project-subdir")
+ assert.HTTPRedirect(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/subdir", nil)
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/subdir", nil,
`<a href="//group.test.io/project/subdir/">Found</a>`)
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/subdir/", nil, "project-subsubdir")
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project2/", nil, "project2-main")
- assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project2/index.html", nil, "project2-main")
- assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io//about.gitlab.com/%2e%2e", nil)
- assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/symlink", nil)
- assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/symlink/index.html", nil)
- assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/symlink/subdir/", nil)
- assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/fifo", nil)
- assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/not-existing-file", nil)
- assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project//about.gitlab.com/%2e%2e", nil)
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/subdir/", nil, "project-subsubdir")
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project2/", nil, "project2-main")
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project2/index.html", nil, "project2-main")
+ assert.HTTPRedirect(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/private.project/", nil)
+ assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io//about.gitlab.com/%2e%2e", nil)
+ assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/symlink", nil)
+ assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/symlink/index.html", nil)
+ assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/symlink/subdir/", nil)
+ assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/fifo", nil)
+ assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/not-existing-file", nil)
+ assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project//about.gitlab.com/%2e%2e", nil)
}
func TestDomainServeHTTP(t *testing.T) {
@@ -64,15 +73,15 @@ func TestDomainServeHTTP(t *testing.T) {
},
}
- assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/", nil, "project2-main")
- assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/index.html", nil, "project2-main")
- assert.HTTPRedirect(t, testDomain.ServeHTTP, "GET", "/subdir", nil)
- assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/subdir", nil,
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/", nil, "project2-main")
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/index.html", nil, "project2-main")
+ assert.HTTPRedirect(t, serveFileOrNotFound(testDomain), "GET", "/subdir", nil)
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir", nil,
`<a href="/subdir/">Found</a>`)
- assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/subdir/", nil, "project2-subdir")
- assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/subdir/index.html", nil, "project2-subdir")
- assert.HTTPError(t, testDomain.ServeHTTP, "GET", "//about.gitlab.com/%2e%2e", nil)
- assert.HTTPError(t, testDomain.ServeHTTP, "GET", "/not-existing-file", nil)
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir/", nil, "project2-subdir")
+ assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir/index.html", nil, "project2-subdir")
+ assert.HTTPError(t, serveFileOrNotFound(testDomain), "GET", "//about.gitlab.com/%2e%2e", nil)
+ assert.HTTPError(t, serveFileOrNotFound(testDomain), "GET", "/not-existing-file", nil)
}
func TestIsHTTPSOnly(t *testing.T) {
@@ -249,7 +258,7 @@ func TestGroupServeHTTPGzip(t *testing.T) {
}
for _, tt := range testSet {
- testHTTPGzip(t, testGroup.ServeHTTP, tt.mode, tt.url, tt.params, tt.acceptEncoding, tt.body, tt.ungzip)
+ testHTTPGzip(t, serveFileOrNotFound(testGroup), tt.mode, tt.url, tt.params, tt.acceptEncoding, tt.body, tt.ungzip)
}
}
@@ -280,15 +289,15 @@ func TestGroup404ServeHTTP(t *testing.T) {
},
}
- testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/project.404/not/existing-file", nil, "Custom 404 project page")
- testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/project.404/", nil, "Custom 404 project page")
- testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/not/existing-file", nil, "Custom 404 group page")
- testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page")
- testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/", nil, "Custom 404 group page")
- assert.HTTPBodyNotContains(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/project.404.symlink/not/existing-file", nil, "Custom 404 project page")
+ testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404/not/existing-file", nil, "Custom 404 project page")
+ testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404/", nil, "Custom 404 project page")
+ testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/not/existing-file", nil, "Custom 404 group page")
+ testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page")
+ testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/", nil, "Custom 404 group page")
+ assert.HTTPBodyNotContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404.symlink/not/existing-file", nil, "Custom 404 project page")
// Ensure the namespace project's custom 404.html is not used by projects
- testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/project.no.404/not/existing-file", nil, "The page you're looking for could not be found.")
+ testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.no.404/not/existing-file", nil, "The page you're looking for could not be found.")
}
func TestDomain404ServeHTTP(t *testing.T) {
@@ -302,8 +311,8 @@ func TestDomain404ServeHTTP(t *testing.T) {
},
}
- testHTTP404(t, testDomain.ServeHTTP, "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page")
- testHTTP404(t, testDomain.ServeHTTP, "GET", "http://group.404.test.io/", nil, "Custom 404 group page")
+ testHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page")
+ testHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/", nil, "Custom 404 group page")
}
func TestPredefined404ServeHTTP(t *testing.T) {
@@ -313,7 +322,7 @@ func TestPredefined404ServeHTTP(t *testing.T) {
group: "group",
}
- testHTTP404(t, testDomain.ServeHTTP, "GET", "http://group.test.io/not-existing-file", nil, "The page you're looking for could not be found")
+ testHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.test.io/not-existing-file", nil, "The page you're looking for could not be found")
}
func TestGroupCertificate(t *testing.T) {
@@ -373,7 +382,7 @@ func TestCacheControlHeaders(t *testing.T) {
require.NoError(t, err)
now := time.Now()
- testGroup.ServeHTTP(w, req)
+ serveFileOrNotFound(testGroup)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "max-age=600", w.Header().Get("Cache-Control"))
diff --git a/internal/domain/map.go b/internal/domain/map.go
index b7cb5c67..d2e7c74f 100644
--- a/internal/domain/map.go
+++ b/internal/domain/map.go
@@ -46,7 +46,7 @@ func (dm Map) addDomain(rootDomain, group, projectName string, config *domainCon
dm.updateDomainMap(domainName, newDomain)
}
-func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly bool) {
+func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly bool, accessControl bool, id uint64) {
domainName := strings.ToLower(group + "." + rootDomain)
groupDomain := dm[domainName]
@@ -58,7 +58,10 @@ func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly
}
groupDomain.projects[strings.ToLower(projectName)] = &project{
- HTTPSOnly: httpsOnly,
+ NamespaceProject: domainName == strings.ToLower(projectName),
+ HTTPSOnly: httpsOnly,
+ AccessControl: accessControl,
+ ID: id,
}
dm[domainName] = groupDomain
@@ -69,11 +72,11 @@ func (dm Map) readProjectConfig(rootDomain string, group, projectName string, co
// This is necessary to preserve the previous behaviour where a
// group domain is created even if no config.json files are
// loaded successfully. Is it safe to remove this?
- dm.updateGroupDomain(rootDomain, group, projectName, false)
+ dm.updateGroupDomain(rootDomain, group, projectName, false, false, 0)
return
}
- dm.updateGroupDomain(rootDomain, group, projectName, config.HTTPSOnly)
+ dm.updateGroupDomain(rootDomain, group, projectName, config.HTTPSOnly, config.AccessControl, config.ID)
for _, domainConfig := range config.Domains {
config := domainConfig // domainConfig is reused for each loop iteration
diff --git a/internal/domain/map_test.go b/internal/domain/map_test.go
index 21d547ac..31ebc016 100644
--- a/internal/domain/map_test.go
+++ b/internal/domain/map_test.go
@@ -51,6 +51,8 @@ func TestReadProjects(t *testing.T) {
"test.my-domain.com",
"test2.my-domain.com",
"no.cert.com",
+ "private.domain.com",
+ "group.auth.test.io",
}
for _, expected := range domains {
diff --git a/internal/httperrors/httperrors.go b/internal/httperrors/httperrors.go
index 92413e07..1ae5224b 100644
--- a/internal/httperrors/httperrors.go
+++ b/internal/httperrors/httperrors.go
@@ -14,6 +14,13 @@ type content struct {
}
var (
+ content401 = content{
+ http.StatusUnauthorized,
+ "Unauthorized (401)",
+ "401",
+ "You don't have permission to access the resource.",
+ `<p>The resource that you are attempting to access is protected and you don't have the necessary permissions to view it.</p>`,
+ }
content404 = content{
http.StatusNotFound,
"The page you're looking for could not be found (404)",
@@ -155,6 +162,11 @@ func serveErrorPage(w http.ResponseWriter, c content) {
fmt.Fprintln(w, generateErrorHTML(c))
}
+// Serve401 returns a 401 error response / HTML page to the http.ResponseWriter
+func Serve401(w http.ResponseWriter) {
+ serveErrorPage(w, content401)
+}
+
// Serve404 returns a 404 error response / HTML page to the http.ResponseWriter
func Serve404(w http.ResponseWriter) {
serveErrorPage(w, content404)
diff --git a/internal/httperrors/httperrors_test.go b/internal/httperrors/httperrors_test.go
index 1a79d850..be532dfe 100644
--- a/internal/httperrors/httperrors_test.go
+++ b/internal/httperrors/httperrors_test.go
@@ -68,6 +68,18 @@ func TestServeErrorPage(t *testing.T) {
assert.Equal(t, w.Status(), testingContent.status)
}
+func TestServe401(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ Serve401(w)
+ assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
+ assert.Equal(t, w.Header().Get("X-Content-Type-Options"), "nosniff")
+ assert.Equal(t, w.Status(), content401.status)
+ assert.Contains(t, w.Content(), content401.title)
+ assert.Contains(t, w.Content(), content401.statusString)
+ assert.Contains(t, w.Content(), content401.header)
+ assert.Contains(t, w.Content(), content401.subHeader)
+}
+
func TestServe404(t *testing.T) {
w := newTestResponseWriter(httptest.NewRecorder())
Serve404(w)
diff --git a/internal/artifact/transport.go b/internal/httptransport/transport.go
index da182df6..207531f4 100644
--- a/internal/artifact/transport.go
+++ b/internal/httptransport/transport.go
@@ -1,4 +1,4 @@
-package artifact
+package httptransport
import (
"crypto/tls"
@@ -16,7 +16,8 @@ var (
sysPoolOnce = &sync.Once{}
sysPool *x509.CertPool
- transport = &http.Transport{
+ // Transport can be used with httpclient with TLS and certificates
+ Transport = &http.Transport{
DialTLS: func(network, addr string) (net.Conn, error) {
return tls.Dial(network, addr, &tls.Config{RootCAs: pool()})
},
diff --git a/main.go b/main.go
index 30f594ea..20979aa8 100644
--- a/main.go
+++ b/main.go
@@ -46,6 +46,11 @@ var (
adminHTTPSListener = flag.String("admin-https-listener", "", "The listen address for the admin API HTTPS listener (optional)")
adminHTTPSCert = flag.String("admin-https-cert", "", "The path to the certificate file for the admin API (optional)")
adminHTTPSKey = flag.String("admin-https-key", "", "The path to the key file for the admin API (optional)")
+ secret = flag.String("auth-secret", "", "Cookie store hash key, should be at least 32 bytes long.")
+ gitLabServer = flag.String("auth-server", "", "GitLab server, for example https://www.gitlab.com")
+ clientID = flag.String("auth-client-id", "", "GitLab application Client ID")
+ clientSecret = flag.String("auth-client-secret", "", "GitLab application Client Secret")
+ redirectURI = flag.String("auth-redirect-uri", "", "GitLab application redirect URI")
disableCrossOriginRequests = flag.Bool("disable-cross-origin-requests", false, "Disable cross-origin requests")
@@ -58,6 +63,12 @@ var (
var (
errArtifactSchemaUnsupported = errors.New("artifacts-server scheme must be either http:// or https://")
errArtifactsServerTimeoutValue = errors.New("artifacts-server-timeout must be greater than or equal to 1")
+
+ errSecretNotDefined = errors.New("auth-secret must be defined if authentication is supported")
+ errClientIDNotDefined = errors.New("auth-client-id must be defined if authentication is supported")
+ errClientSecretNotDefined = errors.New("auth-client-secret must be defined if authentication is supported")
+ errGitLabServerNotDefined = errors.New("auth-server must be defined if authentication is supported")
+ errRedirectURINotDefined = errors.New("auth-redirect-uri must be defined if authentication is supported")
)
func configFromFlags() appConfig {
@@ -107,9 +118,44 @@ func configFromFlags() appConfig {
config.ArtifactsServerTimeout = *artifactsServerTimeout
config.ArtifactsServer = *artifactsServer
}
+
+ checkAuthenticationConfig(config)
+
+ config.StoreSecret = *secret
+ config.ClientID = *clientID
+ config.ClientSecret = *clientSecret
+ config.GitLabServer = *gitLabServer
+ config.RedirectURI = *redirectURI
+
return config
}
+func checkAuthenticationConfig(config appConfig) {
+ if *secret != "" || *clientID != "" || *clientSecret != "" ||
+ *gitLabServer != "" || *redirectURI != "" {
+ // Check all auth params are valid
+ assertAuthConfig()
+ }
+}
+
+func assertAuthConfig() {
+ if *secret == "" {
+ log.Fatal(errSecretNotDefined)
+ }
+ if *clientID == "" {
+ log.Fatal(errClientIDNotDefined)
+ }
+ if *clientSecret == "" {
+ log.Fatal(errClientSecretNotDefined)
+ }
+ if *gitLabServer == "" {
+ log.Fatal(errGitLabServerNotDefined)
+ }
+ if *redirectURI == "" {
+ log.Fatal(errRedirectURINotDefined)
+ }
+}
+
func appMain() {
var showVersion = flag.Bool("version", false, "Show version")
@@ -160,6 +206,11 @@ func appMain() {
"root-key": *pagesRootCert,
"status_path": config.StatusPath,
"use-http-2": config.HTTP2,
+ "auth-secret": config.StoreSecret,
+ "auth-server": config.GitLabServer,
+ "auth-client-id": config.ClientID,
+ "auth-client-secret": config.ClientSecret,
+ "auth-redirect-uri": config.RedirectURI,
}).Debug("Start daemon with configuration")
for _, cs := range [][]io.Closer{
diff --git a/server.go b/server.go
index cfd6b993..120088b0 100644
--- a/server.go
+++ b/server.go
@@ -8,6 +8,7 @@ import (
"os"
"time"
+ "github.com/gorilla/context"
"golang.org/x/net/http2"
)
@@ -29,7 +30,7 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
func listenAndServe(fd uintptr, handler http.HandlerFunc, useHTTP2 bool, tlsConfig *tls.Config) error {
// create server
- server := &http.Server{Handler: handler, TLSConfig: tlsConfig}
+ server := &http.Server{Handler: context.ClearHandler(handler), TLSConfig: tlsConfig}
if useHTTP2 {
err := http2.ConfigureServer(server, &http2.Server{})
diff --git a/shared/pages/group.auth/group.auth.gitlab-example.com/public/index.html b/shared/pages/group.auth/group.auth.gitlab-example.com/public/index.html
new file mode 100644
index 00000000..d86bac9d
--- /dev/null
+++ b/shared/pages/group.auth/group.auth.gitlab-example.com/public/index.html
@@ -0,0 +1 @@
+OK
diff --git a/shared/pages/group.auth/group.auth.gitlab-example.com/public/private.project/index.html b/shared/pages/group.auth/group.auth.gitlab-example.com/public/private.project/index.html
new file mode 100644
index 00000000..7c9933f7
--- /dev/null
+++ b/shared/pages/group.auth/group.auth.gitlab-example.com/public/private.project/index.html
@@ -0,0 +1 @@
+domain project subdirectory
diff --git a/shared/pages/group.auth/private.project.1/config.json b/shared/pages/group.auth/private.project.1/config.json
new file mode 100644
index 00000000..dbff776f
--- /dev/null
+++ b/shared/pages/group.auth/private.project.1/config.json
@@ -0,0 +1 @@
+{ "domains": [], "id": 2000, "access_control": true }
diff --git a/shared/pages/group.auth/private.project.1/public/index.html b/shared/pages/group.auth/private.project.1/public/index.html
new file mode 100644
index 00000000..c8c6761a
--- /dev/null
+++ b/shared/pages/group.auth/private.project.1/public/index.html
@@ -0,0 +1 @@
+private \ No newline at end of file
diff --git a/shared/pages/group.auth/private.project.2/config.json b/shared/pages/group.auth/private.project.2/config.json
new file mode 100644
index 00000000..6c595219
--- /dev/null
+++ b/shared/pages/group.auth/private.project.2/config.json
@@ -0,0 +1 @@
+{ "domains": [], "id": 3000, "access_control": true }
diff --git a/shared/pages/group.auth/private.project.2/public/index.html b/shared/pages/group.auth/private.project.2/public/index.html
new file mode 100644
index 00000000..c8c6761a
--- /dev/null
+++ b/shared/pages/group.auth/private.project.2/public/index.html
@@ -0,0 +1 @@
+private \ No newline at end of file
diff --git a/shared/pages/group.auth/private.project/config.json b/shared/pages/group.auth/private.project/config.json
new file mode 100644
index 00000000..e7d754a0
--- /dev/null
+++ b/shared/pages/group.auth/private.project/config.json
@@ -0,0 +1,10 @@
+{ "domains": [
+ {
+ "domain": "private.domain.com",
+ "id": 1000,
+ "access_control": true
+ }
+ ],
+ "id": 1000,
+ "access_control": true
+}
diff --git a/shared/pages/group.auth/private.project/public/index.html b/shared/pages/group.auth/private.project/public/index.html
new file mode 100644
index 00000000..c8c6761a
--- /dev/null
+++ b/shared/pages/group.auth/private.project/public/index.html
@@ -0,0 +1 @@
+private \ No newline at end of file
diff --git a/shared/pages/group/group.gitlab-example.com/public/project/index.html b/shared/pages/group/group.gitlab-example.com/public/project/index.html
new file mode 100644
index 00000000..7c9933f7
--- /dev/null
+++ b/shared/pages/group/group.gitlab-example.com/public/project/index.html
@@ -0,0 +1 @@
+domain project subdirectory
diff --git a/vendor/github.com/gorilla/context/LICENSE b/vendor/github.com/gorilla/context/LICENSE
new file mode 100644
index 00000000..0e5fb872
--- /dev/null
+++ b/vendor/github.com/gorilla/context/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/gorilla/context/README.md b/vendor/github.com/gorilla/context/README.md
new file mode 100644
index 00000000..08f86693
--- /dev/null
+++ b/vendor/github.com/gorilla/context/README.md
@@ -0,0 +1,10 @@
+context
+=======
+[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context)
+
+gorilla/context is a general purpose registry for global request variables.
+
+> Note: gorilla/context, having been born well before `context.Context` existed, does not play well
+> with the shallow copying of the request that [`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) (added to net/http Go 1.7 onwards) performs. You should either use *just* gorilla/context, or moving forward, the new `http.Request.Context()`.
+
+Read the full documentation here: http://www.gorillatoolkit.org/pkg/context
diff --git a/vendor/github.com/gorilla/context/context.go b/vendor/github.com/gorilla/context/context.go
new file mode 100644
index 00000000..81cb128b
--- /dev/null
+++ b/vendor/github.com/gorilla/context/context.go
@@ -0,0 +1,143 @@
+// Copyright 2012 The Gorilla Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package context
+
+import (
+ "net/http"
+ "sync"
+ "time"
+)
+
+var (
+ mutex sync.RWMutex
+ data = make(map[*http.Request]map[interface{}]interface{})
+ datat = make(map[*http.Request]int64)
+)
+
+// Set stores a value for a given key in a given request.
+func Set(r *http.Request, key, val interface{}) {
+ mutex.Lock()
+ if data[r] == nil {
+ data[r] = make(map[interface{}]interface{})
+ datat[r] = time.Now().Unix()
+ }
+ data[r][key] = val
+ mutex.Unlock()
+}
+
+// Get returns a value stored for a given key in a given request.
+func Get(r *http.Request, key interface{}) interface{} {
+ mutex.RLock()
+ if ctx := data[r]; ctx != nil {
+ value := ctx[key]
+ mutex.RUnlock()
+ return value
+ }
+ mutex.RUnlock()
+ return nil
+}
+
+// GetOk returns stored value and presence state like multi-value return of map access.
+func GetOk(r *http.Request, key interface{}) (interface{}, bool) {
+ mutex.RLock()
+ if _, ok := data[r]; ok {
+ value, ok := data[r][key]
+ mutex.RUnlock()
+ return value, ok
+ }
+ mutex.RUnlock()
+ return nil, false
+}
+
+// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests.
+func GetAll(r *http.Request) map[interface{}]interface{} {
+ mutex.RLock()
+ if context, ok := data[r]; ok {
+ result := make(map[interface{}]interface{}, len(context))
+ for k, v := range context {
+ result[k] = v
+ }
+ mutex.RUnlock()
+ return result
+ }
+ mutex.RUnlock()
+ return nil
+}
+
+// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if
+// the request was registered.
+func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) {
+ mutex.RLock()
+ context, ok := data[r]
+ result := make(map[interface{}]interface{}, len(context))
+ for k, v := range context {
+ result[k] = v
+ }
+ mutex.RUnlock()
+ return result, ok
+}
+
+// Delete removes a value stored for a given key in a given request.
+func Delete(r *http.Request, key interface{}) {
+ mutex.Lock()
+ if data[r] != nil {
+ delete(data[r], key)
+ }
+ mutex.Unlock()
+}
+
+// Clear removes all values stored for a given request.
+//
+// This is usually called by a handler wrapper to clean up request
+// variables at the end of a request lifetime. See ClearHandler().
+func Clear(r *http.Request) {
+ mutex.Lock()
+ clear(r)
+ mutex.Unlock()
+}
+
+// clear is Clear without the lock.
+func clear(r *http.Request) {
+ delete(data, r)
+ delete(datat, r)
+}
+
+// Purge removes request data stored for longer than maxAge, in seconds.
+// It returns the amount of requests removed.
+//
+// If maxAge <= 0, all request data is removed.
+//
+// This is only used for sanity check: in case context cleaning was not
+// properly set some request data can be kept forever, consuming an increasing
+// amount of memory. In case this is detected, Purge() must be called
+// periodically until the problem is fixed.
+func Purge(maxAge int) int {
+ mutex.Lock()
+ count := 0
+ if maxAge <= 0 {
+ count = len(data)
+ data = make(map[*http.Request]map[interface{}]interface{})
+ datat = make(map[*http.Request]int64)
+ } else {
+ min := time.Now().Unix() - int64(maxAge)
+ for r := range data {
+ if datat[r] < min {
+ clear(r)
+ count++
+ }
+ }
+ }
+ mutex.Unlock()
+ return count
+}
+
+// ClearHandler wraps an http.Handler and clears request values at the end
+// of a request lifetime.
+func ClearHandler(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer Clear(r)
+ h.ServeHTTP(w, r)
+ })
+}
diff --git a/vendor/github.com/gorilla/context/doc.go b/vendor/github.com/gorilla/context/doc.go
new file mode 100644
index 00000000..448d1bfc
--- /dev/null
+++ b/vendor/github.com/gorilla/context/doc.go
@@ -0,0 +1,88 @@
+// Copyright 2012 The Gorilla Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+Package context stores values shared during a request lifetime.
+
+Note: gorilla/context, having been born well before `context.Context` existed,
+does not play well > with the shallow copying of the request that
+[`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext)
+(added to net/http Go 1.7 onwards) performs. You should either use *just*
+gorilla/context, or moving forward, the new `http.Request.Context()`.
+
+For example, a router can set variables extracted from the URL and later
+application handlers can access those values, or it can be used to store
+sessions values to be saved at the end of a request. There are several
+others common uses.
+
+The idea was posted by Brad Fitzpatrick to the go-nuts mailing list:
+
+ http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53
+
+Here's the basic usage: first define the keys that you will need. The key
+type is interface{} so a key can be of any type that supports equality.
+Here we define a key using a custom int type to avoid name collisions:
+
+ package foo
+
+ import (
+ "github.com/gorilla/context"
+ )
+
+ type key int
+
+ const MyKey key = 0
+
+Then set a variable. Variables are bound to an http.Request object, so you
+need a request instance to set a value:
+
+ context.Set(r, MyKey, "bar")
+
+The application can later access the variable using the same key you provided:
+
+ func MyHandler(w http.ResponseWriter, r *http.Request) {
+ // val is "bar".
+ val := context.Get(r, foo.MyKey)
+
+ // returns ("bar", true)
+ val, ok := context.GetOk(r, foo.MyKey)
+ // ...
+ }
+
+And that's all about the basic usage. We discuss some other ideas below.
+
+Any type can be stored in the context. To enforce a given type, make the key
+private and wrap Get() and Set() to accept and return values of a specific
+type:
+
+ type key int
+
+ const mykey key = 0
+
+ // GetMyKey returns a value for this package from the request values.
+ func GetMyKey(r *http.Request) SomeType {
+ if rv := context.Get(r, mykey); rv != nil {
+ return rv.(SomeType)
+ }
+ return nil
+ }
+
+ // SetMyKey sets a value for this package in the request values.
+ func SetMyKey(r *http.Request, val SomeType) {
+ context.Set(r, mykey, val)
+ }
+
+Variables must be cleared at the end of a request, to remove all values
+that were stored. This can be done in an http.Handler, after a request was
+served. Just call Clear() passing the request:
+
+ context.Clear(r)
+
+...or use ClearHandler(), which conveniently wraps an http.Handler to clear
+variables at the end of a request lifetime.
+
+The Routers from the packages gorilla/mux and gorilla/pat call Clear()
+so if you are using either of them you don't need to clear the context manually.
+*/
+package context
diff --git a/vendor/github.com/gorilla/securecookie/LICENSE b/vendor/github.com/gorilla/securecookie/LICENSE
new file mode 100644
index 00000000..0e5fb872
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/gorilla/securecookie/README.md b/vendor/github.com/gorilla/securecookie/README.md
new file mode 100644
index 00000000..aa7bd1a5
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/README.md
@@ -0,0 +1,80 @@
+securecookie
+============
+[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/securecookie.png?branch=master)](https://travis-ci.org/gorilla/securecookie)
+[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/securecookie/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/securecookie?badge)
+
+
+securecookie encodes and decodes authenticated and optionally encrypted
+cookie values.
+
+Secure cookies can't be forged, because their values are validated using HMAC.
+When encrypted, the content is also inaccessible to malicious eyes. It is still
+recommended that sensitive data not be stored in cookies, and that HTTPS be used
+to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack).
+
+## Examples
+
+To use it, first create a new SecureCookie instance:
+
+```go
+// Hash keys should be at least 32 bytes long
+var hashKey = []byte("very-secret")
+// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long.
+// Shorter keys may weaken the encryption used.
+var blockKey = []byte("a-lot-secret")
+var s = securecookie.New(hashKey, blockKey)
+```
+
+The hashKey is required, used to authenticate the cookie value using HMAC.
+It is recommended to use a key with 32 or 64 bytes.
+
+The blockKey is optional, used to encrypt the cookie value -- set it to nil
+to not use encryption. If set, the length must correspond to the block size
+of the encryption algorithm. For AES, used by default, valid lengths are
+16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
+
+Strong keys can be created using the convenience function GenerateRandomKey().
+
+Once a SecureCookie instance is set, use it to encode a cookie value:
+
+```go
+func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
+ value := map[string]string{
+ "foo": "bar",
+ }
+ if encoded, err := s.Encode("cookie-name", value); err == nil {
+ cookie := &http.Cookie{
+ Name: "cookie-name",
+ Value: encoded,
+ Path: "/",
+ Secure: true,
+ HttpOnly: true,
+ }
+ http.SetCookie(w, cookie)
+ }
+}
+```
+
+Later, use the same SecureCookie instance to decode and validate a cookie
+value:
+
+```go
+func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
+ if cookie, err := r.Cookie("cookie-name"); err == nil {
+ value := make(map[string]string)
+ if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil {
+ fmt.Fprintf(w, "The value of foo is %q", value["foo"])
+ }
+ }
+}
+```
+
+We stored a map[string]string, but secure cookies can hold any value that
+can be encoded using `encoding/gob`. To store custom types, they must be
+registered first using gob.Register(). For basic types this is not needed;
+it works out of the box. An optional JSON encoder that uses `encoding/json` is
+available for types compatible with JSON.
+
+## License
+
+BSD licensed. See the LICENSE file for details.
diff --git a/vendor/github.com/gorilla/securecookie/doc.go b/vendor/github.com/gorilla/securecookie/doc.go
new file mode 100644
index 00000000..ae89408d
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/doc.go
@@ -0,0 +1,61 @@
+// Copyright 2012 The Gorilla Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+Package securecookie encodes and decodes authenticated and optionally
+encrypted cookie values.
+
+Secure cookies can't be forged, because their values are validated using HMAC.
+When encrypted, the content is also inaccessible to malicious eyes.
+
+To use it, first create a new SecureCookie instance:
+
+ var hashKey = []byte("very-secret")
+ var blockKey = []byte("a-lot-secret")
+ var s = securecookie.New(hashKey, blockKey)
+
+The hashKey is required, used to authenticate the cookie value using HMAC.
+It is recommended to use a key with 32 or 64 bytes.
+
+The blockKey is optional, used to encrypt the cookie value -- set it to nil
+to not use encryption. If set, the length must correspond to the block size
+of the encryption algorithm. For AES, used by default, valid lengths are
+16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
+
+Strong keys can be created using the convenience function GenerateRandomKey().
+
+Once a SecureCookie instance is set, use it to encode a cookie value:
+
+ func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
+ value := map[string]string{
+ "foo": "bar",
+ }
+ if encoded, err := s.Encode("cookie-name", value); err == nil {
+ cookie := &http.Cookie{
+ Name: "cookie-name",
+ Value: encoded,
+ Path: "/",
+ }
+ http.SetCookie(w, cookie)
+ }
+ }
+
+Later, use the same SecureCookie instance to decode and validate a cookie
+value:
+
+ func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
+ if cookie, err := r.Cookie("cookie-name"); err == nil {
+ value := make(map[string]string)
+ if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil {
+ fmt.Fprintf(w, "The value of foo is %q", value["foo"])
+ }
+ }
+ }
+
+We stored a map[string]string, but secure cookies can hold any value that
+can be encoded using encoding/gob. To store custom types, they must be
+registered first using gob.Register(). For basic types this is not needed;
+it works out of the box.
+*/
+package securecookie
diff --git a/vendor/github.com/gorilla/securecookie/fuzz.go b/vendor/github.com/gorilla/securecookie/fuzz.go
new file mode 100644
index 00000000..e4d0534e
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/fuzz.go
@@ -0,0 +1,25 @@
+// +build gofuzz
+
+package securecookie
+
+var hashKey = []byte("very-secret12345")
+var blockKey = []byte("a-lot-secret1234")
+var s = New(hashKey, blockKey)
+
+type Cookie struct {
+ B bool
+ I int
+ S string
+}
+
+func Fuzz(data []byte) int {
+ datas := string(data)
+ var c Cookie
+ if err := s.Decode("fuzz", datas, &c); err != nil {
+ return 0
+ }
+ if _, err := s.Encode("fuzz", c); err != nil {
+ panic(err)
+ }
+ return 1
+}
diff --git a/vendor/github.com/gorilla/securecookie/securecookie.go b/vendor/github.com/gorilla/securecookie/securecookie.go
new file mode 100644
index 00000000..cd4e0976
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/securecookie.go
@@ -0,0 +1,646 @@
+// Copyright 2012 The Gorilla Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package securecookie
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/base64"
+ "encoding/gob"
+ "encoding/json"
+ "fmt"
+ "hash"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Error is the interface of all errors returned by functions in this library.
+type Error interface {
+ error
+
+ // IsUsage returns true for errors indicating the client code probably
+ // uses this library incorrectly. For example, the client may have
+ // failed to provide a valid hash key, or may have failed to configure
+ // the Serializer adequately for encoding value.
+ IsUsage() bool
+
+ // IsDecode returns true for errors indicating that a cookie could not
+ // be decoded and validated. Since cookies are usually untrusted
+ // user-provided input, errors of this type should be expected.
+ // Usually, the proper action is simply to reject the request.
+ IsDecode() bool
+
+ // IsInternal returns true for unexpected errors occurring in the
+ // securecookie implementation.
+ IsInternal() bool
+
+ // Cause, if it returns a non-nil value, indicates that this error was
+ // propagated from some underlying library. If this method returns nil,
+ // this error was raised directly by this library.
+ //
+ // Cause is provided principally for debugging/logging purposes; it is
+ // rare that application logic should perform meaningfully different
+ // logic based on Cause. See, for example, the caveats described on
+ // (MultiError).Cause().
+ Cause() error
+}
+
+// errorType is a bitmask giving the error type(s) of an cookieError value.
+type errorType int
+
+const (
+ usageError = errorType(1 << iota)
+ decodeError
+ internalError
+)
+
+type cookieError struct {
+ typ errorType
+ msg string
+ cause error
+}
+
+func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 }
+func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 }
+func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 }
+
+func (e cookieError) Cause() error { return e.cause }
+
+func (e cookieError) Error() string {
+ parts := []string{"securecookie: "}
+ if e.msg == "" {
+ parts = append(parts, "error")
+ } else {
+ parts = append(parts, e.msg)
+ }
+ if c := e.Cause(); c != nil {
+ parts = append(parts, " - caused by: ", c.Error())
+ }
+ return strings.Join(parts, "")
+}
+
+var (
+ errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"}
+
+ errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"}
+ errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"}
+ errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"}
+ errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"}
+
+ errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"}
+ errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"}
+ errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"}
+ errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"}
+ errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"}
+ errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."}
+ errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."}
+
+ // ErrMacInvalid indicates that cookie decoding failed because the HMAC
+ // could not be extracted and verified. Direct use of this error
+ // variable is deprecated; it is public only for legacy compatibility,
+ // and may be privatized in the future, as it is rarely useful to
+ // distinguish between this error and other Error implementations.
+ ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"}
+)
+
+// Codec defines an interface to encode and decode cookie values.
+type Codec interface {
+ Encode(name string, value interface{}) (string, error)
+ Decode(name, value string, dst interface{}) error
+}
+
+// New returns a new SecureCookie.
+//
+// hashKey is required, used to authenticate values using HMAC. Create it using
+// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes.
+//
+// blockKey is optional, used to encrypt values. Create it using
+// GenerateRandomKey(). The key length must correspond to the block size
+// of the encryption algorithm. For AES, used by default, valid lengths are
+// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
+// The default encoder used for cookie serialization is encoding/gob.
+//
+// Note that keys created using GenerateRandomKey() are not automatically
+// persisted. New keys will be created when the application is restarted, and
+// previously issued cookies will not be able to be decoded.
+func New(hashKey, blockKey []byte) *SecureCookie {
+ s := &SecureCookie{
+ hashKey: hashKey,
+ blockKey: blockKey,
+ hashFunc: sha256.New,
+ maxAge: 86400 * 30,
+ maxLength: 4096,
+ sz: GobEncoder{},
+ }
+ if hashKey == nil {
+ s.err = errHashKeyNotSet
+ }
+ if blockKey != nil {
+ s.BlockFunc(aes.NewCipher)
+ }
+ return s
+}
+
+// SecureCookie encodes and decodes authenticated and optionally encrypted
+// cookie values.
+type SecureCookie struct {
+ hashKey []byte
+ hashFunc func() hash.Hash
+ blockKey []byte
+ block cipher.Block
+ maxLength int
+ maxAge int64
+ minAge int64
+ err error
+ sz Serializer
+ // For testing purposes, the function that returns the current timestamp.
+ // If not set, it will use time.Now().UTC().Unix().
+ timeFunc func() int64
+}
+
+// Serializer provides an interface for providing custom serializers for cookie
+// values.
+type Serializer interface {
+ Serialize(src interface{}) ([]byte, error)
+ Deserialize(src []byte, dst interface{}) error
+}
+
+// GobEncoder encodes cookie values using encoding/gob. This is the simplest
+// encoder and can handle complex types via gob.Register.
+type GobEncoder struct{}
+
+// JSONEncoder encodes cookie values using encoding/json. Users who wish to
+// encode complex types need to satisfy the json.Marshaller and
+// json.Unmarshaller interfaces.
+type JSONEncoder struct{}
+
+// NopEncoder does not encode cookie values, and instead simply accepts a []byte
+// (as an interface{}) and returns a []byte. This is particularly useful when
+// you encoding an object upstream and do not wish to re-encode it.
+type NopEncoder struct{}
+
+// MaxLength restricts the maximum length, in bytes, for the cookie value.
+//
+// Default is 4096, which is the maximum value accepted by Internet Explorer.
+func (s *SecureCookie) MaxLength(value int) *SecureCookie {
+ s.maxLength = value
+ return s
+}
+
+// MaxAge restricts the maximum age, in seconds, for the cookie value.
+//
+// Default is 86400 * 30. Set it to 0 for no restriction.
+func (s *SecureCookie) MaxAge(value int) *SecureCookie {
+ s.maxAge = int64(value)
+ return s
+}
+
+// MinAge restricts the minimum age, in seconds, for the cookie value.
+//
+// Default is 0 (no restriction).
+func (s *SecureCookie) MinAge(value int) *SecureCookie {
+ s.minAge = int64(value)
+ return s
+}
+
+// HashFunc sets the hash function used to create HMAC.
+//
+// Default is crypto/sha256.New.
+func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie {
+ s.hashFunc = f
+ return s
+}
+
+// BlockFunc sets the encryption function used to create a cipher.Block.
+//
+// Default is crypto/aes.New.
+func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie {
+ if s.blockKey == nil {
+ s.err = errBlockKeyNotSet
+ } else if block, err := f(s.blockKey); err == nil {
+ s.block = block
+ } else {
+ s.err = cookieError{cause: err, typ: usageError}
+ }
+ return s
+}
+
+// Encoding sets the encoding/serialization method for cookies.
+//
+// Default is encoding/gob. To encode special structures using encoding/gob,
+// they must be registered first using gob.Register().
+func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie {
+ s.sz = sz
+
+ return s
+}
+
+// Encode encodes a cookie value.
+//
+// It serializes, optionally encrypts, signs with a message authentication code,
+// and finally encodes the value.
+//
+// The name argument is the cookie name. It is stored with the encoded value.
+// The value argument is the value to be encoded. It can be any value that can
+// be encoded using the currently selected serializer; see SetSerializer().
+//
+// It is the client's responsibility to ensure that value, when encoded using
+// the current serialization/encryption settings on s and then base64-encoded,
+// is shorter than the maximum permissible length.
+func (s *SecureCookie) Encode(name string, value interface{}) (string, error) {
+ if s.err != nil {
+ return "", s.err
+ }
+ if s.hashKey == nil {
+ s.err = errHashKeyNotSet
+ return "", s.err
+ }
+ var err error
+ var b []byte
+ // 1. Serialize.
+ if b, err = s.sz.Serialize(value); err != nil {
+ return "", cookieError{cause: err, typ: usageError}
+ }
+ // 2. Encrypt (optional).
+ if s.block != nil {
+ if b, err = encrypt(s.block, b); err != nil {
+ return "", cookieError{cause: err, typ: usageError}
+ }
+ }
+ b = encode(b)
+ // 3. Create MAC for "name|date|value". Extra pipe to be used later.
+ b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b))
+ mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1])
+ // Append mac, remove name.
+ b = append(b, mac...)[len(name)+1:]
+ // 4. Encode to base64.
+ b = encode(b)
+ // 5. Check length.
+ if s.maxLength != 0 && len(b) > s.maxLength {
+ return "", errEncodedValueTooLong
+ }
+ // Done.
+ return string(b), nil
+}
+
+// Decode decodes a cookie value.
+//
+// It decodes, verifies a message authentication code, optionally decrypts and
+// finally deserializes the value.
+//
+// The name argument is the cookie name. It must be the same name used when
+// it was stored. The value argument is the encoded cookie value. The dst
+// argument is where the cookie will be decoded. It must be a pointer.
+func (s *SecureCookie) Decode(name, value string, dst interface{}) error {
+ if s.err != nil {
+ return s.err
+ }
+ if s.hashKey == nil {
+ s.err = errHashKeyNotSet
+ return s.err
+ }
+ // 1. Check length.
+ if s.maxLength != 0 && len(value) > s.maxLength {
+ return errValueToDecodeTooLong
+ }
+ // 2. Decode from base64.
+ b, err := decode([]byte(value))
+ if err != nil {
+ return err
+ }
+ // 3. Verify MAC. Value is "date|value|mac".
+ parts := bytes.SplitN(b, []byte("|"), 3)
+ if len(parts) != 3 {
+ return ErrMacInvalid
+ }
+ h := hmac.New(s.hashFunc, s.hashKey)
+ b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...)
+ if err = verifyMac(h, b, parts[2]); err != nil {
+ return err
+ }
+ // 4. Verify date ranges.
+ var t1 int64
+ if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil {
+ return errTimestampInvalid
+ }
+ t2 := s.timestamp()
+ if s.minAge != 0 && t1 > t2-s.minAge {
+ return errTimestampTooNew
+ }
+ if s.maxAge != 0 && t1 < t2-s.maxAge {
+ return errTimestampExpired
+ }
+ // 5. Decrypt (optional).
+ b, err = decode(parts[1])
+ if err != nil {
+ return err
+ }
+ if s.block != nil {
+ if b, err = decrypt(s.block, b); err != nil {
+ return err
+ }
+ }
+ // 6. Deserialize.
+ if err = s.sz.Deserialize(b, dst); err != nil {
+ return cookieError{cause: err, typ: decodeError}
+ }
+ // Done.
+ return nil
+}
+
+// timestamp returns the current timestamp, in seconds.
+//
+// For testing purposes, the function that generates the timestamp can be
+// overridden. If not set, it will return time.Now().UTC().Unix().
+func (s *SecureCookie) timestamp() int64 {
+ if s.timeFunc == nil {
+ return time.Now().UTC().Unix()
+ }
+ return s.timeFunc()
+}
+
+// Authentication -------------------------------------------------------------
+
+// createMac creates a message authentication code (MAC).
+func createMac(h hash.Hash, value []byte) []byte {
+ h.Write(value)
+ return h.Sum(nil)
+}
+
+// verifyMac verifies that a message authentication code (MAC) is valid.
+func verifyMac(h hash.Hash, value []byte, mac []byte) error {
+ mac2 := createMac(h, value)
+ // Check that both MACs are of equal length, as subtle.ConstantTimeCompare
+ // does not do this prior to Go 1.4.
+ if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 {
+ return nil
+ }
+ return ErrMacInvalid
+}
+
+// Encryption -----------------------------------------------------------------
+
+// encrypt encrypts a value using the given block in counter mode.
+//
+// A random initialization vector (http://goo.gl/zF67k) with the length of the
+// block size is prepended to the resulting ciphertext.
+func encrypt(block cipher.Block, value []byte) ([]byte, error) {
+ iv := GenerateRandomKey(block.BlockSize())
+ if iv == nil {
+ return nil, errGeneratingIV
+ }
+ // Encrypt it.
+ stream := cipher.NewCTR(block, iv)
+ stream.XORKeyStream(value, value)
+ // Return iv + ciphertext.
+ return append(iv, value...), nil
+}
+
+// decrypt decrypts a value using the given block in counter mode.
+//
+// The value to be decrypted must be prepended by a initialization vector
+// (http://goo.gl/zF67k) with the length of the block size.
+func decrypt(block cipher.Block, value []byte) ([]byte, error) {
+ size := block.BlockSize()
+ if len(value) > size {
+ // Extract iv.
+ iv := value[:size]
+ // Extract ciphertext.
+ value = value[size:]
+ // Decrypt it.
+ stream := cipher.NewCTR(block, iv)
+ stream.XORKeyStream(value, value)
+ return value, nil
+ }
+ return nil, errDecryptionFailed
+}
+
+// Serialization --------------------------------------------------------------
+
+// Serialize encodes a value using gob.
+func (e GobEncoder) Serialize(src interface{}) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ enc := gob.NewEncoder(buf)
+ if err := enc.Encode(src); err != nil {
+ return nil, cookieError{cause: err, typ: usageError}
+ }
+ return buf.Bytes(), nil
+}
+
+// Deserialize decodes a value using gob.
+func (e GobEncoder) Deserialize(src []byte, dst interface{}) error {
+ dec := gob.NewDecoder(bytes.NewBuffer(src))
+ if err := dec.Decode(dst); err != nil {
+ return cookieError{cause: err, typ: decodeError}
+ }
+ return nil
+}
+
+// Serialize encodes a value using encoding/json.
+func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ enc := json.NewEncoder(buf)
+ if err := enc.Encode(src); err != nil {
+ return nil, cookieError{cause: err, typ: usageError}
+ }
+ return buf.Bytes(), nil
+}
+
+// Deserialize decodes a value using encoding/json.
+func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error {
+ dec := json.NewDecoder(bytes.NewReader(src))
+ if err := dec.Decode(dst); err != nil {
+ return cookieError{cause: err, typ: decodeError}
+ }
+ return nil
+}
+
+// Serialize passes a []byte through as-is.
+func (e NopEncoder) Serialize(src interface{}) ([]byte, error) {
+ if b, ok := src.([]byte); ok {
+ return b, nil
+ }
+
+ return nil, errValueNotByte
+}
+
+// Deserialize passes a []byte through as-is.
+func (e NopEncoder) Deserialize(src []byte, dst interface{}) error {
+ if dat, ok := dst.(*[]byte); ok {
+ *dat = src
+ return nil
+ }
+ return errValueNotBytePtr
+}
+
+// Encoding -------------------------------------------------------------------
+
+// encode encodes a value using base64.
+func encode(value []byte) []byte {
+ encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value)))
+ base64.URLEncoding.Encode(encoded, value)
+ return encoded
+}
+
+// decode decodes a cookie using base64.
+func decode(value []byte) ([]byte, error) {
+ decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value)))
+ b, err := base64.URLEncoding.Decode(decoded, value)
+ if err != nil {
+ return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"}
+ }
+ return decoded[:b], nil
+}
+
+// Helpers --------------------------------------------------------------------
+
+// GenerateRandomKey creates a random key with the given length in bytes.
+// On failure, returns nil.
+//
+// Callers should explicitly check for the possibility of a nil return, treat
+// it as a failure of the system random number generator, and not continue.
+func GenerateRandomKey(length int) []byte {
+ k := make([]byte, length)
+ if _, err := io.ReadFull(rand.Reader, k); err != nil {
+ return nil
+ }
+ return k
+}
+
+// CodecsFromPairs returns a slice of SecureCookie instances.
+//
+// It is a convenience function to create a list of codecs for key rotation. Note
+// that the generated Codecs will have the default options applied: callers
+// should iterate over each Codec and type-assert the underlying *SecureCookie to
+// change these.
+//
+// Example:
+//
+// codecs := securecookie.CodecsFromPairs(
+// []byte("new-hash-key"),
+// []byte("new-block-key"),
+// []byte("old-hash-key"),
+// []byte("old-block-key"),
+// )
+//
+// // Modify each instance.
+// for _, s := range codecs {
+// if cookie, ok := s.(*securecookie.SecureCookie); ok {
+// cookie.MaxAge(86400 * 7)
+// cookie.SetSerializer(securecookie.JSONEncoder{})
+// cookie.HashFunc(sha512.New512_256)
+// }
+// }
+//
+func CodecsFromPairs(keyPairs ...[]byte) []Codec {
+ codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2)
+ for i := 0; i < len(keyPairs); i += 2 {
+ var blockKey []byte
+ if i+1 < len(keyPairs) {
+ blockKey = keyPairs[i+1]
+ }
+ codecs[i/2] = New(keyPairs[i], blockKey)
+ }
+ return codecs
+}
+
+// EncodeMulti encodes a cookie value using a group of codecs.
+//
+// The codecs are tried in order. Multiple codecs are accepted to allow
+// key rotation.
+//
+// On error, may return a MultiError.
+func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) {
+ if len(codecs) == 0 {
+ return "", errNoCodecs
+ }
+
+ var errors MultiError
+ for _, codec := range codecs {
+ encoded, err := codec.Encode(name, value)
+ if err == nil {
+ return encoded, nil
+ }
+ errors = append(errors, err)
+ }
+ return "", errors
+}
+
+// DecodeMulti decodes a cookie value using a group of codecs.
+//
+// The codecs are tried in order. Multiple codecs are accepted to allow
+// key rotation.
+//
+// On error, may return a MultiError.
+func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error {
+ if len(codecs) == 0 {
+ return errNoCodecs
+ }
+
+ var errors MultiError
+ for _, codec := range codecs {
+ err := codec.Decode(name, value, dst)
+ if err == nil {
+ return nil
+ }
+ errors = append(errors, err)
+ }
+ return errors
+}
+
+// MultiError groups multiple errors.
+type MultiError []error
+
+func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) }
+func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) }
+func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) }
+
+// Cause returns nil for MultiError; there is no unique underlying cause in the
+// general case.
+//
+// Note: we could conceivably return a non-nil Cause only when there is exactly
+// one child error with a Cause. However, it would be brittle for client code
+// to rely on the arity of causes inside a MultiError, so we have opted not to
+// provide this functionality. Clients which really wish to access the Causes
+// of the underlying errors are free to iterate through the errors themselves.
+func (m MultiError) Cause() error { return nil }
+
+func (m MultiError) Error() string {
+ s, n := "", 0
+ for _, e := range m {
+ if e != nil {
+ if n == 0 {
+ s = e.Error()
+ }
+ n++
+ }
+ }
+ switch n {
+ case 0:
+ return "(0 errors)"
+ case 1:
+ return s
+ case 2:
+ return s + " (and 1 other error)"
+ }
+ return fmt.Sprintf("%s (and %d other errors)", s, n-1)
+}
+
+// any returns true if any element of m is an Error for which pred returns true.
+func (m MultiError) any(pred func(Error) bool) bool {
+ for _, e := range m {
+ if ourErr, ok := e.(Error); ok && pred(ourErr) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/github.com/gorilla/sessions/LICENSE b/vendor/github.com/gorilla/sessions/LICENSE
new file mode 100644
index 00000000..0e5fb872
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/gorilla/sessions/README.md b/vendor/github.com/gorilla/sessions/README.md
new file mode 100644
index 00000000..c9e0e92c
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/README.md
@@ -0,0 +1,92 @@
+sessions
+========
+[![GoDoc](https://godoc.org/github.com/gorilla/sessions?status.svg)](https://godoc.org/github.com/gorilla/sessions) [![Build Status](https://travis-ci.org/gorilla/sessions.svg?branch=master)](https://travis-ci.org/gorilla/sessions)
+[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/sessions/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/sessions?badge)
+
+
+gorilla/sessions provides cookie and filesystem sessions and infrastructure for
+custom session backends.
+
+The key features are:
+
+* Simple API: use it as an easy way to set signed (and optionally
+ encrypted) cookies.
+* Built-in backends to store sessions in cookies or the filesystem.
+* Flash messages: session values that last until read.
+* Convenient way to switch session persistency (aka "remember me") and set
+ other attributes.
+* Mechanism to rotate authentication and encryption keys.
+* Multiple sessions per request, even using different backends.
+* Interfaces and infrastructure for custom session backends: sessions from
+ different stores can be retrieved and batch-saved using a common API.
+
+Let's start with an example that shows the sessions API in a nutshell:
+
+```go
+ import (
+ "net/http"
+ "github.com/gorilla/sessions"
+ )
+
+ var store = sessions.NewCookieStore([]byte("something-very-secret"))
+
+ func MyHandler(w http.ResponseWriter, r *http.Request) {
+ // Get a session. We're ignoring the error resulted from decoding an
+ // existing session: Get() always returns a session, even if empty.
+ session, _ := store.Get(r, "session-name")
+ // Set some session values.
+ session.Values["foo"] = "bar"
+ session.Values[42] = 43
+ // Save it before we write to the response/return from the handler.
+ session.Save(r, w)
+ }
+```
+
+First we initialize a session store calling `NewCookieStore()` and passing a
+secret key used to authenticate the session. Inside the handler, we call
+`store.Get()` to retrieve an existing session or create a new one. Then we set
+some session values in session.Values, which is a `map[interface{}]interface{}`.
+And finally we call `session.Save()` to save the session in the response.
+
+Important Note: If you aren't using gorilla/mux, you need to wrap your handlers
+with
+[`context.ClearHandler`](http://www.gorillatoolkit.org/pkg/context#ClearHandler)
+or else you will leak memory! An easy way to do this is to wrap the top-level
+mux when calling http.ListenAndServe:
+
+```go
+ http.ListenAndServe(":8080", context.ClearHandler(http.DefaultServeMux))
+```
+
+The ClearHandler function is provided by the gorilla/context package.
+
+More examples are available [on the Gorilla
+website](http://www.gorillatoolkit.org/pkg/sessions).
+
+## Store Implementations
+
+Other implementations of the `sessions.Store` interface:
+
+* [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB
+* [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt
+* [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase
+* [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS
+* [github.com/savaki/dynastore](https://github.com/savaki/dynastore) - DynamoDB on AWS (Official AWS library)
+* [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache
+* [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine
+* [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB
+* [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL
+* [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster
+* [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL
+* [github.com/boj/redistore](https://github.com/boj/redistore) - Redis
+* [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB
+* [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak
+* [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite
+* [github.com/wader/gormstore](https://github.com/wader/gormstore) - GORM (MySQL, PostgreSQL, SQLite)
+* [github.com/gernest/qlstore](https://github.com/gernest/qlstore) - ql
+* [github.com/quasoft/memstore](https://github.com/quasoft/memstore) - In-memory implementation for use in unit tests
+* [github.com/lafriks/xormstore](https://github.com/lafriks/xormstore) - XORM (MySQL, PostgreSQL, SQLite, Microsoft SQL Server, TiDB)
+
+## License
+
+BSD licensed. See the LICENSE file for details.
diff --git a/vendor/github.com/gorilla/sessions/doc.go b/vendor/github.com/gorilla/sessions/doc.go
new file mode 100644
index 00000000..57a52917
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/doc.go
@@ -0,0 +1,198 @@
+// Copyright 2012 The Gorilla Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+Package sessions provides cookie and filesystem sessions and
+infrastructure for custom session backends.
+
+The key features are:
+
+ * Simple API: use it as an easy way to set signed (and optionally
+ encrypted) cookies.
+ * Built-in backends to store sessions in cookies or the filesystem.
+ * Flash messages: session values that last until read.
+ * Convenient way to switch session persistency (aka "remember me") and set
+ other attributes.
+ * Mechanism to rotate authentication and encryption keys.
+ * Multiple sessions per request, even using different backends.
+ * Interfaces and infrastructure for custom session backends: sessions from
+ different stores can be retrieved and batch-saved using a common API.
+
+Let's start with an example that shows the sessions API in a nutshell:
+
+ import (
+ "net/http"
+ "github.com/gorilla/sessions"
+ )
+
+ var store = sessions.NewCookieStore([]byte("something-very-secret"))
+
+ func MyHandler(w http.ResponseWriter, r *http.Request) {
+ // Get a session. Get() always returns a session, even if empty.
+ session, err := store.Get(r, "session-name")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Set some session values.
+ session.Values["foo"] = "bar"
+ session.Values[42] = 43
+ // Save it before we write to the response/return from the handler.
+ session.Save(r, w)
+ }
+
+First we initialize a session store calling NewCookieStore() and passing a
+secret key used to authenticate the session. Inside the handler, we call
+store.Get() to retrieve an existing session or a new one. Then we set some
+session values in session.Values, which is a map[interface{}]interface{}.
+And finally we call session.Save() to save the session in the response.
+
+Note that in production code, we should check for errors when calling
+session.Save(r, w), and either display an error message or otherwise handle it.
+
+Save must be called before writing to the response, otherwise the session
+cookie will not be sent to the client.
+
+Important Note: If you aren't using gorilla/mux, you need to wrap your handlers
+with context.ClearHandler as or else you will leak memory! An easy way to do this
+is to wrap the top-level mux when calling http.ListenAndServe:
+
+ http.ListenAndServe(":8080", context.ClearHandler(http.DefaultServeMux))
+
+The ClearHandler function is provided by the gorilla/context package.
+
+That's all you need to know for the basic usage. Let's take a look at other
+options, starting with flash messages.
+
+Flash messages are session values that last until read. The term appeared with
+Ruby On Rails a few years back. When we request a flash message, it is removed
+from the session. To add a flash, call session.AddFlash(), and to get all
+flashes, call session.Flashes(). Here is an example:
+
+ func MyHandler(w http.ResponseWriter, r *http.Request) {
+ // Get a session.
+ session, err := store.Get(r, "session-name")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Get the previous flashes, if any.
+ if flashes := session.Flashes(); len(flashes) > 0 {
+ // Use the flash values.
+ } else {
+ // Set a new flash.
+ session.AddFlash("Hello, flash messages world!")
+ }
+ session.Save(r, w)
+ }
+
+Flash messages are useful to set information to be read after a redirection,
+like after form submissions.
+
+There may also be cases where you want to store a complex datatype within a
+session, such as a struct. Sessions are serialised using the encoding/gob package,
+so it is easy to register new datatypes for storage in sessions:
+
+ import(
+ "encoding/gob"
+ "github.com/gorilla/sessions"
+ )
+
+ type Person struct {
+ FirstName string
+ LastName string
+ Email string
+ Age int
+ }
+
+ type M map[string]interface{}
+
+ func init() {
+
+ gob.Register(&Person{})
+ gob.Register(&M{})
+ }
+
+As it's not possible to pass a raw type as a parameter to a function, gob.Register()
+relies on us passing it a value of the desired type. In the example above we've passed
+it a pointer to a struct and a pointer to a custom type representing a
+map[string]interface. (We could have passed non-pointer values if we wished.) This will
+then allow us to serialise/deserialise values of those types to and from our sessions.
+
+Note that because session values are stored in a map[string]interface{}, there's
+a need to type-assert data when retrieving it. We'll use the Person struct we registered above:
+
+ func MyHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := store.Get(r, "session-name")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Retrieve our struct and type-assert it
+ val := session.Values["person"]
+ var person = &Person{}
+ if person, ok := val.(*Person); !ok {
+ // Handle the case that it's not an expected type
+ }
+
+ // Now we can use our person object
+ }
+
+By default, session cookies last for a month. This is probably too long for
+some cases, but it is easy to change this and other attributes during
+runtime. Sessions can be configured individually or the store can be
+configured and then all sessions saved using it will use that configuration.
+We access session.Options or store.Options to set a new configuration. The
+fields are basically a subset of http.Cookie fields. Let's change the
+maximum age of a session to one week:
+
+ session.Options = &sessions.Options{
+ Path: "/",
+ MaxAge: 86400 * 7,
+ HttpOnly: true,
+ }
+
+Sometimes we may want to change authentication and/or encryption keys without
+breaking existing sessions. The CookieStore supports key rotation, and to use
+it you just need to set multiple authentication and encryption keys, in pairs,
+to be tested in order:
+
+ var store = sessions.NewCookieStore(
+ []byte("new-authentication-key"),
+ []byte("new-encryption-key"),
+ []byte("old-authentication-key"),
+ []byte("old-encryption-key"),
+ )
+
+New sessions will be saved using the first pair. Old sessions can still be
+read because the first pair will fail, and the second will be tested. This
+makes it easy to "rotate" secret keys and still be able to validate existing
+sessions. Note: for all pairs the encryption key is optional; set it to nil
+or omit it and and encryption won't be used.
+
+Multiple sessions can be used in the same request, even with different
+session backends. When this happens, calling Save() on each session
+individually would be cumbersome, so we have a way to save all sessions
+at once: it's sessions.Save(). Here's an example:
+
+ var store = sessions.NewCookieStore([]byte("something-very-secret"))
+
+ func MyHandler(w http.ResponseWriter, r *http.Request) {
+ // Get a session and set a value.
+ session1, _ := store.Get(r, "session-one")
+ session1.Values["foo"] = "bar"
+ // Get another session and set another value.
+ session2, _ := store.Get(r, "session-two")
+ session2.Values[42] = 43
+ // Save all sessions.
+ sessions.Save(r, w)
+ }
+
+This is possible because when we call Get() from a session store, it adds the
+session to a common registry. Save() uses it to save all registered sessions.
+*/
+package sessions
diff --git a/vendor/github.com/gorilla/sessions/go.mod b/vendor/github.com/gorilla/sessions/go.mod
new file mode 100644
index 00000000..bb9ad35e
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/go.mod
@@ -0,0 +1,6 @@
+module "github.com/gorilla/sessions"
+
+require (
+ "github.com/gorilla/context" v1.1
+ "github.com/gorilla/securecookie" v1.1
+)
diff --git a/vendor/github.com/gorilla/sessions/lex.go b/vendor/github.com/gorilla/sessions/lex.go
new file mode 100644
index 00000000..4bbbe109
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/lex.go
@@ -0,0 +1,102 @@
+// This file contains code adapted from the Go standard library
+// https://github.com/golang/go/blob/39ad0fd0789872f9469167be7fe9578625ff246e/src/net/http/lex.go
+
+package sessions
+
+import "strings"
+
+var isTokenTable = [127]bool{
+ '!': true,
+ '#': true,
+ '$': true,
+ '%': true,
+ '&': true,
+ '\'': true,
+ '*': true,
+ '+': true,
+ '-': true,
+ '.': true,
+ '0': true,
+ '1': true,
+ '2': true,
+ '3': true,
+ '4': true,
+ '5': true,
+ '6': true,
+ '7': true,
+ '8': true,
+ '9': true,
+ 'A': true,
+ 'B': true,
+ 'C': true,
+ 'D': true,
+ 'E': true,
+ 'F': true,
+ 'G': true,
+ 'H': true,
+ 'I': true,
+ 'J': true,
+ 'K': true,
+ 'L': true,
+ 'M': true,
+ 'N': true,
+ 'O': true,
+ 'P': true,
+ 'Q': true,
+ 'R': true,
+ 'S': true,
+ 'T': true,
+ 'U': true,
+ 'W': true,
+ 'V': true,
+ 'X': true,
+ 'Y': true,
+ 'Z': true,
+ '^': true,
+ '_': true,
+ '`': true,
+ 'a': true,
+ 'b': true,
+ 'c': true,
+ 'd': true,
+ 'e': true,
+ 'f': true,
+ 'g': true,
+ 'h': true,
+ 'i': true,
+ 'j': true,
+ 'k': true,
+ 'l': true,
+ 'm': true,
+ 'n': true,
+ 'o': true,
+ 'p': true,
+ 'q': true,
+ 'r': true,
+ 's': true,
+ 't': true,
+ 'u': true,
+ 'v': true,
+ 'w': true,
+ 'x': true,
+ 'y': true,
+ 'z': true,
+ '|': true,
+ '~': true,
+}
+
+func isToken(r rune) bool {
+ i := int(r)
+ return i < len(isTokenTable) && isTokenTable[i]
+}
+
+func isNotToken(r rune) bool {
+ return !isToken(r)
+}
+
+func isCookieNameValid(raw string) bool {
+ if raw == "" {
+ return false
+ }
+ return strings.IndexFunc(raw, isNotToken) < 0
+}
diff --git a/vendor/github.com/gorilla/sessions/sessions.go b/vendor/github.com/gorilla/sessions/sessions.go
new file mode 100644
index 00000000..9870e310
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/sessions.go
@@ -0,0 +1,243 @@
+// Copyright 2012 The Gorilla Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sessions
+
+import (
+ "encoding/gob"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/context"
+)
+
+// Default flashes key.
+const flashesKey = "_flash"
+
+// Options --------------------------------------------------------------------
+
+// Options stores configuration for a session or session store.
+//
+// Fields are a subset of http.Cookie fields.
+type Options struct {
+ Path string
+ Domain string
+ // MaxAge=0 means no Max-Age attribute specified and the cookie will be
+ // deleted after the browser session ends.
+ // MaxAge<0 means delete cookie immediately.
+ // MaxAge>0 means Max-Age attribute present and given in seconds.
+ MaxAge int
+ Secure bool
+ HttpOnly bool
+}
+
+// Session --------------------------------------------------------------------
+
+// NewSession is called by session stores to create a new session instance.
+func NewSession(store Store, name string) *Session {
+ return &Session{
+ Values: make(map[interface{}]interface{}),
+ store: store,
+ name: name,
+ Options: new(Options),
+ }
+}
+
+// Session stores the values and optional configuration for a session.
+type Session struct {
+ // The ID of the session, generated by stores. It should not be used for
+ // user data.
+ ID string
+ // Values contains the user-data for the session.
+ Values map[interface{}]interface{}
+ Options *Options
+ IsNew bool
+ store Store
+ name string
+}
+
+// Flashes returns a slice of flash messages from the session.
+//
+// A single variadic argument is accepted, and it is optional: it defines
+// the flash key. If not defined "_flash" is used by default.
+func (s *Session) Flashes(vars ...string) []interface{} {
+ var flashes []interface{}
+ key := flashesKey
+ if len(vars) > 0 {
+ key = vars[0]
+ }
+ if v, ok := s.Values[key]; ok {
+ // Drop the flashes and return it.
+ delete(s.Values, key)
+ flashes = v.([]interface{})
+ }
+ return flashes
+}
+
+// AddFlash adds a flash message to the session.
+//
+// A single variadic argument is accepted, and it is optional: it defines
+// the flash key. If not defined "_flash" is used by default.
+func (s *Session) AddFlash(value interface{}, vars ...string) {
+ key := flashesKey
+ if len(vars) > 0 {
+ key = vars[0]
+ }
+ var flashes []interface{}
+ if v, ok := s.Values[key]; ok {
+ flashes = v.([]interface{})
+ }
+ s.Values[key] = append(flashes, value)
+}
+
+// Save is a convenience method to save this session. It is the same as calling
+// store.Save(request, response, session). You should call Save before writing to
+// the response or returning from the handler.
+func (s *Session) Save(r *http.Request, w http.ResponseWriter) error {
+ return s.store.Save(r, w, s)
+}
+
+// Name returns the name used to register the session.
+func (s *Session) Name() string {
+ return s.name
+}
+
+// Store returns the session store used to register the session.
+func (s *Session) Store() Store {
+ return s.store
+}
+
+// Registry -------------------------------------------------------------------
+
+// sessionInfo stores a session tracked by the registry.
+type sessionInfo struct {
+ s *Session
+ e error
+}
+
+// contextKey is the type used to store the registry in the context.
+type contextKey int
+
+// registryKey is the key used to store the registry in the context.
+const registryKey contextKey = 0
+
+// GetRegistry returns a registry instance for the current request.
+func GetRegistry(r *http.Request) *Registry {
+ registry := context.Get(r, registryKey)
+ if registry != nil {
+ return registry.(*Registry)
+ }
+ newRegistry := &Registry{
+ request: r,
+ sessions: make(map[string]sessionInfo),
+ }
+ context.Set(r, registryKey, newRegistry)
+ return newRegistry
+}
+
+// Registry stores sessions used during a request.
+type Registry struct {
+ request *http.Request
+ sessions map[string]sessionInfo
+}
+
+// Get registers and returns a session for the given name and session store.
+//
+// It returns a new session if there are no sessions registered for the name.
+func (s *Registry) Get(store Store, name string) (session *Session, err error) {
+ if !isCookieNameValid(name) {
+ return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name)
+ }
+ if info, ok := s.sessions[name]; ok {
+ session, err = info.s, info.e
+ } else {
+ session, err = store.New(s.request, name)
+ session.name = name
+ s.sessions[name] = sessionInfo{s: session, e: err}
+ }
+ session.store = store
+ return
+}
+
+// Save saves all sessions registered for the current request.
+func (s *Registry) Save(w http.ResponseWriter) error {
+ var errMulti MultiError
+ for name, info := range s.sessions {
+ session := info.s
+ if session.store == nil {
+ errMulti = append(errMulti, fmt.Errorf(
+ "sessions: missing store for session %q", name))
+ } else if err := session.store.Save(s.request, w, session); err != nil {
+ errMulti = append(errMulti, fmt.Errorf(
+ "sessions: error saving session %q -- %v", name, err))
+ }
+ }
+ if errMulti != nil {
+ return errMulti
+ }
+ return nil
+}
+
+// Helpers --------------------------------------------------------------------
+
+func init() {
+ gob.Register([]interface{}{})
+}
+
+// Save saves all sessions used during the current request.
+func Save(r *http.Request, w http.ResponseWriter) error {
+ return GetRegistry(r).Save(w)
+}
+
+// NewCookie returns an http.Cookie with the options set. It also sets
+// the Expires field calculated based on the MaxAge value, for Internet
+// Explorer compatibility.
+func NewCookie(name, value string, options *Options) *http.Cookie {
+ cookie := &http.Cookie{
+ Name: name,
+ Value: value,
+ Path: options.Path,
+ Domain: options.Domain,
+ MaxAge: options.MaxAge,
+ Secure: options.Secure,
+ HttpOnly: options.HttpOnly,
+ }
+ if options.MaxAge > 0 {
+ d := time.Duration(options.MaxAge) * time.Second
+ cookie.Expires = time.Now().Add(d)
+ } else if options.MaxAge < 0 {
+ // Set it to the past to expire now.
+ cookie.Expires = time.Unix(1, 0)
+ }
+ return cookie
+}
+
+// Error ----------------------------------------------------------------------
+
+// MultiError stores multiple errors.
+//
+// Borrowed from the App Engine SDK.
+type MultiError []error
+
+func (m MultiError) Error() string {
+ s, n := "", 0
+ for _, e := range m {
+ if e != nil {
+ if n == 0 {
+ s = e.Error()
+ }
+ n++
+ }
+ }
+ switch n {
+ case 0:
+ return "(0 errors)"
+ case 1:
+ return s
+ case 2:
+ return s + " (and 1 other error)"
+ }
+ return fmt.Sprintf("%s (and %d other errors)", s, n-1)
+}
diff --git a/vendor/github.com/gorilla/sessions/store.go b/vendor/github.com/gorilla/sessions/store.go
new file mode 100644
index 00000000..4ff6b6c3
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/store.go
@@ -0,0 +1,295 @@
+// Copyright 2012 The Gorilla Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sessions
+
+import (
+ "encoding/base32"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/gorilla/securecookie"
+)
+
+// Store is an interface for custom session stores.
+//
+// See CookieStore and FilesystemStore for examples.
+type Store interface {
+ // Get should return a cached session.
+ Get(r *http.Request, name string) (*Session, error)
+
+ // New should create and return a new session.
+ //
+ // Note that New should never return a nil session, even in the case of
+ // an error if using the Registry infrastructure to cache the session.
+ New(r *http.Request, name string) (*Session, error)
+
+ // Save should persist session to the underlying store implementation.
+ Save(r *http.Request, w http.ResponseWriter, s *Session) error
+}
+
+// CookieStore ----------------------------------------------------------------
+
+// NewCookieStore returns a new CookieStore.
+//
+// Keys are defined in pairs to allow key rotation, but the common case is
+// to set a single authentication key and optionally an encryption key.
+//
+// The first key in a pair is used for authentication and the second for
+// encryption. The encryption key can be set to nil or omitted in the last
+// pair, but the authentication key is required in all pairs.
+//
+// It is recommended to use an authentication key with 32 or 64 bytes.
+// The encryption key, if set, must be either 16, 24, or 32 bytes to select
+// AES-128, AES-192, or AES-256 modes.
+//
+// Use the convenience function securecookie.GenerateRandomKey() to create
+// strong keys.
+func NewCookieStore(keyPairs ...[]byte) *CookieStore {
+ cs := &CookieStore{
+ Codecs: securecookie.CodecsFromPairs(keyPairs...),
+ Options: &Options{
+ Path: "/",
+ MaxAge: 86400 * 30,
+ },
+ }
+
+ cs.MaxAge(cs.Options.MaxAge)
+ return cs
+}
+
+// CookieStore stores sessions using secure cookies.
+type CookieStore struct {
+ Codecs []securecookie.Codec
+ Options *Options // default configuration
+}
+
+// Get returns a session for the given name after adding it to the registry.
+//
+// It returns a new session if the sessions doesn't exist. Access IsNew on
+// the session to check if it is an existing session or a new one.
+//
+// It returns a new session and an error if the session exists but could
+// not be decoded.
+func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) {
+ return GetRegistry(r).Get(s, name)
+}
+
+// New returns a session for the given name without adding it to the registry.
+//
+// The difference between New() and Get() is that calling New() twice will
+// decode the session data twice, while Get() registers and reuses the same
+// decoded session after the first call.
+func (s *CookieStore) New(r *http.Request, name string) (*Session, error) {
+ session := NewSession(s, name)
+ opts := *s.Options
+ session.Options = &opts
+ session.IsNew = true
+ var err error
+ if c, errCookie := r.Cookie(name); errCookie == nil {
+ err = securecookie.DecodeMulti(name, c.Value, &session.Values,
+ s.Codecs...)
+ if err == nil {
+ session.IsNew = false
+ }
+ }
+ return session, err
+}
+
+// Save adds a single session to the response.
+func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter,
+ session *Session) error {
+ encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
+ s.Codecs...)
+ if err != nil {
+ return err
+ }
+ http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
+ return nil
+}
+
+// MaxAge sets the maximum age for the store and the underlying cookie
+// implementation. Individual sessions can be deleted by setting Options.MaxAge
+// = -1 for that session.
+func (s *CookieStore) MaxAge(age int) {
+ s.Options.MaxAge = age
+
+ // Set the maxAge for each securecookie instance.
+ for _, codec := range s.Codecs {
+ if sc, ok := codec.(*securecookie.SecureCookie); ok {
+ sc.MaxAge(age)
+ }
+ }
+}
+
+// FilesystemStore ------------------------------------------------------------
+
+var fileMutex sync.RWMutex
+
+// NewFilesystemStore returns a new FilesystemStore.
+//
+// The path argument is the directory where sessions will be saved. If empty
+// it will use os.TempDir().
+//
+// See NewCookieStore() for a description of the other parameters.
+func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore {
+ if path == "" {
+ path = os.TempDir()
+ }
+ fs := &FilesystemStore{
+ Codecs: securecookie.CodecsFromPairs(keyPairs...),
+ Options: &Options{
+ Path: "/",
+ MaxAge: 86400 * 30,
+ },
+ path: path,
+ }
+
+ fs.MaxAge(fs.Options.MaxAge)
+ return fs
+}
+
+// FilesystemStore stores sessions in the filesystem.
+//
+// It also serves as a reference for custom stores.
+//
+// This store is still experimental and not well tested. Feedback is welcome.
+type FilesystemStore struct {
+ Codecs []securecookie.Codec
+ Options *Options // default configuration
+ path string
+}
+
+// MaxLength restricts the maximum length of new sessions to l.
+// If l is 0 there is no limit to the size of a session, use with caution.
+// The default for a new FilesystemStore is 4096.
+func (s *FilesystemStore) MaxLength(l int) {
+ for _, c := range s.Codecs {
+ if codec, ok := c.(*securecookie.SecureCookie); ok {
+ codec.MaxLength(l)
+ }
+ }
+}
+
+// Get returns a session for the given name after adding it to the registry.
+//
+// See CookieStore.Get().
+func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) {
+ return GetRegistry(r).Get(s, name)
+}
+
+// New returns a session for the given name without adding it to the registry.
+//
+// See CookieStore.New().
+func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) {
+ session := NewSession(s, name)
+ opts := *s.Options
+ session.Options = &opts
+ session.IsNew = true
+ var err error
+ if c, errCookie := r.Cookie(name); errCookie == nil {
+ err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
+ if err == nil {
+ err = s.load(session)
+ if err == nil {
+ session.IsNew = false
+ }
+ }
+ }
+ return session, err
+}
+
+// Save adds a single session to the response.
+//
+// If the Options.MaxAge of the session is <= 0 then the session file will be
+// deleted from the store path. With this process it enforces the properly
+// session cookie handling so no need to trust in the cookie management in the
+// web browser.
+func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter,
+ session *Session) error {
+ // Delete if max-age is <= 0
+ if session.Options.MaxAge <= 0 {
+ if err := s.erase(session); err != nil {
+ return err
+ }
+ http.SetCookie(w, NewCookie(session.Name(), "", session.Options))
+ return nil
+ }
+
+ if session.ID == "" {
+ // Because the ID is used in the filename, encode it to
+ // use alphanumeric characters only.
+ session.ID = strings.TrimRight(
+ base32.StdEncoding.EncodeToString(
+ securecookie.GenerateRandomKey(32)), "=")
+ }
+ if err := s.save(session); err != nil {
+ return err
+ }
+ encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
+ s.Codecs...)
+ if err != nil {
+ return err
+ }
+ http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
+ return nil
+}
+
+// MaxAge sets the maximum age for the store and the underlying cookie
+// implementation. Individual sessions can be deleted by setting Options.MaxAge
+// = -1 for that session.
+func (s *FilesystemStore) MaxAge(age int) {
+ s.Options.MaxAge = age
+
+ // Set the maxAge for each securecookie instance.
+ for _, codec := range s.Codecs {
+ if sc, ok := codec.(*securecookie.SecureCookie); ok {
+ sc.MaxAge(age)
+ }
+ }
+}
+
+// save writes encoded session.Values to a file.
+func (s *FilesystemStore) save(session *Session) error {
+ encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
+ s.Codecs...)
+ if err != nil {
+ return err
+ }
+ filename := filepath.Join(s.path, "session_"+session.ID)
+ fileMutex.Lock()
+ defer fileMutex.Unlock()
+ return ioutil.WriteFile(filename, []byte(encoded), 0600)
+}
+
+// load reads a file and decodes its content into session.Values.
+func (s *FilesystemStore) load(session *Session) error {
+ filename := filepath.Join(s.path, "session_"+session.ID)
+ fileMutex.RLock()
+ defer fileMutex.RUnlock()
+ fdata, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return err
+ }
+ if err = securecookie.DecodeMulti(session.Name(), string(fdata),
+ &session.Values, s.Codecs...); err != nil {
+ return err
+ }
+ return nil
+}
+
+// delete session file
+func (s *FilesystemStore) erase(session *Session) error {
+ filename := filepath.Join(s.path, "session_"+session.ID)
+
+ fileMutex.RLock()
+ defer fileMutex.RUnlock()
+
+ err := os.Remove(filename)
+ return err
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 6936fed9..c7357c5c 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -73,6 +73,24 @@
"versionExact": "v1.1.0"
},
{
+ "checksumSHA1": "g/V4qrXjUGG9B+e3hB+4NAYJ5Gs=",
+ "path": "github.com/gorilla/context",
+ "revision": "08b5f424b9271eedf6f9f0ce86cb9396ed337a42",
+ "revisionTime": "2016-08-17T18:46:32Z"
+ },
+ {
+ "checksumSHA1": "ucTBCc7dDRKLGPsYfAzu/Gq63qA=",
+ "path": "github.com/gorilla/securecookie",
+ "revision": "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983",
+ "revisionTime": "2017-02-24T19:38:04Z"
+ },
+ {
+ "checksumSHA1": "/jOLCzAcN8p1GakgUYaWOs4tjw8=",
+ "path": "github.com/gorilla/sessions",
+ "revision": "a2f2a3de9a4a575047f73e3e36bc85ecc3546391",
+ "revisionTime": "2018-03-21T16:38:55Z"
+ },
+ {
"checksumSHA1": "5TKR3lamABvUhxkopYnphszS+Xc=",
"path": "github.com/grpc-ecosystem/go-grpc-middleware",
"revision": "c250d6563d4d4c20252cd865923440e829844f4e",