[mirror] Scalable static site server for Git forges (like GitHub Pages)
at v0.1.0 4.7 kB view raw
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}