Basic HTTP Auth (#163).

This commit is contained in:
jimafisk 2023-01-31 15:14:31 -05:00
parent 513e79832a
commit faeb8ae499
4 changed files with 125 additions and 4 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

35
server/upstream/auth.go Normal file
View File

@ -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
}