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:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2019-09-24 16:05:27 +0300
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2019-09-25 15:54:42 +0300
commit950c1cbf8b4de8af7d1933ab9ff25b946ffe3ddc (patch)
tree1056c12f48204a334a99ef6b46aa2d2242a246ed /internal/domain/domain.go
parente5d6997a68f323bc345928d14ac902ac506b4a67 (diff)
Extract disk serving from domain package
Diffstat (limited to 'internal/domain/domain.go')
-rw-r--r--internal/domain/domain.go330
1 files changed, 11 insertions, 319 deletions
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)
}