From faeb8ae49907f8cbf89f725130d616b33774be43 Mon Sep 17 00:00:00 2001 From: jimafisk Date: Tue, 31 Jan 2023 15:14:31 -0500 Subject: [PATCH] Basic HTTP Auth (#163). --- cmd/main.go | 4 ++- server/handler/handler.go | 57 ++++++++++++++++++++++++++++++++-- server/handler/handler_auth.go | 33 ++++++++++++++++++++ server/upstream/auth.go | 35 +++++++++++++++++++++ 4 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 server/handler/handler_auth.go create mode 100644 server/upstream/auth.go diff --git a/cmd/main.go b/cmd/main.go index b72013a..e96b3e2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -78,6 +78,8 @@ func Serve(ctx *cli.Context) error { challengeCache := cache.NewKeyValueCache() // canonicalDomainCache stores canonical domains canonicalDomainCache := cache.NewKeyValueCache() + // authCache stores basic HTTP Auth credentials + authCache := cache.NewKeyValueCache() // dnsLookupCache stores DNS lookups for custom domains dnsLookupCache := cache.NewKeyValueCache() // clientResponseCache stores responses from the Gitea server @@ -93,7 +95,7 @@ func Serve(ctx *cli.Context) error { giteaClient, rawInfoPage, BlacklistedPaths, allowedCorsDomains, - dnsLookupCache, canonicalDomainCache) + dnsLookupCache, canonicalDomainCache, authCache) httpHandler := server.SetupHTTPACMEChallengeServer(challengeCache) diff --git a/server/handler/handler.go b/server/handler/handler.go index 78301e9..0f27927 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -25,12 +25,26 @@ func Handler(mainDomainSuffix, rawDomain string, giteaClient *gitea.Client, rawInfoPage string, blacklistedPaths, allowedCorsDomains []string, - dnsLookupCache, canonicalDomainCache cache.SetGetKey, + dnsLookupCache, canonicalDomainCache, authCache 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 @@ -39,8 +53,6 @@ func Handler(mainDomainSuffix, rawDomain string, // Enable browser caching for up to 10 minutes ctx.RespWriter.Header().Set("Cache-Control", "public, max-age=600") - trimmedHost := ctx.TrimHostPort() - // Add HSTS for RawDomain and MainDomainSuffix if hsts := getHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" { ctx.RespWriter.Header().Set("Strict-Transport-Security", hsts) @@ -109,5 +121,44 @@ func Handler(mainDomainSuffix, rawDomain string, pathElements, dnsLookupCache, canonicalDomainCache) } + } } + +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 +} diff --git a/server/handler/handler_auth.go b/server/handler/handler_auth.go new file mode 100644 index 0000000..b90e144 --- /dev/null +++ b/server/handler/handler_auth.go @@ -0,0 +1,33 @@ +package handler + +import ( + "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" + "github.com/rs/zerolog" +) + +func handleAuth(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client, + mainDomainSuffix string, + trimmedHost string, + dnsLookupCache, authCache cache.SetGetKey, +) []string { + // Get credentials for a given branch/repo/owner + targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, dnsLookupCache) + var credentials []string + canonicalLink := false + + // Try to use the given repo on the given branch or the default branch + log.Debug().Msg("auth preparations, trying to get credentials") + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TargetOwner: targetOwner, + TargetRepo: targetRepo, + TargetBranch: targetBranch, + }, canonicalLink); works { + credentials = targetOpt.CheckAuth(giteaClient, authCache) + } + + return credentials +} diff --git a/server/upstream/auth.go b/server/upstream/auth.go new file mode 100644 index 0000000..ac6225b --- /dev/null +++ b/server/upstream/auth.go @@ -0,0 +1,35 @@ +package upstream + +import ( + "strings" + "time" + + "github.com/rs/zerolog/log" + + "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/gitea" +) + +// authCacheTimeout specifies the timeout for the auth cache. +var authCacheTimeout = 5 * time.Minute + +const authConfig = ".auth" + +// CheckAuth returns the username and password for basic HTTP Auth specified in the repo (using the `.auth` file). +func (o *Options) CheckAuth(giteaClient *gitea.Client, authCache cache.SetGetKey) []string { + var credentials []string + if cachedValue, ok := authCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok { + credentials = cachedValue.([]string) + } else { + body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, authConfig) + if err == nil { + for _, authLine := range strings.Split(string(body), "\n") { + credentials = append(credentials, authLine) + } + } else { + log.Error().Err(err).Msgf("could not read %s of %s/%s", authConfig, o.TargetOwner, o.TargetRepo) + } + authCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, credentials, authCacheTimeout) + } + return credentials +}