diff --git a/README.md b/README.md index b7c9be8..4efe4f5 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ Authorization DNS is used for authorization of content updates, either via TXT records or by pattern matching. The authorization flow proceeds sequentially in the following order, with the first of multiple applicable rule taking precedence: 1. **Development Mode:** If the environment variable `INSECURE` is set to the value `very`, the request is authorized to update from any clone URL. -2. **DNS Challenge:** If the method is `PUT` or `POST`, and a well-formed `Authorization:` header is provided containing a ``, and a TXT record lookup at `_git-pages-challenge.` returns a record whose concatenated value equals `SHA256(" ")`, the request is authorized to update from any clone URL. +2. **DNS Challenge:** If the method is `PUT` or `POST`, and a well-formed `Authorization:` header is provided containing a ``, and a TXT record lookup at `_git-pages-challenge.` returns a record whose concatenated value equals `SHA256(" ")`, the request is authorized to update from any clone URL. - **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, the request is authorized to update from clone URLs in the set. +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, the request is authorized to update from clone URLs in the set. 4. **Wildcard Match:** 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`, the request is authorized to update from a *matching* clone URL. - - **Index repository:** If the request URL is `scheme:///`, a *matching* clone URL is computed as `sprintf([wildcard].clone-url, , [wildcard].index-repo)`. - - **Project repository:** If the request URL is `scheme:////`, a *matching* clone URL is computed as `sprintf([wildcard].clone-url, , )`. + - **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 ``. 5. **Default Deny:** Otherwise, the request is not authorized. diff --git a/config.toml.example b/config.toml.example index 59839fe..f1928c9 100644 --- a/config.toml.example +++ b/config.toml.example @@ -5,8 +5,8 @@ health = "tcp/:3002" # [wildcard] # domain = "codeberg.page" -# clone-url = "https://codeberg.org/%s/%s.git" -# index-repo = "%s.codeberg.page" +# clone-url = "https://codeberg.org//.git" +# index-repos = [".codeberg.page", "pages"] [backend] type = "fs" diff --git a/go.mod b/go.mod index 0423a5c..96a3044 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/maypok86/otter/v2 v2.2.1 github.com/minio/minio-go/v7 v7.0.95 github.com/pelletier/go-toml/v2 v2.2.4 + github.com/valyala/fasttemplate v1.2.2 google.golang.org/protobuf v1.36.9 ) @@ -36,6 +37,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/tinylib/msgp v1.3.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect golang.org/x/net v0.43.0 // indirect diff --git a/go.sum b/go.sum index 887cb83..b9c023f 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= diff --git a/src/auth.go b/src/auth.go index 0ba49e8..7a910fd 100644 --- a/src/auth.go +++ b/src/auth.go @@ -147,18 +147,30 @@ func authorizeWildcardMatch(r *http.Request) ([]string, error) { return nil, err } - if slices.Equal(hostParts[1:], strings.Split(config.Wildcard.Domain, ".")) { + if slices.Equal(hostParts[1:], wildcardPattern.Domain) { userName := hostParts[0] - repoName := projectName - if repoName == ".index" { - repoName = fmt.Sprintf(config.Wildcard.IndexRepo, userName) + var repoURLs []string + repoURLTemplate := wildcardPattern.CloneURL + if projectName == ".index" { + for _, indexRepoTemplate := range wildcardPattern.IndexRepos { + indexRepo := indexRepoTemplate.ExecuteString(map[string]any{"user": userName}) + repoURLs = append(repoURLs, repoURLTemplate.ExecuteString(map[string]interface{}{ + "user": userName, + "project": indexRepo, + })) + } + } else { + repoURLs = append(repoURLs, repoURLTemplate.ExecuteString(map[string]interface{}{ + "user": userName, + "project": projectName, + })) + } + return repoURLs, nil + } else { + return nil, AuthError{ + http.StatusUnauthorized, + fmt.Sprintf("domain %s does not match wildcard *.%s", host, config.Wildcard.Domain), } - return []string{fmt.Sprintf(config.Wildcard.CloneURL, userName, repoName)}, nil - } - - return nil, AuthError{ - http.StatusUnauthorized, - fmt.Sprintf("domain %s does not match wildcard *.%s", host, config.Wildcard.Domain), } } diff --git a/src/config.go b/src/config.go index 9bd050b..ff6439b 100644 --- a/src/config.go +++ b/src/config.go @@ -1,9 +1,12 @@ package main import ( + "log" "os" + "strings" "github.com/pelletier/go-toml/v2" + "github.com/valyala/fasttemplate" ) type CacheConfig struct { @@ -19,9 +22,9 @@ type Config struct { Health string `toml:"health"` } `toml:"listen"` Wildcard struct { - Domain string `toml:"domain"` - CloneURL string `toml:"clone-url"` - IndexRepo string `toml:"index-repo"` + Domain string `toml:"domain"` + CloneURL string `toml:"clone-url"` + IndexRepos []string `toml:"index-repos"` } `toml:"wildcard"` Backend struct { Type string `toml:"type"` @@ -41,7 +44,14 @@ type Config struct { } `toml:"backend"` } +type WildcardPattern struct { + Domain []string + CloneURL *fasttemplate.Template + IndexRepos []*fasttemplate.Template +} + var config Config +var wildcardPattern WildcardPattern func ReadConfig(path string) error { file, err := os.Open(path) @@ -70,3 +80,25 @@ func UpdateConfigEnv() { updateFromEnv(&config.Backend.S3.Region, "S3_REGION") updateFromEnv(&config.Backend.S3.Bucket, "S3_BUCKET") } + +func CompileWildcardPattern() { + wildcardPattern = WildcardPattern{ + Domain: strings.Split(config.Wildcard.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, "<", ">") + if err != nil { + log.Fatalf("wildcard pattern: clone URL: %s", err) + } else { + wildcardPattern.IndexRepos = append(wildcardPattern.IndexRepos, template) + } + } +} diff --git a/src/main.go b/src/main.go index 2d9bee0..c61b7d3 100644 --- a/src/main.go +++ b/src/main.go @@ -43,6 +43,7 @@ func main() { log.Fatalln("config:", err) } UpdateConfigEnv() // environment takes priority + CompileWildcardPattern() switch config.LogFormat { case "short":