[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.
This commit is contained in:
Catherine
2025-12-03 00:38:02 +00:00
parent c250922f8d
commit f089208ca7
6 changed files with 83 additions and 90 deletions

View File

@@ -16,7 +16,10 @@ clone-url = "https://codeberg.org/<user>/<project>.git"
index-repos = ["<user>.codeberg.page", "pages"]
index-repo-branch = "main"
authorization = "forgejo"
fallback-proxy-to = "https://codeberg.page"
[fallback] # non-default section
proxy-to = "https://codeberg.page"
insecure = false
[storage]
type = "fs"
@@ -24,7 +27,7 @@ type = "fs"
[storage.fs]
root = "./data"
[storage.s3] # non-default bucket configuration
[storage.s3] # non-default section
endpoint = "play.min.io"
access-key-id = "Q3AM3UQ867SPQQA43P2F"
secret-access-key = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"

View File

@@ -1,6 +1,7 @@
package git_pages
import (
"context"
"crypto/tls"
"fmt"
"net"
@@ -34,31 +35,9 @@ func ServeCaddy(w http.ResponseWriter, r *http.Request) {
// Pages v2, which would under some circumstances return certificates with subjectAltName
// not valid for the SNI. Go's TLS stack makes `tls.Dial` return an error for these,
// thankfully making it unnecessary to examine X.509 certificates manually here.)
for _, wildcardConfig := range config.Wildcard {
if wildcardConfig.FallbackProxyTo == "" {
continue
}
fallbackURL, err := url.Parse(wildcardConfig.FallbackProxyTo)
if err != nil {
continue
}
if fallbackURL.Scheme != "https" {
continue
}
connectHost := fallbackURL.Host
if fallbackURL.Port() != "" {
connectHost += ":" + fallbackURL.Port()
} else {
connectHost += ":443"
}
logc.Printf(r.Context(), "caddy: check TLS %s", fallbackURL)
connection, err := tls.Dial("tcp", connectHost, &tls.Config{ServerName: domain})
if err != nil {
continue
}
connection.Close()
found = true
break
found, err = tryDialWithSNI(r.Context(), domain)
if err != nil {
logc.Printf(r.Context(), "caddy err: check SNI: %s\n", err)
}
}
@@ -74,3 +53,32 @@ func ServeCaddy(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, err)
}
}
func tryDialWithSNI(ctx context.Context, domain string) (bool, error) {
if config.Fallback.ProxyTo == "" {
return false, nil
}
fallbackURL, err := url.Parse(config.Fallback.ProxyTo)
if err != nil {
return false, err
}
if fallbackURL.Scheme != "https" {
return false, nil
}
connectHost := fallbackURL.Host
if fallbackURL.Port() != "" {
connectHost += ":" + fallbackURL.Port()
} else {
connectHost += ":443"
}
logc.Printf(ctx, "caddy: check TLS %s", fallbackURL)
connection, err := tls.Dial("tcp", connectHost, &tls.Config{ServerName: domain})
if err != nil {
return false, err
}
connection.Close()
return true, nil
}

View File

@@ -41,6 +41,7 @@ type Config struct {
LogLevel string `toml:"log-level" default:"info"`
Server ServerConfig `toml:"server"`
Wildcard []WildcardConfig `toml:"wildcard"`
Fallback FallbackConfig `toml:"fallback"`
Storage StorageConfig `toml:"storage"`
Limits LimitsConfig `toml:"limits"`
Observability ObservabilityConfig `toml:"observability"`
@@ -53,13 +54,16 @@ type ServerConfig struct {
}
type WildcardConfig struct {
Domain string `toml:"domain"`
CloneURL string `toml:"clone-url"`
IndexRepos []string `toml:"index-repos" default:"[]"`
IndexRepoBranch string `toml:"index-repo-branch" default:"pages"`
Authorization string `toml:"authorization"`
FallbackProxyTo string `toml:"fallback-proxy-to"`
FallbackInsecure bool `toml:"fallback-insecure"`
Domain string `toml:"domain"`
CloneURL string `toml:"clone-url"`
IndexRepos []string `toml:"index-repos" default:"[]"`
IndexRepoBranch string `toml:"index-repo-branch" default:"pages"`
Authorization string `toml:"authorization"`
}
type FallbackConfig struct {
ProxyTo string `toml:"proxy-to"`
Insecure bool `toml:"insecure"`
}
type CacheConfig struct {

View File

@@ -2,6 +2,7 @@ package git_pages
import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
@@ -10,6 +11,7 @@ import (
"log/slog"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"runtime/debug"
@@ -22,6 +24,7 @@ import (
var config *Config
var wildcards []*WildcardPattern
var fallback http.Handler
var backend Backend
func configureFeatures() (err error) {
@@ -63,6 +66,31 @@ func configureWildcards() (err error) {
}
}
func configureFallback() (err error) {
if config.Fallback.ProxyTo != "" {
var fallbackURL *url.URL
fallbackURL, err = url.Parse(config.Fallback.ProxyTo)
if err != nil {
err = fmt.Errorf("fallback: %w", err)
return
}
fallback = &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(fallbackURL)
r.Out.Host = r.In.Host
r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: config.Fallback.Insecure,
},
},
}
}
return
}
func listen(name string, listen string) net.Listener {
if listen == "-" {
return nil
@@ -230,6 +258,7 @@ func Main() {
configureFeatures(),
configureMemLimit(),
configureWildcards(),
configureFallback(),
); err != nil {
log.Fatalln(err)
}
@@ -392,6 +421,7 @@ func Main() {
configureFeatures(),
configureMemLimit(),
configureWildcards(),
configureFallback(),
); err != nil {
// At this point the configuration is in an in-between, corrupted state, so
// the only reasonable choice is to crash.

View File

@@ -136,8 +136,10 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
result := <-indexManifestCh
manifest, manifestMtime, err = result.manifest, result.manifestMtime, result.err
if manifest == nil && errors.Is(err, ErrObjectNotFound) {
if found, fallbackErr := HandleWildcardFallback(w, r); found {
return fallbackErr
if fallback != nil {
logc.Printf(r.Context(), "fallback: %s via %s", host, config.Fallback.ProxyTo)
fallback.ServeHTTP(w, r)
return nil
} else {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "site not found\n")

View File

@@ -1,11 +1,7 @@
package git_pages
import (
"crypto/tls"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"slices"
"strings"
@@ -18,8 +14,6 @@ type WildcardPattern struct {
IndexRepos []*fasttemplate.Template
IndexBranch string
Authorization bool
FallbackURL *url.URL
Fallback http.Handler
}
func (pattern *WildcardPattern) GetHost() string {
@@ -78,30 +72,6 @@ func (pattern *WildcardPattern) ApplyTemplate(userName string, projectName strin
return repoURLs, branch
}
func (pattern *WildcardPattern) IsFallbackFor(host string) bool {
if pattern.Fallback == nil {
return false
}
_, found := pattern.Matches(host)
return found
}
func HandleWildcardFallback(w http.ResponseWriter, r *http.Request) (bool, error) {
host, err := GetHost(r)
if err != nil {
return false, err
}
for _, pattern := range wildcards {
if pattern.IsFallbackFor(host) {
logc.Printf(r.Context(), "proxy: %s via %s", pattern.GetHost(), pattern.FallbackURL)
pattern.Fallback.ServeHTTP(w, r)
return true, nil
}
}
return false, nil
}
func TranslateWildcards(configs []WildcardConfig) ([]*WildcardPattern, error) {
var wildcardPatterns []*WildcardPattern
for _, config := range configs {
@@ -134,36 +104,12 @@ func TranslateWildcards(configs []WildcardConfig) ([]*WildcardPattern, error) {
}
}
var fallbackURL *url.URL
var fallback http.Handler
if config.FallbackProxyTo != "" {
fallbackURL, err = url.Parse(config.FallbackProxyTo)
if err != nil {
return nil, fmt.Errorf("wildcard pattern: fallback URL: %w", err)
}
fallback = &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(fallbackURL)
r.Out.Host = r.In.Host
r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: config.FallbackInsecure,
},
},
}
}
wildcardPatterns = append(wildcardPatterns, &WildcardPattern{
Domain: strings.Split(config.Domain, "."),
CloneURL: cloneURLTemplate,
IndexRepos: indexRepoTemplates,
IndexBranch: indexRepoBranch,
Authorization: authorization,
FallbackURL: fallbackURL,
Fallback: fallback,
})
}
return wildcardPatterns, nil