rio/internal/repo/repo.go
Alexander "PapaTutuWawa c0b87be246
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: Log CNAME expectations on mismatch
2024-01-06 20:09:59 +01:00

219 lines
5.0 KiB
Go

package repo
//go:generate mockgen -destination mock_repo_test.go -package repo code.gitea.io/sdk/gitea Client
import (
"errors"
"strings"
"time"
"git.polynom.me/rio/internal/constants"
"github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus"
)
var (
pathCache = cache.New(1*time.Hour, 1*time.Hour)
// Caching the existence of an user
userCache = cache.New(24*time.Hour, 12*time.Hour)
// Caches the existence of a Content-Security-Policy
// Mapping: Repository key -> CSPCacheEntry
cspCache = cache.New(24*time.Hour, 12*time.Hour)
)
type PageCacheEntry struct {
Repository Repository
Path string
}
type CSPCacheEntry struct {
CSP string
LastRequested time.Time
}
func makePageCacheKey(domain, path string) string {
return domain + "/" + path
}
func makeCSPCacheKey(username, repositoryName string) string {
return username + ":" + repositoryName
}
// / Try to find the repository with name @reponame of the user @username. If @cname
// / is not "", then it also verifies that the repository contains a "CNAME" with
// / the value of @cname as its content. @host, @domain, and @path are passed for
// / caching on success.
func lookupRepositoryAndCache(username, reponame, branchName, host, domain, path, cname string, giteaClient *GiteaClient) (*Repository, error) {
log.Debugf("CNAME: %s", cname)
log.Debugf("Looking up repository %s/%s", username, reponame)
repo, err := giteaClient.getRepository(username, reponame)
if err != nil {
return nil, err
}
if !giteaClient.hasBranch(username, reponame, branchName) {
return nil, errors.New("Specified branch does not exist")
}
// Check if the CNAME file matches
if cname != "" {
log.Debug("Checking CNAME")
file, _, err := giteaClient.GetFile(
username,
reponame,
constants.PagesBranch,
"CNAME",
nil,
)
if err != nil {
log.Errorf(
"Could not verify CNAME of %s/%s@%s: %v\n",
username,
reponame,
constants.PagesBranch,
err,
)
return nil, err
}
cnameContent := strings.Trim(
string(file[:]),
"\n",
)
log.Debugf("CNAME Content: %s", cnameContent)
if cnameContent != cname {
log.Warnf("CNAME mismatch: Got '%s', expected '%s'", cname, cnameContent)
return nil, errors.New("CNAME mismatch")
}
}
// Cache data
pathCache.Set(
makePageCacheKey(domain, path),
PageCacheEntry{
repo,
path,
},
cache.DefaultExpiration,
)
return &repo, nil
}
func RepoFromPath(username, host, cname, path string, giteaClient *GiteaClient) (*Repository, string, error) {
domain := host
// Guess the repository
key := makePageCacheKey(domain, path)
entry, found := pathCache.Get(key)
if found {
pageEntry := entry.(PageCacheEntry)
return &pageEntry.Repository, pageEntry.Path, nil
}
// Allow specifying the repository name in the TXT record
reponame := ""
if cname != "" {
repoLookup, err := giteaClient.lookupRepoTXT(cname)
if err == nil && repoLookup != "" {
log.Infof(
"TXT lookup for %s resulted in choosing repository %s",
cname,
repoLookup,
)
reponame = repoLookup
}
}
pathParts := strings.Split(path, "/")
if reponame == "" && len(pathParts) > 1 {
log.Debugf("Trying repository %s", pathParts[0])
modifiedPath := strings.Join(pathParts[1:], "/")
repo, err := lookupRepositoryAndCache(
username,
pathParts[0],
constants.PagesBranch,
host,
domain,
modifiedPath,
cname,
giteaClient,
)
if err == nil {
return repo, modifiedPath, nil
}
}
if reponame == "" {
reponame = domain
}
log.Debugf("Trying repository %s/%s", username, reponame)
repo, err := lookupRepositoryAndCache(
username,
reponame,
constants.PagesBranch,
host,
domain,
path,
cname,
giteaClient,
)
return repo, path, err
}
// Checks if the username exists as an organisation or an user on the Gitea
// instance, so that an attacker can't just request certificates for random
// usernames.
func CanRequestCertificate(username string, giteaClient *GiteaClient) bool {
if _, found := userCache.Get(username); found {
return true
}
hasUser := giteaClient.hasUser(username)
if hasUser {
userCache.Set(username, true, cache.DefaultExpiration)
}
return hasUser
}
// Checks the repository username/repository@PagesBranch for a file named CSP. If it exists,
// read it and return the value. If it does not exist, return defaultCsp.
func GetCSPForRepository(username, repositoryName, defaultCsp string, giteaClient *GiteaClient) string {
key := makeCSPCacheKey(username, repositoryName)
cachedCsp, found := cspCache.Get(key)
var since time.Time
if found {
since = cachedCsp.(CSPCacheEntry).LastRequested
}
fetchedCsp, changed, err := giteaClient.GetFile(
username,
repositoryName,
constants.PagesBranch,
"CSP",
&since,
)
csp := ""
if err != nil {
if found {
return cachedCsp.(CSPCacheEntry).CSP
}
csp = defaultCsp
} else {
csp = string(fetchedCsp)
if !found || changed {
cspCache.Set(key, CSPCacheEntry{
CSP: csp,
LastRequested: time.Now(),
}, cache.DefaultExpiration)
}
}
return csp
}