[breaking-change] Allow multiple wildcard domains to be configured.

This commit is contained in:
Catherine
2025-09-21 00:29:33 +00:00
parent acf948ac6b
commit d5302e4358
4 changed files with 58 additions and 46 deletions

View File

@@ -86,16 +86,16 @@ The authorization flow for content updates (`PUT`, `DELETE`, `POST` requests) pr
- **`Pages` scheme:** Request includes an `Authorization: Pages <token>` header.
- **`Basic` scheme:** Request includes an `Authorization: Basic <basic>` header, where `<basic>` is equal to `Base64("Pages:<token>")`. (Useful for non-Forgejo forges.)
3. **DNS Allowlist:** If the method is `PUT` or `POST`, and a TXT record lookup at `_git-pages-repository.<host>` 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://<user>.<host>/`, a *matching* clone URL is computed by templating `[wildcard.clone-url]` with `<user>` and `<project>`, where `<project>` is computed by templating each element of `[wildcard].index-repos` with `<user>`.
- **Project repository:** If the request URL is `scheme://<user>.<host>/<project>/`, a *matching* clone URL is computed by templating `[wildcard.clone-url]` with `<user>` and `<project>`.
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://<user>.<host>/`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `<user>` and `<project>`, where `<project>` is computed by templating each element of `[[wildcard]].index-repos` with `<user>`, and `[[wildcard]]` is the section where the match occurred.
- **Project repository:** If the request URL is `scheme://<user>.<host>/<project>/`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `<user>` and `<project>`, 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.

View File

@@ -3,7 +3,7 @@ pages = "tcp/:3000"
caddy = "tcp/:3001"
health = "tcp/:3002"
# [wildcard]
# [[wildcard]]
# domain = "codeberg.page"
# clone-url = "https://codeberg.org/<user>/<project>.git"
# index-repos = ["<user>.codeberg.page", "pages"]

View File

@@ -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
}
}
}

View File

@@ -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)
}
}