package rules import ( "net" "net/http" "slices" "strings" "github.com/yusing/godoxy/internal/route/routes" gperr "github.com/yusing/goutils/errs" httputils "github.com/yusing/goutils/http" ) type RuleOn struct { raw string checker Checker isResponseChecker bool } func (on *RuleOn) IsResponseChecker() bool { return on.isResponseChecker } func (on *RuleOn) Check(w http.ResponseWriter, r *http.Request) bool { return on.checker.Check(w, r) } const ( OnDefault = "default" OnHeader = "header" OnQuery = "query" OnCookie = "cookie" OnForm = "form" OnPostForm = "postform" OnProto = "proto" OnMethod = "method" OnHost = "host" OnPath = "path" OnRemote = "remote" OnBasicAuth = "basic_auth" OnRoute = "route" // on response OnResponseHeader = "resp_header" OnStatus = "status" ) var checkers = map[string]struct { help Help validate ValidateFunc builder func(args any) CheckFunc isResponseChecker bool }{ OnDefault: { help: Help{ command: OnDefault, description: makeLines( "The default rule is matched when no other rules are matched.", ), args: map[string]string{}, }, validate: func(args []string) (any, gperr.Error) { if len(args) != 0 { return nil, ErrExpectNoArg } return nil, nil }, builder: func(args any) CheckFunc { return func(w http.ResponseWriter, r *http.Request) bool { return false } }, // this should never be called }, OnHeader: { help: Help{ command: OnHeader, description: makeLines( "Value supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnHeader, "username", "user"), helpExample(OnHeader, "username", helpFuncCall("glob", "user*")), helpExample(OnHeader, "username", helpFuncCall("regex", "user.*")), ), args: map[string]string{ "key": "the header key", "[value]": "the header value", }, }, validate: toKVOptionalVMatcher, builder: func(args any) CheckFunc { k, matcher := args.(*MapValueMatcher).Unpack() if matcher == nil { return func(w http.ResponseWriter, r *http.Request) bool { return len(r.Header[k]) > 0 } } return func(w http.ResponseWriter, r *http.Request) bool { return slices.ContainsFunc(r.Header[k], matcher) } }, }, OnResponseHeader: { isResponseChecker: true, help: Help{ command: OnResponseHeader, description: makeLines( "Value supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnResponseHeader, "username", "user"), helpExample(OnResponseHeader, "username", helpFuncCall("glob", "user*")), helpExample(OnResponseHeader, "username", helpFuncCall("regex", "user.*")), ), args: map[string]string{ "key": "the response header key", "[value]": "the response header value", }, }, validate: toKVOptionalVMatcher, builder: func(args any) CheckFunc { k, matcher := args.(*MapValueMatcher).Unpack() if matcher == nil { return func(w http.ResponseWriter, r *http.Request) bool { return len(httputils.GetInitResponseModifier(w).Header()[k]) > 0 } } return func(w http.ResponseWriter, r *http.Request) bool { return slices.ContainsFunc(httputils.GetInitResponseModifier(w).Header()[k], matcher) } }, }, OnQuery: { help: Help{ command: OnQuery, description: makeLines( "Value supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnQuery, "username", "user"), helpExample(OnQuery, "username", helpFuncCall("glob", "user*")), helpExample(OnQuery, "username", helpFuncCall("regex", "user.*")), ), args: map[string]string{ "key": "the query key", "[value]": "the query value", }, }, validate: toKVOptionalVMatcher, builder: func(args any) CheckFunc { k, matcher := args.(*MapValueMatcher).Unpack() if matcher == nil { return func(w http.ResponseWriter, r *http.Request) bool { return len(httputils.GetSharedData(w).GetQueries(r)[k]) > 0 } } return func(w http.ResponseWriter, r *http.Request) bool { return slices.ContainsFunc(httputils.GetSharedData(w).GetQueries(r)[k], matcher) } }, }, OnCookie: { help: Help{ command: OnCookie, description: makeLines( "Value supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnCookie, "username", "user"), helpExample(OnCookie, "username", helpFuncCall("glob", "user*")), helpExample(OnCookie, "username", helpFuncCall("regex", "user.*")), ), args: map[string]string{ "key": "the cookie key", "[value]": "the cookie value", }, }, validate: toKVOptionalVMatcher, builder: func(args any) CheckFunc { k, matcher := args.(*MapValueMatcher).Unpack() if matcher == nil { return func(w http.ResponseWriter, r *http.Request) bool { cookies := httputils.GetSharedData(w).GetCookies(r) for _, cookie := range cookies { if cookie.Name == k { return true } } return false } } return func(w http.ResponseWriter, r *http.Request) bool { cookies := httputils.GetSharedData(w).GetCookies(r) for _, cookie := range cookies { if cookie.Name == k { if matcher(cookie.Value) { return true } } } return false } }, }, OnForm: { help: Help{ command: OnForm, description: makeLines( "Value supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnForm, "username", "user"), helpExample(OnForm, "username", helpFuncCall("glob", "user*")), helpExample(OnForm, "username", helpFuncCall("regex", "user.*")), ), args: map[string]string{ "key": "the form key", "[value]": "the form value", }, }, validate: toKVOptionalVMatcher, builder: func(args any) CheckFunc { k, matcher := args.(*MapValueMatcher).Unpack() if matcher == nil { return func(w http.ResponseWriter, r *http.Request) bool { return r.FormValue(k) != "" } } return func(w http.ResponseWriter, r *http.Request) bool { return matcher(r.FormValue(k)) } }, }, OnPostForm: { help: Help{ command: OnPostForm, description: makeLines( "Value supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnPostForm, "username", "user"), helpExample(OnPostForm, "username", helpFuncCall("glob", "user*")), helpExample(OnPostForm, "username", helpFuncCall("regex", "user.*")), ), args: map[string]string{ "key": "the form key", "[value]": "the form value", }, }, validate: toKVOptionalVMatcher, builder: func(args any) CheckFunc { k, matcher := args.(*MapValueMatcher).Unpack() if matcher == nil { return func(w http.ResponseWriter, r *http.Request) bool { return r.PostFormValue(k) != "" } } return func(w http.ResponseWriter, r *http.Request) bool { return matcher(r.PostFormValue(k)) } }, }, OnProto: { help: Help{ command: OnProto, args: map[string]string{ "proto": "the http protocol (http, https, h3)", }, }, validate: func(args []string) (any, gperr.Error) { if len(args) != 1 { return nil, ErrExpectOneArg } proto := args[0] if proto != "http" && proto != "https" && proto != "h3" { return nil, ErrInvalidArguments.Withf("proto: %q", proto) } return proto, nil }, builder: func(args any) CheckFunc { proto := args.(string) switch proto { case "http": return func(w http.ResponseWriter, r *http.Request) bool { return r.TLS == nil } case "https": return func(w http.ResponseWriter, r *http.Request) bool { return r.TLS != nil } default: // h3 return func(w http.ResponseWriter, r *http.Request) bool { return r.TLS != nil && r.ProtoMajor == 3 } } }, }, OnMethod: { help: Help{ command: OnMethod, args: map[string]string{ "method": "the http method", }, }, validate: validateMethod, builder: func(args any) CheckFunc { method := args.(string) return func(w http.ResponseWriter, r *http.Request) bool { return r.Method == method } }, }, OnHost: { help: Help{ command: OnHost, description: makeLines( "Supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnHost, "example.com"), helpExample(OnHost, helpFuncCall("glob", "example*.com")), helpExample(OnHost, helpFuncCall("regex", `(example\w+\.com)`)), helpExample(OnHost, helpFuncCall("regex", `example\.com$`)), ), args: map[string]string{ "host": "the host name", }, }, validate: validateSingleMatcher, builder: func(args any) CheckFunc { matcher := args.(Matcher) return func(w http.ResponseWriter, r *http.Request) bool { return matcher(r.Host) } }, }, OnPath: { help: Help{ command: OnPath, description: makeLines( "Supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnPath, "/path/to"), helpExample(OnPath, helpFuncCall("glob", "/path/to/*")), helpExample(OnPath, helpFuncCall("regex", `^/path/to/.*$`)), helpExample(OnPath, helpFuncCall("regex", `/path/[A-Z]+/`)), ), args: map[string]string{ "path": "the request path", }, }, validate: validateURLPathMatcher, builder: func(args any) CheckFunc { matcher := args.(Matcher) return func(w http.ResponseWriter, r *http.Request) bool { reqPath := r.URL.Path if len(reqPath) > 0 && reqPath[0] != '/' { reqPath = "/" + reqPath } return matcher(reqPath) } }, }, OnRemote: { help: Help{ command: OnRemote, args: map[string]string{ "ip|cidr": "the remote ip or cidr", }, }, validate: validateCIDR, builder: func(args any) CheckFunc { ipnet := args.(*net.IPNet) // for /32 (IPv4) or /128 (IPv6), just compare the IP if ones, bits := ipnet.Mask.Size(); ones == bits { wantIP := ipnet.IP return func(w http.ResponseWriter, r *http.Request) bool { ip := httputils.GetSharedData(w).GetRemoteIP(r) if ip == nil { return false } return ip.Equal(wantIP) } } return func(w http.ResponseWriter, r *http.Request) bool { ip := httputils.GetSharedData(w).GetRemoteIP(r) if ip == nil { return false } return ipnet.Contains(ip) } }, }, OnBasicAuth: { help: Help{ command: OnBasicAuth, args: map[string]string{ "username": "the username", "password": "the password encrypted with bcrypt", }, }, validate: validateUserBCryptPassword, builder: func(args any) CheckFunc { cred := args.(*HashedCrendentials) return func(w http.ResponseWriter, r *http.Request) bool { return cred.Match(httputils.GetSharedData(w).GetBasicAuth(r)) } }, }, OnRoute: { help: Help{ command: OnRoute, description: makeLines( "Supports string, glob pattern, or regex pattern, e.g.:", helpExample(OnRoute, "example"), helpExample(OnRoute, helpFuncCall("glob", "example*")), helpExample(OnRoute, helpFuncCall("regex", "example\\w+")), ), args: map[string]string{ "route": "the route name", }, }, validate: validateSingleMatcher, builder: func(args any) CheckFunc { matcher := args.(Matcher) return func(_ http.ResponseWriter, r *http.Request) bool { return matcher(routes.TryGetUpstreamName(r)) } }, }, OnStatus: { isResponseChecker: true, help: Help{ command: OnStatus, description: makeLines( "Supported formats are:", helpExample(OnStatus, ""), helpExample(OnStatus, "-"), helpExample(OnStatus, "1xx"), helpExample(OnStatus, "2xx"), helpExample(OnStatus, "3xx"), helpExample(OnStatus, "4xx"), helpExample(OnStatus, "5xx"), ), args: map[string]string{ "status": "the status code range", }, }, validate: validateStatusRange, builder: func(args any) CheckFunc { beg, end := args.(*IntTuple).Unpack() if beg == end { return func(w http.ResponseWriter, _ *http.Request) bool { return httputils.GetInitResponseModifier(w).StatusCode() == beg } } return func(w http.ResponseWriter, _ *http.Request) bool { statusCode := httputils.GetInitResponseModifier(w).StatusCode() return statusCode >= beg && statusCode <= end } }, }, } var ( asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} andSeps = [256]uint8{'&': 1, '\n': 1} ) func indexAnd(s string) int { for i := range s { if andSeps[s[i]] != 0 { return i } } return -1 } func countAnd(s string) int { n := 0 for i := range s { if andSeps[s[i]] != 0 { n++ } } return n } // splitAnd splits a string by "&" and "\n" with all spaces removed. // empty strings are not included in the result. func splitAnd(s string) []string { if s == "" { return []string{} } n := countAnd(s) a := make([]string, n+1) i := 0 for i < n { end := indexAnd(s) if end == -1 { break } beg := 0 // trim leading spaces for beg < end && asciiSpace[s[beg]] != 0 { beg++ } // trim trailing spaces next := end + 1 for end-1 > beg && asciiSpace[s[end-1]] != 0 { end-- } // skip empty segments if end > beg { a[i] = s[beg:end] i++ } s = s[next:] } s = strings.TrimSpace(s) if s != "" { a[i] = s i++ } return a[:i] } // splitPipe splits a string by "|" but respects quotes, brackets, and escaped characters. // It's similar to the parser.go logic but specifically for pipe splitting. func splitPipe(s string) []string { if s == "" { return []string{} } var result []string var current strings.Builder escaped := false quote := rune(0) brackets := 0 for _, r := range s { if escaped { current.WriteRune(r) escaped = false continue } switch r { case '\\': escaped = true current.WriteRune(r) case '"', '\'', '`': if quote == 0 && brackets == 0 { quote = r } else if r == quote { quote = 0 } current.WriteRune(r) case '(': brackets++ current.WriteRune(r) case ')': if brackets > 0 { brackets-- } current.WriteRune(r) case '|': if quote == 0 && brackets == 0 { // Found a pipe outside quotes/brackets, split here result = append(result, strings.TrimSpace(current.String())) current.Reset() } else { current.WriteRune(r) } default: current.WriteRune(r) } } // Add the last part if current.Len() > 0 { result = append(result, strings.TrimSpace(current.String())) } return result } // Parse implements strutils.Parser. func (on *RuleOn) Parse(v string) error { on.raw = v rules := splitAnd(v) checkAnd := make(CheckMatchAll, 0, len(rules)) errs := gperr.NewBuilder("rule.on syntax errors") isResponseChecker := false for i, rule := range rules { if rule == "" { continue } parsed, isResp, err := parseOn(rule) if err != nil { errs.Add(err.Subjectf("line %d", i+1)) continue } if isResp { isResponseChecker = true } checkAnd = append(checkAnd, parsed) } on.checker = checkAnd on.isResponseChecker = isResponseChecker return errs.Error() } func (on *RuleOn) String() string { return on.raw } func (on *RuleOn) MarshalText() ([]byte, error) { return []byte(on.String()), nil } func parseOn(line string) (Checker, bool, gperr.Error) { ors := splitPipe(line) if len(ors) > 1 { errs := gperr.NewBuilder("rule.on syntax errors") checkOr := make(CheckMatchSingle, len(ors)) isResponseChecker := false for i, or := range ors { curCheckers, isResp, err := parseOn(or) if err != nil { errs.Add(err) continue } if isResp { isResponseChecker = true } checkOr[i] = curCheckers.(CheckFunc) } if err := errs.Error(); err != nil { return nil, false, err } return checkOr, isResponseChecker, nil } subject, args, err := parse(line) if err != nil { return nil, false, err } negate := false if strings.HasPrefix(subject, "!") { negate = true subject = subject[1:] } checker, ok := checkers[subject] if !ok { return nil, false, ErrInvalidOnTarget.Subject(subject) } validArgs, err := checker.validate(args) if err != nil { return nil, false, err.With(checker.help.Error()) } checkFunc := checker.builder(validArgs) if negate { origCheckFunc := checkFunc checkFunc = func(w http.ResponseWriter, r *http.Request) bool { return !origCheckFunc(w, r) } } return checkFunc, checker.isResponseChecker, nil }