From e3032c82333782877b73cc77993834ac209d9108 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 3 Feb 2024 15:39:31 +0100 Subject: [PATCH] feat: Move the CNAME into the rio.json --- cmd/rio/main.go | 30 +++++---- internal/context/context.go | 6 ++ internal/context/{cache.go => info.go} | 2 + internal/context/path.go | 39 +++++++++++ internal/context/user.go | 24 +++++++ internal/repo/repo.go | 93 ++++++++------------------ internal/server/tls.go | 8 +-- 7 files changed, 120 insertions(+), 82 deletions(-) rename internal/context/{cache.go => info.go} (98%) create mode 100644 internal/context/path.go create mode 100644 internal/context/user.go diff --git a/cmd/rio/main.go b/cmd/rio/main.go index 075ecb9..8ebc604 100644 --- a/cmd/rio/main.go +++ b/cmd/rio/main.go @@ -56,7 +56,7 @@ func handleSubdomain(ctx *context.GlobalContext, domain, cname, path string, req domain, cname, path, - ctx.Gitea, + ctx, ) if err != nil { log.Errorf("Failed to get repo: %s", err) @@ -209,6 +209,20 @@ func runServer(ctx *cli.Context) error { 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 acmeEmail == "" || acmeFile == "" || certsFile == "" || acmeDnsProvider == "" { 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, &cache, acmeClient, - &giteaClient, + globalCtx, ) 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 servers := 2 if acmeDisable { diff --git a/internal/context/context.go b/internal/context/context.go index 2cc6c7c..10cec07 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -11,6 +11,12 @@ import ( type CacheContext struct { // Cache for general repository information RepositoryInformationCache cache.Cache + + // Cache for path resolutions + RepositoryPathCache cache.Cache + + // Cache for username lookups + UsernameCache cache.Cache } type GlobalContext struct { diff --git a/internal/context/cache.go b/internal/context/info.go similarity index 98% rename from internal/context/cache.go rename to internal/context/info.go index d83bbeb..ba2caf0 100644 --- a/internal/context/cache.go +++ b/internal/context/info.go @@ -9,6 +9,8 @@ import ( type RepositoryInformation struct { // Headers to include in every response Headers map[string]string + + CNAME string } func repoInfoKey(owner, name string) string { diff --git a/internal/context/path.go b/internal/context/path.go new file mode 100644 index 0000000..9a2691a --- /dev/null +++ b/internal/context/path.go @@ -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) +} diff --git a/internal/context/user.go b/internal/context/user.go new file mode 100644 index 0000000..ff6f5e8 --- /dev/null +++ b/internal/context/user.go @@ -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) +} diff --git a/internal/repo/repo.go b/internal/repo/repo.go index 8e3e529..eb6a4de 100644 --- a/internal/repo/repo.go +++ b/internal/repo/repo.go @@ -7,13 +7,11 @@ import ( "errors" "slices" "strings" - "time" "git.polynom.me/rio/internal/constants" "git.polynom.me/rio/internal/context" "git.polynom.me/rio/internal/gitea" - "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" ) @@ -28,96 +26,62 @@ var ( } ) -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) -) - -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) { +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 := giteaClient.GetRepository(username, reponame) + repo, err := ctx.Gitea.GetRepository(username, reponame) if err != nil { 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") } // 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 + 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") } - 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) + 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 - pathCache.Set( - makePageCacheKey(domain, path), - PageCacheEntry{ - repo, - path, + ctx.Cache.SetRepositoryPath( + domain, + path, + context.RepositoryPathInformation{ + Repository: repo, + Path: 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 *gitea.GiteaClient) (*gitea.Repository, string, error) { +func RepoFromPath(username, host, cname, path string, ctx *context.GlobalContext) (*gitea.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 + 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 := giteaClient.LookupRepoTXT(host) + repoLookup, err := ctx.Gitea.LookupRepoTXT(host) if err == nil && repoLookup != "" { log.Infof( "TXT lookup for %s resulted in choosing repository %s", @@ -141,7 +105,7 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.GiteaCl domain, modifiedPath, cname, - giteaClient, + ctx, ) if err == nil { return repo, modifiedPath, nil @@ -160,7 +124,7 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.GiteaCl domain, path, cname, - giteaClient, + ctx, ) 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 // instance, so that an attacker can't just request certificates for random // usernames. -func CanRequestCertificate(username string, giteaClient *gitea.GiteaClient) bool { - if _, found := userCache.Get(username); found { +func CanRequestCertificate(username string, ctx *context.GlobalContext) bool { + found := ctx.Cache.GetUser(username) + if found { return true } - hasUser := giteaClient.HasUser(username) + hasUser := ctx.Gitea.HasUser(username) if hasUser { - userCache.Set(username, true, cache.DefaultExpiration) + ctx.Cache.SetUser(username) } return hasUser } diff --git a/internal/server/tls.go b/internal/server/tls.go index 3d477e5..05eb864 100644 --- a/internal/server/tls.go +++ b/internal/server/tls.go @@ -7,8 +7,8 @@ import ( "sync" "git.polynom.me/rio/internal/certificates" + "git.polynom.me/rio/internal/context" "git.polynom.me/rio/internal/dns" - "git.polynom.me/rio/internal/gitea" "git.polynom.me/rio/internal/repo" "github.com/go-acme/lego/v4/lego" @@ -82,7 +82,7 @@ func getUsername(sni, pagesDomain string) (string, error) { 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{ GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { // Validate that we should even care about this domain @@ -100,7 +100,7 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat if cert.IsValid() { return cert.TlsCertificate, nil } else { - if !isPagesDomain && !repo.CanRequestCertificate(username, giteaClient) { + if !isPagesDomain && !repo.CanRequestCertificate(username, ctx) { log.Warnf( "Cannot renew certificate for %s because CanRequestCertificate(%s) returned false", info.ServerName, @@ -129,7 +129,7 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat return newCert.TlsCertificate, nil } } else { - if !isPagesDomain && !repo.CanRequestCertificate(username, giteaClient) { + if !isPagesDomain && !repo.CanRequestCertificate(username, ctx) { log.Warnf( "Cannot request certificate for %s because CanRequestCertificate(%s) returned false", info.ServerName,