224 lines
5.2 KiB
Go
224 lines
5.2 KiB
Go
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
|
|
}
|