diff --git a/cmd/rio/main.go b/cmd/rio/main.go index 5cc1d88..1585e98 100644 --- a/cmd/rio/main.go +++ b/cmd/rio/main.go @@ -23,7 +23,7 @@ import ( "github.com/urfave/cli/v2" ) -func handleSubdomain(domain string, cname string, path, giteaUrl string, giteaClient *gitea.Client, w http.ResponseWriter) { +func handleSubdomain(domain string, cname string, path, giteaUrl string, giteaClient *repo.GiteaClient, w http.ResponseWriter) { hostParts := strings.Split(domain, ".") username := hostParts[0] @@ -45,10 +45,10 @@ func handleSubdomain(domain string, cname string, path, giteaUrl string, giteaCl return } - pages.ServeFile(username, repo.Name, path, giteaUrl, w) + pages.ServeFile(username, repo.Name, path, giteaClient, w) } -func Handler(pagesDomain, giteaUrl string, giteaClient *gitea.Client) http.HandlerFunc { +func Handler(pagesDomain, giteaUrl string, giteaClient *repo.GiteaClient) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Server", "rio") @@ -97,12 +97,16 @@ func runServer(ctx *cli.Context) error { // Setup the Gitea stuff httpClient := http.Client{Timeout: 10 * time.Second} - giteaClient, err := gitea.NewClient( + giteaApiClient, err := gitea.NewClient( giteaUrl, gitea.SetHTTPClient(&httpClient), gitea.SetToken(""), gitea.SetUserAgent("rio"), ) + if err != nil { + return err + } + giteaClient := repo.NewGiteaClient(giteaUrl, giteaApiClient) // Listen on the port addr := ctx.String("listen-host") + ":" + ctx.String("listen-port") @@ -163,12 +167,12 @@ func runServer(ctx *cli.Context) error { certsFile, &cache, acmeClient, - giteaClient, + &giteaClient, ) listener = tls.NewListener(listener, tlsConfig) } - if err := http.Serve(listener, Handler(domain, giteaUrl, giteaClient)); err != nil { + if err := http.Serve(listener, Handler(domain, giteaUrl, &giteaClient)); err != nil { fmt.Printf("Listening failed") return err } diff --git a/go.mod b/go.mod index 2f5886d..366fd47 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli/v2 v2.27.1 + go.uber.org/mock v0.4.0 ) require ( @@ -18,6 +19,8 @@ require ( github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/miekg/dns v1.1.55 // indirect + github.com/onsi/gomega v1.27.6 // indirect + github.com/petergtz/pegomock/v4 v4.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.17.0 // indirect diff --git a/go.sum b/go.sum index f863457..7659f80 100644 --- a/go.sum +++ b/go.sum @@ -17,12 +17,17 @@ github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyM github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/petergtz/pegomock/v4 v4.0.0 h1:BIGMUof4NXc+xBbuFk0VBfK5Ls7DplcP+LWz4hfYWsY= +github.com/petergtz/pegomock/v4 v4.0.0/go.mod h1:Xscaw/kXYcuh9sGsns+If19FnSMMQy4Wz60YJTn3XOU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -38,6 +43,8 @@ github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6S github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/internal/constants/const.go b/internal/constants/const.go new file mode 100644 index 0000000..9bd3065 --- /dev/null +++ b/internal/constants/const.go @@ -0,0 +1,5 @@ +package constants + +const ( + PagesBranch = "pages" +) diff --git a/internal/pages/pages.go b/internal/pages/pages.go index f5cf5a6..d397054 100644 --- a/internal/pages/pages.go +++ b/internal/pages/pages.go @@ -1,22 +1,18 @@ package pages import ( - "fmt" - "io" "mime" "net/http" "strings" "time" + "git.polynom.me/rio/internal/constants" + "git.polynom.me/rio/internal/repo" + "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" ) -const ( - // The branch name on which files must reside. - PagesBranch = "pages" -) - var ( pageCache = cache.New(6*time.Hour, 1*time.Hour) ) @@ -31,7 +27,7 @@ func makePageContentCacheEntry(username, path string) string { return username + ":" + path } -func ServeFile(username, reponame, path, giteaUrl string, w http.ResponseWriter) { +func ServeFile(username, reponame, path string, giteaClient *repo.GiteaClient, w http.ResponseWriter) { // Provide a default if path == "" { path = "/index.html" @@ -47,30 +43,23 @@ func ServeFile(username, reponame, path, giteaUrl string, w http.ResponseWriter) var content []byte var mimeType string var err error + var since *time.Time = nil if found { log.Debugf("Returning %s from cache", path) content = entry.(PageContentCache).Content mimeType = entry.(PageContentCache).mimeType + sinceRaw := entry.(PageContentCache).RequestedAt + since = &sinceRaw } - // We have to do the raw request manually because the Gitea SDK does not allow - // passing the If-Modfied-Since header. - apiUrl := fmt.Sprintf( - "%s/api/v1/repos/%s/%s/raw/%s?ref=%s", - giteaUrl, + content, changed, err := giteaClient.GetFile( username, reponame, + constants.PagesBranch, path, - PagesBranch, + since, ) - client := &http.Client{} - req, err := http.NewRequest("GET", apiUrl, nil) - if found { - since := entry.(PageContentCache).RequestedAt.Format(time.RFC1123) - log.Debugf("Found %s in cache. Adding '%s' as If-Modified-Since", key, since) - req.Header.Add("If-Modified-Since", since) - } - resp, err := client.Do(req) + if err != nil { if !found { log.Errorf("Failed to get file %s/%s/%s (%s)", username, reponame, path, err) @@ -84,10 +73,8 @@ func ServeFile(username, reponame, path, giteaUrl string, w http.ResponseWriter) return } - defer resp.Body.Close() - log.Debugf("Gitea API request returned %d", resp.StatusCode) - if found && resp.StatusCode == 302 { + if found && !changed { log.Debugf("Page %s is unchanged and cached in memory", path) w.WriteHeader(200) w.Header().Set("Content-Type", mimeType) @@ -95,19 +82,6 @@ func ServeFile(username, reponame, path, giteaUrl string, w http.ResponseWriter) return } - // Correctly propagate 404s. - if resp.StatusCode == 404 { - w.WriteHeader(404) - return - } - - content, err = io.ReadAll(resp.Body) - if err != nil { - log.Errorf("Failed to get file %s/%s/%s (%s)", username, reponame, path, err) - w.WriteHeader(404) - return - } - pathParts := strings.Split(path, ".") ext := pathParts[len(pathParts)-1] mimeType = mime.TypeByExtension("." + ext) diff --git a/internal/repo/client.go b/internal/repo/client.go new file mode 100644 index 0000000..57344d4 --- /dev/null +++ b/internal/repo/client.go @@ -0,0 +1,111 @@ +package repo + +import ( + "fmt" + "io" + "net/http" + "time" + + "code.gitea.io/sdk/gitea" + + "git.polynom.me/rio/internal/dns" +) + +// Returns true if the repository at / exists, false if it +// does not. +type GetRepositoryMethod func(username, repositoryName string) (Repository, error) + +// Returns , nil if the file exists at path (relative to the repository) in +// /@ exists. If not, returns "", error. +type GetFileMethod func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) + +type LookupCNAMEMethod func(domain string) (string, error) + +type LookupRepoTXTMethod func(domain string) (string, error) + +type HasBranchMethod func(username, repositoryName, branchName string) bool + +type HasUserMethod func(username string) bool + +type Repository struct { + Name string +} + +type GiteaClient struct { + getRepository GetRepositoryMethod + hasBranch HasBranchMethod + hasUser HasUserMethod + GetFile GetFileMethod + lookupCNAME LookupCNAMEMethod + lookupRepoTXT LookupRepoTXTMethod +} + +func NewGiteaClient(giteaUrl string, giteaClient *gitea.Client) GiteaClient { + return GiteaClient{ + getRepository: func(username, repositoryName string) (Repository, error) { + repo, _, err := giteaClient.GetRepo(username, repositoryName) + if err != nil { + return Repository{}, err + } + + return Repository{ + Name: repo.Name, + }, nil + }, + hasBranch: func(username, repositoryName, branchName string) bool { + res, _, err := giteaClient.ListRepoBranches(username, repositoryName, gitea.ListRepoBranchesOptions{}) + if err != nil { + return false + } + + for _, branch := range res { + if branch.Name == branchName { + return true + } + } + return false + }, + hasUser: func(username string) bool { + _, _, err := giteaClient.GetUserInfo(username) + return err == nil + }, + GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) { + // We have to do the raw request manually because the Gitea SDK does not allow + // passing the If-Modfied-Since header. + apiUrl := fmt.Sprintf( + "%s/api/v1/repos/%s/%s/raw/%s?ref=%s", + giteaUrl, + username, + repositoryName, + path, + branch, + ) + client := &http.Client{} + req, err := http.NewRequest("GET", apiUrl, nil) + if since != nil { + sinceFormat := since.Format(time.RFC1123) + req.Header.Add("If-Modified-Since", sinceFormat) + } + resp, err := client.Do(req) + if err != nil { + return []byte{}, true, err + } + defer resp.Body.Close() + content, err := io.ReadAll(resp.Body) + + if err != nil { + return []byte{}, true, err + } else if resp.StatusCode == 302 { + return []byte{}, false, nil + } else { + return content, true, err + } + }, + lookupCNAME: func(domain string) (string, error) { + return dns.LookupCNAME(domain) + }, + lookupRepoTXT: func(domain string) (string, error) { + return dns.LookupRepoTXT(domain) + }, + } +} diff --git a/internal/repo/repo.go b/internal/repo/repo.go index ac7dc37..902c61e 100644 --- a/internal/repo/repo.go +++ b/internal/repo/repo.go @@ -1,14 +1,14 @@ package repo +//go:generate mockgen -destination mock_repo_test.go -package repo code.gitea.io/sdk/gitea Client + import ( "errors" "strings" "time" - "git.polynom.me/rio/internal/dns" - "git.polynom.me/rio/internal/pages" + "git.polynom.me/rio/internal/constants" - "code.gitea.io/sdk/gitea" "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" ) @@ -21,7 +21,7 @@ var ( ) type PageCacheEntry struct { - Repository *gitea.Repository + Repository Repository Path string } @@ -33,24 +33,36 @@ func makePageCacheKey(domain, path string) string { // / is not "", then it also verifies that the repository contains a "CNAME" with // / the value of @cname as its content. @host, @domain, and @path are passed for // / caching on success. -func lookupRepositoryAndCache(username, reponame, host, domain, path, cname string, giteaClient *gitea.Client) (*gitea.Repository, error) { +func lookupRepositoryAndCache(username, reponame, branchName, host, domain, path, cname string, giteaClient *GiteaClient) (*Repository, error) { + log.Debugf("CNAME: %s", cname) log.Debugf("Looking up repository %s/%s", username, reponame) - repo, _, err := giteaClient.GetRepo(username, reponame) + repo, err := giteaClient.getRepository(username, reponame) if err != nil { return nil, err } + if !giteaClient.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, - repo.Name, - pages.PagesBranch, + reponame, + constants.PagesBranch, "CNAME", - false, + nil, ) if err != nil { - log.Errorf("Could not verify CNAME of %s/%s: %v\n", username, repo.Name, err) + log.Errorf( + "Could not verify CNAME of %s/%s@%s: %v\n", + username, + reponame, + constants.PagesBranch, + err, + ) return nil, err } @@ -58,6 +70,8 @@ func lookupRepositoryAndCache(username, reponame, host, domain, path, cname stri string(file[:]), "\n", ) + + log.Debugf("CNAME Content: %s", cnameContent) if cnameContent != cname { return nil, errors.New("CNAME mismatch") } @@ -72,10 +86,10 @@ func lookupRepositoryAndCache(username, reponame, host, domain, path, cname stri }, cache.DefaultExpiration, ) - return repo, nil + return &repo, nil } -func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) (*gitea.Repository, string, error) { +func RepoFromPath(username, host, cname, path string, giteaClient *GiteaClient) (*Repository, string, error) { domain := host // Guess the repository @@ -83,16 +97,31 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) entry, found := pathCache.Get(key) if found { pageEntry := entry.(PageCacheEntry) - return pageEntry.Repository, pageEntry.Path, nil + return &pageEntry.Repository, pageEntry.Path, nil + } + + // Allow specifying the repository name in the TXT record + reponame := "" + if cname != "" { + repoLookup, err := giteaClient.lookupRepoTXT(cname) + if err == nil && repoLookup != "" { + log.Infof( + "TXT lookup for %s resulted in choosing repository %s", + cname, + repoLookup, + ) + reponame = repoLookup + } } pathParts := strings.Split(path, "/") - if len(pathParts) > 1 { + 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, @@ -104,29 +133,14 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) } } - // Allow specifying the repository name in the TXT record - reponame := domain - lookupDomain := domain - if cname != "" { - lookupDomain = cname + if reponame == "" { + reponame = domain } - repoLookup, err := dns.LookupRepoTXT(lookupDomain) - if err != nil && repoLookup != "" { - log.Infof( - "TXT lookup for %s resulted in choosing repository %s", - lookupDomain, - repoLookup, - ) - reponame = repoLookup - } else if cname != "" { - // Allow naming the repository "example.org" (But give the TXT record preference) - reponame = cname - } - log.Debugf("Trying repository %s/%s", username, reponame) repo, err := lookupRepositoryAndCache( username, reponame, + constants.PagesBranch, host, domain, path, @@ -139,15 +153,14 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) // 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 { +func CanRequestCertificate(username string, giteaClient *GiteaClient) bool { if _, found := userCache.Get(username); found { return true } - user, _, err := giteaClient.GetUserInfo(username) - if user != nil && err == nil { + hasUser := giteaClient.hasUser(username) + if hasUser { userCache.Set(username, true, cache.DefaultExpiration) - return true } - return false + return hasUser } diff --git a/internal/repo/repo_test.go b/internal/repo/repo_test.go index 80f02b6..c533d37 100644 --- a/internal/repo/repo_test.go +++ b/internal/repo/repo_test.go @@ -1,39 +1,373 @@ package repo import ( - "net/http" + "errors" "testing" "time" - "code.gitea.io/sdk/gitea" + log "github.com/sirupsen/logrus" ) -var ( - giteaClient, _ = gitea.NewClient( - "https://git.polynom.me", - gitea.SetHTTPClient(&http.Client{Timeout: 10 * time.Second}), - gitea.SetToken(""), - gitea.SetUserAgent("rio/testing"), - ) -) +func clearCache() { + pathCache.Flush() + userCache.Flush() +} -func TestCanRequestCertificatePositiveUser(t *testing.T) { - res := CanRequestCertificate("papatutuwawa", giteaClient) - if !res { - t.Fatalf("User papatutuwawa should be servable") +func TestPickingCorrectRepositoryDefault(t *testing.T) { + // Test that we default to the . repository, if we have only + // one path component. + defer clearCache() + + log.SetLevel(log.DebugLevel) + client := GiteaClient{ + getRepository: func(username, repositoryName string) (Repository, error) { + if username != "example-user" { + t.Fatalf("Called with unknown user %s", username) + } + if repositoryName != "example-user.pages.example.org" { + t.Fatalf("Called with unknown repository %s", repositoryName) + } + + return Repository{}, nil + }, + hasBranch: func(username, repositoryName, branchName string) bool { + if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" { + return true + } + + return false + }, + GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) { + t.Fatal("getFile called") + return []byte{}, true, nil + }, + lookupCNAME: func(domain string) (string, error) { + t.Fatal("lookupCNAME called") + return "", nil + }, + lookupRepoTXT: func(domain string) (string, error) { + t.Fatal("lookupRepoTXT called") + return "", nil + }, + } + + res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "index.html", &client) + if err != nil { + t.Fatalf("An error occured: %v", err) + } + if res == nil { + t.Fatal("Result is nil") + } + if path != "index.html" { + t.Fatalf("Returned path is invalid: %s", path) } } -func TestCanRequestCertificatePositiveOrganisation(t *testing.T) { - res := CanRequestCertificate("moxxy", giteaClient) - if !res { - t.Fatalf("Organisation moxxy should be servable") +func TestPickingCorrectRepositoryDefaultSubdirectory(t *testing.T) { + // Test that we return the default repository when the first path component does + // not correspong to an existing repository. + defer clearCache() + + log.SetLevel(log.DebugLevel) + client := GiteaClient{ + getRepository: func(username, repositoryName string) (Repository, error) { + if username != "example-user" { + t.Fatalf("Called with unknown user %s", username) + } + if repositoryName == "assets" { + return Repository{}, errors.New("Repository does not exist") + } else if repositoryName == "example-user.pages.example.org" { + return Repository{}, nil + } else { + t.Fatalf("Called with unknown repository %s", repositoryName) + return Repository{}, nil + } + }, + hasBranch: func(username, repositoryName, branchName string) bool { + if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" { + return true + } + + return false + }, + GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) { + t.Fatal("getFile called") + return []byte{}, true, nil + }, + lookupCNAME: func(domain string) (string, error) { + t.Fatal("lookupCNAME called") + return "", nil + }, + lookupRepoTXT: func(domain string) (string, error) { + t.Fatal("lookupRepoTXT called") + return "", nil + }, + } + + res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "assets/index.css", &client) + if err != nil { + t.Fatalf("An error occured: %v", err) + } + if res == nil { + t.Fatal("Result is nil") + } + if path != "assets/index.css" { + t.Fatalf("Returned path is invalid: %s", path) } } -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") +func TestPickingCorrectRepositorySubdirectoryNoPagesBranch(t *testing.T) { + // Test that we're picking the correct repository when the first path component + // returns a repository without a pages branch. + defer clearCache() + + log.SetLevel(log.DebugLevel) + client := GiteaClient{ + getRepository: func(username, repositoryName string) (Repository, error) { + if username != "example-user" { + t.Fatalf("Called with unknown user %s", username) + } + if repositoryName == "blog" { + return Repository{ + Name: "blog", + }, nil + } else if repositoryName == "example-user.pages.example.org" { + return Repository{ + Name: "example-user.pages.example.org", + }, nil + } else { + t.Fatalf("Called with unknown repository %s", repositoryName) + return Repository{}, nil + } + }, + hasBranch: func(username, repositoryName, branchName string) bool { + if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" { + return true + } + + return false + }, + GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) { + t.Fatal("getFile called") + return []byte{}, true, nil + }, + lookupCNAME: func(domain string) (string, error) { + t.Fatal("lookupCNAME called") + return "", nil + }, + lookupRepoTXT: func(domain string) (string, error) { + t.Fatal("lookupRepoTXT called") + return "", nil + }, + } + + res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "blog/post1.html", &client) + if err != nil { + t.Fatalf("An error occured: %v", err) + } + if res == nil { + t.Fatal("Result is nil") + } + if res.Name != "example-user.pages.example.org" { + t.Fatalf("Invalid repository selected: %s", res.Name) + } + if path != "blog/post1.html" { + t.Fatalf("Returned path is invalid: %s", path) + } +} + +func TestPickingNoRepositoryInvalidCNAME(t *testing.T) { + // Test that we're not picking a repository if the CNAME validation fails. + defer clearCache() + + log.SetLevel(log.DebugLevel) + client := GiteaClient{ + getRepository: func(username, repositoryName string) (Repository, error) { + if username == "example-user" && repositoryName == "example-user.pages.example.org" { + return Repository{ + Name: "example-user.pages.example.org", + }, nil + } else { + t.Fatalf("Called with unknown repository %s", repositoryName) + return Repository{}, nil + } + }, + hasBranch: func(username, repositoryName, branchName string) bool { + if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" { + return true + } + + return false + }, + GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) { + if username == "example-user" && repositoryName == "example-user.pages.example.org" && branch == "pages" && path == "CNAME" { + return []byte("some-other-domain.local"), true, nil + } + + t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path) + return []byte{}, true, nil + }, + lookupCNAME: func(domain string) (string, error) { + return "", errors.New("No CNAME") + }, + lookupRepoTXT: func(domain string) (string, error) { + return "", nil + }, + } + + _, _, err := RepoFromPath("example-user", "example-user.pages.example.org", "example-user.local", "index.html", &client) + if err == nil { + t.Fatal("Repository returned even though CNAME validation should fail") + } +} + +func TestPickingRepositoryValidCNAME(t *testing.T) { + // Test that we're picking a repository, given a CNAME, if the CNAME validation succeeds. + defer clearCache() + + log.SetLevel(log.DebugLevel) + client := GiteaClient{ + getRepository: func(username, repositoryName string) (Repository, error) { + if username == "example-user" && repositoryName == "example-user.pages.example.org" { + return Repository{ + Name: "example-user.pages.example.org", + }, nil + } else { + t.Fatalf("Called with unknown repository %s", repositoryName) + return Repository{}, nil + } + }, + hasBranch: func(username, repositoryName, branchName string) bool { + if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" { + return true + } + + return false + }, + GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) { + if username == "example-user" && repositoryName == "example-user.pages.example.org" && branch == "pages" && path == "CNAME" { + return []byte("example-user.local"), true, nil + } + + t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path) + return []byte{}, true, nil + }, + lookupCNAME: func(domain string) (string, error) { + return "", errors.New("No CNAME") + }, + lookupRepoTXT: func(domain string) (string, error) { + return "", nil + }, + } + + repo, _, err := RepoFromPath("example-user", "example-user.pages.example.org", "example-user.local", "index.html", &client) + if err != nil { + t.Fatalf("Error returned: %v", err) + } + if repo.Name != "example-user.pages.example.org" { + t.Fatalf("Invalid repository name returned: %s", repo.Name) + } +} + +func TestPickingRepositoryValidCNAMEWithTXTLookup(t *testing.T) { + // Test that we're picking a repository, given a CNAME, if the CNAME validation succeeds + // and the TXT lookup returns something different. + defer clearCache() + + log.SetLevel(log.DebugLevel) + client := GiteaClient{ + getRepository: func(username, repositoryName string) (Repository, error) { + if username == "example-user" && repositoryName == "some-different-repository" { + return Repository{ + Name: "some-different-repository", + }, nil + } else { + t.Fatalf("Called with unknown repository %s", repositoryName) + return Repository{}, nil + } + }, + hasBranch: func(username, repositoryName, branchName string) bool { + if username == "example-user" && repositoryName == "some-different-repository" && branchName == "pages" { + return true + } + + return false + }, + GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) { + if username == "example-user" && repositoryName == "some-different-repository" && branch == "pages" && path == "CNAME" { + return []byte("example-user.local"), true, nil + } + + t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path) + return []byte{}, true, nil + }, + lookupCNAME: func(domain string) (string, error) { + return "", errors.New("No CNAME") + }, + lookupRepoTXT: func(domain string) (string, error) { + if domain == "example-user.local" { + return "some-different-repository", nil + } + return "", nil + }, + } + + repo, _, err := RepoFromPath("example-user", "example-user.pages.example.org", "example-user.local", "index.html", &client) + if err != nil { + t.Fatalf("Error returned: %v", err) + } + if repo.Name != "some-different-repository" { + t.Fatalf("Invalid repository name returned: %s", repo.Name) + } +} + +func TestPickingRepositoryValidCNAMEWithTXTLookupAndSubdirectory(t *testing.T) { + // Test that we're picking a repository, given a CNAME, if the CNAME validation succeeds + // and the TXT lookup returns something different. Additionally, we now have a subdirectory + defer clearCache() + + log.SetLevel(log.DebugLevel) + client := GiteaClient{ + getRepository: func(username, repositoryName string) (Repository, error) { + if username == "example-user" && repositoryName == "some-different-repository" { + return Repository{ + Name: "some-different-repository", + }, nil + } + + return Repository{}, errors.New("Unknown repository") + }, + hasBranch: func(username, repositoryName, branchName string) bool { + if username == "example-user" && repositoryName == "some-different-repository" && branchName == "pages" { + return true + } + + return false + }, + GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) { + if username == "example-user" && repositoryName == "some-different-repository" && branch == "pages" && path == "CNAME" { + return []byte("example-user.local"), true, nil + } + + t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path) + return []byte{}, true, nil + }, + lookupCNAME: func(domain string) (string, error) { + return "", errors.New("No CNAME") + }, + lookupRepoTXT: func(domain string) (string, error) { + if domain == "example-user.local" { + return "some-different-repository", nil + } + return "", nil + }, + } + + repo, _, err := RepoFromPath("example-user", "example-user.pages.example.org", "example-user.local", "blog/index.html", &client) + if err != nil { + t.Fatalf("Error returned: %v", err) + } + if repo.Name != "some-different-repository" { + t.Fatalf("Invalid repository name returned: %s", repo.Name) } } diff --git a/internal/server/tls.go b/internal/server/tls.go index 67298a6..3080794 100644 --- a/internal/server/tls.go +++ b/internal/server/tls.go @@ -9,7 +9,6 @@ import ( "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" @@ -62,7 +61,7 @@ func getDomainKey(domain, pagesDomain string) string { return domain } -func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client, giteaClient *gitea.Client) *tls.Config { +func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client, giteaClient *repo.GiteaClient) *tls.Config { return &tls.Config{ GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { // Validate that we should even care about this domain