mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
naive implementation of caddy like route rules, dependencies upgrade
This commit is contained in:
parent
1b40f81fcc
commit
35c0463829
9 changed files with 577 additions and 15 deletions
|
@ -22,8 +22,8 @@ lint:
|
|||
- yamllint
|
||||
enabled:
|
||||
- hadolint@2.12.1-beta
|
||||
- actionlint@1.7.5
|
||||
- checkov@3.2.346
|
||||
- actionlint@1.7.6
|
||||
- checkov@3.2.347
|
||||
- git-diff-check
|
||||
- gofmt@1.20.4
|
||||
- golangci-lint@1.62.2
|
||||
|
@ -32,7 +32,7 @@ lint:
|
|||
- prettier@3.4.2
|
||||
- shellcheck@0.10.0
|
||||
- shfmt@3.6.0
|
||||
- trufflehog@3.88.0
|
||||
- trufflehog@3.88.1
|
||||
actions:
|
||||
disabled:
|
||||
- trunk-announce
|
||||
|
|
9
go.mod
9
go.mod
|
@ -9,12 +9,13 @@ require (
|
|||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/go-acme/lego/v4 v4.21.0
|
||||
github.com/go-playground/validator/v10 v10.23.0
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/gotify/server/v2 v2.6.1
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
golang.org/x/net v0.33.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/time v0.9.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
@ -64,13 +65,13 @@ require (
|
|||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/oauth2 v0.25.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
|
18
go.sum
18
go.sum
|
@ -52,6 +52,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
|
@ -157,8 +159,8 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
|
@ -167,8 +169,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -195,8 +197,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
|||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -208,8 +210,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:
|
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
|
@ -10,6 +10,16 @@ func NewServeMux() ServeMux {
|
|||
return ServeMux{http.NewServeMux()}
|
||||
}
|
||||
|
||||
func (mux ServeMux) Handle(pattern string, handler http.Handler) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
mux.ServeMux.Handle(pattern, handler)
|
||||
return
|
||||
}
|
||||
|
||||
func (mux ServeMux) HandleFunc(pattern string, handler http.HandlerFunc) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
|
|
@ -121,6 +121,9 @@ func (r *HTTPRoute) Start(parent task.Parent) E.Error {
|
|||
case len(pathPatterns) == 1 && pathPatterns[0] == "/":
|
||||
r.handler = r.rp
|
||||
default:
|
||||
logger.Warn().
|
||||
Str("route", r.TargetName()).
|
||||
Msg("`path_patterns` is deprecated. Use `rules` instead.")
|
||||
mux := gphttp.NewServeMux()
|
||||
patErrs := E.NewBuilder("invalid path pattern(s)")
|
||||
for _, p := range pathPatterns {
|
||||
|
@ -134,6 +137,10 @@ func (r *HTTPRoute) Start(parent task.Parent) E.Error {
|
|||
}
|
||||
}
|
||||
|
||||
if len(r.Raw.Rules) > 0 {
|
||||
r.handler = r.Raw.Rules.BuildHandler(r.rp)
|
||||
}
|
||||
|
||||
if r.HealthMon != nil {
|
||||
if err := r.HealthMon.Start(r.task); err != nil {
|
||||
E.LogWarn("health monitor error", err, &r.l)
|
||||
|
|
|
@ -30,6 +30,7 @@ type (
|
|||
Port string `json:"port,omitempty"`
|
||||
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
|
||||
PathPatterns []string `json:"path_patterns,omitempty"`
|
||||
Rules Rules `json:"rules,omitempty"`
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||
LoadBalance *loadbalance.Config `json:"load_balance,omitempty"`
|
||||
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"`
|
||||
|
|
367
internal/route/types/rules.go
Normal file
367
internal/route/types/rules.go
Normal file
|
@ -0,0 +1,367 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type (
|
||||
Rules []Rule
|
||||
Rule struct {
|
||||
Name string `json:"name" validate:"required,unique"`
|
||||
On RuleOn `json:"on"`
|
||||
Do Command `json:"do"`
|
||||
}
|
||||
RuleOn struct {
|
||||
raw string
|
||||
checkers []CheckFulfill
|
||||
}
|
||||
Command struct {
|
||||
raw string
|
||||
CommandExecutor
|
||||
}
|
||||
CheckFulfill func(r *http.Request) bool
|
||||
RequestObjectRetriever struct {
|
||||
expectedArgs int
|
||||
retrieve func(r *http.Request, args []string) string
|
||||
equal func(v, want string) bool
|
||||
}
|
||||
CommandExecutor struct {
|
||||
http.HandlerFunc
|
||||
proceed bool
|
||||
}
|
||||
CommandBuilder struct {
|
||||
expectedArgs int
|
||||
build func(args []string) CommandExecutor
|
||||
}
|
||||
)
|
||||
|
||||
/*
|
||||
proxy.app1.rules: |
|
||||
- name: default
|
||||
do: |
|
||||
rewrite / /index.html
|
||||
serve /var/www/goaccess
|
||||
- name: ws
|
||||
on: |
|
||||
header Connection upgrade
|
||||
header Upgrade websocket
|
||||
do: proxy $upstream_url
|
||||
*/
|
||||
|
||||
var (
|
||||
ErrUnterminatedQuotes = E.New("unterminated quotes")
|
||||
ErrUnsupportedEscapeChar = E.New("unsupported escape char")
|
||||
ErrUnknownDirective = E.New("unknown directive")
|
||||
ErrInvalidArguments = E.New("invalid arguments")
|
||||
ErrInvalidCriteria = E.New("invalid criteria")
|
||||
ErrInvalidCriteriaTarget = E.New("invalid criteria target")
|
||||
)
|
||||
|
||||
var retrievers = map[string]RequestObjectRetriever{
|
||||
"header": {1, func(r *http.Request, args []string) string {
|
||||
return r.Header.Get(args[0])
|
||||
}, nil},
|
||||
"query": {1, func(r *http.Request, args []string) string {
|
||||
return r.URL.Query().Get(args[0])
|
||||
}, nil},
|
||||
"method": {0, func(r *http.Request, _ []string) string {
|
||||
return r.Method
|
||||
}, nil},
|
||||
"path": {0, func(r *http.Request, _ []string) string {
|
||||
return r.URL.Path
|
||||
}, func(v, want string) bool {
|
||||
return strutils.GlobMatch(want, v)
|
||||
}},
|
||||
"remote": {0, func(r *http.Request, _ []string) string {
|
||||
return r.RemoteAddr
|
||||
}, nil},
|
||||
}
|
||||
|
||||
var commands = map[string]CommandBuilder{
|
||||
"rewrite": {2, func(args []string) CommandExecutor {
|
||||
orig, repl := args[0], args[1]
|
||||
return CommandExecutor{
|
||||
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, orig, repl, 1)
|
||||
r.URL.RawPath = r.URL.EscapedPath()
|
||||
r.RequestURI = r.URL.String()
|
||||
},
|
||||
proceed: true,
|
||||
}
|
||||
}},
|
||||
"serve": {1, func(args []string) CommandExecutor {
|
||||
return CommandExecutor{
|
||||
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, path.Join(args[0], path.Clean(r.URL.Path)))
|
||||
},
|
||||
proceed: false,
|
||||
}
|
||||
}},
|
||||
"redirect": {1, func(args []string) CommandExecutor {
|
||||
target := args[0]
|
||||
return CommandExecutor{
|
||||
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
|
||||
},
|
||||
proceed: false,
|
||||
}
|
||||
}},
|
||||
"error": {2, func(args []string) CommandExecutor {
|
||||
codeStr, text := args[0], args[1]
|
||||
code, err := strconv.Atoi(codeStr)
|
||||
if err != nil {
|
||||
code = http.StatusNotFound
|
||||
}
|
||||
return CommandExecutor{
|
||||
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, text, code)
|
||||
},
|
||||
proceed: false,
|
||||
}
|
||||
}},
|
||||
"proxy": {1, func(args []string) CommandExecutor {
|
||||
target := args[0]
|
||||
return CommandExecutor{
|
||||
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Scheme = "http"
|
||||
r.URL.Host = target
|
||||
r.URL.RawPath = r.URL.EscapedPath()
|
||||
r.RequestURI = r.URL.String()
|
||||
},
|
||||
proceed: true,
|
||||
}
|
||||
}},
|
||||
}
|
||||
|
||||
var escapedChars = map[rune]rune{
|
||||
'n': '\n',
|
||||
't': '\t',
|
||||
'r': '\r',
|
||||
'\'': '\'',
|
||||
'"': '"',
|
||||
' ': ' ',
|
||||
}
|
||||
|
||||
// BuildHandler returns a http.HandlerFunc that implements the rules.
|
||||
//
|
||||
// Bypass rules are executed first
|
||||
// if a bypass rule matches,
|
||||
// the request is passed to the upstream and no more rules are executed.
|
||||
//
|
||||
// Other rules are executed later
|
||||
// if no rule matches, the default rule is executed
|
||||
// if no rule matches and default rule is not set,
|
||||
// the request is passed to the upstream.
|
||||
func (rules Rules) BuildHandler(up *gphttp.ReverseProxy) http.HandlerFunc {
|
||||
// move bypass rules to the front.
|
||||
bypassRules := make(Rules, 0, len(rules))
|
||||
otherRules := make(Rules, 0, len(rules))
|
||||
|
||||
var defaultRule Rule
|
||||
|
||||
for _, rule := range rules {
|
||||
switch {
|
||||
case rule.Do.isBypass():
|
||||
bypassRules = append(bypassRules, rule)
|
||||
case rule.Name == "default":
|
||||
defaultRule = rule
|
||||
default:
|
||||
otherRules = append(otherRules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
// free allocated empty slices
|
||||
// before passing them to the handler.
|
||||
if len(bypassRules) == 0 {
|
||||
bypassRules = []Rule{}
|
||||
}
|
||||
if len(otherRules) == 0 {
|
||||
otherRules = []Rule{defaultRule}
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
hasMatch := false
|
||||
for _, rule := range bypassRules {
|
||||
if rule.On.MatchAll(r) {
|
||||
up.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, rule := range otherRules {
|
||||
if rule.On.MatchAll(r) {
|
||||
hasMatch = true
|
||||
rule.Do.HandlerFunc(w, r)
|
||||
if !rule.Do.proceed {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasMatch || defaultRule.Do.isBypass() {
|
||||
up.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
defaultRule.Do.HandlerFunc(w, r)
|
||||
if !defaultRule.Do.proceed {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse line to subject and args
|
||||
// with support for quotes and escaped chars, e.g.
|
||||
//
|
||||
// error 403 "Forbidden 'foo' 'bar'"
|
||||
// error 403 Forbidden\ \"foo\"\ \"bar\".
|
||||
func parse(v string) (subject string, args []string, err E.Error) {
|
||||
v = strings.TrimSpace(v)
|
||||
var buf strings.Builder
|
||||
escaped := false
|
||||
quotes := make([]rune, 0, 4)
|
||||
flush := func() {
|
||||
if subject == "" {
|
||||
subject = buf.String()
|
||||
} else {
|
||||
args = append(args, buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
for _, r := range v {
|
||||
if escaped {
|
||||
if ch, ok := escapedChars[r]; ok {
|
||||
buf.WriteRune(ch)
|
||||
} else {
|
||||
err = ErrUnsupportedEscapeChar.Subjectf("\\%c", r)
|
||||
return
|
||||
}
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '\\':
|
||||
escaped = true
|
||||
continue
|
||||
case '"', '\'':
|
||||
switch {
|
||||
case len(quotes) > 0 && quotes[len(quotes)-1] == r:
|
||||
quotes = quotes[:len(quotes)-1]
|
||||
if len(quotes) == 0 {
|
||||
flush()
|
||||
} else {
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
case len(quotes) == 0:
|
||||
quotes = append(quotes, r)
|
||||
default:
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
case ' ':
|
||||
flush()
|
||||
default:
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
if len(quotes) > 0 {
|
||||
err = ErrUnterminatedQuotes
|
||||
} else {
|
||||
flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (on *RuleOn) Parse(v string) E.Error {
|
||||
lines := strutils.SplitLine(v)
|
||||
on.checkers = make([]CheckFulfill, 0, len(lines))
|
||||
on.raw = v
|
||||
|
||||
errs := E.NewBuilder("rule.on syntax errors")
|
||||
for i, line := range lines {
|
||||
subject, args, err := parse(line)
|
||||
if err != nil {
|
||||
errs.Add(err.Subjectf("line %d", i+1))
|
||||
continue
|
||||
}
|
||||
retriever, ok := retrievers[subject]
|
||||
if !ok {
|
||||
errs.Add(ErrInvalidCriteriaTarget.Subject(subject).Subjectf("line %d", i+1))
|
||||
continue
|
||||
}
|
||||
nArgs := retriever.expectedArgs
|
||||
if len(args) != nArgs+1 {
|
||||
errs.Add(ErrInvalidArguments.Subject(subject).Subjectf("line %d", i+1))
|
||||
continue
|
||||
}
|
||||
equal := retriever.equal
|
||||
if equal == nil {
|
||||
equal = func(a, b string) bool {
|
||||
return a == b
|
||||
}
|
||||
}
|
||||
on.checkers = append(on.checkers, func(r *http.Request) bool {
|
||||
return equal(retriever.retrieve(r, args[:nArgs]), args[nArgs])
|
||||
})
|
||||
}
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
func (on *RuleOn) MatchAll(r *http.Request) bool {
|
||||
for _, match := range on.checkers {
|
||||
if !match(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (cmd *Command) Parse(v string) E.Error {
|
||||
cmd.raw = v
|
||||
directive, args, err := parse(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if directive == "bypass" {
|
||||
if len(args) != 0 {
|
||||
return ErrInvalidArguments.Subject(directive)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
builder, ok := commands[directive]
|
||||
if !ok {
|
||||
return ErrUnknownDirective.Subject(directive)
|
||||
}
|
||||
if len(args) != builder.expectedArgs {
|
||||
return ErrInvalidArguments.Subject(directive)
|
||||
}
|
||||
cmd.CommandExecutor = builder.build(args)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *Command) isBypass() bool {
|
||||
return cmd.HandlerFunc == nil
|
||||
}
|
||||
|
||||
func (on *RuleOn) String() string {
|
||||
return on.raw
|
||||
}
|
||||
|
||||
func (on *RuleOn) MarshalJSON() ([]byte, error) {
|
||||
return []byte("\"" + on.String() + "\""), nil
|
||||
}
|
||||
|
||||
func (cmd *Command) String() string {
|
||||
return cmd.raw
|
||||
}
|
||||
|
||||
func (cmd *Command) MarshalJSON() ([]byte, error) {
|
||||
return []byte("\"" + cmd.String() + "\""), nil
|
||||
}
|
146
internal/route/types/rules_test.go
Normal file
146
internal/route/types/rules_test.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestParseSubjectArgs(t *testing.T) {
|
||||
t.Run("without quotes", func(t *testing.T) {
|
||||
subject, args, err := parse("rewrite / /foo/bar")
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, subject, "rewrite")
|
||||
ExpectDeepEqual(t, args, []string{"/", "/foo/bar"})
|
||||
})
|
||||
t.Run("with quotes", func(t *testing.T) {
|
||||
subject, args, err := parse(`error 403 "Forbidden 'foo' 'bar'."`)
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, subject, "error")
|
||||
ExpectDeepEqual(t, args, []string{"403", "Forbidden 'foo' 'bar'."})
|
||||
})
|
||||
t.Run("with escaped", func(t *testing.T) {
|
||||
subject, args, err := parse(`error 403 Forbidden\ \"foo\"\ \"bar\".`)
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, subject, "error")
|
||||
ExpectDeepEqual(t, args, []string{"403", "Forbidden \"foo\" \"bar\"."})
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseCommands(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr error
|
||||
}{
|
||||
// bypass tests
|
||||
{
|
||||
name: "bypass_valid",
|
||||
input: "bypass",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "bypass_invalid_with_args",
|
||||
input: "bypass /",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
// rewrite tests
|
||||
{
|
||||
name: "rewrite_valid",
|
||||
input: "rewrite / /foo/bar",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "rewrite_missing_target",
|
||||
input: "rewrite /",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "rewrite_too_many_args",
|
||||
input: "rewrite / / /",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
// serve tests
|
||||
{
|
||||
name: "serve_valid",
|
||||
input: "serve /var/www",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "serve_missing_path",
|
||||
input: "serve ",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "serve_too_many_args",
|
||||
input: "serve / / /",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
// redirect tests
|
||||
{
|
||||
name: "redirect_valid",
|
||||
input: "redirect /",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "redirect_too_many_args",
|
||||
input: "redirect / /",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
// error directive tests
|
||||
{
|
||||
name: "error_valid",
|
||||
input: "error 404 Not\\ Found",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "error_missing_status_code",
|
||||
input: "error Not\\ Found",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "error_too_many_args",
|
||||
input: "error 404 Not\\ Found extra",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "error_unescaped_space",
|
||||
input: "error 404 Not Found",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
// proxy directive tests
|
||||
{
|
||||
name: "proxy_valid",
|
||||
input: "proxy localhost:8080",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "proxy_missing_target",
|
||||
input: "proxy",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "proxy_too_many_args",
|
||||
input: "proxy localhost:8080 extra",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
// unknown directive test
|
||||
{
|
||||
name: "unknown_directive",
|
||||
input: "unknown /",
|
||||
wantErr: ErrUnknownDirective,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := Command{}
|
||||
err := cmd.Parse(tt.input)
|
||||
if tt.wantErr != nil {
|
||||
ExpectError(t, tt.wantErr, err)
|
||||
} else {
|
||||
ExpectNoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
28
internal/utils/strutils/glob.go
Normal file
28
internal/utils/strutils/glob.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package strutils
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
)
|
||||
|
||||
var (
|
||||
globPatterns = make(map[string]glob.Glob)
|
||||
globPatternsMu sync.Mutex
|
||||
)
|
||||
|
||||
func GlobMatch(pattern string, s string) bool {
|
||||
if glob, ok := globPatterns[pattern]; ok {
|
||||
return glob.Match(s)
|
||||
}
|
||||
|
||||
globPatternsMu.Lock()
|
||||
defer globPatternsMu.Unlock()
|
||||
|
||||
glob, err := glob.Compile(pattern)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
globPatterns[pattern] = glob
|
||||
return glob.Match(s)
|
||||
}
|
Loading…
Add table
Reference in a new issue