From d5302e4358c18adc879b7e9b1246cd4b6258462e Mon Sep 17 00:00:00 2001 From: Catherine Date: Sun, 21 Sep 2025 00:29:33 +0000 Subject: [PATCH] [breaking-change] Allow multiple wildcard domains to be configured. --- README.md | 8 +++--- conf/config.toml.example | 2 +- src/auth.go | 54 +++++++++++++++++++++------------------- src/config.go | 40 ++++++++++++++++++----------- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 74d1b02..02ecb0c 100644 --- a/README.md +++ b/README.md @@ -86,16 +86,16 @@ The authorization flow for content updates (`PUT`, `DELETE`, `POST` requests) pr - **`Pages` scheme:** Request includes an `Authorization: Pages ` header. - **`Basic` scheme:** Request includes an `Authorization: Basic ` header, where `` is equal to `Base64("Pages:")`. (Useful for non-Forgejo forges.) 3. **DNS Allowlist:** If the method is `PUT` or `POST`, and a TXT record lookup at `_git-pages-repository.` returns a set of well-formed absolute URLs, and (for `PUT` requests) the body contains a repository URL, and the requested clone URLs is contained in this set of URLs, the request is authorized. -4. **Wildcard Match (Site):** If the method is `POST`, and a `[wildcard]` configuration section is present, and the suffix of a hostname (compared label-wise) is equal to `[wildcard].domain`, and (for `PUT` requests) the body contains a repository URL, and the requested clone URL is a *matching* clone URL, the request is authorized. - - **Index repository:** If the request URL is `scheme://./`, a *matching* clone URL is computed by templating `[wildcard.clone-url]` with `` and ``, where `` is computed by templating each element of `[wildcard].index-repos` with ``. - - **Project repository:** If the request URL is `scheme://.//`, a *matching* clone URL is computed by templating `[wildcard.clone-url]` with `` and ``. +4. **Wildcard Match (Site):** If the method is `POST`, and a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, and (for `PUT` requests) the body contains a repository URL, and the requested clone URL is a *matching* clone URL, the request is authorized. + - **Index repository:** If the request URL is `scheme://./`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `` and ``, where `` is computed by templating each element of `[[wildcard]].index-repos` with ``, and `[[wildcard]]` is the section where the match occurred. + - **Project repository:** If the request URL is `scheme://.//`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `` and ``, and `[[wildcard]]` is the section where the match occurred. 5. **Default Deny:** Otherwise, the request is not authorized. The authorization flow for metadata retrieval (`GET` requests with site paths starting with `.git-pages/`) in the following order, with the first of multiple applicable rule taking precedence: 1. **Development Mode:** Same as for content updates. 2. **DNS Challenge:** Same as for content updates. -3. **Wildcard Match (Domain):** If a `[wildcard]` configuration section is present, and the suffix of a hostname (compared label-wise) is equal to `[wildcard].domain`, the request is authorized. +3. **Wildcard Match (Domain):** If a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, the request is authorized. 4. **Default Deny:** Otherwise, the request is not authorized. diff --git a/conf/config.toml.example b/conf/config.toml.example index f1928c9..36a29a1 100644 --- a/conf/config.toml.example +++ b/conf/config.toml.example @@ -3,7 +3,7 @@ pages = "tcp/:3000" caddy = "tcp/:3001" health = "tcp/:3002" -# [wildcard] +# [[wildcard]] # domain = "codeberg.page" # clone-url = "https://codeberg.org//.git" # index-repos = [".codeberg.page", "pages"] diff --git a/src/auth.go b/src/auth.go index d37a872..8e5b99d 100644 --- a/src/auth.go +++ b/src/auth.go @@ -165,24 +165,24 @@ func authorizeDNSAllowlist(r *http.Request) (*Authorization, error) { return &Authorization{repoURLs}, err } -func authorizeWildcardMatchHost(r *http.Request) (*Authorization, error) { +func authorizeWildcardMatchHost(r *http.Request, pattern *WildcardPattern) (*Authorization, error) { host, err := GetHost(r) if err != nil { return nil, err } hostParts := strings.Split(host, ".") - if slices.Equal(hostParts[1:], wildcardPattern.Domain) { + if slices.Equal(hostParts[1:], pattern.Domain) { return &Authorization{}, nil } else { return nil, AuthError{ http.StatusUnauthorized, - fmt.Sprintf("domain %s does not match wildcard *.%s", host, config.Wildcard.Domain), + fmt.Sprintf("domain %s does not match wildcard %s", host, pattern.GetHost()), } } } -func authorizeWildcardMatchSite(r *http.Request) (*Authorization, error) { +func authorizeWildcardMatchSite(r *http.Request, pattern *WildcardPattern) (*Authorization, error) { host, err := GetHost(r) if err != nil { return nil, err @@ -194,12 +194,12 @@ func authorizeWildcardMatchSite(r *http.Request) (*Authorization, error) { } hostParts := strings.Split(host, ".") - if slices.Equal(hostParts[1:], wildcardPattern.Domain) { + if slices.Equal(hostParts[1:], pattern.Domain) { userName := hostParts[0] var repoURLs []string - repoURLTemplate := wildcardPattern.CloneURL + repoURLTemplate := pattern.CloneURL if projectName == ".index" { - for _, indexRepoTemplate := range wildcardPattern.IndexRepos { + for _, indexRepoTemplate := range pattern.IndexRepos { indexRepo := indexRepoTemplate.ExecuteString(map[string]any{"user": userName}) repoURLs = append(repoURLs, repoURLTemplate.ExecuteString(map[string]interface{}{ "user": userName, @@ -216,7 +216,7 @@ func authorizeWildcardMatchSite(r *http.Request) (*Authorization, error) { } else { return nil, AuthError{ http.StatusUnauthorized, - fmt.Sprintf("domain %s does not match wildcard *.%s", host, config.Wildcard.Domain), + fmt.Sprintf("domain %s does not match wildcard %s", host, pattern.GetHost()), } } } @@ -239,15 +239,16 @@ func AuthorizeMetadataRetrieval(r *http.Request) (*Authorization, error) { return auth, nil } - auth, err = authorizeWildcardMatchHost(r) - if err != nil && IsUnauthorized(err) { - causes = append(causes, err) - } else if err != nil { // bad request - return nil, err - } else { - log.Printf("auth: wildcard *.%s\n", - config.Wildcard.Domain) - return auth, nil + for _, pattern := range wildcardPatterns { + auth, err = authorizeWildcardMatchHost(r, pattern) + if err != nil && IsUnauthorized(err) { + causes = append(causes, err) + } else if err != nil { // bad request + return nil, err + } else { + log.Printf("auth: wildcard %s\n", pattern.GetHost()) + return auth, nil + } } return nil, errors.Join(causes...) @@ -290,15 +291,16 @@ func AuthorizeUpdateFromRepository(r *http.Request) (*Authorization, error) { // Wildcard match is only available for webhooks, not the REST API. if r.Method == http.MethodPost { - auth, err = authorizeWildcardMatchSite(r) - if err != nil && IsUnauthorized(err) { - causes = append(causes, err) - } else if err != nil { // bad request - return nil, err - } else { - log.Printf("auth: wildcard *.%s: allow %v\n", - config.Wildcard.Domain, auth.repoURLs) - return auth, nil + for _, pattern := range wildcardPatterns { + auth, err = authorizeWildcardMatchSite(r, pattern) + if err != nil && IsUnauthorized(err) { + causes = append(causes, err) + } else if err != nil { // bad request + return nil, err + } else { + log.Printf("auth: wildcard %s: allow %v\n", pattern.GetHost(), auth.repoURLs) + return auth, nil + } } } diff --git a/src/config.go b/src/config.go index e54e265..5819d79 100644 --- a/src/config.go +++ b/src/config.go @@ -21,7 +21,7 @@ type Config struct { Caddy string `toml:"caddy"` Health string `toml:"health"` } `toml:"listen"` - Wildcard struct { + Wildcard []struct { Domain string `toml:"domain"` CloneURL string `toml:"clone-url"` IndexRepos []string `toml:"index-repos"` @@ -107,26 +107,36 @@ type WildcardPattern struct { IndexRepos []*fasttemplate.Template } -var wildcardPattern WildcardPattern +func (pattern *WildcardPattern) GetHost() string { + parts := []string{"*"} + parts = append(parts, pattern.Domain...) + return strings.Join(parts, ".") +} + +var wildcardPatterns []*WildcardPattern func CompileWildcardPattern() { - wildcardPattern = WildcardPattern{ - Domain: strings.Split(config.Wildcard.Domain, "."), - } + for _, configWildcard := range config.Wildcard { + wildcardPattern := WildcardPattern{ + Domain: strings.Split(configWildcard.Domain, "."), + } - template, err := fasttemplate.NewTemplate(config.Wildcard.CloneURL, "<", ">") - if err != nil { - log.Fatalf("wildcard pattern: clone URL: %s", err) - } else { - wildcardPattern.CloneURL = template - } - - for _, indexRepo := range config.Wildcard.IndexRepos { - template, err := fasttemplate.NewTemplate(indexRepo, "<", ">") + template, err := fasttemplate.NewTemplate(configWildcard.CloneURL, "<", ">") if err != nil { log.Fatalf("wildcard pattern: clone URL: %s", err) } else { - wildcardPattern.IndexRepos = append(wildcardPattern.IndexRepos, template) + wildcardPattern.CloneURL = template } + + for _, indexRepo := range configWildcard.IndexRepos { + template, err := fasttemplate.NewTemplate(indexRepo, "<", ">") + if err != nil { + log.Fatalf("wildcard pattern: clone URL: %s", err) + } else { + wildcardPattern.IndexRepos = append(wildcardPattern.IndexRepos, template) + } + } + + wildcardPatterns = append(wildcardPatterns, &wildcardPattern) } }