package handler import ( "net/http" "strings" "github.com/rs/zerolog/log" "codeberg.org/codeberg/pages/html" "codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/context" "codeberg.org/codeberg/pages/server/gitea" "codeberg.org/codeberg/pages/server/version" ) const ( headerAccessControlAllowOrigin = "Access-Control-Allow-Origin" headerAccessControlAllowMethods = "Access-Control-Allow-Methods" defaultPagesRepo = "pages" ) // Handler handles a single HTTP request to the web server. func Handler(mainDomainSuffix, rawDomain string, giteaClient *gitea.Client, rawInfoPage string, blacklistedPaths, allowedCorsDomains []string, defaultPagesBranches []string, dnsLookupCache, canonicalDomainCache, redirectsCache cache.SetGetKey, ) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { log := log.With().Strs("Handler", []string{req.Host, req.RequestURI}).Logger() ctx := context.New(w, req) trimmedHost := ctx.TrimHostPort() credentials := handleAuth(log, ctx, giteaClient, mainDomainSuffix, trimmedHost, dnsLookupCache, authCache) if len(credentials) > 0 { authenticated := enforceBasicHTTPAuth(credentials, w, req) if !authenticated { return } } 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 ctx.RespWriter.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") // Enable browser caching for up to 10 minutes ctx.RespWriter.Header().Set("Cache-Control", "public, max-age=600") // Add HSTS for RawDomain and MainDomainSuffix if hsts := getHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" { ctx.RespWriter.Header().Set("Strict-Transport-Security", hsts) } // Handle all http methods ctx.RespWriter.Header().Set("Allow", http.MethodGet+", "+http.MethodHead+", "+http.MethodOptions) switch ctx.Req.Method { case http.MethodOptions: // return Allow header ctx.RespWriter.WriteHeader(http.StatusNoContent) return case http.MethodGet, http.MethodHead: // end switch case and handle allowed requests break default: // Block all methods not required for static pages ctx.String("Method not allowed", http.StatusMethodNotAllowed) return } // Block blacklisted paths (like ACME challenges) for _, blacklistedPath := range blacklistedPaths { if strings.HasPrefix(ctx.Path(), blacklistedPath) { html.ReturnErrorPage(ctx, "requested blacklisted path", http.StatusForbidden) return } } // Allow CORS for specified domains allowCors := false for _, allowedCorsDomain := range allowedCorsDomains { if strings.EqualFold(trimmedHost, allowedCorsDomain) { allowCors = true break } } if allowCors { ctx.RespWriter.Header().Set(headerAccessControlAllowOrigin, "*") ctx.RespWriter.Header().Set(headerAccessControlAllowMethods, http.MethodGet+", "+http.MethodHead) } // Prepare request information to Gitea pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/") if rawDomain != "" && strings.EqualFold(trimmedHost, rawDomain) { log.Debug().Msg("raw domain request detecded") handleRaw(log, ctx, giteaClient, mainDomainSuffix, rawInfoPage, trimmedHost, pathElements, canonicalDomainCache, redirectsCache) } else if strings.HasSuffix(trimmedHost, mainDomainSuffix) { log.Debug().Msg("subdomain request detecded") handleSubDomain(log, ctx, giteaClient, mainDomainSuffix, defaultPagesBranches, trimmedHost, pathElements, canonicalDomainCache, redirectsCache) } else { log.Debug().Msg("custom domain request detecded") handleCustomDomain(log, ctx, giteaClient, mainDomainSuffix, trimmedHost, pathElements, defaultPagesBranches[0], dnsLookupCache, canonicalDomainCache, redirectsCache) } } } func enforceBasicHTTPAuth(credentials []string, w http.ResponseWriter, req *http.Request) bool { authorizedUsers := getAuthorizedUsers(credentials) username, password, ok := req.BasicAuth() if !ok { w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message": "No basic auth present"}`)) return false } if !isAuthorized(username, password, authorizedUsers) { w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message": "Invalid username or password"}`)) return false } return true } func getAuthorizedUsers(credentials []string) map[string]string { authorizedUsers := make(map[string]string) for _, authLine := range credentials { authLineParts := strings.Split(authLine, ",") user := strings.TrimSpace(authLineParts[0]) password := strings.TrimSpace(authLineParts[1]) authorizedUsers[user] = password } return authorizedUsers } func isAuthorized(username, password string, credentials map[string]string) bool { pass, ok := credentials[username] if !ok { return false } return password == pass }