mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-14 03:01:48 +00:00
Proxy requests for unknown sites via wildcard fallback URL (if any).
This commit is contained in:
@@ -7,6 +7,7 @@ health = "tcp/:3002"
|
||||
# domain = "codeberg.page"
|
||||
# clone-url = "https://codeberg.org/<user>/<project>.git"
|
||||
# index-repos = ["<user>.codeberg.page", "pages"]
|
||||
# fallback-proxy-to = "https://codeberg.page"
|
||||
|
||||
[backend]
|
||||
type = "fs"
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
13
src/main.go
13
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)
|
||||
|
||||
18
src/pages.go
18
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)
|
||||
|
||||
105
src/wildcard.go
Normal file
105
src/wildcard.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user