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 } 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 != host { log.Warnf("CNAME mismatch: Repo '%s', Host '%s'", cnameContent, host) return nil, errors.New("CNAME mismatch") } } // Cache data pathCache.Set( makePageCacheKey(domain, path), PageCacheEntry{ repo, path, }, cache.DefaultExpiration, ) return &repo, nil } // host is the domain name we're accessed from. cname is the domain that host is pointing // if, if we're accessed via a CNAME. If not, then cname is "". 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(host) if err == nil && repoLookup != "" { log.Infof( "TXT lookup for %s resulted in choosing repository %s", host, repoLookup, ) reponame = repoLookup } } pathParts := strings.Split(path, "/") log.Debugf("reponame='%s' len(pathParts)='%d'", reponame, len(pathParts)) 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 }