2024-01-01 13:19:19 +00:00
|
|
|
package repo
|
2023-12-31 12:26:56 +00:00
|
|
|
|
2024-01-06 13:47:47 +00:00
|
|
|
//go:generate mockgen -destination mock_repo_test.go -package repo code.gitea.io/sdk/gitea Client
|
|
|
|
|
2023-12-31 12:26:56 +00:00
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2024-01-06 13:47:47 +00:00
|
|
|
"git.polynom.me/rio/internal/constants"
|
2024-01-01 13:19:19 +00:00
|
|
|
|
2023-12-31 12:26:56 +00:00
|
|
|
"github.com/patrickmn/go-cache"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2023-12-31 23:38:39 +00:00
|
|
|
pathCache = cache.New(1*time.Hour, 1*time.Hour)
|
2024-01-01 19:05:20 +00:00
|
|
|
|
|
|
|
// Caching the existence of an user
|
|
|
|
userCache = cache.New(24*time.Hour, 12*time.Hour)
|
2024-01-06 16:42:08 +00:00
|
|
|
|
|
|
|
// Caches the existence of a Content-Security-Policy
|
|
|
|
// Mapping: Repository key -> CSPCacheEntry
|
|
|
|
cspCache = cache.New(24*time.Hour, 12*time.Hour)
|
2023-12-31 12:26:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type PageCacheEntry struct {
|
2024-01-06 13:47:47 +00:00
|
|
|
Repository Repository
|
2023-12-31 23:38:39 +00:00
|
|
|
Path string
|
2023-12-31 12:26:56 +00:00
|
|
|
}
|
|
|
|
|
2024-01-06 16:42:08 +00:00
|
|
|
type CSPCacheEntry struct {
|
|
|
|
CSP string
|
|
|
|
LastRequested time.Time
|
|
|
|
}
|
|
|
|
|
2023-12-31 12:26:56 +00:00
|
|
|
func makePageCacheKey(domain, path string) string {
|
|
|
|
return domain + "/" + path
|
|
|
|
}
|
|
|
|
|
2024-01-06 16:42:08 +00:00
|
|
|
func makeCSPCacheKey(username, repositoryName string) string {
|
|
|
|
return username + ":" + repositoryName
|
|
|
|
}
|
|
|
|
|
2024-01-06 13:47:47 +00:00
|
|
|
func lookupRepositoryAndCache(username, reponame, branchName, host, domain, path, cname string, giteaClient *GiteaClient) (*Repository, error) {
|
|
|
|
log.Debugf("CNAME: %s", cname)
|
2023-12-31 12:26:56 +00:00
|
|
|
log.Debugf("Looking up repository %s/%s", username, reponame)
|
2024-01-06 13:47:47 +00:00
|
|
|
repo, err := giteaClient.getRepository(username, reponame)
|
2023-12-31 12:26:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-01-06 13:47:47 +00:00
|
|
|
if !giteaClient.hasBranch(username, reponame, branchName) {
|
|
|
|
return nil, errors.New("Specified branch does not exist")
|
|
|
|
}
|
|
|
|
|
2023-12-31 12:26:56 +00:00
|
|
|
// Check if the CNAME file matches
|
|
|
|
if cname != "" {
|
2024-01-06 13:47:47 +00:00
|
|
|
log.Debug("Checking CNAME")
|
2023-12-31 12:26:56 +00:00
|
|
|
file, _, err := giteaClient.GetFile(
|
|
|
|
username,
|
2024-01-06 13:47:47 +00:00
|
|
|
reponame,
|
|
|
|
constants.PagesBranch,
|
2023-12-31 12:26:56 +00:00
|
|
|
"CNAME",
|
2024-01-06 13:47:47 +00:00
|
|
|
nil,
|
2023-12-31 12:26:56 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
2024-01-06 13:47:47 +00:00
|
|
|
log.Errorf(
|
|
|
|
"Could not verify CNAME of %s/%s@%s: %v\n",
|
|
|
|
username,
|
|
|
|
reponame,
|
|
|
|
constants.PagesBranch,
|
|
|
|
err,
|
|
|
|
)
|
2023-12-31 12:26:56 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cnameContent := strings.Trim(
|
|
|
|
string(file[:]),
|
|
|
|
"\n",
|
|
|
|
)
|
2024-01-06 13:47:47 +00:00
|
|
|
|
|
|
|
log.Debugf("CNAME Content: %s", cnameContent)
|
2024-01-06 20:26:09 +00:00
|
|
|
if cnameContent != host {
|
|
|
|
log.Warnf("CNAME mismatch: Repo '%s', Host '%s'", cnameContent, host)
|
2023-12-31 12:26:56 +00:00
|
|
|
return nil, errors.New("CNAME mismatch")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache data
|
|
|
|
pathCache.Set(
|
|
|
|
makePageCacheKey(domain, path),
|
|
|
|
PageCacheEntry{
|
|
|
|
repo,
|
|
|
|
path,
|
|
|
|
},
|
|
|
|
cache.DefaultExpiration,
|
|
|
|
)
|
2024-01-06 13:47:47 +00:00
|
|
|
return &repo, nil
|
2023-12-31 12:26:56 +00:00
|
|
|
}
|
|
|
|
|
2024-01-06 20:26:09 +00:00
|
|
|
// 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 "".
|
2024-01-06 13:47:47 +00:00
|
|
|
func RepoFromPath(username, host, cname, path string, giteaClient *GiteaClient) (*Repository, string, error) {
|
2023-12-31 12:26:56 +00:00
|
|
|
domain := host
|
|
|
|
|
|
|
|
// Guess the repository
|
|
|
|
key := makePageCacheKey(domain, path)
|
|
|
|
entry, found := pathCache.Get(key)
|
|
|
|
if found {
|
|
|
|
pageEntry := entry.(PageCacheEntry)
|
2024-01-06 13:47:47 +00:00
|
|
|
return &pageEntry.Repository, pageEntry.Path, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Allow specifying the repository name in the TXT record
|
|
|
|
reponame := ""
|
|
|
|
if cname != "" {
|
2024-01-06 20:26:09 +00:00
|
|
|
repoLookup, err := giteaClient.lookupRepoTXT(host)
|
2024-01-06 13:47:47 +00:00
|
|
|
if err == nil && repoLookup != "" {
|
|
|
|
log.Infof(
|
|
|
|
"TXT lookup for %s resulted in choosing repository %s",
|
2024-01-06 20:26:09 +00:00
|
|
|
host,
|
2024-01-06 13:47:47 +00:00
|
|
|
repoLookup,
|
|
|
|
)
|
|
|
|
reponame = repoLookup
|
|
|
|
}
|
2023-12-31 12:26:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pathParts := strings.Split(path, "/")
|
2024-01-06 13:47:47 +00:00
|
|
|
if reponame == "" && len(pathParts) > 1 {
|
2023-12-31 12:26:56 +00:00
|
|
|
log.Debugf("Trying repository %s", pathParts[0])
|
|
|
|
modifiedPath := strings.Join(pathParts[1:], "/")
|
|
|
|
repo, err := lookupRepositoryAndCache(
|
|
|
|
username,
|
|
|
|
pathParts[0],
|
2024-01-06 13:47:47 +00:00
|
|
|
constants.PagesBranch,
|
2023-12-31 12:26:56 +00:00
|
|
|
host,
|
|
|
|
domain,
|
|
|
|
modifiedPath,
|
|
|
|
cname,
|
|
|
|
giteaClient,
|
|
|
|
)
|
|
|
|
if err == nil {
|
|
|
|
return repo, modifiedPath, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-06 13:47:47 +00:00
|
|
|
if reponame == "" {
|
|
|
|
reponame = domain
|
2023-12-31 21:41:51 +00:00
|
|
|
}
|
2023-12-31 13:15:39 +00:00
|
|
|
log.Debugf("Trying repository %s/%s", username, reponame)
|
2023-12-31 12:26:56 +00:00
|
|
|
repo, err := lookupRepositoryAndCache(
|
|
|
|
username,
|
2023-12-31 13:15:39 +00:00
|
|
|
reponame,
|
2024-01-06 13:47:47 +00:00
|
|
|
constants.PagesBranch,
|
2023-12-31 12:26:56 +00:00
|
|
|
host,
|
|
|
|
domain,
|
|
|
|
path,
|
|
|
|
cname,
|
|
|
|
giteaClient,
|
|
|
|
)
|
|
|
|
return repo, path, err
|
|
|
|
}
|
2024-01-01 19:05:20 +00:00
|
|
|
|
|
|
|
// 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.
|
2024-01-06 13:47:47 +00:00
|
|
|
func CanRequestCertificate(username string, giteaClient *GiteaClient) bool {
|
2024-01-01 19:05:20 +00:00
|
|
|
if _, found := userCache.Get(username); found {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2024-01-06 13:47:47 +00:00
|
|
|
hasUser := giteaClient.hasUser(username)
|
|
|
|
if hasUser {
|
2024-01-01 19:05:20 +00:00
|
|
|
userCache.Set(username, true, cache.DefaultExpiration)
|
|
|
|
}
|
2024-01-06 13:47:47 +00:00
|
|
|
return hasUser
|
2024-01-01 19:05:20 +00:00
|
|
|
}
|
2024-01-06 16:42:08 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|