diff --git a/404.go b/404.go
deleted file mode 100644
index 0ac0be5406d535a8ef7c6b8032ee640966b41fd3..0000000000000000000000000000000000000000
--- a/404.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package main
-
-import (
- "fmt"
- "net/http"
-)
-
-const predefined404 = `
-
-
-
- The page you're looking for could not be found (404)
-
-
-
-
-
- 
- 404
-
- The page you're looking for could not be found.
-
- Make sure the address is correct and that the page hasn't moved.
-
-
-`
-
-func serve404(w http.ResponseWriter) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.Header().Set("X-Content-Type-Options", "nosniff")
- w.WriteHeader(http.StatusNotFound)
- fmt.Fprintln(w, predefined404)
-}
diff --git a/acceptance_test.go b/acceptance_test.go
index 8adef2a61e4d6dae4f249f47400864360069ac37..3e575bdf14958992d52980949a9c2bd461dbf21c 100644
--- a/acceptance_test.go
+++ b/acceptance_test.go
@@ -2,10 +2,13 @@ package main
import (
"flag"
+ "fmt"
"io/ioutil"
"net/http"
+ "net/http/httptest"
"os"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -207,3 +210,97 @@ func TestStatusPage(t *testing.T) {
defer rsp.Body.Close()
assert.Equal(t, http.StatusOK, rsp.StatusCode)
}
+
+func TestProxyRequest(t *testing.T) {
+ skipUnlessEnabled(t)
+ content := "Title of the document"
+ contentLength := int64(len(content))
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/projects/1/jobs/2/artifacts/delayed_200.html":
+ time.Sleep(2 * time.Second)
+ case "/projects/1/jobs/2/artifacts/200.html":
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprint(w, content)
+ case "/projects/1/jobs/2/artifacts/500.html":
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprint(w, content)
+ default:
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusNotFound)
+ fmt.Fprint(w, content)
+ }
+ }))
+ defer testServer.Close()
+ cases := []struct {
+ Path string
+ Status int
+ BinaryOption string
+ Content string
+ Length int64
+ CacheControl string
+ ContentType string
+ Description string
+ }{
+ {
+ "200.html",
+ http.StatusOK,
+ "",
+ content,
+ contentLength,
+ "max-age=3600",
+ "text/html; charset=utf-8",
+ "basic proxied request",
+ },
+ {
+ "delayed_200.html",
+ http.StatusBadGateway,
+ "-artifacts-server-timeout=1",
+ "",
+ 0,
+ "",
+ "text/html; charset=utf-8",
+ "502 error while attempting to proxy",
+ },
+ {
+ "404.html",
+ http.StatusNotFound,
+ "",
+ "",
+ 0,
+ "",
+ "text/html; charset=utf-8",
+ "Proxying 404 from server",
+ },
+ {
+ "500.html",
+ http.StatusInternalServerError,
+ "",
+ "",
+ 0,
+ "",
+ "text/html; charset=utf-8",
+ "Proxying 500 from server",
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(fmt.Sprintf("Proxy Request Test: %s", c.Description), func(t *testing.T) {
+ teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-artifacts-server="+testServer.URL, c.BinaryOption)
+ defer teardown()
+ resp, err := GetPageFromListener(t, httpListener, "artifact~1~2.gitlab-example.com", c.Path)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+ assert.Equal(t, c.Status, resp.StatusCode)
+ assert.Equal(t, c.ContentType, resp.Header.Get("Content-Type"))
+ if !((c.Status == http.StatusBadGateway) || (c.Status == http.StatusNotFound) || (c.Status == http.StatusInternalServerError)) {
+ body, err := ioutil.ReadAll(resp.Body)
+ require.NoError(t, err)
+ assert.Equal(t, c.Content, string(body))
+ assert.Equal(t, c.Length, resp.ContentLength)
+ assert.Equal(t, c.CacheControl, resp.Header.Get("Cache-Control"))
+ }
+ })
+ }
+}
diff --git a/app.go b/app.go
index 61b1176c5e95e48357e542d27d66a4a111a0fc1e..3714582aedaefc4b7d91b0bda81f733072292ee6 100644
--- a/app.go
+++ b/app.go
@@ -14,6 +14,8 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/cors"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/artifact"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
"gitlab.com/gitlab-org/gitlab-pages/metrics"
)
@@ -26,8 +28,9 @@ var (
type theApp struct {
appConfig
- domains domains
- lock sync.RWMutex
+ domains domains
+ lock sync.RWMutex
+ Artifact *artifact.Artifact
}
func (a *theApp) domain(host string) *domain {
@@ -79,9 +82,16 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo
if err != nil {
host = r.Host
}
+
+ // In the event a host is prefixed with the artifact prefix an artifact
+ // value is created, and an attempt to proxy the request is made
+ if a.Artifact.TryMakeRequest(host, &w, r) {
+ return
+ }
+
domain := a.domain(host)
if domain == nil {
- serve404(&w)
+ httperrors.Serve404(&w)
return
}
@@ -173,5 +183,9 @@ func (a *theApp) Run() {
func runApp(config appConfig) {
a := theApp{appConfig: config}
+
+ if config.ArtifactsServer != "" {
+ a.Artifact = artifact.New(config.ArtifactsServer, config.ArtifactsServerTimeout, config.Domain)
+ }
a.Run()
}
diff --git a/app_config.go b/app_config.go
index e45dda24f35c702ef17b0c65c4dd794a9eb8e718..dd47ed017a6e9d368a37959d678ec9dbea108ce3 100644
--- a/app_config.go
+++ b/app_config.go
@@ -1,10 +1,11 @@
package main
type appConfig struct {
- Domain string
-
- RootCertificate []byte
- RootKey []byte
+ Domain string
+ ArtifactsServer string
+ ArtifactsServerTimeout int
+ RootCertificate []byte
+ RootKey []byte
ListenHTTP []uintptr
ListenHTTPS []uintptr
diff --git a/domain.go b/domain.go
index 45e59f946a1db2bfec9a6adb27769be7536f5fd7..fc6611e1574273eb4295c81b90ed7200a0d1e5bd 100644
--- a/domain.go
+++ b/domain.go
@@ -13,6 +13,7 @@ import (
"strings"
"time"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
"gitlab.com/gitlab-org/gitlab-pages/internal/httputil"
)
@@ -215,7 +216,7 @@ func (d *domain) serveFromGroup(w http.ResponseWriter, r *http.Request) {
}
// Serve generic not found
- serve404(w)
+ httperrors.Serve404(w)
}
func (d *domain) serveFromConfig(w http.ResponseWriter, r *http.Request) {
@@ -230,7 +231,7 @@ func (d *domain) serveFromConfig(w http.ResponseWriter, r *http.Request) {
}
// Serve generic not found
- serve404(w)
+ httperrors.Serve404(w)
}
func (d *domain) ensureCertificate() (*tls.Certificate, error) {
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
new file mode 100644
index 0000000000000000000000000000000000000000..bcb525ac5e5a62441315a854cc7f2cb5765ecf33
--- /dev/null
+++ b/internal/artifact/artifact.go
@@ -0,0 +1,120 @@
+package artifact
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
+)
+
+const (
+ baseURL = "/projects/%s/jobs/%s/artifacts"
+ hostPatternTemplate = `(?i)\Aartifact~(\d+)~(\d+)\.%s\z`
+ minStatusCode = 200
+ maxStatusCode = 299
+)
+
+// Artifact is a struct that is made up of a url.URL, http.Client, and
+// regexp.Regexp that is used to proxy requests where applicable.
+type Artifact struct {
+ server string
+ client *http.Client
+ pattern *regexp.Regexp
+}
+
+// New when provided the arguments defined herein, returns a pointer to an
+// Artifact that is used to proxy requests.
+func New(s string, timeout int, pagesDomain string) *Artifact {
+ return &Artifact{
+ server: s,
+ client: &http.Client{Timeout: time.Second * time.Duration(timeout)},
+ pattern: hostPatternGen(pagesDomain),
+ }
+
+}
+
+// TryMakeRequest will attempt to proxy a request and write it to the argument
+// http.ResponseWriter, ultimately returning a bool that indicates if the
+// http.ResponseWriter has been written to in any capacity.
+func (a *Artifact) TryMakeRequest(host string, w http.ResponseWriter, r *http.Request) bool {
+ if a == nil || a.server == "" {
+ return false
+ }
+
+ reqURL, ok := a.buildURL(host, r.URL.Path)
+ if !ok {
+ return false
+ }
+
+ resp, err := a.client.Get(reqURL.String())
+ if err != nil {
+ httperrors.Serve502(w)
+ return true
+ }
+
+ if resp.StatusCode == http.StatusNotFound {
+ httperrors.Serve404(w)
+ return true
+ }
+
+ if resp.StatusCode == http.StatusInternalServerError {
+ httperrors.Serve500(w)
+ return true
+ }
+
+ // we only cache responses within the 2xx series response codes
+ if (resp.StatusCode >= minStatusCode) && (resp.StatusCode <= maxStatusCode) {
+ w.Header().Set("Cache-Control", "max-age=3600")
+ }
+
+ w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
+ w.Header().Set("Content-Length", strconv.FormatInt(resp.ContentLength, 10))
+ w.WriteHeader(resp.StatusCode)
+ io.Copy(w, resp.Body)
+ return true
+}
+
+// buildURL returns a pointer to a url.URL for where the request should be
+// proxied to. The returned bool will indicate if there is some sort of issue
+// with the url while it is being generated.
+func (a *Artifact) buildURL(host, path string) (*url.URL, bool) {
+ ids := a.pattern.FindAllStringSubmatch(host, -1)
+ if len(ids) != 1 || len(ids[0]) != 3 {
+ return nil, false
+ }
+
+ strippedIds := ids[0][1:3]
+ body := fmt.Sprintf(baseURL, strippedIds[0], strippedIds[1])
+ ourPath := a.server
+ if strings.HasSuffix(ourPath, "/") {
+ ourPath = ourPath[0:len(ourPath)-1] + body
+ } else {
+ ourPath = ourPath + body
+ }
+
+ if len(path) == 0 || strings.HasPrefix(path, "/") {
+ ourPath = ourPath + path
+ } else {
+ ourPath = ourPath + "/" + path
+ }
+
+ u, err := url.Parse(ourPath)
+ if err != nil {
+ return nil, false
+ }
+ return u, true
+}
+
+// hostPatternGen returns a pointer to a regexp.Regexp that is made up of
+// the constant hostPatternTemplate and the argument which represents the pages domain.
+// This is used to ensure that the requested page meets not only the hostPatternTemplate
+// requirements, but is suffixed with the proper pagesDomain.
+func hostPatternGen(pagesDomain string) *regexp.Regexp {
+ return regexp.MustCompile(fmt.Sprintf(hostPatternTemplate, regexp.QuoteMeta(pagesDomain)))
+}
diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ad2285f651b571f6a0735c85d90193acc9350082
--- /dev/null
+++ b/internal/artifact/artifact_test.go
@@ -0,0 +1,303 @@
+package artifact
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTryMakeRequest(t *testing.T) {
+ content := "Title of the document"
+ contentType := "text/html; charset=utf-8"
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", contentType)
+ switch r.URL.Path {
+ case "/projects/1/jobs/2/artifacts/200.html":
+ w.WriteHeader(http.StatusOK)
+ case "/projects/1/jobs/2/artifacts/max-caching.html":
+ w.WriteHeader(http.StatusIMUsed)
+ case "/projects/1/jobs/2/artifacts/non-caching.html":
+ w.WriteHeader(http.StatusTeapot)
+ case "/projects/1/jobs/2/artifacts/500.html":
+ w.WriteHeader(http.StatusInternalServerError)
+ case "/projects/1/jobs/2/artifacts/404.html":
+ w.WriteHeader(http.StatusNotFound)
+ }
+ fmt.Fprint(w, content)
+ }))
+ defer testServer.Close()
+
+ cases := []struct {
+ Path string
+ Status int
+ Content string
+ Length string
+ CacheControl string
+ ContentType string
+ Description string
+ }{
+ {
+ "/200.html",
+ http.StatusOK,
+ content,
+ "90",
+ "max-age=3600",
+ "text/html; charset=utf-8",
+ "basic successful request",
+ },
+ {
+ "/max-caching.html",
+ http.StatusIMUsed,
+ content,
+ "90",
+ "max-age=3600",
+ "text/html; charset=utf-8",
+ "max caching request",
+ },
+ {
+ "/non-caching.html",
+ http.StatusTeapot,
+ content,
+ "90",
+ "",
+ "text/html; charset=utf-8",
+ "no caching request",
+ },
+ }
+
+ for _, c := range cases {
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse(c.Path)
+ assert.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+ art := &Artifact{
+ server: testServer.URL,
+ client: &http.Client{Timeout: time.Second * time.Duration(1)},
+ pattern: regexp.MustCompile(fmt.Sprintf(hostPatternTemplate, "gitlab-example.io")),
+ }
+
+ assert.True(t, art.TryMakeRequest("artifact~1~2.gitlab-example.io", result, r))
+ assert.Equal(t, c.ContentType, result.Header().Get("Content-Type"))
+ assert.Equal(t, c.Length, result.Header().Get("Content-Length"))
+ assert.Equal(t, c.CacheControl, result.Header().Get("Cache-Control"))
+ assert.Equal(t, c.Content, string(result.Body.Bytes()))
+ assert.Equal(t, c.Status, result.Code)
+ }
+}
+
+func TestBuildURL(t *testing.T) {
+ cases := []struct {
+ RawServer string
+ Host string
+ Path string
+ Expected string
+ PagesDomain string
+ Ok bool
+ Description string
+ }{
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~1~2.gitlab.io",
+ "/path/to/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/path/to/file.txt",
+ "gitlab.io",
+ true,
+ "basic case",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "/path/to/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/path/to/file.txt",
+ "gitlab.io",
+ true,
+ "basic case 2",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~1~2.gitlab.io",
+ "path/to/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/path/to/file.txt",
+ "gitlab.io",
+ true,
+ "basic case 3",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "path/to/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/path/to/file.txt",
+ "gitlab.io",
+ true,
+ "basic case 4",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~1~2.gitlab.io",
+ "",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts",
+ "gitlab.io",
+ true,
+ "basic case 5",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts",
+ "gitlab.io",
+ true,
+ "basic case 6",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~1~2.gitlab.io",
+ "/",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/",
+ "gitlab.io",
+ true,
+ "basic case 7",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "/",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/",
+ "gitlab.io",
+ true,
+ "basic case 8",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~100000~200000.gitlab.io",
+ "/file.txt",
+ "https://gitlab.com/api/v4/projects/100000/jobs/200000/artifacts/file.txt",
+ "gitlab.io",
+ true,
+ "expanded case",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/file.txt",
+ "gitlab.io",
+ true,
+ "server with tailing slash",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~A~B.gitlab.io",
+ "/index.html",
+ "",
+ "example.com",
+ false,
+ "non matching domain and request",
+ },
+ {
+ "",
+ "artifact~A~B.gitlab.io",
+ "",
+ "",
+ "",
+ false,
+ "un-parseable Host",
+ },
+ }
+
+ for _, c := range cases {
+ a := &Artifact{server: c.RawServer, pattern: regexp.MustCompile(fmt.Sprintf(hostPatternTemplate, c.PagesDomain))}
+ u, ok := a.buildURL(c.Host, c.Path)
+ assert.Equal(t, c.Ok, ok, c.Description)
+ if c.Ok {
+ assert.Equal(t, c.Expected, u.String(), c.Description)
+ }
+ }
+}
+
+func TestMatchHostGen(t *testing.T) {
+ cases := []struct {
+ URLHost string
+ PagesDomain string
+ Expected bool
+ Description string
+ }{
+ {
+ "artifact~1~2.gitlab.io",
+ "gitlab.io",
+ true,
+ "basic case",
+ },
+ {
+ "ARTIFACT~1~2.gitlab.io",
+ "gitlab.io",
+ true,
+ "capital letters case",
+ },
+ {
+ "ARTIFACT~11234~2908908.gitlab.io",
+ "gitlab.io",
+ true,
+ "additional capital letters case",
+ },
+ {
+ "artifact~10000~20000.gitlab.io",
+ "gitlab.io",
+ true,
+ "expanded case",
+ },
+ {
+ "artifact~86753095555~55550935768.gitlab.io",
+ "gitlab.io",
+ true,
+ "large number case",
+ },
+ {
+ "artifact~one~two.gitlab.io",
+ "gitlab.io",
+ false,
+ "letters rather than numbers",
+ },
+ {
+ "artifact~One111~tWo222.gitlab.io",
+ "gitlab.io",
+ false,
+ "Mixture of alphanumeric",
+ },
+ {
+ "artifact~!@#$%~%$#@!.gitlab.io",
+ "gitlab.io",
+ false,
+ "special characters",
+ },
+ {
+ "artifact~1.gitlab.io",
+ "gitlab.io",
+ false,
+ "not enough ids",
+ },
+ {
+ "artifact~1~2~34444~1~4.gitlab.io",
+ "gitlab.io",
+ false,
+ "too many ids",
+ },
+ {
+ "artifact~1~2.gitlab.io",
+ "otherhost.io",
+ false,
+ "different domain / suffix",
+ },
+ }
+
+ for _, c := range cases {
+ reg := hostPatternGen(c.PagesDomain)
+ assert.Equal(t, c.Expected, reg.MatchString(c.URLHost), c.Description)
+ }
+}
diff --git a/internal/httperrors/httperrors.go b/internal/httperrors/httperrors.go
new file mode 100644
index 0000000000000000000000000000000000000000..82d43fc54439c0dff4e8805ef9eb3228c5f8ad92
--- /dev/null
+++ b/internal/httperrors/httperrors.go
@@ -0,0 +1,162 @@
+package httperrors
+
+import (
+ "fmt"
+ "net/http"
+)
+
+type content struct {
+ status int
+ title string
+ statusString string
+ header string
+ subHeader string
+}
+
+var (
+ content404 = content{
+ http.StatusNotFound,
+ "The page you're looking for could not be found (404)",
+ "404",
+ "The page you're looking for could not be found.",
+ `The resource that you are attempting to access does not exist or you don't have the necessary permissions to view it.
+ Make sure the address is correct and that the page hasn't moved.
+ Please contact your GitLab administrator if you think this is a mistake.
`,
+ }
+ content500 = content{
+ http.StatusInternalServerError,
+ "Something went wrong (500)",
+ "500",
+ "Whoops, something went wrong on our end.",
+ `Try refreshing the page, or going back and attempting the action again.
+ Please contact your GitLab administrator if this problem persists.
`,
+ }
+
+ content502 = content{
+ http.StatusBadGateway,
+ "Something went wrong (502)",
+ "502",
+ "Whoops, something went wrong on our end.",
+ `Try refreshing the page, or going back and attempting the action again.
+ Please contact your GitLab administrator if this problem persists.
`,
+ }
+)
+
+const predefinedErrorPage = `
+
+
+
+
+ %v
+
+
+
+
+
+
+ %v
+
+
+
+
+
+`
+
+func generateErrorHTML(c content) string {
+ return fmt.Sprintf(predefinedErrorPage, c.title, c.statusString, c.header, c.subHeader)
+}
+
+func serveErrorPage(w http.ResponseWriter, c content) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+ w.WriteHeader(c.status)
+ fmt.Fprintln(w, generateErrorHTML(c))
+}
+
+// Serve404 returns a 404 error response / HTML page to the http.ResponseWriter
+func Serve404(w http.ResponseWriter) {
+ serveErrorPage(w, content404)
+}
+
+// Serve500 returns a 500 error response / HTML page to the http.ResponseWriter
+func Serve500(w http.ResponseWriter) {
+ serveErrorPage(w, content500)
+}
+
+// Serve502 returns a 502 error response / HTML page to the http.ResponseWriter
+func Serve502(w http.ResponseWriter) {
+ serveErrorPage(w, content502)
+}
diff --git a/internal/httperrors/httperrors_test.go b/internal/httperrors/httperrors_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..1a79d8506ec8a4d38f2d7b0320de0fa26d21305c
--- /dev/null
+++ b/internal/httperrors/httperrors_test.go
@@ -0,0 +1,105 @@
+package httperrors
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// creates a new implementation of http.ResponseWriter that allows the
+// casting of values in order to aid testing efforts.
+type testResponseWriter struct {
+ status int
+ content string
+ http.ResponseWriter
+}
+
+func newTestResponseWriter(w http.ResponseWriter) *testResponseWriter {
+ return &testResponseWriter{0, "", w}
+}
+
+func (w *testResponseWriter) Status() int {
+ return w.status
+}
+
+func (w *testResponseWriter) Content() string {
+ return w.content
+}
+
+func (w *testResponseWriter) Header() http.Header {
+ return w.ResponseWriter.Header()
+}
+
+func (w *testResponseWriter) Write(data []byte) (int, error) {
+ w.content = string(data)
+ return w.ResponseWriter.Write(data)
+}
+
+func (w *testResponseWriter) WriteHeader(statusCode int) {
+ w.status = statusCode
+ w.ResponseWriter.WriteHeader(statusCode)
+}
+
+var (
+ testingContent = content{
+ http.StatusNotFound,
+ "Title",
+ "533",
+ "Header test",
+ "subheader text",
+ }
+)
+
+func TestGenerateemailHTML(t *testing.T) {
+ actual := generateErrorHTML(testingContent)
+ assert.Contains(t, actual, testingContent.title)
+ assert.Contains(t, actual, testingContent.statusString)
+ assert.Contains(t, actual, testingContent.header)
+ assert.Contains(t, actual, testingContent.subHeader)
+}
+
+func TestServeErrorPage(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ serveErrorPage(w, testingContent)
+ 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(), testingContent.status)
+}
+
+func TestServe404(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ Serve404(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(), content404.status)
+ assert.Contains(t, w.Content(), content404.title)
+ assert.Contains(t, w.Content(), content404.statusString)
+ assert.Contains(t, w.Content(), content404.header)
+ assert.Contains(t, w.Content(), content404.subHeader)
+}
+
+func TestServe500(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ Serve500(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(), content500.status)
+ assert.Contains(t, w.Content(), content500.title)
+ assert.Contains(t, w.Content(), content500.statusString)
+ assert.Contains(t, w.Content(), content500.header)
+ assert.Contains(t, w.Content(), content500.subHeader)
+}
+
+func TestServe502(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ Serve502(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(), content502.status)
+ assert.Contains(t, w.Content(), content502.title)
+ assert.Contains(t, w.Content(), content502.statusString)
+ assert.Contains(t, w.Content(), content502.header)
+ assert.Contains(t, w.Content(), content502.subHeader)
+}
diff --git a/main.go b/main.go
index cacddb4f768975069381a760774bd5009a492452..1cfac4090d0adaa7e3c3e9e2ded8bed4c2d91b5e 100644
--- a/main.go
+++ b/main.go
@@ -1,8 +1,10 @@
package main
import (
+ "errors"
"flag"
"log"
+ "net/url"
"os"
"strings"
)
@@ -14,18 +16,22 @@ var VERSION = "dev"
var REVISION = "HEAD"
var (
- pagesRootCert = flag.String("root-cert", "", "The default path to file certificate to serve static pages")
- pagesRootKey = flag.String("root-key", "", "The default path to file certificate to serve static pages")
- redirectHTTP = flag.Bool("redirect-http", false, "Redirect pages from HTTP to HTTPS")
- useHTTP2 = flag.Bool("use-http2", true, "Enable HTTP2 support")
- pagesRoot = flag.String("pages-root", "shared/pages", "The directory where pages are stored")
- pagesDomain = flag.String("pages-domain", "gitlab-example.com", "The domain to serve static pages")
- pagesStatus = flag.String("pages-status", "", "The url path for a status page, e.g., /@status")
- metricsAddress = flag.String("metrics-address", "", "The address to listen on for metrics requests")
- daemonUID = flag.Uint("daemon-uid", 0, "Drop privileges to this user")
- daemonGID = flag.Uint("daemon-gid", 0, "Drop privileges to this group")
-
- disableCrossOriginRequests = flag.Bool("disable-cross-origin-requests", false, "Disable cross-origin requests")
+ pagesRootCert = flag.String("root-cert", "", "The default path to file certificate to serve static pages")
+ pagesRootKey = flag.String("root-key", "", "The default path to file certificate to serve static pages")
+ redirectHTTP = flag.Bool("redirect-http", false, "Redirect pages from HTTP to HTTPS")
+ useHTTP2 = flag.Bool("use-http2", true, "Enable HTTP2 support")
+ pagesRoot = flag.String("pages-root", "shared/pages", "The directory where pages are stored")
+ pagesDomain = flag.String("pages-domain", "gitlab-example.com", "The domain to serve static pages")
+ artifactsServer = flag.String("artifacts-server", "", "API URL to proxy artifact requests to, e.g.: 'https://gitlab.com/api/v4'")
+ artifactsServerTimeout = flag.Int("artifacts-server-timeout", 10, "Timeout (in seconds) for a proxied request to the artifacts server")
+ pagesStatus = flag.String("pages-status", "", "The url path for a status page, e.g., /@status")
+ metricsAddress = flag.String("metrics-address", "", "The address to listen on for metrics requests")
+ daemonUID = flag.Uint("daemon-uid", 0, "Drop privileges to this user")
+ daemonGID = flag.Uint("daemon-gid", 0, "Drop privileges to this group")
+
+ disableCrossOriginRequests = flag.Bool("disable-cross-origin-requests", false, "Disable cross-origin requests")
+ 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")
)
func configFromFlags() appConfig {
@@ -45,6 +51,27 @@ func configFromFlags() appConfig {
config.RootKey = readFile(*pagesRootKey)
}
+ if *artifactsServerTimeout < 1 {
+ log.Fatal(errArtifactsServerTimeoutValue)
+ }
+
+ if *artifactsServer != "" {
+ u, err := url.Parse(*artifactsServer)
+ if err != nil {
+ log.Fatal(err)
+ }
+ // url.Parse ensures that the Scheme arttribute is always lower case.
+ if u.Scheme != "http" && u.Scheme != "https" {
+ log.Fatal(errArtifactSchemaUnsupported)
+ }
+
+ if *artifactsServerTimeout < 1 {
+ log.Fatal(errArtifactsServerTimeoutValue)
+ }
+
+ config.ArtifactsServerTimeout = *artifactsServerTimeout
+ config.ArtifactsServer = *artifactsServer
+ }
return config
}