package repo //go:generate mockgen -destination mock_repo_test.go -package repo code.gitea.io/sdk/gitea Client import ( "encoding/json" "errors" "slices" "strings" "git.polynom.me/rio/internal/constants" "git.polynom.me/rio/internal/context" "git.polynom.me/rio/internal/gitea" log "github.com/sirupsen/logrus" ) var ( ForbiddenHeaders = []string{ "content-length", "content-type", "date", "location", "strict-transport-security", "set-cookie", } ) func lookupRepositoryAndCache(username, reponame, branchName, host, domain, path, cname string, ctx *context.GlobalContext) (*gitea.Repository, error) { log.Debugf("CNAME: %s", cname) log.Debugf("Looking up repository %s/%s", username, reponame) repo, err := ctx.Gitea.GetRepository(username, reponame) if err != nil { return nil, err } if !ctx.Gitea.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") repoInfo := GetRepositoryInformation(username, reponame, ctx) if repoInfo == nil { log.Warn("Repository does not contain a rio.json file") return nil, errors.New("No CNAME available in repository") } log.Debugf("CNAME Content: \"%s\"", repoInfo.CNAME) if repoInfo.CNAME != host { log.Warnf("CNAME mismatch: Repo '%s', Host '%s'", repoInfo.CNAME, host) return nil, errors.New("CNAME mismatch") } } // Cache data ctx.Cache.SetRepositoryPath( domain, path, context.RepositoryPathInformation{ Repository: repo, Path: path, }, ) 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, ctx *context.GlobalContext) (*gitea.Repository, string, error) { domain := host // Guess the repository entry := ctx.Cache.GetRepositoryPath(domain, path) if entry != nil { return &entry.Repository, entry.Path, nil } // Allow specifying the repository name in the TXT record reponame := "" if cname != "" { repoLookup, err := ctx.Gitea.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, ctx, ) 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, ctx, ) 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, ctx *context.GlobalContext) bool { found := ctx.Cache.GetUser(username) if found { return true } hasUser := ctx.Gitea.HasUser(username) if hasUser { ctx.Cache.SetUser(username) } return hasUser } func filterHeaders(headers map[string]interface{}) map[string]string { newHeaders := make(map[string]string) for key, value := range headers { if slices.Contains[[]string, string](ForbiddenHeaders, strings.ToLower(key)) { continue } switch value.(type) { case string: newHeaders[key] = value.(string) } } return newHeaders } func GetRepositoryInformation(owner, repoName string, ctx *context.GlobalContext) *context.RepositoryInformation { res := ctx.Cache.GetRepositoryInformation(owner, repoName) if res != nil { return res } fetchedConfig, _, err := ctx.Gitea.GetFile( owner, repoName, constants.PagesBranch, "rio.json", nil, ) if err != nil { log.Errorf("Failed to request rio.json for %s/%s:%v", owner, repoName, err) return nil } var payload map[string]interface{} err = json.Unmarshal(fetchedConfig, &payload) if err != nil { log.Errorf("Failed to unmarshal rio.json for %s/%s:%v", owner, repoName, err) return nil } headers, found := payload["headers"] if !found { log.Warnf("Did not find headers key in rio.json for %s/%s", owner, repoName) headers = make(map[string]interface{}) } else { switch headers.(type) { case map[string]interface{}: // NOOP default: log.Warn("headers attribute has invalid data type") headers = make(map[string]string) } } cname, found := payload["CNAME"] if found { switch cname.(type) { case string: // NOOP default: log.Warnf("CNAME attribute is not a string for %s/%s", owner, repoName) cname = "" } } else { cname = "" } info := context.RepositoryInformation{ Headers: filterHeaders(headers.(map[string]interface{})), CNAME: cname.(string), } ctx.Cache.SetRepositoryInformation(owner, repoName, info) return &info }