From 950c1cbf8b4de8af7d1933ab9ff25b946ffe3ddc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 24 Sep 2019 15:05:27 +0200 Subject: Extract disk serving from domain package --- internal/domain/domain.go | 330 ++-------------------------------- internal/domain/domain_test.go | 23 --- internal/serving/disk/errors.go | 18 ++ internal/serving/disk/group.go | 64 +++++++ internal/serving/disk/helpers.go | 77 ++++++++ internal/serving/disk/helpers_test.go | 32 ++++ internal/serving/disk/project.go | 58 ++++++ internal/serving/disk/reader.go | 162 +++++++++++++++++ internal/serving/handler.go | 7 - internal/serving/serving.go | 30 +++- internal/source/groups/map.go | 17 +- 11 files changed, 459 insertions(+), 359 deletions(-) create mode 100644 internal/serving/disk/errors.go create mode 100644 internal/serving/disk/group.go create mode 100644 internal/serving/disk/helpers.go create mode 100644 internal/serving/disk/helpers_test.go create mode 100644 internal/serving/disk/project.go create mode 100644 internal/serving/disk/reader.go delete mode 100644 internal/serving/handler.go (limited to 'internal') diff --git a/internal/domain/domain.go b/internal/domain/domain.go index f342bd72..9678f5fc 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -3,32 +3,14 @@ package domain import ( "crypto/tls" "errors" - "fmt" - "io" - "mime" "net/http" - "os" - "path/filepath" - "strconv" - "strings" "sync" "time" - "golang.org/x/sys/unix" - "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" - "gitlab.com/gitlab-org/gitlab-pages/internal/httputil" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving" ) -type locationDirectoryError struct { - FullPath string - RelativePath string -} - -type locationFileNoExtensionError struct { - FullPath string -} - // GroupConfig represents a per-request config for a group domain type GroupConfig interface { IsHTTPSOnly(*http.Request) bool @@ -36,7 +18,6 @@ type GroupConfig interface { IsNamespaceProject(*http.Request) bool ProjectID(*http.Request) uint64 ProjectExists(*http.Request) bool - ProjectWithSubpath(*http.Request) (string, string, error) } // Domain is a domain that gitlab-pages can serve. @@ -52,6 +33,7 @@ type Domain struct { AccessControl bool GroupConfig GroupConfig // handles group domain config + Serving serving.Serving certificate *tls.Certificate certificateError error @@ -75,41 +57,6 @@ func (d *Domain) isCustomDomain() bool { return d.GroupConfig == nil } -func (l *locationDirectoryError) Error() string { - return "location error accessing directory where file expected" -} - -func (l *locationFileNoExtensionError) Error() string { - return "error accessing a path without an extension" -} - -func acceptsGZip(r *http.Request) bool { - if r.Header.Get("Range") != "" { - return false - } - - offers := []string{"gzip", "identity"} - acceptedEncoding := httputil.NegotiateContentEncoding(r, offers) - return acceptedEncoding == "gzip" -} - -func handleGZip(w http.ResponseWriter, r *http.Request, fullPath string) string { - if !acceptsGZip(r) { - return fullPath - } - - gzipPath := fullPath + ".gz" - - // Ensure the .gz file is not a symlink - if fi, err := os.Lstat(gzipPath); err != nil || !fi.Mode().IsRegular() { - return fullPath - } - - w.Header().Set("Content-Encoding", "gzip") - - return gzipPath -} - // IsHTTPSOnly figures out if the request should be handled with HTTPS // only by looking at group and project level config. func (d *Domain) IsHTTPSOnly(r *http.Request) bool { @@ -151,20 +98,7 @@ func (d *Domain) HasAcmeChallenge(token string) bool { return false } - _, err := d.resolvePath(d.Project, ".well-known/acme-challenge", token) - - // there is an acme challenge on disk - if err == nil { - return true - } - - _, err = d.resolvePath(d.Project, ".well-known/acme-challenge", token, "index.html") - - if err == nil { - return true - } - - return false + return d.Serving.HasAcmeChallenge(token) } // IsNamespaceProject figures out if the request is to a namespace project @@ -209,234 +143,6 @@ func (d *Domain) HasProject(r *http.Request) bool { return d.GroupConfig.ProjectExists(r) } -// Detect file's content-type either by extension or mime-sniffing. -// Implementation is adapted from Golang's `http.serveContent()` -// See https://github.com/golang/go/blob/902fc114272978a40d2e65c2510a18e870077559/src/net/http/fs.go#L194 -func (d *Domain) detectContentType(path string) (string, error) { - contentType := mime.TypeByExtension(filepath.Ext(path)) - - if contentType == "" { - var buf [512]byte - - file, err := os.Open(path) - if err != nil { - return "", err - } - - defer file.Close() - - // Using `io.ReadFull()` because `file.Read()` may be chunked. - // Ignoring errors because we don't care if the 512 bytes cannot be read. - n, _ := io.ReadFull(file, buf[:]) - contentType = http.DetectContentType(buf[:n]) - } - - return contentType, nil -} - -func (d *Domain) serveFile(w http.ResponseWriter, r *http.Request, origPath string) error { - fullPath := handleGZip(w, r, origPath) - - file, err := openNoFollow(fullPath) - if err != nil { - return err - } - - defer file.Close() - - fi, err := file.Stat() - if err != nil { - return err - } - - 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)) - } - - contentType, err := d.detectContentType(origPath) - if err != nil { - return err - } - - w.Header().Set("Content-Type", contentType) - http.ServeContent(w, r, origPath, fi.ModTime(), file) - - return nil -} - -func (d *Domain) serveCustomFile(w http.ResponseWriter, r *http.Request, code int, origPath string) error { - fullPath := handleGZip(w, r, origPath) - - // Open and serve content of file - file, err := openNoFollow(fullPath) - if err != nil { - return err - } - defer file.Close() - - fi, err := file.Stat() - if err != nil { - return err - } - - contentType, err := d.detectContentType(origPath) - if err != nil { - return err - } - - w.Header().Set("Content-Type", contentType) - w.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10)) - w.WriteHeader(code) - - if r.Method != "HEAD" { - _, err := io.CopyN(w, file, fi.Size()) - return err - } - - return nil -} - -// Resolve the HTTP request to a path on disk, converting requests for -// directories to requests for index.html inside the directory if appropriate. -func (d *Domain) resolvePath(projectName string, subPath ...string) (string, error) { - publicPath := filepath.Join(d.Group, projectName, "public") - - // Don't use filepath.Join as cleans the path, - // where we want to traverse full path as supplied by user - // (including ..) - testPath := publicPath + "/" + strings.Join(subPath, "/") - fullPath, err := filepath.EvalSymlinks(testPath) - if err != nil { - if endsWithoutHTMLExtension(testPath) { - return "", &locationFileNoExtensionError{ - FullPath: fullPath, - } - } - - return "", err - } - - // The requested path resolved to somewhere outside of the public/ directory - if !strings.HasPrefix(fullPath, publicPath+"/") && fullPath != publicPath { - return "", fmt.Errorf("%q should be in %q", fullPath, publicPath) - } - - fi, err := os.Lstat(fullPath) - if err != nil { - return "", err - } - - // The requested path is a directory, so try index.html via recursion - if fi.IsDir() { - return "", &locationDirectoryError{ - FullPath: fullPath, - RelativePath: strings.TrimPrefix(fullPath, publicPath), - } - } - - // The file exists, but is not a supported type to serve. Perhaps a block - // special device or something else that may be a security risk. - if !fi.Mode().IsRegular() { - return "", fmt.Errorf("%s: is not a regular file", fullPath) - } - - return fullPath, nil -} - -func (d *Domain) tryNotFound(w http.ResponseWriter, r *http.Request, projectName string) error { - page404, err := d.resolvePath(projectName, "404.html") - if err != nil { - return err - } - - err = d.serveCustomFile(w, r, http.StatusNotFound, page404) - if err != nil { - return err - } - return nil -} - -func (d *Domain) tryFile(w http.ResponseWriter, r *http.Request, projectName string, subPath ...string) error { - fullPath, err := d.resolvePath(projectName, subPath...) - - if locationError, _ := err.(*locationDirectoryError); locationError != nil { - if endsWithSlash(r.URL.Path) { - fullPath, err = d.resolvePath(projectName, filepath.Join(subPath...), "index.html") - } else { - // Concat Host with URL.Path - redirectPath := "//" + r.Host + "/" - redirectPath += strings.TrimPrefix(r.URL.Path, "/") - - // Ensure that there's always "/" at end - redirectPath = strings.TrimSuffix(redirectPath, "/") + "/" - http.Redirect(w, r, redirectPath, 302) - return nil - } - } - - if locationError, _ := err.(*locationFileNoExtensionError); locationError != nil { - fullPath, err = d.resolvePath(projectName, strings.TrimSuffix(filepath.Join(subPath...), "/")+".html") - } - - if err != nil { - return err - } - - return d.serveFile(w, r, fullPath) -} - -func (d *Domain) serveFileFromGroup(w http.ResponseWriter, r *http.Request) bool { - projectName, subPath, err := d.GroupConfig.ProjectWithSubpath(r) - if err != nil { - httperrors.Serve404(w) - return true - } - - if d.tryFile(w, r, projectName, subPath) == nil { - return true - } - - return false -} - -func (d *Domain) serveNotFoundFromGroup(w http.ResponseWriter, r *http.Request) { - projectName, _, err := d.GroupConfig.ProjectWithSubpath(r) - - if err != nil { - httperrors.Serve404(w) - return - } - - // Try serving custom not-found page - if d.tryNotFound(w, r, projectName) == nil { - return - } - - // Generic 404 - httperrors.Serve404(w) -} - -func (d *Domain) serveFileFromConfig(w http.ResponseWriter, r *http.Request) bool { - // Try to serve file for http://host/... => /group/project/... - if d.tryFile(w, r, d.Project, r.URL.Path) == nil { - return true - } - - return false -} - -func (d *Domain) 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.Project) == nil { - return - } - - // Serve generic not found - httperrors.Serve404(w) -} - // EnsureCertificate parses the PEM-encoded certificate for the domain func (d *Domain) EnsureCertificate() (*tls.Certificate, error) { if !d.isCustomDomain() { @@ -454,42 +160,28 @@ func (d *Domain) EnsureCertificate() (*tls.Certificate, error) { return d.certificate, d.certificateError } -// ServeFileHTTP implements http.Handler. Returns true if something was served, false if not. +// ServeFileHTTP returns true if something was served, false if not. func (d *Domain) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool { if d == nil { httperrors.Serve404(w) return true } - if d.isCustomDomain() { - return d.serveFileFromConfig(w, r) + 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)) } - return d.serveFileFromGroup(w, r) + return d.Serving.ServeFileHTTP(w, r) } -// ServeNotFoundHTTP implements http.Handler. Serves the not found pages from the projects. +// ServeNotFoundHTTP serves the not found pages from the projects. func (d *Domain) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) { if d == nil { httperrors.Serve404(w) return } - if d.isCustomDomain() { - d.serveNotFoundFromConfig(w, r) - } else { - d.serveNotFoundFromGroup(w, r) - } -} - -func endsWithSlash(path string) bool { - return strings.HasSuffix(path, "/") -} - -func endsWithoutHTMLExtension(path string) bool { - return !strings.HasSuffix(path, ".html") -} - -func openNoFollow(path string) (*os.File, error) { - return os.OpenFile(path, os.O_RDONLY|unix.O_NOFOLLOW, 0) + d.Serving.ServeNotFoundHTTP(w, r) } diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go index 552b2f15..4ba3aba2 100644 --- a/internal/domain/domain_test.go +++ b/internal/domain/domain_test.go @@ -246,29 +246,6 @@ func TestDomainCertificate(t *testing.T) { require.NoError(t, err) } -func TestOpenNoFollow(t *testing.T) { - tmpfile, err := ioutil.TempFile("", "link-test") - require.NoError(t, err) - defer tmpfile.Close() - - orig := tmpfile.Name() - softLink := orig + ".link" - defer os.Remove(orig) - - source, err := openNoFollow(orig) - require.NoError(t, err) - require.NotNil(t, source) - defer source.Close() - - err = os.Symlink(orig, softLink) - require.NoError(t, err) - defer os.Remove(softLink) - - link, err := openNoFollow(softLink) - require.Error(t, err) - require.Nil(t, link) -} - var chdirSet = false func setUpTests(t require.TestingT) func() { diff --git a/internal/serving/disk/errors.go b/internal/serving/disk/errors.go new file mode 100644 index 00000000..5e55220b --- /dev/null +++ b/internal/serving/disk/errors.go @@ -0,0 +1,18 @@ +package disk + +type locationDirectoryError struct { + FullPath string + RelativePath string +} + +type locationFileNoExtensionError struct { + FullPath string +} + +func (l *locationDirectoryError) Error() string { + return "location error accessing directory where file expected" +} + +func (l *locationFileNoExtensionError) Error() string { + return "error accessing a path without an extension" +} diff --git a/internal/serving/disk/group.go b/internal/serving/disk/group.go new file mode 100644 index 00000000..78a4ff1f --- /dev/null +++ b/internal/serving/disk/group.go @@ -0,0 +1,64 @@ +package disk + +import ( + "net/http" + + "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" +) + +// Group serving represents a resource that can be served from a directory +// representing GitLab group +type Group struct { + Resolver + *Reader +} + +type Resolver interface { + ProjectWithSubpath(*http.Request) (string, string, error) +} + +// ServeFileHTTP returns true if something was served, false if not. +func (g *Group) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool { + return g.serveFileFromGroup(w, r) +} + +// ServeNotFoundHTTP serves the not found pages from the projects. +func (g *Group) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) { + g.serveNotFoundFromGroup(w, r) +} + +func (g *Group) HasAcmeChallenge(token string) bool { + return false +} + +func (g *Group) serveFileFromGroup(w http.ResponseWriter, r *http.Request) bool { + projectName, subPath, err := g.Resolver.ProjectWithSubpath(r) + + if err != nil { + httperrors.Serve404(w) + return true + } + + if g.tryFile(w, r, projectName, subPath) == nil { + return true + } + + return false +} + +func (g *Group) serveNotFoundFromGroup(w http.ResponseWriter, r *http.Request) { + projectName, _, err := g.Resolver.ProjectWithSubpath(r) + + if err != nil { + httperrors.Serve404(w) + return + } + + // Try serving custom not-found page + if g.tryNotFound(w, r, projectName) == nil { + return + } + + // Generic 404 + httperrors.Serve404(w) +} diff --git a/internal/serving/disk/helpers.go b/internal/serving/disk/helpers.go new file mode 100644 index 00000000..7ace39be --- /dev/null +++ b/internal/serving/disk/helpers.go @@ -0,0 +1,77 @@ +package disk + +import ( + "io" + "mime" + "net/http" + "os" + "path/filepath" + "strings" + + "gitlab.com/gitlab-org/gitlab-pages/internal/httputil" + "golang.org/x/sys/unix" +) + +func endsWithSlash(path string) bool { + return strings.HasSuffix(path, "/") +} + +func endsWithoutHTMLExtension(path string) bool { + return !strings.HasSuffix(path, ".html") +} + +func openNoFollow(path string) (*os.File, error) { + return os.OpenFile(path, os.O_RDONLY|unix.O_NOFOLLOW, 0) +} + +// Detect file's content-type either by extension or mime-sniffing. +// Implementation is adapted from Golang's `http.serveContent()` +// See https://github.com/golang/go/blob/902fc114272978a40d2e65c2510a18e870077559/src/net/http/fs.go#L194 +func detectContentType(path string) (string, error) { + contentType := mime.TypeByExtension(filepath.Ext(path)) + + if contentType == "" { + var buf [512]byte + + file, err := os.Open(path) + if err != nil { + return "", err + } + + defer file.Close() + + // Using `io.ReadFull()` because `file.Read()` may be chunked. + // Ignoring errors because we don't care if the 512 bytes cannot be read. + n, _ := io.ReadFull(file, buf[:]) + contentType = http.DetectContentType(buf[:n]) + } + + return contentType, nil +} + +func acceptsGZip(r *http.Request) bool { + if r.Header.Get("Range") != "" { + return false + } + + offers := []string{"gzip", "identity"} + acceptedEncoding := httputil.NegotiateContentEncoding(r, offers) + return acceptedEncoding == "gzip" +} + +func handleGZip(w http.ResponseWriter, r *http.Request, fullPath string) string { + if !acceptsGZip(r) { + return fullPath + } + + gzipPath := fullPath + ".gz" + + // Ensure the .gz file is not a symlink + if fi, err := os.Lstat(gzipPath); err != nil || !fi.Mode().IsRegular() { + return fullPath + } + + w.Header().Set("Content-Encoding", "gzip") + + return gzipPath +} diff --git a/internal/serving/disk/helpers_test.go b/internal/serving/disk/helpers_test.go new file mode 100644 index 00000000..eea51500 --- /dev/null +++ b/internal/serving/disk/helpers_test.go @@ -0,0 +1,32 @@ +package disk + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOpenNoFollow(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "link-test") + require.NoError(t, err) + defer tmpfile.Close() + + orig := tmpfile.Name() + softLink := orig + ".link" + defer os.Remove(orig) + + source, err := openNoFollow(orig) + require.NoError(t, err) + require.NotNil(t, source) + defer source.Close() + + err = os.Symlink(orig, softLink) + require.NoError(t, err) + defer os.Remove(softLink) + + link, err := openNoFollow(softLink) + require.Error(t, err) + require.Nil(t, link) +} diff --git a/internal/serving/disk/project.go b/internal/serving/disk/project.go new file mode 100644 index 00000000..b2940f3d --- /dev/null +++ b/internal/serving/disk/project.go @@ -0,0 +1,58 @@ +package disk + +import ( + "net/http" + + "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" +) + +// Custom serving represent a resource that can be served from a directory +// representing GitLab project +type Project struct { + Location string + *Reader +} + +// ServeFileHTTP returns true if something was served, false if not. +func (p *Project) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool { + return p.serveFileFromConfig(w, r) +} + +// ServeNotFoundHTTP serves the not found pages from the projects. +func (p *Project) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) { + p.serveNotFoundFromConfig(w, r) +} + +func (p *Project) HasAcmeChallenge(token string) bool { + _, err := p.resolvePath(p.Location, ".well-known/acme-challenge", token) + // there is an acme challenge on disk + if err == nil { + return true + } + + _, err = p.resolvePath(p.Location, ".well-known/acme-challenge", token, "index.html") + if err == nil { + return true + } + + return false +} + +func (p *Project) serveFileFromConfig(w http.ResponseWriter, r *http.Request) bool { + // Try to serve file for http://host/... => /group/project/... + if p.tryFile(w, r, p.Location, r.URL.Path) == nil { + return true + } + + return false +} + +func (p *Project) serveNotFoundFromConfig(w http.ResponseWriter, r *http.Request) { + // Try serving not found page for http://host/ => /group/project/404.html + if p.tryNotFound(w, r, p.Location) == nil { + return + } + + // Serve generic not found + httperrors.Serve404(w) +} diff --git a/internal/serving/disk/reader.go b/internal/serving/disk/reader.go new file mode 100644 index 00000000..068b4b47 --- /dev/null +++ b/internal/serving/disk/reader.go @@ -0,0 +1,162 @@ +package disk + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" +) + +type Reader struct { + Group string +} + +func (reader *Reader) tryFile(w http.ResponseWriter, r *http.Request, projectName string, subPath ...string) error { + fullPath, err := reader.resolvePath(projectName, subPath...) + + if locationError, _ := err.(*locationDirectoryError); locationError != nil { + if endsWithSlash(r.URL.Path) { + fullPath, err = reader.resolvePath(projectName, filepath.Join(subPath...), "index.html") + } else { + // Concat Host with URL.Path + redirectPath := "//" + r.Host + "/" + redirectPath += strings.TrimPrefix(r.URL.Path, "/") + + // Ensure that there's always "/" at end + redirectPath = strings.TrimSuffix(redirectPath, "/") + "/" + http.Redirect(w, r, redirectPath, 302) + return nil + } + } + + if locationError, _ := err.(*locationFileNoExtensionError); locationError != nil { + fullPath, err = reader.resolvePath(projectName, strings.TrimSuffix(filepath.Join(subPath...), "/")+".html") + } + + if err != nil { + return err + } + + return reader.serveFile(w, r, fullPath) +} + +func (reader *Reader) tryNotFound(w http.ResponseWriter, r *http.Request, projectName string) error { + page404, err := reader.resolvePath(projectName, "404.html") + if err != nil { + return err + } + + err = reader.serveCustomFile(w, r, http.StatusNotFound, page404) + if err != nil { + return err + } + return nil +} + +// Resolve the HTTP request to a path on disk, converting requests for +// directories to requests for index.html inside the directory if appropriate. +func (reader *Reader) resolvePath(projectName string, subPath ...string) (string, error) { + publicPath := filepath.Join(reader.Group, projectName, "public") + + // Don't use filepath.Join as cleans the path, + // where we want to traverse full path as supplied by user + // (including ..) + testPath := publicPath + "/" + strings.Join(subPath, "/") + fullPath, err := filepath.EvalSymlinks(testPath) + if err != nil { + if endsWithoutHTMLExtension(testPath) { + return "", &locationFileNoExtensionError{ + FullPath: fullPath, + } + } + + return "", err + } + + // The requested path resolved to somewhere outside of the public/ directory + if !strings.HasPrefix(fullPath, publicPath+"/") && fullPath != publicPath { + return "", fmt.Errorf("%q should be in %q", fullPath, publicPath) + } + + fi, err := os.Lstat(fullPath) + if err != nil { + return "", err + } + + // The requested path is a directory, so try index.html via recursion + if fi.IsDir() { + return "", &locationDirectoryError{ + FullPath: fullPath, + RelativePath: strings.TrimPrefix(fullPath, publicPath), + } + } + + // The file exists, but is not a supported type to serve. Perhaps a block + // special device or something else that may be a security risk. + if !fi.Mode().IsRegular() { + return "", fmt.Errorf("%s: is not a regular file", fullPath) + } + + return fullPath, nil +} + +func (reader *Reader) serveFile(w http.ResponseWriter, r *http.Request, origPath string) error { + fullPath := handleGZip(w, r, origPath) + + file, err := openNoFollow(fullPath) + if err != nil { + return err + } + + defer file.Close() + + fi, err := file.Stat() + if err != nil { + return err + } + + contentType, err := detectContentType(origPath) + if err != nil { + return err + } + + w.Header().Set("Content-Type", contentType) + http.ServeContent(w, r, origPath, fi.ModTime(), file) + + return nil +} + +func (reader *Reader) serveCustomFile(w http.ResponseWriter, r *http.Request, code int, origPath string) error { + fullPath := handleGZip(w, r, origPath) + + // Open and serve content of file + file, err := openNoFollow(fullPath) + if err != nil { + return err + } + defer file.Close() + + fi, err := file.Stat() + if err != nil { + return err + } + + contentType, err := detectContentType(origPath) + if err != nil { + return err + } + + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10)) + w.WriteHeader(code) + + if r.Method != "HEAD" { + _, err := io.CopyN(w, file, fi.Size()) + return err + } + + return nil +} diff --git a/internal/serving/handler.go b/internal/serving/handler.go deleted file mode 100644 index 60b8dba9..00000000 --- a/internal/serving/handler.go +++ /dev/null @@ -1,7 +0,0 @@ -package serving - -// LegacyHandler is a struct that encapsulates legacy ResponseWriter and -// Request with all the details about a domain / address that needs to be -// served -type LegacyHandler struct { -} diff --git a/internal/serving/serving.go b/internal/serving/serving.go index bc0bb61d..0007d582 100644 --- a/internal/serving/serving.go +++ b/internal/serving/serving.go @@ -1,9 +1,31 @@ package serving -import "net/http" +import ( + "net/http" + + "gitlab.com/gitlab-org/gitlab-pages/internal/serving/disk" +) -// Serving represents an interface used to serve pages for a given domain / -// address type Serving interface { - ServeHTTP(http.ResponseWriter, *http.Request) + ServeFileHTTP(http.ResponseWriter, *http.Request) bool + ServeNotFoundHTTP(http.ResponseWriter, *http.Request) + HasAcmeChallenge(token string) bool +} + +func NewProjectDiskServing(project, group string) Serving { + return &disk.Project{ + Location: project, + Reader: &disk.Reader{ + Group: group, + }, + } +} + +func NewGroupDiskServing(group string, resolver disk.Resolver) Serving { + return &disk.Group{ + Resolver: resolver, + Reader: &disk.Reader{ + Group: group, + }, + } } diff --git a/internal/source/groups/map.go b/internal/source/groups/map.go index c72b2fe7..602e15ff 100644 --- a/internal/source/groups/map.go +++ b/internal/source/groups/map.go @@ -13,6 +13,7 @@ import ( log "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-pages/internal/domain" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving" "gitlab.com/gitlab-org/gitlab-pages/metrics" ) @@ -45,6 +46,7 @@ func (dm Map) addDomain(rootDomain, groupName, projectName string, config *Domai HTTPSOnly: config.HTTPSOnly, ProjectID: config.ID, AccessControl: config.AccessControl, + Serving: serving.NewProjectDiskServing(projectName, groupName), } var domainName string @@ -57,13 +59,16 @@ func (dm Map) updateGroupDomain(rootDomain, groupName, projectPath string, https groupDomain := dm[domainName] if groupDomain == nil { + group := &Group{ + name: groupName, + projects: make(projects), + subgroups: make(subgroups), + } + groupDomain = &domain.Domain{ - Group: groupName, - GroupConfig: &Group{ - name: groupName, - projects: make(projects), - subgroups: make(subgroups), - }, + Group: groupName, + GroupConfig: group, + Serving: serving.NewGroupDiskServing(groupName, group), } } -- cgit v1.2.3