[mirror] Scalable static site server for Git forges (like GitHub Pages)
1package git_pages
2
3import (
4 "crypto/tls"
5 "fmt"
6 "log"
7 "net/http"
8 "net/http/httputil"
9 "net/url"
10 "slices"
11 "strings"
12
13 "github.com/valyala/fasttemplate"
14)
15
16type WildcardPattern struct {
17 Domain []string
18 CloneURL *fasttemplate.Template
19 IndexRepos []*fasttemplate.Template
20 IndexBranch string
21 Authorization bool
22 FallbackURL *url.URL
23 Fallback http.Handler
24}
25
26func (pattern *WildcardPattern) GetHost() string {
27 parts := []string{"*"}
28 parts = append(parts, pattern.Domain...)
29 return strings.Join(parts, ".")
30}
31
32// Returns `subdomain, found` where if `found == true`, `subdomain` contains the part of `host`
33// corresponding to the * in the domain pattern.
34func (pattern *WildcardPattern) Matches(host string) (string, bool) {
35 hostParts := strings.Split(host, ".")
36 hostLen := len(hostParts)
37 patternLen := len(pattern.Domain)
38
39 // host must have at least one more part than the pattern domain
40 if hostLen <= patternLen {
41 return "", false
42 }
43
44 // break the host parts into <subdomain parts> and <domain parts>
45 mid := hostLen - patternLen
46 prefix := hostParts[:mid]
47 suffix := hostParts[mid:]
48
49 // check if the suffix matches the domain
50 if !slices.Equal(suffix, pattern.Domain) {
51 return "", false
52 }
53
54 // return all the subdomain parts
55 subdomain := strings.Join(prefix, ".")
56 return subdomain, true
57}
58
59func (pattern *WildcardPattern) ApplyTemplate(userName string, projectName string) ([]string, string) {
60 var repoURLs []string
61 var branch string
62 repoURLTemplate := pattern.CloneURL
63 if projectName == ".index" {
64 for _, indexRepoTemplate := range pattern.IndexRepos {
65 indexRepo := indexRepoTemplate.ExecuteString(map[string]any{"user": userName})
66 repoURLs = append(repoURLs, repoURLTemplate.ExecuteString(map[string]any{
67 "user": userName,
68 "project": indexRepo,
69 }))
70 }
71 branch = pattern.IndexBranch
72 } else {
73 repoURLs = append(repoURLs, repoURLTemplate.ExecuteString(map[string]any{
74 "user": userName,
75 "project": projectName,
76 }))
77 branch = "pages"
78 }
79 return repoURLs, branch
80}
81
82func (pattern *WildcardPattern) IsFallbackFor(host string) bool {
83 if pattern.Fallback == nil {
84 return false
85 }
86 _, found := pattern.Matches(host)
87 return found
88}
89
90func HandleWildcardFallback(w http.ResponseWriter, r *http.Request) (bool, error) {
91 host, err := GetHost(r)
92 if err != nil {
93 return false, err
94 }
95
96 for _, pattern := range wildcards {
97 if pattern.IsFallbackFor(host) {
98 log.Printf("proxy: %s via %s", pattern.GetHost(), pattern.FallbackURL)
99 pattern.Fallback.ServeHTTP(w, r)
100 return true, nil
101 }
102 }
103 return false, nil
104}
105
106func TranslateWildcards(configs []WildcardConfig) ([]*WildcardPattern, error) {
107 var wildcardPatterns []*WildcardPattern
108 for _, config := range configs {
109 cloneURLTemplate, err := fasttemplate.NewTemplate(config.CloneURL, "<", ">")
110 if err != nil {
111 return nil, fmt.Errorf("wildcard pattern: clone URL: %w", err)
112 }
113
114 var indexRepoTemplates []*fasttemplate.Template
115 var indexRepoBranch string = config.IndexRepoBranch
116 for _, indexRepo := range config.IndexRepos {
117 indexRepoTemplate, err := fasttemplate.NewTemplate(indexRepo, "<", ">")
118 if err != nil {
119 return nil, fmt.Errorf("wildcard pattern: index repo: %w", err)
120 }
121 indexRepoTemplates = append(indexRepoTemplates, indexRepoTemplate)
122 }
123
124 authorization := false
125 if config.Authorization != "" {
126 if slices.Contains([]string{"gogs", "gitea", "forgejo"}, config.Authorization) {
127 // Currently these are the only supported forges, and the authorization mechanism
128 // is the same for all of them.
129 authorization = true
130 } else {
131 return nil, fmt.Errorf(
132 "wildcard pattern: unknown authorization mechanism: %s",
133 config.Authorization,
134 )
135 }
136 }
137
138 var fallbackURL *url.URL
139 var fallback http.Handler
140 if config.FallbackProxyTo != "" {
141 fallbackURL, err = url.Parse(config.FallbackProxyTo)
142 if err != nil {
143 return nil, fmt.Errorf("wildcard pattern: fallback URL: %w", err)
144 }
145
146 fallback = &httputil.ReverseProxy{
147 Rewrite: func(r *httputil.ProxyRequest) {
148 r.SetURL(fallbackURL)
149 r.Out.Host = r.In.Host
150 r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
151 },
152 Transport: &http.Transport{
153 TLSClientConfig: &tls.Config{
154 InsecureSkipVerify: config.FallbackInsecure,
155 },
156 },
157 }
158 }
159
160 wildcardPatterns = append(wildcardPatterns, &WildcardPattern{
161 Domain: strings.Split(config.Domain, "."),
162 CloneURL: cloneURLTemplate,
163 IndexRepos: indexRepoTemplates,
164 IndexBranch: indexRepoBranch,
165 Authorization: authorization,
166 FallbackURL: fallbackURL,
167 Fallback: fallback,
168 })
169 }
170 return wildcardPatterns, nil
171}