diff --git a/cmd/rio/main.go b/cmd/rio/main.go index 08eec7e..d02cdb9 100644 --- a/cmd/rio/main.go +++ b/cmd/rio/main.go @@ -97,7 +97,7 @@ func runServer(ctx *cli.Context) error { // Setup the Gitea stuff httpClient := http.Client{Timeout: 10 * time.Second} - client, err := gitea.NewClient( + giteaClient, err := gitea.NewClient( giteaUrl, gitea.SetHTTPClient(&httpClient), gitea.SetToken(""), @@ -162,11 +162,12 @@ func runServer(ctx *cli.Context) error { certsFile, &cache, acmeClient, + giteaClient, ) listener = tls.NewListener(listener, tlsConfig) } - if err := http.Serve(listener, Handler(domain, giteaUrl, client)); err != nil { + if err := http.Serve(listener, Handler(domain, giteaUrl, giteaClient)); err != nil { fmt.Printf("Listening failed") return err } diff --git a/internal/repo/repo.go b/internal/repo/repo.go index 74cac2b..ac7dc37 100644 --- a/internal/repo/repo.go +++ b/internal/repo/repo.go @@ -15,6 +15,9 @@ import ( 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 { @@ -132,3 +135,19 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) ) 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, giteaClient *gitea.Client) bool { + if _, found := userCache.Get(username); found { + return true + } + + user, _, err := giteaClient.GetUserInfo(username) + if user != nil && err == nil { + userCache.Set(username, true, cache.DefaultExpiration) + return true + } + return false +} diff --git a/internal/repo/repo_test.go b/internal/repo/repo_test.go new file mode 100644 index 0000000..80f02b6 --- /dev/null +++ b/internal/repo/repo_test.go @@ -0,0 +1,39 @@ +package repo + +import ( + "net/http" + "testing" + "time" + + "code.gitea.io/sdk/gitea" +) + +var ( + giteaClient, _ = gitea.NewClient( + "https://git.polynom.me", + gitea.SetHTTPClient(&http.Client{Timeout: 10 * time.Second}), + gitea.SetToken(""), + gitea.SetUserAgent("rio/testing"), + ) +) + +func TestCanRequestCertificatePositiveUser(t *testing.T) { + res := CanRequestCertificate("papatutuwawa", giteaClient) + if !res { + t.Fatalf("User papatutuwawa should be servable") + } +} + +func TestCanRequestCertificatePositiveOrganisation(t *testing.T) { + res := CanRequestCertificate("moxxy", giteaClient) + if !res { + t.Fatalf("Organisation moxxy should be servable") + } +} + +func TestCanRequestCertificateNegative(t *testing.T) { + res := CanRequestCertificate("user-who-does-not-exist", giteaClient) + if res { + t.Fatalf("User user-who-does-not-exist should not be servable") + } +} diff --git a/internal/server/tls.go b/internal/server/tls.go index 160a93d..4b25e0c 100644 --- a/internal/server/tls.go +++ b/internal/server/tls.go @@ -7,7 +7,9 @@ import ( "git.polynom.me/rio/internal/certificates" "git.polynom.me/rio/internal/dns" + "git.polynom.me/rio/internal/repo" + "code.gitea.io/sdk/gitea" "github.com/go-acme/lego/v4/lego" log "github.com/sirupsen/logrus" @@ -41,16 +43,16 @@ func unlockDomain(domain string) { delete(workingDomains, domain) } -func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client) *tls.Config { +func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client, giteaClient *gitea.Client) *tls.Config { return &tls.Config{ - InsecureSkipVerify: true, GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { // Validate that we should even care about this domain + cname := "" if !strings.HasSuffix(info.ServerName, pagesDomain) { // Note: We do not check err here because err != nil // always implies that cname == "", which does not have // pagesDomain as a suffix. - cname, _ := dns.LookupCNAME(info.ServerName) + cname, _ = dns.LookupCNAME(info.ServerName) if !strings.HasSuffix(cname, pagesDomain) { log.Warnf("Got ServerName for Domain %s that we're not responsible for", info.ServerName) return cache.FallbackCertificate.TlsCertificate, nil @@ -64,11 +66,31 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat domain = "*." + pagesDomain }*/ + // Figure out a username for later username checks + username := "" + if cname == "" { + // domain ends on pagesDomain + username = strings.Split(domain, ".")[0] + } else { + // cname ends on pagesDomain + username = strings.Split(cname, ".")[0] + } + + // Find the correct certificate cert, found := cache.Certificates[info.ServerName] if found { if cert.IsValid() { return cert.TlsCertificate, nil } else { + if !repo.CanRequestCertificate(username, giteaClient) { + log.Warnf( + "Cannot renew certificate for %s because CanRequestCertificate(%s) returned false", + domain, + username, + ) + return cert.TlsCertificate, nil + } + // If we're already working on the domain, // return the old certificate if lockIfUnlockedDomain(domain) { @@ -76,7 +98,7 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat } defer unlockDomain(domain) - // Renew + // Renew the certificate log.Infof("Certificate for %s expired, renewing", domain) newCert, err := certificates.RenewCertificate(&cert, acmeClient) if err != nil { @@ -89,6 +111,15 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat return newCert.TlsCertificate, nil } } else { + if !repo.CanRequestCertificate(username, giteaClient) { + log.Warnf( + "Cannot request certificate for %s because CanRequestCertificate(%s) returned false", + domain, + username, + ) + return cache.FallbackCertificate.TlsCertificate, nil + } + // Don't request if we're already requesting. if lockIfUnlockedDomain(domain) { return cache.FallbackCertificate.TlsCertificate, nil