fork of whitequark.org/git-pages with mods for tangled

[breaking-change] Make fallback handler per-instance, not per-wildcard.

There was never a particularly good reason to tie the fallback handler
to a wildcard domain; most importantly, this prevented it from being
used for custom domains, which is required for migrating custom domains
from Codeberg Pages v2 server.

+5 -2
conf/config.example.toml
··· 16 index-repos = ["<user>.codeberg.page", "pages"] 17 index-repo-branch = "main" 18 authorization = "forgejo" 19 - fallback-proxy-to = "https://codeberg.page" 20 21 [storage] 22 type = "fs" ··· 24 [storage.fs] 25 root = "./data" 26 27 - [storage.s3] # non-default bucket configuration 28 endpoint = "play.min.io" 29 access-key-id = "Q3AM3UQ867SPQQA43P2F" 30 secret-access-key = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
··· 16 index-repos = ["<user>.codeberg.page", "pages"] 17 index-repo-branch = "main" 18 authorization = "forgejo" 19 + 20 + [fallback] # non-default section 21 + proxy-to = "https://codeberg.page" 22 + insecure = false 23 24 [storage] 25 type = "fs" ··· 27 [storage.fs] 28 root = "./data" 29 30 + [storage.s3] # non-default section 31 endpoint = "play.min.io" 32 access-key-id = "Q3AM3UQ867SPQQA43P2F" 33 secret-access-key = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
+33 -25
src/caddy.go
··· 1 package git_pages 2 3 import ( 4 "crypto/tls" 5 "fmt" 6 "net" ··· 34 // Pages v2, which would under some circumstances return certificates with subjectAltName 35 // not valid for the SNI. Go's TLS stack makes `tls.Dial` return an error for these, 36 // thankfully making it unnecessary to examine X.509 certificates manually here.) 37 - for _, wildcardConfig := range config.Wildcard { 38 - if wildcardConfig.FallbackProxyTo == "" { 39 - continue 40 - } 41 - fallbackURL, err := url.Parse(wildcardConfig.FallbackProxyTo) 42 - if err != nil { 43 - continue 44 - } 45 - if fallbackURL.Scheme != "https" { 46 - continue 47 - } 48 - connectHost := fallbackURL.Host 49 - if fallbackURL.Port() != "" { 50 - connectHost += ":" + fallbackURL.Port() 51 - } else { 52 - connectHost += ":443" 53 - } 54 - logc.Printf(r.Context(), "caddy: check TLS %s", fallbackURL) 55 - connection, err := tls.Dial("tcp", connectHost, &tls.Config{ServerName: domain}) 56 - if err != nil { 57 - continue 58 - } 59 - connection.Close() 60 - found = true 61 - break 62 } 63 } 64 ··· 74 fmt.Fprintln(w, err) 75 } 76 }
··· 1 package git_pages 2 3 import ( 4 + "context" 5 "crypto/tls" 6 "fmt" 7 "net" ··· 35 // Pages v2, which would under some circumstances return certificates with subjectAltName 36 // not valid for the SNI. Go's TLS stack makes `tls.Dial` return an error for these, 37 // thankfully making it unnecessary to examine X.509 certificates manually here.) 38 + found, err = tryDialWithSNI(r.Context(), domain) 39 + if err != nil { 40 + logc.Printf(r.Context(), "caddy err: check SNI: %s\n", err) 41 } 42 } 43 ··· 53 fmt.Fprintln(w, err) 54 } 55 } 56 + 57 + func tryDialWithSNI(ctx context.Context, domain string) (bool, error) { 58 + if config.Fallback.ProxyTo == "" { 59 + return false, nil 60 + } 61 + 62 + fallbackURL, err := url.Parse(config.Fallback.ProxyTo) 63 + if err != nil { 64 + return false, err 65 + } 66 + if fallbackURL.Scheme != "https" { 67 + return false, nil 68 + } 69 + 70 + connectHost := fallbackURL.Host 71 + if fallbackURL.Port() != "" { 72 + connectHost += ":" + fallbackURL.Port() 73 + } else { 74 + connectHost += ":443" 75 + } 76 + 77 + logc.Printf(ctx, "caddy: check TLS %s", fallbackURL) 78 + connection, err := tls.Dial("tcp", connectHost, &tls.Config{ServerName: domain}) 79 + if err != nil { 80 + return false, err 81 + } 82 + connection.Close() 83 + return true, nil 84 + }
+11 -7
src/config.go
··· 41 LogLevel string `toml:"log-level" default:"info"` 42 Server ServerConfig `toml:"server"` 43 Wildcard []WildcardConfig `toml:"wildcard"` 44 Storage StorageConfig `toml:"storage"` 45 Limits LimitsConfig `toml:"limits"` 46 Observability ObservabilityConfig `toml:"observability"` ··· 53 } 54 55 type WildcardConfig struct { 56 - Domain string `toml:"domain"` 57 - CloneURL string `toml:"clone-url"` 58 - IndexRepos []string `toml:"index-repos" default:"[]"` 59 - IndexRepoBranch string `toml:"index-repo-branch" default:"pages"` 60 - Authorization string `toml:"authorization"` 61 - FallbackProxyTo string `toml:"fallback-proxy-to"` 62 - FallbackInsecure bool `toml:"fallback-insecure"` 63 } 64 65 type CacheConfig struct {
··· 41 LogLevel string `toml:"log-level" default:"info"` 42 Server ServerConfig `toml:"server"` 43 Wildcard []WildcardConfig `toml:"wildcard"` 44 + Fallback FallbackConfig `toml:"fallback"` 45 Storage StorageConfig `toml:"storage"` 46 Limits LimitsConfig `toml:"limits"` 47 Observability ObservabilityConfig `toml:"observability"` ··· 54 } 55 56 type WildcardConfig struct { 57 + Domain string `toml:"domain"` 58 + CloneURL string `toml:"clone-url"` 59 + IndexRepos []string `toml:"index-repos" default:"[]"` 60 + IndexRepoBranch string `toml:"index-repo-branch" default:"pages"` 61 + Authorization string `toml:"authorization"` 62 + } 63 + 64 + type FallbackConfig struct { 65 + ProxyTo string `toml:"proxy-to"` 66 + Insecure bool `toml:"insecure"` 67 } 68 69 type CacheConfig struct {
+30
src/main.go
··· 2 3 import ( 4 "context" 5 "errors" 6 "flag" 7 "fmt" ··· 10 "log/slog" 11 "net" 12 "net/http" 13 "net/url" 14 "os" 15 "runtime/debug" ··· 22 23 var config *Config 24 var wildcards []*WildcardPattern 25 var backend Backend 26 27 func configureFeatures() (err error) { ··· 63 } 64 } 65 66 func listen(name string, listen string) net.Listener { 67 if listen == "-" { 68 return nil ··· 230 configureFeatures(), 231 configureMemLimit(), 232 configureWildcards(), 233 ); err != nil { 234 log.Fatalln(err) 235 } ··· 392 configureFeatures(), 393 configureMemLimit(), 394 configureWildcards(), 395 ); err != nil { 396 // At this point the configuration is in an in-between, corrupted state, so 397 // the only reasonable choice is to crash.
··· 2 3 import ( 4 "context" 5 + "crypto/tls" 6 "errors" 7 "flag" 8 "fmt" ··· 11 "log/slog" 12 "net" 13 "net/http" 14 + "net/http/httputil" 15 "net/url" 16 "os" 17 "runtime/debug" ··· 24 25 var config *Config 26 var wildcards []*WildcardPattern 27 + var fallback http.Handler 28 var backend Backend 29 30 func configureFeatures() (err error) { ··· 66 } 67 } 68 69 + func configureFallback() (err error) { 70 + if config.Fallback.ProxyTo != "" { 71 + var fallbackURL *url.URL 72 + fallbackURL, err = url.Parse(config.Fallback.ProxyTo) 73 + if err != nil { 74 + err = fmt.Errorf("fallback: %w", err) 75 + return 76 + } 77 + 78 + fallback = &httputil.ReverseProxy{ 79 + Rewrite: func(r *httputil.ProxyRequest) { 80 + r.SetURL(fallbackURL) 81 + r.Out.Host = r.In.Host 82 + r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] 83 + }, 84 + Transport: &http.Transport{ 85 + TLSClientConfig: &tls.Config{ 86 + InsecureSkipVerify: config.Fallback.Insecure, 87 + }, 88 + }, 89 + } 90 + } 91 + return 92 + } 93 + 94 func listen(name string, listen string) net.Listener { 95 if listen == "-" { 96 return nil ··· 258 configureFeatures(), 259 configureMemLimit(), 260 configureWildcards(), 261 + configureFallback(), 262 ); err != nil { 263 log.Fatalln(err) 264 } ··· 421 configureFeatures(), 422 configureMemLimit(), 423 configureWildcards(), 424 + configureFallback(), 425 ); err != nil { 426 // At this point the configuration is in an in-between, corrupted state, so 427 // the only reasonable choice is to crash.
+4 -2
src/pages.go
··· 136 result := <-indexManifestCh 137 manifest, manifestMtime, err = result.manifest, result.manifestMtime, result.err 138 if manifest == nil && errors.Is(err, ErrObjectNotFound) { 139 - if found, fallbackErr := HandleWildcardFallback(w, r); found { 140 - return fallbackErr 141 } else { 142 w.WriteHeader(http.StatusNotFound) 143 fmt.Fprintf(w, "site not found\n")
··· 136 result := <-indexManifestCh 137 manifest, manifestMtime, err = result.manifest, result.manifestMtime, result.err 138 if manifest == nil && errors.Is(err, ErrObjectNotFound) { 139 + if fallback != nil { 140 + logc.Printf(r.Context(), "fallback: %s via %s", host, config.Fallback.ProxyTo) 141 + fallback.ServeHTTP(w, r) 142 + return nil 143 } else { 144 w.WriteHeader(http.StatusNotFound) 145 fmt.Fprintf(w, "site not found\n")
-54
src/wildcard.go
··· 1 package git_pages 2 3 import ( 4 - "crypto/tls" 5 "fmt" 6 - "net/http" 7 - "net/http/httputil" 8 - "net/url" 9 "slices" 10 "strings" 11 ··· 18 IndexRepos []*fasttemplate.Template 19 IndexBranch string 20 Authorization bool 21 - FallbackURL *url.URL 22 - Fallback http.Handler 23 } 24 25 func (pattern *WildcardPattern) GetHost() string { ··· 78 return repoURLs, branch 79 } 80 81 - func (pattern *WildcardPattern) IsFallbackFor(host string) bool { 82 - if pattern.Fallback == nil { 83 - return false 84 - } 85 - _, found := pattern.Matches(host) 86 - return found 87 - } 88 - 89 - func HandleWildcardFallback(w http.ResponseWriter, r *http.Request) (bool, error) { 90 - host, err := GetHost(r) 91 - if err != nil { 92 - return false, err 93 - } 94 - 95 - for _, pattern := range wildcards { 96 - if pattern.IsFallbackFor(host) { 97 - logc.Printf(r.Context(), "proxy: %s via %s", pattern.GetHost(), pattern.FallbackURL) 98 - pattern.Fallback.ServeHTTP(w, r) 99 - return true, nil 100 - } 101 - } 102 - return false, nil 103 - } 104 - 105 func TranslateWildcards(configs []WildcardConfig) ([]*WildcardPattern, error) { 106 var wildcardPatterns []*WildcardPattern 107 for _, config := range configs { ··· 134 } 135 } 136 137 - var fallbackURL *url.URL 138 - var fallback http.Handler 139 - if config.FallbackProxyTo != "" { 140 - fallbackURL, err = url.Parse(config.FallbackProxyTo) 141 - if err != nil { 142 - return nil, fmt.Errorf("wildcard pattern: fallback URL: %w", err) 143 - } 144 - 145 - fallback = &httputil.ReverseProxy{ 146 - Rewrite: func(r *httputil.ProxyRequest) { 147 - r.SetURL(fallbackURL) 148 - r.Out.Host = r.In.Host 149 - r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] 150 - }, 151 - Transport: &http.Transport{ 152 - TLSClientConfig: &tls.Config{ 153 - InsecureSkipVerify: config.FallbackInsecure, 154 - }, 155 - }, 156 - } 157 - } 158 - 159 wildcardPatterns = append(wildcardPatterns, &WildcardPattern{ 160 Domain: strings.Split(config.Domain, "."), 161 CloneURL: cloneURLTemplate, 162 IndexRepos: indexRepoTemplates, 163 IndexBranch: indexRepoBranch, 164 Authorization: authorization, 165 - FallbackURL: fallbackURL, 166 - Fallback: fallback, 167 }) 168 } 169 return wildcardPatterns, nil
··· 1 package git_pages 2 3 import ( 4 "fmt" 5 "slices" 6 "strings" 7 ··· 14 IndexRepos []*fasttemplate.Template 15 IndexBranch string 16 Authorization bool 17 } 18 19 func (pattern *WildcardPattern) GetHost() string { ··· 72 return repoURLs, branch 73 } 74 75 func TranslateWildcards(configs []WildcardConfig) ([]*WildcardPattern, error) { 76 var wildcardPatterns []*WildcardPattern 77 for _, config := range configs { ··· 104 } 105 } 106 107 wildcardPatterns = append(wildcardPatterns, &WildcardPattern{ 108 Domain: strings.Split(config.Domain, "."), 109 CloneURL: cloneURLTemplate, 110 IndexRepos: indexRepoTemplates, 111 IndexBranch: indexRepoBranch, 112 Authorization: authorization, 113 }) 114 } 115 return wildcardPatterns, nil