[mirror] Scalable static site server for Git forges (like GitHub Pages)

[breaking-change] Only allow a single `[[wildcard]].index-repo`.

The git-pages webhook security model depends on there being
a 1:1 mapping between site URLs and repositories; being able to
specify multiple of them breaks this model, as anyone could switch
the published site from one to the other if both repositories exist.

+34 -46
+1 -1
conf/config.example.toml
··· 12 [[wildcard]] # non-default section 13 domain = "codeberg.page" 14 clone-url = "https://codeberg.org/<user>/<project>.git" 15 - index-repos = ["<user>.codeberg.page", "pages"] 16 index-repo-branch = "main" 17 authorization = "forgejo" 18
··· 12 [[wildcard]] # non-default section 13 domain = "codeberg.page" 14 clone-url = "https://codeberg.org/<user>/<project>.git" 15 + index-repo = "pages" 16 index-repo-branch = "main" 17 authorization = "forgejo" 18
+14 -19
src/auth.go
··· 265 } 266 267 if userName, found := pattern.Matches(host); found { 268 - repoURLs, branch := pattern.ApplyTemplate(userName, projectName) 269 - return &Authorization{repoURLs, branch}, nil 270 } else { 271 return nil, AuthError{ 272 http.StatusUnauthorized, ··· 632 } 633 634 if userName, found := pattern.Matches(host); found { 635 - repoURLs, branch := pattern.ApplyTemplate(userName, projectName) 636 - for _, repoURL := range repoURLs { 637 - parsedRepoURL, err := url.Parse(repoURL) 638 - if err != nil { 639 - panic(err) // misconfiguration 640 - } 641 642 - if err = checkGogsRepositoryPushPermission(parsedRepoURL, authorization); err != nil { 643 - errs = append(errs, err) 644 - continue 645 - } 646 647 - // This will actually be ignored by the caller of AuthorizeUpdateFromArchive, 648 - // but we return this information as it makes sense to do contextually here. 649 - return &Authorization{ 650 - []string{repoURL}, 651 - branch, 652 - }, nil 653 - } 654 } 655 } 656
··· 265 } 266 267 if userName, found := pattern.Matches(host); found { 268 + repoURL, branch := pattern.ApplyTemplate(userName, projectName) 269 + return &Authorization{[]string{repoURL}, branch}, nil 270 } else { 271 return nil, AuthError{ 272 http.StatusUnauthorized, ··· 632 } 633 634 if userName, found := pattern.Matches(host); found { 635 + repoURL, branch := pattern.ApplyTemplate(userName, projectName) 636 + parsedRepoURL, err := url.Parse(repoURL) 637 + if err != nil { 638 + panic(err) // misconfiguration 639 + } 640 641 + if err = checkGogsRepositoryPushPermission(parsedRepoURL, authorization); err != nil { 642 + errs = append(errs, err) 643 + continue 644 + } 645 646 + // This will actually be ignored by the caller of AuthorizeUpdateFromArchive, 647 + // but we return this information as it makes sense to do contextually here. 648 + return &Authorization{[]string{repoURL}, branch}, nil 649 } 650 } 651
+5 -5
src/config.go
··· 79 } 80 81 type WildcardConfig struct { 82 - Domain string `toml:"domain"` 83 - CloneURL string `toml:"clone-url"` // URL template, not an exact URL 84 - IndexRepos []string `toml:"index-repos" default:"[]"` 85 - IndexRepoBranch string `toml:"index-repo-branch" default:"pages"` 86 - Authorization string `toml:"authorization"` 87 } 88 89 type FallbackConfig struct {
··· 79 } 80 81 type WildcardConfig struct { 82 + Domain string `toml:"domain"` 83 + CloneURL string `toml:"clone-url"` // URL template, not an exact URL 84 + IndexRepo string `toml:"index-repo" default:"pages"` 85 + IndexRepoBranch string `toml:"index-repo-branch" default:"pages"` 86 + Authorization string `toml:"authorization"` 87 } 88 89 type FallbackConfig struct {
+14 -21
src/wildcard.go
··· 11 type WildcardPattern struct { 12 Domain []string 13 CloneURL *fasttemplate.Template 14 - IndexRepos []*fasttemplate.Template 15 IndexBranch string 16 Authorization bool 17 } ··· 49 return subdomain, true 50 } 51 52 - func (pattern *WildcardPattern) ApplyTemplate(userName string, projectName string) ([]string, string) { 53 - var repoURLs []string 54 var branch string 55 repoURLTemplate := pattern.CloneURL 56 if projectName == ".index" { 57 - for _, indexRepoTemplate := range pattern.IndexRepos { 58 - indexRepo := indexRepoTemplate.ExecuteString(map[string]any{"user": userName}) 59 - repoURLs = append(repoURLs, repoURLTemplate.ExecuteString(map[string]any{ 60 - "user": userName, 61 - "project": indexRepo, 62 - })) 63 - } 64 branch = pattern.IndexBranch 65 } else { 66 - repoURLs = append(repoURLs, repoURLTemplate.ExecuteString(map[string]any{ 67 "user": userName, 68 "project": projectName, 69 - })) 70 branch = "pages" 71 } 72 - return repoURLs, branch 73 } 74 75 func TranslateWildcards(configs []WildcardConfig) ([]*WildcardPattern, error) { ··· 80 return nil, fmt.Errorf("wildcard pattern: clone URL: %w", err) 81 } 82 83 - var indexRepoTemplates []*fasttemplate.Template 84 var indexRepoBranch string = config.IndexRepoBranch 85 - for _, indexRepo := range config.IndexRepos { 86 - indexRepoTemplate, err := fasttemplate.NewTemplate(indexRepo, "<", ">") 87 - if err != nil { 88 - return nil, fmt.Errorf("wildcard pattern: index repo: %w", err) 89 - } 90 - indexRepoTemplates = append(indexRepoTemplates, indexRepoTemplate) 91 } 92 93 authorization := false ··· 107 wildcardPatterns = append(wildcardPatterns, &WildcardPattern{ 108 Domain: strings.Split(config.Domain, "."), 109 CloneURL: cloneURLTemplate, 110 - IndexRepos: indexRepoTemplates, 111 IndexBranch: indexRepoBranch, 112 Authorization: authorization, 113 })
··· 11 type WildcardPattern struct { 12 Domain []string 13 CloneURL *fasttemplate.Template 14 + IndexRepo *fasttemplate.Template 15 IndexBranch string 16 Authorization bool 17 } ··· 49 return subdomain, true 50 } 51 52 + func (pattern *WildcardPattern) ApplyTemplate(userName string, projectName string) (string, string) { 53 + var repoURL string 54 var branch string 55 repoURLTemplate := pattern.CloneURL 56 if projectName == ".index" { 57 + repoURL = repoURLTemplate.ExecuteString(map[string]any{ 58 + "user": userName, 59 + "project": pattern.IndexRepo.ExecuteString(map[string]any{"user": userName}), 60 + }) 61 branch = pattern.IndexBranch 62 } else { 63 + repoURL = repoURLTemplate.ExecuteString(map[string]any{ 64 "user": userName, 65 "project": projectName, 66 + }) 67 branch = "pages" 68 } 69 + return repoURL, branch 70 } 71 72 func TranslateWildcards(configs []WildcardConfig) ([]*WildcardPattern, error) { ··· 77 return nil, fmt.Errorf("wildcard pattern: clone URL: %w", err) 78 } 79 80 var indexRepoBranch string = config.IndexRepoBranch 81 + indexRepoTemplate, err := fasttemplate.NewTemplate(config.IndexRepo, "<", ">") 82 + if err != nil { 83 + return nil, fmt.Errorf("wildcard pattern: index repo: %w", err) 84 } 85 86 authorization := false ··· 100 wildcardPatterns = append(wildcardPatterns, &WildcardPattern{ 101 Domain: strings.Split(config.Domain, "."), 102 CloneURL: cloneURLTemplate, 103 + IndexRepo: indexRepoTemplate, 104 IndexBranch: indexRepoBranch, 105 Authorization: authorization, 106 })