diff --git a/app/handlers/static.go b/app/handlers/static.go index 1496dd2..3b55878 100644 --- a/app/handlers/static.go +++ b/app/handlers/static.go @@ -1,6 +1,7 @@ package handlers import ( + "log" "os" "path/filepath" "quay/internal/config" @@ -18,7 +19,14 @@ func NewStaticHandler(storagePath string, siteMap map[string]config.SiteConfig) urlPath := filepath.Clean(c.Path()) for _, rule := range site.ForwardRules { - if rule.Source == urlPath { + if rule.Regex && rule.Compiled != nil { + match := rule.Compiled.FindStringSubmatchIndex(urlPath) + if match != nil { + dest := rule.Compiled.ExpandString([]byte{}, rule.Destination, urlPath, match) + log.Printf("Forwarding %s to %s based on regex rule", urlPath, dest) + return c.Redirect().Status(rule.StatusCode).To(string(dest)) + } + } else if rule.Source == urlPath { return c.Redirect().Status(rule.StatusCode).To(rule.Destination) } } diff --git a/config.example.yaml b/config.example.yaml index 4189a63..0a93fa8 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -12,9 +12,16 @@ sites: - source: /npm destination: https://www.npmjs.com/package/my-lib status_code: 302 - - source: /github - destination: https://github.com/yourname/my-lib + + - source: ^/docs/v(\d+)$ + destination: https://v$1.docs.my-lib.com/ + status_code: 301 + regex: true + + - source: ^/blog/(?P[^/]+)$ + destination: /posts/${slug} status_code: 302 + regex: true - repo: portfolio owner: yourname diff --git a/internal/config/config.go b/internal/config/config.go index 60d4253..10a0d20 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,10 @@ package config import ( + "fmt" "os" + "regexp" + "strconv" "gopkg.in/yaml.v3" ) @@ -10,6 +13,8 @@ type ForwardRule struct { Source string `yaml:"source"` Destination string `yaml:"destination"` StatusCode int `yaml:"status_code"` + Regex bool `yaml:"regex"` + Compiled *regexp.Regexp } type SiteConfig struct { @@ -40,32 +45,42 @@ func (c Error) Error() string { func validateConfig(config *Config) error { for i, site := range config.Sites { if site.Name == "" { - return &Error{Field: "sites[" + string(i) + "].name", Message: "Name is required"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].name", Message: "Name is required"} } if site.Repo == "" { - return &Error{Field: "sites[" + string(i) + "].repo", Message: "Repo is required"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].repo", Message: "Repo is required"} } if site.Owner == "" { - return &Error{Field: "sites[" + string(i) + "].owner", Message: "Owner is required"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].owner", Message: "Owner is required"} } if site.Branch == "" { - return &Error{Field: "sites[" + string(i) + "].branch", Message: "Branch is required"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].branch", Message: "Branch is required"} } if site.Domain == "" { - return &Error{Field: "sites[" + string(i) + "].domain", Message: "Domain is required"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].domain", Message: "Domain is required"} } for j, rule := range site.ForwardRules { if rule.Source == "" { - return &Error{Field: "sites[" + string(i) + "].forward_rules[" + string(j) + "].source", Message: "Source is required"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].forward_rules[" + strconv.Itoa(j) + "].source", Message: "Source is required"} } if rule.Destination == "" { - return &Error{Field: "sites[" + string(i) + "].forward_rules[" + string(j) + "].destination", Message: "Destination is required"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].forward_rules[" + strconv.Itoa(j) + "].destination", Message: "Destination is required"} } if rule.StatusCode == 0 { - return &Error{Field: "sites[" + string(i) + "].forward_rules[" + string(j) + "].status_code", Message: "Status code is required"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].forward_rules[" + strconv.Itoa(j) + "].status_code", Message: "Status code is required"} } if rule.StatusCode < 300 || rule.StatusCode >= 400 { - return &Error{Field: "sites[" + string(i) + "].forward_rules[" + string(j) + "].status_code", Message: "Status code must be a valid redirect code (300-399)"} + return &Error{Field: "sites[" + strconv.Itoa(i) + "].forward_rules[" + strconv.Itoa(j) + "].status_code", Message: "Status code must be a valid redirect code (300-399)"} + } + if rule.Regex { + re, err := regexp.Compile(rule.Source) + if err != nil { + return &Error{ + Field: fmt.Sprintf("sites[%d].forward_rules[%d].source", i, j), + Message: "Invalid regex: " + err.Error(), + } + } + config.Sites[i].ForwardRules[j].Compiled = re } } } @@ -90,7 +105,13 @@ func Load(path string) (*Config, error) { if err != nil { return nil, err } - expanded := os.Expand(string(f), os.Getenv) + expanded := os.Expand(string(f), func(key string) string { + val, ok := os.LookupEnv(key) + if !ok { + return "${" + key + "}" // env vars that aren't set are left as is to not cause errors with regex patterns + } + return val + }) var config Config err = yaml.Unmarshal([]byte(expanded), &config) if err != nil {