diff options
author | Vladimir Shushlin <vshushlin@gitlab.com> | 2019-06-03 14:22:03 +0300 |
---|---|---|
committer | Nick Thomas <nick@gitlab.com> | 2019-06-03 14:22:03 +0300 |
commit | 9df35356572e09dc2c0907113bf64479204e46a9 (patch) | |
tree | 70297534cace3ad4c6015df32757690cc9244992 | |
parent | 80fa0bb4e200a6b3b9194766dd209de28d1cf08a (diff) |
Redirect unknown ACME challenges to the GitLab instance
-rw-r--r-- | acceptance_test.go | 68 | ||||
-rw-r--r-- | app.go | 18 | ||||
-rw-r--r-- | config_test.go | 54 | ||||
-rw-r--r-- | internal/acme/acme.go | 62 | ||||
-rw-r--r-- | internal/acme/acme_test.go | 54 | ||||
-rw-r--r-- | internal/domain/domain.go | 34 | ||||
-rw-r--r-- | internal/domain/domain_test.go | 97 | ||||
-rw-r--r-- | internal/domain/map_test.go | 2 | ||||
-rw-r--r-- | internal/host/host.go | 23 | ||||
-rw-r--r-- | internal/host/host_test.go | 18 | ||||
-rw-r--r-- | internal/testhelpers/testhelpers.go | 44 | ||||
-rw-r--r-- | main.go | 47 | ||||
-rw-r--r-- | shared/pages/group.acme/with.acme.challenge/config.json | 6 | ||||
-rw-r--r-- | shared/pages/group.acme/with.acme.challenge/public/.well-known/acme-challenge/existingtoken | 1 | ||||
-rw-r--r-- | shared/pages/group.acme/with.acme.challenge/public/index.html | 1 |
15 files changed, 467 insertions, 62 deletions
diff --git a/acceptance_test.go b/acceptance_test.go index b22d5e97..eaa32318 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -381,7 +381,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 14") + assert.Contains(t, string(body), "gitlab_pages_domains_served_total 16") } } @@ -800,6 +800,72 @@ func makeGitLabPagesAccessStub(t *testing.T) *httptest.Server { })) } +var existingAcmeTokenPath = "/.well-known/acme-challenge/existingtoken" +var notexistingAcmeTokenPath = "/.well-known/acme-challenge/notexistingtoken" + +func TestAcmeChallengesWhenItIsConfigured(t *testing.T) { + skipUnlessEnabled(t) + + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-gitlab-server=https://gitlab-acme.com") + defer teardown() + + t.Run("When domain folder contains requested acme challenge it responds with it", func(t *testing.T) { + rsp, err := GetRedirectPage(t, httpListener, "withacmechallenge.domain.com", + existingAcmeTokenPath) + + defer rsp.Body.Close() + require.NoError(t, err) + require.Equal(t, http.StatusOK, rsp.StatusCode) + body, _ := ioutil.ReadAll(rsp.Body) + require.Equal(t, "this is token\n", string(body)) + }) + + t.Run("When domain folder doesn't contains requested acme challenge it redirects to GitLab", + func(t *testing.T) { + rsp, err := GetRedirectPage(t, httpListener, "withacmechallenge.domain.com", + notexistingAcmeTokenPath) + + defer rsp.Body.Close() + require.NoError(t, err) + require.Equal(t, http.StatusTemporaryRedirect, rsp.StatusCode) + + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + + require.Equal(t, url.String(), "https://gitlab-acme.com/-/acme-challenge?domain=withacmechallenge.domain.com&token=notexistingtoken") + }, + ) +} + +func TestAcmeChallengesWhenItIsNotConfigured(t *testing.T) { + skipUnlessEnabled(t) + + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "") + defer teardown() + + t.Run("When domain folder contains requested acme challenge it responds with it", func(t *testing.T) { + rsp, err := GetRedirectPage(t, httpListener, "withacmechallenge.domain.com", + existingAcmeTokenPath) + + defer rsp.Body.Close() + require.NoError(t, err) + require.Equal(t, http.StatusOK, rsp.StatusCode) + body, _ := ioutil.ReadAll(rsp.Body) + require.Equal(t, "this is token\n", string(body)) + }) + + t.Run("When domain folder doesn't contains requested acme challenge it returns 404", + func(t *testing.T) { + rsp, err := GetRedirectPage(t, httpListener, "withacmechallenge.domain.com", + notexistingAcmeTokenPath) + + defer rsp.Body.Close() + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, rsp.StatusCode) + }, + ) +} + func TestAccessControlUnderCustomDomain(t *testing.T) { skipUnlessEnabled(t, "not-inplace-chroot") @@ -17,6 +17,7 @@ import ( log "github.com/sirupsen/logrus" mimedb "gitlab.com/lupine/go-mimedb" + "gitlab.com/gitlab-org/gitlab-pages/internal/acme" "gitlab.com/gitlab-org/gitlab-pages/internal/admin" "gitlab.com/gitlab-org/gitlab-pages/internal/artifact" "gitlab.com/gitlab-org/gitlab-pages/internal/auth" @@ -38,10 +39,11 @@ var ( type theApp struct { appConfig - dm domain.Map - lock sync.RWMutex - Artifact *artifact.Artifact - Auth *auth.Auth + dm domain.Map + lock sync.RWMutex + Artifact *artifact.Artifact + Auth *auth.Auth + AcmeMiddleware *acme.Middleware } func (a *theApp) isReady() bool { @@ -166,6 +168,10 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo host, domain := a.getHostAndDomain(r) + if a.AcmeMiddleware.ServeAcmeChallenges(&w, r, domain) { + return + } + if a.Auth.TryAuthenticate(&w, r, a.dm, &a.lock) { return } @@ -360,6 +366,10 @@ func runApp(config appConfig) { config.RedirectURI, config.GitLabServer) } + if config.GitLabServer != "" { + a.AcmeMiddleware = &acme.Middleware{GitlabURL: config.GitLabServer} + } + configureLogging(config.LogFormat, config.LogVerbose) if err := mimedb.LoadTypes(); err != nil { diff --git a/config_test.go b/config_test.go new file mode 100644 index 00000000..3ec51c56 --- /dev/null +++ b/config_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGitLabServerFromFlags(t *testing.T) { + tests := []struct { + name string + gitLabServer string + gitLabAuthServer string + artifactsServer string + expected string + }{ + { + name: "When gitLabServer is set", + gitLabServer: "gitlabserver.com", + gitLabAuthServer: "authserver.com", + artifactsServer: "https://artifactsserver.com", + expected: "gitlabserver.com", + }, + { + name: "When auth server is set", + gitLabServer: "", + gitLabAuthServer: "authserver.com", + artifactsServer: "https://artifactsserver.com", + expected: "authserver.com", + }, + { + name: "When only artifacts server is set", + gitLabServer: "", + gitLabAuthServer: "", + artifactsServer: "https://artifactsserver.com", + expected: "artifactsserver.com", + }, + { + name: "When only artifacts server includes path", + gitLabServer: "", + gitLabAuthServer: "", + artifactsServer: "https://artifactsserver.com:8080/api/path", + expected: "artifactsserver.com", + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gitLabServer = &test.gitLabServer + gitLabAuthServer = &test.gitLabAuthServer + artifactsServer = &test.artifactsServer + assert.Equal(t, test.expected, gitlabServerFromFlags()) + }) + } +} diff --git a/internal/acme/acme.go b/internal/acme/acme.go new file mode 100644 index 00000000..89881f34 --- /dev/null +++ b/internal/acme/acme.go @@ -0,0 +1,62 @@ +package acme + +import ( + "net/http" + "net/url" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" + + "gitlab.com/gitlab-org/gitlab-pages/internal/host" +) + +// Middleware handles acme challenges by redirecting them to GitLab instance +type Middleware struct { + GitlabURL string +} + +// Domain interface represent D from domain package +type Domain interface { + HasAcmeChallenge(string) bool +} + +// ServeAcmeChallenges identifies if request is acme-challenge and redirects to GitLab in that case +func (m *Middleware) ServeAcmeChallenges(w http.ResponseWriter, r *http.Request, domain Domain) bool { + if m == nil { + return false + } + + if !isAcmeChallenge(r.URL.Path) { + return false + } + + if domain.HasAcmeChallenge(filepath.Base(r.URL.Path)) { + return false + } + + return m.redirectToGitlab(w, r) +} + +func isAcmeChallenge(path string) bool { + return strings.HasPrefix(filepath.Clean(path), "/.well-known/acme-challenge/") +} + +func (m *Middleware) redirectToGitlab(w http.ResponseWriter, r *http.Request) bool { + redirectURL, err := url.Parse(m.GitlabURL) + if err != nil { + log.WithError(err).Error("Can't parse GitLab URL for acme challenge redirect") + return false + } + + redirectURL.Path = "/-/acme-challenge" + query := redirectURL.Query() + query.Set("domain", host.FromRequest(r)) + query.Set("token", filepath.Base(r.URL.Path)) + redirectURL.RawQuery = query.Encode() + + log.WithField("redirect_url", redirectURL).Debug("Redirecting to GitLab for processing acme challenge") + + http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect) + return true +} diff --git a/internal/acme/acme_test.go b/internal/acme/acme_test.go new file mode 100644 index 00000000..c0daefeb --- /dev/null +++ b/internal/acme/acme_test.go @@ -0,0 +1,54 @@ +package acme + +import ( + "net/http" + "testing" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" +) + +type domainStub struct { + hasAcmeChallenge bool +} + +func (d *domainStub) HasAcmeChallenge(_ string) bool { + return d.hasAcmeChallenge +} + +func serveAcmeOrNotFound(m *Middleware, domain Domain) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !m.ServeAcmeChallenges(w, r, domain) { + http.NotFound(w, r) + } + } +} + +const ( + baseURL = "http://example.com" + indexURL = baseURL + "/index.html" + challengeURL = baseURL + "/.well-known/acme-challenge/token" +) + +var ( + domainWithChallenge = &domainStub{hasAcmeChallenge: true} + domain = &domainStub{hasAcmeChallenge: false} + middleware = &Middleware{GitlabURL: "https://gitlab.example.com"} +) + +func TestServeAcmeChallengesNotConfigured(t *testing.T) { + testhelpers.AssertHTTP404(t, serveAcmeOrNotFound(nil, domain), "GET", challengeURL, nil, nil) +} + +func TestServeAcmeChallengeWhenPresent(t *testing.T) { + testhelpers.AssertHTTP404(t, serveAcmeOrNotFound(middleware, domainWithChallenge), "GET", challengeURL, nil, nil) +} + +func TestServeAcmeChallengeWhenMissing(t *testing.T) { + testhelpers.AssertRedirectTo( + t, serveAcmeOrNotFound(middleware, domain), + "GET", challengeURL, nil, + "https://gitlab.example.com/-/acme-challenge?domain=example.com&token=token", + ) + + testhelpers.AssertHTTP404(t, serveAcmeOrNotFound(middleware, domain), "GET", indexURL, nil, nil) +} diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 1455d436..4ce8a561 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "mime" - "net" "net/http" "os" "path/filepath" @@ -17,6 +16,7 @@ import ( "golang.org/x/sys/unix" + "gitlab.com/gitlab-org/gitlab-pages/internal/host" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" "gitlab.com/gitlab-org/gitlab-pages/internal/httputil" ) @@ -106,16 +106,6 @@ func handleGZip(w http.ResponseWriter, r *http.Request, fullPath string) string return gzipPath } -func getHost(r *http.Request) string { - host := strings.ToLower(r.Host) - - if splitHost, _, err := net.SplitHostPort(host); err == nil { - host = splitHost - } - - return host -} - // Look up a project inside the domain based on the host and path. Returns the // project and its name (if applicable) func (d *D) getProjectWithSubpath(r *http.Request) (*project, string, string) { @@ -131,7 +121,7 @@ func (d *D) getProjectWithSubpath(r *http.Request) (*project, string, string) { // Since the URL doesn't specify a project (e.g. http://mydomain.gitlab.io), // return the group project if it exists. - if host := getHost(r); host != "" { + if host := host.FromRequest(r); host != "" { if groupProject := d.projects[host]; groupProject != nil { return groupProject, host, strings.Join(split[1:], "/") } @@ -179,6 +169,26 @@ func (d *D) IsAccessControlEnabled(r *http.Request) bool { return false } +// HasAcmeChallenge checks domain directory contains particular acme challenge +func (d *D) HasAcmeChallenge(token string) bool { + if d == nil { + return false + } + + if d.config == nil { + return false + } + + _, err := d.resolvePath(d.projectName, ".well-known/acme-challenge", token) + + // there is an acme challenge on disk + if err == nil { + return true + } + + return false +} + // IsNamespaceProject figures out if the request is to a namespace project func (d *D) IsNamespaceProject(r *http.Request) bool { if d == nil { diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go index add9b616..a05dbe7e 100644 --- a/internal/domain/domain_test.go +++ b/internal/domain/domain_test.go @@ -3,7 +3,6 @@ package domain import ( "compress/gzip" "io/ioutil" - "mime" "net/http" "net/http/httptest" "net/url" @@ -15,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-pages/internal/fixture" + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func serveFileOrNotFound(domain *D) http.HandlerFunc { @@ -25,11 +25,6 @@ func serveFileOrNotFound(domain *D) http.HandlerFunc { } } -func assertRedirectTo(t *testing.T, h http.HandlerFunc, method string, url string, values url.Values, expectedURL string) { - assert.HTTPRedirect(t, h, method, url, values) - assert.HTTPBodyContains(t, h, method, url, values, `<a href="//`+expectedURL+`">Found</a>`) -} - func testGroupServeHTTPHost(t *testing.T, host string) { testGroup := &D{ projectName: "", @@ -53,12 +48,12 @@ func testGroupServeHTTPHost(t *testing.T, host string) { assert.HTTPBodyContains(t, serve, "GET", makeURL("/"), nil, "main-dir") assert.HTTPBodyContains(t, serve, "GET", makeURL("/index"), nil, "main-dir") assert.HTTPBodyContains(t, serve, "GET", makeURL("/index.html"), nil, "main-dir") - assertRedirectTo(t, serve, "GET", makeURL("/project"), nil, host+"/project/") + testhelpers.AssertRedirectTo(t, serve, "GET", makeURL("/project"), nil, "//"+host+"/project/") assert.HTTPBodyContains(t, serve, "GET", makeURL("/project/"), nil, "project-subdir") assert.HTTPBodyContains(t, serve, "GET", makeURL("/project/index"), nil, "project-subdir") assert.HTTPBodyContains(t, serve, "GET", makeURL("/project/index/"), nil, "project-subdir") assert.HTTPBodyContains(t, serve, "GET", makeURL("/project/index.html"), nil, "project-subdir") - assertRedirectTo(t, serve, "GET", makeURL("/project/subdir"), nil, host+"/project/subdir/") + testhelpers.AssertRedirectTo(t, serve, "GET", makeURL("/project/subdir"), nil, "//"+host+"/project/subdir/") assert.HTTPBodyContains(t, serve, "GET", makeURL("/project/subdir/"), nil, "project-subsubdir") assert.HTTPBodyContains(t, serve, "GET", makeURL("/project2/"), nil, "project2-main") assert.HTTPBodyContains(t, serve, "GET", makeURL("/project2/index"), nil, "project2-main") @@ -205,7 +200,61 @@ func TestIsHTTPSOnly(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, test.url, nil) - assert.Equal(t, test.domain.IsHTTPSOnly(req), test.expected) + assert.Equal(t, test.expected, test.domain.IsHTTPSOnly(req)) + }) + } +} + +func TestHasAcmeChallenge(t *testing.T) { + cleanup := setUpTests(t) + defer cleanup() + + tests := []struct { + name string + domain *D + token string + expected bool + }{ + { + name: "Project containing acme challenge", + domain: &D{ + group: group{name: "group.acme"}, + projectName: "with.acme.challenge", + config: &domainConfig{HTTPSOnly: true}, + }, + token: "existingtoken", + expected: true, + }, + { + name: "Project containing another token", + domain: &D{ + group: group{name: "group.acme"}, + projectName: "with.acme.challenge", + config: &domainConfig{HTTPSOnly: true}, + }, + token: "notexistingtoken", + expected: false, + }, + { + name: "nil domain", + domain: nil, + token: "existingtoken", + expected: false, + }, + { + name: "Domain without config", + domain: &D{ + group: group{name: "group.acme"}, + projectName: "with.acme.challenge", + config: nil, + }, + token: "existingtoken", + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, test.domain.HasAcmeChallenge(test.token)) }) } } @@ -304,18 +353,6 @@ func TestGroupServeHTTPGzip(t *testing.T) { } } -func testHTTP404(t *testing.T, handler http.HandlerFunc, mode, url string, values url.Values, str interface{}) { - w := httptest.NewRecorder() - req, err := http.NewRequest(mode, url+"?"+values.Encode(), nil) - require.NoError(t, err) - handler(w, req) - - contentType, _, _ := mime.ParseMediaType(w.Header().Get("Content-Type")) - assert.Equal(t, http.StatusNotFound, w.Code, "HTTP status") - assert.Equal(t, "text/html", contentType, "Content-Type") - assert.Contains(t, w.Body.String(), str) -} - func TestGroup404ServeHTTP(t *testing.T) { cleanup := setUpTests(t) defer cleanup() @@ -334,15 +371,15 @@ func TestGroup404ServeHTTP(t *testing.T) { }, } - 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") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404/not/existing-file", nil, "Custom 404 project page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404/", nil, "Custom 404 project page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/not/existing-file", nil, "Custom 404 group page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") + testhelpers.AssertHTTP404(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, 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.") + testhelpers.AssertHTTP404(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) { @@ -357,8 +394,8 @@ func TestDomain404ServeHTTP(t *testing.T) { }, } - 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") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/", nil, "Custom 404 group page") } func TestPredefined404ServeHTTP(t *testing.T) { @@ -369,7 +406,7 @@ func TestPredefined404ServeHTTP(t *testing.T) { group: group{name: "group"}, } - testHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.test.io/not-existing-file", nil, "The page you're looking for could not be found") + testhelpers.AssertHTTP404(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) { diff --git a/internal/domain/map_test.go b/internal/domain/map_test.go index dc5e8648..156b039f 100644 --- a/internal/domain/map_test.go +++ b/internal/domain/map_test.go @@ -55,6 +55,8 @@ func TestReadProjects(t *testing.T) { "no.cert.com", "private.domain.com", "group.auth.test.io", + "group.acme.test.io", + "withacmechallenge.domain.com", "capitalgroup.test.io", } diff --git a/internal/host/host.go b/internal/host/host.go new file mode 100644 index 00000000..dd9b1c8a --- /dev/null +++ b/internal/host/host.go @@ -0,0 +1,23 @@ +package host + +import ( + "net" + "net/http" + "strings" +) + +// FromString tries to split host port from string, returns host or initial string if fail +func FromString(s string) string { + host := strings.ToLower(s) + + if splitHost, _, err := net.SplitHostPort(host); err == nil { + host = splitHost + } + + return host +} + +// FromRequest tries to split host port from r.Host, returns host or initial string if fail +func FromRequest(r *http.Request) string { + return FromString(r.Host) +} diff --git a/internal/host/host_test.go b/internal/host/host_test.go new file mode 100644 index 00000000..8395d3fc --- /dev/null +++ b/internal/host/host_test.go @@ -0,0 +1,18 @@ +package host + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFromString(t *testing.T) { + assert.Equal(t, "example.com", FromString("example.com")) + assert.Equal(t, "example.com", FromString("eXAmpLe.com")) + assert.Equal(t, "example.com", FromString("example.com:8080")) +} + +func TestFromRequest(t *testing.T) { + assert.Equal(t, "example.com", FromRequest(httptest.NewRequest("GET", "example.com:8080/123", nil))) +} diff --git a/internal/testhelpers/testhelpers.go b/internal/testhelpers/testhelpers.go new file mode 100644 index 00000000..1c3a1eba --- /dev/null +++ b/internal/testhelpers/testhelpers.go @@ -0,0 +1,44 @@ +package testhelpers + +import ( + "mime" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// AssertHTTP404 asserts handler returns 404 with provided str body +func AssertHTTP404(t *testing.T, handler http.HandlerFunc, mode, url string, values url.Values, str interface{}) { + w := httptest.NewRecorder() + req, err := http.NewRequest(mode, url+"?"+values.Encode(), nil) + require.NoError(t, err) + handler(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code, "HTTP status") + + if str != nil { + contentType, _, _ := mime.ParseMediaType(w.Header().Get("Content-Type")) + assert.Equal(t, "text/html", contentType, "Content-Type") + assert.Contains(t, w.Body.String(), str) + } +} + +// AssertRedirectTo asserts that handler redirects to particular URL +func AssertRedirectTo(t *testing.T, handler http.HandlerFunc, method string, + url string, values url.Values, expectedURL string) { + + assert.HTTPRedirect(t, handler, method, url, values) + + recorder := httptest.NewRecorder() + + req, _ := http.NewRequest(method, url, nil) + req.URL.RawQuery = values.Encode() + + handler(recorder, req) + + assert.Equal(t, expectedURL, recorder.Header().Get("Location")) +} @@ -11,6 +11,7 @@ import ( "github.com/namsral/flag" log "github.com/sirupsen/logrus" + "gitlab.com/gitlab-org/gitlab-pages/internal/host" "gitlab.com/gitlab-org/gitlab-pages/internal/tlsconfig" ) @@ -48,7 +49,8 @@ var ( 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") + gitLabAuthServer = flag.String("auth-server", "", "DEPRECATED, use gitlab-server instead. GitLab server, for example https://www.gitlab.com") + gitLabServer = flag.String("gitlab-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") @@ -72,10 +74,24 @@ var ( 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") + errGitLabServerNotDefined = errors.New("gitlab-server must be defined if authentication is supported") errRedirectURINotDefined = errors.New("auth-redirect-uri must be defined if authentication is supported") ) +func gitlabServerFromFlags() string { + if *gitLabServer != "" { + return *gitLabServer + } + + if *gitLabAuthServer != "" { + log.Warn("auth-server parameter is deprecated, use gitlab-server instead") + return *gitLabAuthServer + } + + url, _ := url.Parse(*artifactsServer) + return host.FromString(url.Host) +} + func configFromFlags() appConfig { var config appConfig @@ -129,39 +145,40 @@ func configFromFlags() appConfig { config.ArtifactsServer = *artifactsServer } - checkAuthenticationConfig(config) + config.GitLabServer = gitlabServerFromFlags() config.StoreSecret = *secret config.ClientID = *clientID config.ClientSecret = *clientSecret - config.GitLabServer = *gitLabServer config.RedirectURI = *redirectURI + checkAuthenticationConfig(config) + return config } func checkAuthenticationConfig(config appConfig) { - if *secret != "" || *clientID != "" || *clientSecret != "" || - *gitLabServer != "" || *redirectURI != "" { - // Check all auth params are valid - assertAuthConfig() + if config.StoreSecret == "" && config.ClientID == "" && + config.ClientSecret == "" && config.RedirectURI == "" { + return } + assertAuthConfig(config) } -func assertAuthConfig() { - if *secret == "" { +func assertAuthConfig(config appConfig) { + if config.StoreSecret == "" { log.Fatal(errSecretNotDefined) } - if *clientID == "" { + if config.ClientID == "" { log.Fatal(errClientIDNotDefined) } - if *clientSecret == "" { + if config.ClientSecret == "" { log.Fatal(errClientSecretNotDefined) } - if *gitLabServer == "" { + if config.GitLabServer == "" { log.Fatal(errGitLabServerNotDefined) } - if *redirectURI == "" { + if config.RedirectURI == "" { log.Fatal(errRedirectURINotDefined) } } @@ -222,7 +239,7 @@ func appMain() { "tls-max-version": *tlsMaxVersion, "use-http-2": config.HTTP2, "auth-secret": config.StoreSecret, - "auth-server": config.GitLabServer, + "gitlab-server": config.GitLabServer, "auth-client-id": config.ClientID, "auth-client-secret": config.ClientSecret, "auth-redirect-uri": config.RedirectURI, diff --git a/shared/pages/group.acme/with.acme.challenge/config.json b/shared/pages/group.acme/with.acme.challenge/config.json new file mode 100644 index 00000000..f50ba7fa --- /dev/null +++ b/shared/pages/group.acme/with.acme.challenge/config.json @@ -0,0 +1,6 @@ +{ "domains": [ + { + "domain": "withacmechallenge.domain.com" + } + ] +} diff --git a/shared/pages/group.acme/with.acme.challenge/public/.well-known/acme-challenge/existingtoken b/shared/pages/group.acme/with.acme.challenge/public/.well-known/acme-challenge/existingtoken new file mode 100644 index 00000000..84455e1d --- /dev/null +++ b/shared/pages/group.acme/with.acme.challenge/public/.well-known/acme-challenge/existingtoken @@ -0,0 +1 @@ +this is token diff --git a/shared/pages/group.acme/with.acme.challenge/public/index.html b/shared/pages/group.acme/with.acme.challenge/public/index.html new file mode 100644 index 00000000..9015a7a3 --- /dev/null +++ b/shared/pages/group.acme/with.acme.challenge/public/index.html @@ -0,0 +1 @@ +index |