feat: Move the CNAME into the rio.json

This commit is contained in:
PapaTutuWawa 2024-02-03 15:39:31 +01:00
parent cf85380ddb
commit e3032c8233
7 changed files with 120 additions and 82 deletions

View File

@ -56,7 +56,7 @@ func handleSubdomain(ctx *context.GlobalContext, domain, cname, path string, req
domain, domain,
cname, cname,
path, path,
ctx.Gitea, ctx,
) )
if err != nil { if err != nil {
log.Errorf("Failed to get repo: %s", err) log.Errorf("Failed to get repo: %s", err)
@ -209,6 +209,20 @@ func runServer(ctx *cli.Context) error {
return err return err
} }
// Prepare the context
cacheCtx := context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
UsernameCache: context.MakeUsernameCache(),
}
globalCtx := &context.GlobalContext{
DefaultCSP: defaultCsp,
PagesDomain: domain,
Gitea: &giteaClient,
MetricConfig: &lokiConfig,
Cache: &cacheCtx,
}
if !acmeDisable { if !acmeDisable {
if acmeEmail == "" || acmeFile == "" || certsFile == "" || acmeDnsProvider == "" { if acmeEmail == "" || acmeFile == "" || certsFile == "" || acmeDnsProvider == "" {
return errors.New("The options acme-dns-provider, acme-file, acme-email, and certs-file are required") return errors.New("The options acme-dns-provider, acme-file, acme-email, and certs-file are required")
@ -262,23 +276,11 @@ func runServer(ctx *cli.Context) error {
certsFile, certsFile,
&cache, &cache,
acmeClient, acmeClient,
&giteaClient, globalCtx,
) )
listener = tls.NewListener(listener, tlsConfig) listener = tls.NewListener(listener, tlsConfig)
} }
// Prepare the context
cacheCtx := context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
}
globalCtx := &context.GlobalContext{
DefaultCSP: defaultCsp,
PagesDomain: domain,
Gitea: &giteaClient,
MetricConfig: &lokiConfig,
Cache: &cacheCtx,
}
var waitGroup sync.WaitGroup var waitGroup sync.WaitGroup
servers := 2 servers := 2
if acmeDisable { if acmeDisable {

View File

@ -11,6 +11,12 @@ import (
type CacheContext struct { type CacheContext struct {
// Cache for general repository information // Cache for general repository information
RepositoryInformationCache cache.Cache RepositoryInformationCache cache.Cache
// Cache for path resolutions
RepositoryPathCache cache.Cache
// Cache for username lookups
UsernameCache cache.Cache
} }
type GlobalContext struct { type GlobalContext struct {

View File

@ -9,6 +9,8 @@ import (
type RepositoryInformation struct { type RepositoryInformation struct {
// Headers to include in every response // Headers to include in every response
Headers map[string]string Headers map[string]string
CNAME string
} }
func repoInfoKey(owner, name string) string { func repoInfoKey(owner, name string) string {

39
internal/context/path.go Normal file
View File

@ -0,0 +1,39 @@
package context
import (
"time"
"git.polynom.me/rio/internal/gitea"
"github.com/patrickmn/go-cache"
)
type RepositoryPathInformation struct {
Repository gitea.Repository
Path string
}
func pathInfoKey(domain, path string) string {
return domain + "/" + path
}
func (c *CacheContext) GetRepositoryPath(domain, path string) *RepositoryPathInformation {
data, found := c.RepositoryPathCache.Get(pathInfoKey(domain, path))
if !found {
return nil
}
typedData := data.(RepositoryPathInformation)
return &typedData
}
func (c *CacheContext) SetRepositoryPath(domain, path string, info RepositoryPathInformation) {
c.RepositoryPathCache.Set(
pathInfoKey(domain, path),
info,
cache.DefaultExpiration,
)
}
func MakeRepoPathCache() cache.Cache {
return *cache.New(24*time.Hour, 12*time.Hour)
}

24
internal/context/user.go Normal file
View File

@ -0,0 +1,24 @@
package context
import (
"time"
"github.com/patrickmn/go-cache"
)
func (c *CacheContext) GetUser(username string) bool {
_, found := c.UsernameCache.Get(username)
return found
}
func (c *CacheContext) SetUser(username string) {
c.UsernameCache.Set(
username,
true,
cache.DefaultExpiration,
)
}
func MakeUsernameCache() cache.Cache {
return *cache.New(24*time.Hour, 12*time.Hour)
}

View File

@ -7,13 +7,11 @@ import (
"errors" "errors"
"slices" "slices"
"strings" "strings"
"time"
"git.polynom.me/rio/internal/constants" "git.polynom.me/rio/internal/constants"
"git.polynom.me/rio/internal/context" "git.polynom.me/rio/internal/context"
"git.polynom.me/rio/internal/gitea" "git.polynom.me/rio/internal/gitea"
"github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -28,96 +26,62 @@ var (
} }
) )
var ( func lookupRepositoryAndCache(username, reponame, branchName, host, domain, path, cname string, ctx *context.GlobalContext) (*gitea.Repository, error) {
pathCache = cache.New(1*time.Hour, 1*time.Hour)
// Caching the existence of an user
userCache = cache.New(24*time.Hour, 12*time.Hour)
)
type PageCacheEntry struct {
Repository gitea.Repository
Path string
}
func makePageCacheKey(domain, path string) string {
return domain + "/" + path
}
func lookupRepositoryAndCache(username, reponame, branchName, host, domain, path, cname string, giteaClient *gitea.GiteaClient) (*gitea.Repository, error) {
log.Debugf("CNAME: %s", cname) log.Debugf("CNAME: %s", cname)
log.Debugf("Looking up repository %s/%s", username, reponame) log.Debugf("Looking up repository %s/%s", username, reponame)
repo, err := giteaClient.GetRepository(username, reponame) repo, err := ctx.Gitea.GetRepository(username, reponame)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !giteaClient.HasBranch(username, reponame, branchName) { if !ctx.Gitea.HasBranch(username, reponame, branchName) {
return nil, errors.New("Specified branch does not exist") return nil, errors.New("Specified branch does not exist")
} }
// Check if the CNAME file matches // Check if the CNAME file matches
if cname != "" { if cname != "" {
log.Debug("Checking CNAME") log.Debug("Checking CNAME")
file, _, err := giteaClient.GetFile( repoInfo := GetRepositoryInformation(username, reponame, ctx)
username, if repoInfo == nil {
reponame, log.Warn("Repository does not contain a rio.json file")
constants.PagesBranch, return nil, errors.New("No CNAME available in repository")
"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( log.Debugf("CNAME Content: \"%s\"", repoInfo.CNAME)
string(file[:]), if repoInfo.CNAME != host {
"\n", log.Warnf("CNAME mismatch: Repo '%s', Host '%s'", repoInfo.CNAME, host)
)
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") return nil, errors.New("CNAME mismatch")
} }
} }
// Cache data // Cache data
pathCache.Set( ctx.Cache.SetRepositoryPath(
makePageCacheKey(domain, path), domain,
PageCacheEntry{
repo,
path, path,
context.RepositoryPathInformation{
Repository: repo,
Path: path,
}, },
cache.DefaultExpiration,
) )
return &repo, nil return &repo, nil
} }
// host is the domain name we're accessed from. cname is the domain that host is pointing // 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 "". // if, if we're accessed via a CNAME. If not, then cname is "".
func RepoFromPath(username, host, cname, path string, giteaClient *gitea.GiteaClient) (*gitea.Repository, string, error) { func RepoFromPath(username, host, cname, path string, ctx *context.GlobalContext) (*gitea.Repository, string, error) {
domain := host domain := host
// Guess the repository // Guess the repository
key := makePageCacheKey(domain, path) entry := ctx.Cache.GetRepositoryPath(domain, path)
entry, found := pathCache.Get(key) if entry != nil {
if found { return &entry.Repository, entry.Path, nil
pageEntry := entry.(PageCacheEntry)
return &pageEntry.Repository, pageEntry.Path, nil
} }
// Allow specifying the repository name in the TXT record // Allow specifying the repository name in the TXT record
reponame := "" reponame := ""
if cname != "" { if cname != "" {
repoLookup, err := giteaClient.LookupRepoTXT(host) repoLookup, err := ctx.Gitea.LookupRepoTXT(host)
if err == nil && repoLookup != "" { if err == nil && repoLookup != "" {
log.Infof( log.Infof(
"TXT lookup for %s resulted in choosing repository %s", "TXT lookup for %s resulted in choosing repository %s",
@ -141,7 +105,7 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.GiteaCl
domain, domain,
modifiedPath, modifiedPath,
cname, cname,
giteaClient, ctx,
) )
if err == nil { if err == nil {
return repo, modifiedPath, nil return repo, modifiedPath, nil
@ -160,7 +124,7 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.GiteaCl
domain, domain,
path, path,
cname, cname,
giteaClient, ctx,
) )
return repo, path, err return repo, path, err
} }
@ -168,14 +132,15 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.GiteaCl
// Checks if the username exists as an organisation or an user on the Gitea // 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 // instance, so that an attacker can't just request certificates for random
// usernames. // usernames.
func CanRequestCertificate(username string, giteaClient *gitea.GiteaClient) bool { func CanRequestCertificate(username string, ctx *context.GlobalContext) bool {
if _, found := userCache.Get(username); found { found := ctx.Cache.GetUser(username)
if found {
return true return true
} }
hasUser := giteaClient.HasUser(username) hasUser := ctx.Gitea.HasUser(username)
if hasUser { if hasUser {
userCache.Set(username, true, cache.DefaultExpiration) ctx.Cache.SetUser(username)
} }
return hasUser return hasUser
} }

View File

@ -7,8 +7,8 @@ import (
"sync" "sync"
"git.polynom.me/rio/internal/certificates" "git.polynom.me/rio/internal/certificates"
"git.polynom.me/rio/internal/context"
"git.polynom.me/rio/internal/dns" "git.polynom.me/rio/internal/dns"
"git.polynom.me/rio/internal/gitea"
"git.polynom.me/rio/internal/repo" "git.polynom.me/rio/internal/repo"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
@ -82,7 +82,7 @@ func getUsername(sni, pagesDomain string) (string, error) {
return dns.ExtractUsername(pagesDomain, sni), nil return dns.ExtractUsername(pagesDomain, sni), nil
} }
func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client, giteaClient *gitea.GiteaClient) *tls.Config { func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client, ctx *context.GlobalContext) *tls.Config {
return &tls.Config{ return &tls.Config{
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
// Validate that we should even care about this domain // Validate that we should even care about this domain
@ -100,7 +100,7 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat
if cert.IsValid() { if cert.IsValid() {
return cert.TlsCertificate, nil return cert.TlsCertificate, nil
} else { } else {
if !isPagesDomain && !repo.CanRequestCertificate(username, giteaClient) { if !isPagesDomain && !repo.CanRequestCertificate(username, ctx) {
log.Warnf( log.Warnf(
"Cannot renew certificate for %s because CanRequestCertificate(%s) returned false", "Cannot renew certificate for %s because CanRequestCertificate(%s) returned false",
info.ServerName, info.ServerName,
@ -129,7 +129,7 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat
return newCert.TlsCertificate, nil return newCert.TlsCertificate, nil
} }
} else { } else {
if !isPagesDomain && !repo.CanRequestCertificate(username, giteaClient) { if !isPagesDomain && !repo.CanRequestCertificate(username, ctx) {
log.Warnf( log.Warnf(
"Cannot request certificate for %s because CanRequestCertificate(%s) returned false", "Cannot request certificate for %s because CanRequestCertificate(%s) returned false",
info.ServerName, info.ServerName,