From 5765fa7ffae1fe4b325e900a670b97fe31094ae0 Mon Sep 17 00:00:00 2001 From: Catherine Date: Sun, 21 Sep 2025 02:28:03 +0000 Subject: [PATCH] Proxy requests for unknown sites via wildcard fallback URL (if any). --- conf/config.toml.example | 1 + src/auth.go | 7 +-- src/backend.go | 41 ++++++++++++--- src/config.go | 77 ++-------------------------- src/main.go | 13 +++-- src/pages.go | 18 ++++--- src/wildcard.go | 105 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 168 insertions(+), 94 deletions(-) create mode 100644 src/wildcard.go diff --git a/conf/config.toml.example b/conf/config.toml.example index 36a29a1..46ddec9 100644 --- a/conf/config.toml.example +++ b/conf/config.toml.example @@ -7,6 +7,7 @@ health = "tcp/:3002" # domain = "codeberg.page" # clone-url = "https://codeberg.org//.git" # index-repos = [".codeberg.page", "pages"] +# fallback-proxy-to = "https://codeberg.page" [backend] type = "fs" diff --git a/src/auth.go b/src/auth.go index 8e5b99d..ac20d08 100644 --- a/src/auth.go +++ b/src/auth.go @@ -171,8 +171,7 @@ func authorizeWildcardMatchHost(r *http.Request, pattern *WildcardPattern) (*Aut return nil, err } - hostParts := strings.Split(host, ".") - if slices.Equal(hostParts[1:], pattern.Domain) { + if _, found := pattern.Matches(host); found { return &Authorization{}, nil } else { return nil, AuthError{ @@ -193,9 +192,7 @@ func authorizeWildcardMatchSite(r *http.Request, pattern *WildcardPattern) (*Aut return nil, err } - hostParts := strings.Split(host, ".") - if slices.Equal(hostParts[1:], pattern.Domain) { - userName := hostParts[0] + if userName, found := pattern.Matches(host); found { var repoURLs []string repoURLTemplate := pattern.CloneURL if projectName == ".index" { diff --git a/src/backend.go b/src/backend.go index 48185a7..f0468fe 100644 --- a/src/backend.go +++ b/src/backend.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "io" "slices" "strings" @@ -10,6 +11,15 @@ import ( var errNotFound = errors.New("not found") +func splitBlobName(name string) []string { + algo, hash, found := strings.Cut(name, "-") + if found { + return slices.Concat([]string{algo}, splitBlobName(hash)) + } else { + return []string{name[0:2], name[2:4], name[4:]} + } +} + type Backend interface { // Retrieve a blob. Returns `reader, mtime, err`. GetBlob(name string) (io.ReadSeeker, time.Time, error) @@ -41,11 +51,30 @@ type Backend interface { CheckDomain(domain string) (bool, error) } -func splitBlobName(name string) []string { - algo, hash, found := strings.Cut(name, "-") - if found { - return slices.Concat([]string{algo}, splitBlobName(hash)) - } else { - return []string{name[0:2], name[2:4], name[4:]} +var backend Backend + +func ConfigureBackend() error { + var err error + switch config.Backend.Type { + case "fs": + if backend, err = NewFSBackend(config.Backend.FS.Root); err != nil { + return fmt.Errorf("fs backend: %w", err) + } + + case "s3": + if backend, err = NewS3Backend( + config.Backend.S3.Endpoint, + config.Backend.S3.Insecure, + config.Backend.S3.AccessKeyID, + config.Backend.S3.SecretAccessKey, + config.Backend.S3.Region, + config.Backend.S3.Bucket, + ); err != nil { + return fmt.Errorf("s3 backend: %w", err) + } + + default: + return fmt.Errorf("unknown backend: %s", config.Backend.Type) } + return nil } diff --git a/src/config.go b/src/config.go index 5819d79..a9e61dd 100644 --- a/src/config.go +++ b/src/config.go @@ -1,12 +1,9 @@ package main import ( - "log" "os" - "strings" "github.com/pelletier/go-toml/v2" - "github.com/valyala/fasttemplate" ) type CacheConfig struct { @@ -22,9 +19,10 @@ type Config struct { Health string `toml:"health"` } `toml:"listen"` Wildcard []struct { - Domain string `toml:"domain"` - CloneURL string `toml:"clone-url"` - IndexRepos []string `toml:"index-repos"` + Domain string `toml:"domain"` + CloneURL string `toml:"clone-url"` + IndexRepos []string `toml:"index-repos"` + FallbackProxyTo string `toml:"fallback-proxy-to"` } `toml:"wildcard"` Backend struct { Type string `toml:"type"` @@ -73,70 +71,3 @@ func UpdateConfigEnv() { updateFromEnv(&config.Backend.S3.Region, "S3_REGION") updateFromEnv(&config.Backend.S3.Bucket, "S3_BUCKET") } - -var backend Backend - -func ConfigureBackend() { - var err error - switch config.Backend.Type { - case "fs": - if backend, err = NewFSBackend(config.Backend.FS.Root); err != nil { - log.Fatalln("fs backend:", err) - } - - case "s3": - if backend, err = NewS3Backend( - config.Backend.S3.Endpoint, - config.Backend.S3.Insecure, - config.Backend.S3.AccessKeyID, - config.Backend.S3.SecretAccessKey, - config.Backend.S3.Region, - config.Backend.S3.Bucket, - ); err != nil { - log.Fatalln("s3 backend:", err) - } - - default: - log.Fatalln("unknown backend:", config.Backend.Type) - } -} - -type WildcardPattern struct { - Domain []string - CloneURL *fasttemplate.Template - IndexRepos []*fasttemplate.Template -} - -func (pattern *WildcardPattern) GetHost() string { - parts := []string{"*"} - parts = append(parts, pattern.Domain...) - return strings.Join(parts, ".") -} - -var wildcardPatterns []*WildcardPattern - -func CompileWildcardPattern() { - for _, configWildcard := range config.Wildcard { - wildcardPattern := WildcardPattern{ - Domain: strings.Split(configWildcard.Domain, "."), - } - - template, err := fasttemplate.NewTemplate(configWildcard.CloneURL, "<", ">") - if err != nil { - log.Fatalf("wildcard pattern: clone URL: %s", err) - } else { - wildcardPattern.CloneURL = template - } - - for _, indexRepo := range configWildcard.IndexRepos { - template, err := fasttemplate.NewTemplate(indexRepo, "<", ">") - if err != nil { - log.Fatalf("wildcard pattern: clone URL: %s", err) - } else { - wildcardPattern.IndexRepos = append(wildcardPattern.IndexRepos, template) - } - } - - wildcardPatterns = append(wildcardPatterns, &wildcardPattern) - } -} diff --git a/src/main.go b/src/main.go index 6540b7d..ccbdff1 100644 --- a/src/main.go +++ b/src/main.go @@ -89,7 +89,9 @@ func main() { ) if *getManifest != "" { - ConfigureBackend() + if err := ConfigureBackend(); err != nil { + log.Fatalln(err) + } webRoot := *getManifest if !strings.Contains(webRoot, "/") { @@ -110,8 +112,13 @@ func main() { caddyListener := listen("caddy", config.Listen.Caddy) healthListener := listen("health", config.Listen.Health) - ConfigureBackend() - CompileWildcardPattern() + if err := ConfigureBackend(); err != nil { + log.Fatalln(err) + } + + if err := ConfigureWildcards(); err != nil { + log.Fatalln(err) + } go serve(pagesListener, ServePages) go serve(caddyListener, ServeCaddy) diff --git a/src/pages.go b/src/pages.go index 2ac630e..42dec45 100644 --- a/src/pages.go +++ b/src/pages.go @@ -33,10 +33,6 @@ func getPage(w http.ResponseWriter, r *http.Request) error { return err } - // allow JavaScript code to access responses (including errors) even across origins - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Max-Age", "86400") - sitePath, _ = strings.CutPrefix(r.URL.Path, "/") if projectName, projectPath, found := strings.Cut(sitePath, "/"); found { projectManifest, err := backend.GetManifest(makeWebRoot(host, projectName)) @@ -47,12 +43,20 @@ func getPage(w http.ResponseWriter, r *http.Request) error { if manifest == nil { manifest, err = backend.GetManifest(makeWebRoot(host, ".index")) if manifest == nil { - w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "site not found\n") - return err + if found, fallbackErr := HandleWildcardFallback(w, r); found { + return fallbackErr + } else { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "site not found\n") + return err + } } } + // allow JavaScript code to access responses (including errors) even across origins + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Max-Age", "86400") + if sitePath == ".git-pages" { // metadata directory name shouldn't be served even if present in site manifest w.WriteHeader(http.StatusNotFound) diff --git a/src/wildcard.go b/src/wildcard.go new file mode 100644 index 0000000..b3a9f3f --- /dev/null +++ b/src/wildcard.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + "slices" + "strings" + + "github.com/valyala/fasttemplate" +) + +type WildcardPattern struct { + Domain []string + CloneURL *fasttemplate.Template + IndexRepos []*fasttemplate.Template + FallbackURL *url.URL +} + +var wildcardPatterns []*WildcardPattern + +func (pattern *WildcardPattern) GetHost() string { + parts := []string{"*"} + parts = append(parts, pattern.Domain...) + return strings.Join(parts, ".") +} + +// Returns `subdomain, found` where if `found == true`, `subdomain` contains the part of `host` +// corresponding to the * in the domain pattern. +func (pattern *WildcardPattern) Matches(host string) (string, bool) { + hostParts := strings.Split(host, ".") + if len(hostParts) != 1+len(pattern.Domain) || !slices.Equal(hostParts[1:], pattern.Domain) { + return "", false + } + return hostParts[0], true +} + +func (pattern *WildcardPattern) IsFallbackFor(host string) bool { + if pattern.FallbackURL == 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 wildcardPatterns { + if pattern.IsFallbackFor(host) { + log.Printf("proxy: %s via %s", pattern.GetHost(), pattern.FallbackURL) + + (&httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(pattern.FallbackURL) + r.Out.Host = r.In.Host + r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] + }, + }).ServeHTTP(w, r) + + return true, nil + } + } + + return false, nil +} + +func ConfigureWildcards() error { + for _, configWildcard := range config.Wildcard { + wildcardPattern := WildcardPattern{ + Domain: strings.Split(configWildcard.Domain, "."), + } + + template, err := fasttemplate.NewTemplate(configWildcard.CloneURL, "<", ">") + if err != nil { + return fmt.Errorf("wildcard pattern: clone URL: %w", err) + } else { + wildcardPattern.CloneURL = template + } + + for _, indexRepo := range configWildcard.IndexRepos { + template, err := fasttemplate.NewTemplate(indexRepo, "<", ">") + if err != nil { + return fmt.Errorf("wildcard pattern: clone URL: %w", err) + } else { + wildcardPattern.IndexRepos = append(wildcardPattern.IndexRepos, template) + } + } + + if configWildcard.FallbackProxyTo != "" { + wildcardPattern.FallbackURL, err = url.Parse(configWildcard.FallbackProxyTo) + if err != nil { + return fmt.Errorf("wildcard pattern: fallback URL: %w", err) + } + } + + wildcardPatterns = append(wildcardPatterns, &wildcardPattern) + } + return nil +}