diff --git a/cmd/main.go b/cmd/main.go index a380695..dc2c621 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,15 +24,15 @@ import ( // AllowedCorsDomains lists the domains for which Cross-Origin Resource Sharing is allowed. // TODO: make it a flag -var AllowedCorsDomains = [][]byte{ - []byte("fonts.codeberg.org"), - []byte("design.codeberg.org"), +var AllowedCorsDomains = []string{ + "fonts.codeberg.org", + "design.codeberg.org", } // BlacklistedPaths specifies forbidden path prefixes for all Codeberg Pages. // TODO: Make it a flag too -var BlacklistedPaths = [][]byte{ - []byte("/.well-known/acme-challenge/"), +var BlacklistedPaths = []string{ + "/.well-known/acme-challenge/", } // Serve sets up and starts the web server. @@ -65,7 +65,7 @@ func Serve(ctx *cli.Context) error { allowedCorsDomains := AllowedCorsDomains if len(rawDomain) != 0 { - allowedCorsDomains = append(allowedCorsDomains, []byte(rawDomain)) + allowedCorsDomains = append(allowedCorsDomains, rawDomain) } // Make sure MainDomain has a trailing dot, and GiteaRoot has no trailing slash diff --git a/server/context/context.go b/server/context/context.go index aee3e95..83cfed8 100644 --- a/server/context/context.go +++ b/server/context/context.go @@ -10,6 +10,13 @@ type Context struct { Req *http.Request } +func New(w http.ResponseWriter, r *http.Request) *Context { + return &Context{ + RespWriter: w, + Req: r, + } +} + func (c *Context) Context() stdContext.Context { if c.Req != nil { return c.Req.Context() @@ -27,3 +34,10 @@ func (c *Context) Response() *http.Response { func (c *Context) Redirect(uri string, statusCode int) { http.Redirect(c.RespWriter, c.Req, uri, statusCode) } + +// Path returns requested path. +// +// The returned bytes are valid until your request handler returns. +func (c *Context) Path() string { + return c.Req.URL.Path +} diff --git a/server/handler.go b/server/handler.go index 7350fbb..0c6a001 100644 --- a/server/handler.go +++ b/server/handler.go @@ -13,6 +13,7 @@ import ( "codeberg.org/codeberg/pages/html" "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" "codeberg.org/codeberg/pages/server/dns" "codeberg.org/codeberg/pages/server/gitea" "codeberg.org/codeberg/pages/server/upstream" @@ -20,29 +21,35 @@ import ( "codeberg.org/codeberg/pages/server/version" ) +const ( + headerAccessControlAllowOrigin = "Access-Control-Allow-Origin" + headerAccessControlAllowMethods = "Access-Control-Allow-Methods" +) + // Handler handles a single HTTP request to the web server. func Handler(mainDomainSuffix, rawDomain []byte, giteaClient *gitea.Client, giteaRoot, rawInfoPage string, - blacklistedPaths, allowedCorsDomains [][]byte, + blacklistedPaths, allowedCorsDomains []string, dnsLookupCache, canonicalDomainCache cache.SetGetKey, ) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { log := log.With().Strs("Handler", []string{string(req.Host), req.RequestURI}).Logger() + ctx := context.New(w, req) - w.Header().Set("Server", "CodebergPages/"+version.Version) + ctx.RespWriter.Header().Set("Server", "CodebergPages/"+version.Version) // Force new default from specification (since November 2020) - see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin - w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + ctx.RespWriter.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") // Enable browser caching for up to 10 minutes - w.Header().Set("Cache-Control", "public, max-age=600") + ctx.RespWriter.Header().Set("Cache-Control", "public, max-age=600") trimmedHost := utils.TrimHostPort([]byte(req.Host)) // Add HSTS for RawDomain and MainDomainSuffix if hsts := GetHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" { - w.Header().Set("Strict-Transport-Security", hsts) + ctx.RespWriter.Header().Set("Strict-Transport-Security", hsts) } // Block all methods not required for static pages @@ -54,7 +61,7 @@ func Handler(mainDomainSuffix, rawDomain []byte, // Block blacklisted paths (like ACME challenges) for _, blacklistedPath := range blacklistedPaths { - if bytes.HasPrefix(ctx.Path(), blacklistedPath) { + if strings.HasPrefix(ctx.Path(), blacklistedPath) { html.ReturnErrorPage(ctx, fasthttp.StatusForbidden) return } @@ -69,13 +76,13 @@ func Handler(mainDomainSuffix, rawDomain []byte, } } if allowCors { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD") + ctx.RespWriter.Header().Set(headerAccessControlAllowOrigin, "*") + ctx.RespWriter.Header().Set(headerAccessControlAllowMethods, http.MethodGet+", "+http.MethodHead) } - w.Header().Set("Allow", "GET, HEAD, OPTIONS") + ctx.RespWriter.Header().Set("Allow", http.MethodGet+", "+http.MethodHead+", "+http.MethodOptions) if ctx.IsOptions() { - ctx.Response.Header.SetStatusCode(http.StatusNoContent) + ctx.Response().StatusCode = http.StatusNoContent return } @@ -113,8 +120,8 @@ func Handler(mainDomainSuffix, rawDomain []byte, if canonicalLink != "" { // Hide from search machines & add canonical link - ctx.Response.Header.Set("X-Robots-Tag", "noarchive, noindex") - ctx.Response.Header.Set("Link", + ctx.Response().Header.Set("X-Robots-Tag", "noarchive, noindex") + ctx.Response().Header.Set("Link", strings.NewReplacer("%b", targetBranch, "%p", targetPath).Replace(canonicalLink)+ "; rel=\"canonical\"", ) @@ -136,7 +143,7 @@ func Handler(mainDomainSuffix, rawDomain []byte, targetOptions.ForbiddenMimeTypes["text/html"] = true targetOptions.DefaultMimeType = "text/plain; charset=utf-8" - pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") + pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/") if len(pathElements) < 2 { // https://{RawDomain}/{owner}/{repo}[/@{branch}]/{path} is required ctx.Redirect(rawInfoPage, http.StatusTemporaryRedirect) @@ -178,7 +185,7 @@ func Handler(mainDomainSuffix, rawDomain []byte, // Serve pages from subdomains of MainDomainSuffix log.Debug().Msg("main domain suffix") - pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") + pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/") targetOwner = string(bytes.TrimSuffix(trimmedHost, mainDomainSuffix)) targetRepo = pathElements[0] targetPath = strings.Trim(strings.Join(pathElements[1:], "/"), "/") @@ -267,7 +274,7 @@ func Handler(mainDomainSuffix, rawDomain []byte, return } - pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") + pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/") canonicalLink := "" if strings.HasPrefix(pathElements[0], "@") { targetBranch = pathElements[0][1:] @@ -287,7 +294,7 @@ func Handler(mainDomainSuffix, rawDomain []byte, // only redirect if the target is also a codeberg page! targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix), dnsLookupCache) if targetOwner != "" { - ctx.Redirect("https://"+canonicalDomain+string(ctx.RequestURI()), http.StatusTemporaryRedirect) + ctx.Redirect("https://"+canonicalDomain+string(ctx.Path()), http.StatusTemporaryRedirect) return } diff --git a/server/handler_fasthttp.go b/server/handler_fasthttp.go index 2e15ab4..5d102d3 100644 --- a/server/handler_fasthttp.go +++ b/server/handler_fasthttp.go @@ -23,7 +23,7 @@ import ( func Handler(mainDomainSuffix, rawDomain []byte, giteaClient *gitea.Client, giteaRoot, rawInfoPage string, - blacklistedPaths, allowedCorsDomains [][]byte, + blacklistedPaths, allowedCorsDomains []string, dnsLookupCache, canonicalDomainCache cache.SetGetKey, ) func(ctx *fasthttp.RequestCtx) { return func(ctx *fasthttp.RequestCtx) { @@ -53,7 +53,7 @@ func Handler(mainDomainSuffix, rawDomain []byte, // Block blacklisted paths (like ACME challenges) for _, blacklistedPath := range blacklistedPaths { - if bytes.HasPrefix(ctx.Path(), blacklistedPath) { + if bytes.HasPrefix(ctx.Path(), []byte(blacklistedPath)) { html.ReturnErrorPage(ctx, fasthttp.StatusForbidden) return } @@ -62,7 +62,7 @@ func Handler(mainDomainSuffix, rawDomain []byte, // Allow CORS for specified domains allowCors := false for _, allowedCorsDomain := range allowedCorsDomains { - if bytes.Equal(trimmedHost, allowedCorsDomain) { + if bytes.Equal(trimmedHost, []byte(allowedCorsDomain)) { allowCors = true break } diff --git a/server/upstream/upstream_std.go b/server/upstream/upstream_std.go index 28ca272..eb34ebf 100644 --- a/server/upstream/upstream_std.go +++ b/server/upstream/upstream_std.go @@ -70,7 +70,7 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin } // compatibility fix for GitHub Pages (/example → /example.html) optionsForIndexPages.appendTrailingSlash = false - optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Req.URL.Path, "/") + ".html" + optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html" optionsForIndexPages.TargetPath = o.TargetPath + ".html" if optionsForIndexPages.Upstream(ctx, giteaClient) { return true @@ -100,12 +100,12 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin // Append trailing slash if missing (for index files), and redirect to fix filenames in general // o.appendTrailingSlash is only true when looking for index pages - if o.appendTrailingSlash && !strings.HasSuffix(ctx.Req.URL.Path, "/") { - ctx.Redirect(ctx.Req.URL.Path+"/", http.StatusTemporaryRedirect) + if o.appendTrailingSlash && !strings.HasSuffix(ctx.Path(), "/") { + ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect) return true } - if strings.HasSuffix(ctx.Req.URL.Path, "/index.html") { - ctx.Redirect(strings.TrimSuffix(ctx.Req.URL.Path, "index.html"), http.StatusTemporaryRedirect) + if strings.HasSuffix(ctx.Path(), "/index.html") { + ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect) return true } if o.redirectIfExists != "" {