mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-17 23:03:49 +01:00
* chore(deps): update submodule goutils * docs(http): remove default client from README.md * refactor(rules): introduce block DSL, phase-based execution, and flow validation - add block syntax parser/scanner with nested @blocks and elif/else support - restructure rule execution into explicit pre/post phases with phase flags - classify commands by phase and termination behavior - enforce flow semantics (default rule handling, dead-rule detection) - expand HTTP flow coverage with block + YAML parity tests and benches - refresh rules README/spec and update playground/docs integration - Default rules act as fallback handlers that execute only when no matching non-default rule exists in the pre phase - IfElseBlockCommand now returns early when a condition matches with a nil Do block, instead of falling through to else blocks - Add nil check for auth handler to allow requests when no auth is configured * fix(rules): buffer log output before writing to stdout/stderr * refactor(api/rules): remove IsResponseRule field from ParsedRule and related logic * docs(rules): update examples to use block syntax
337 lines
8.9 KiB
Go
337 lines
8.9 KiB
Go
package rules
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
httputils "github.com/yusing/goutils/http"
|
|
ioutils "github.com/yusing/goutils/io"
|
|
)
|
|
|
|
type (
|
|
FieldHandler struct {
|
|
set, add, remove HandlerFunc
|
|
}
|
|
FieldModifier string
|
|
)
|
|
|
|
const (
|
|
ModFieldSet FieldModifier = "set"
|
|
ModFieldAdd FieldModifier = "add"
|
|
ModFieldRemove FieldModifier = "remove"
|
|
)
|
|
|
|
const (
|
|
FieldHeader = "header"
|
|
FieldResponseHeader = "resp_header"
|
|
FieldQuery = "query"
|
|
FieldCookie = "cookie"
|
|
FieldBody = "body"
|
|
FieldResponseBody = "resp_body"
|
|
FieldStatusCode = "status"
|
|
)
|
|
|
|
var AllFields = []string{FieldHeader, FieldResponseHeader, FieldQuery, FieldCookie, FieldBody, FieldResponseBody, FieldStatusCode}
|
|
|
|
// NOTE: should not use canonicalized header keys, respect to user's input
|
|
var modFields = map[string]struct {
|
|
help Help
|
|
validate ValidateFunc
|
|
builder func(args any) *FieldHandler
|
|
}{
|
|
FieldHeader: {
|
|
help: Help{
|
|
command: FieldHeader,
|
|
args: map[string]string{
|
|
"key": "the header key",
|
|
"value": "the header template",
|
|
},
|
|
},
|
|
validate: validatePreRequestKVTemplate,
|
|
builder: func(args any) *FieldHandler {
|
|
k, tmpl := args.(*keyValueTemplate).Unpack()
|
|
return &FieldHandler{
|
|
set: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
v, _, err := tmpl.ExpandVarsToString(w, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Header[k] = []string{v}
|
|
return nil
|
|
},
|
|
add: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
v, _, err := tmpl.ExpandVarsToString(w, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Header[k] = append(r.Header[k], v)
|
|
return nil
|
|
},
|
|
remove: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
delete(r.Header, k)
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
FieldResponseHeader: {
|
|
help: Help{
|
|
command: FieldResponseHeader,
|
|
args: map[string]string{
|
|
"key": "the response header key",
|
|
"value": "the response header template",
|
|
},
|
|
},
|
|
validate: validatePostResponseKVTemplate,
|
|
builder: func(args any) *FieldHandler {
|
|
k, tmpl := args.(*keyValueTemplate).Unpack()
|
|
return &FieldHandler{
|
|
set: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
v, _, err := tmpl.ExpandVarsToString(w, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.Header()[k] = []string{v}
|
|
return nil
|
|
},
|
|
add: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
v, _, err := tmpl.ExpandVarsToString(w, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.Header()[k] = append(w.Header()[k], v)
|
|
return nil
|
|
},
|
|
remove: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
delete(w.Header(), k)
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
FieldQuery: {
|
|
help: Help{
|
|
command: FieldQuery,
|
|
args: map[string]string{
|
|
"key": "the query key",
|
|
"value": "the query template",
|
|
},
|
|
},
|
|
validate: validatePreRequestKVTemplate,
|
|
builder: func(args any) *FieldHandler {
|
|
k, tmpl := args.(*keyValueTemplate).Unpack()
|
|
return &FieldHandler{
|
|
set: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
v, _, err := tmpl.ExpandVarsToString(w, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.SharedData().UpdateQueries(r, func(queries url.Values) {
|
|
queries.Set(k, v)
|
|
})
|
|
return nil
|
|
},
|
|
add: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
v, _, err := tmpl.ExpandVarsToString(w, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.SharedData().UpdateQueries(r, func(queries url.Values) {
|
|
queries.Add(k, v)
|
|
})
|
|
return nil
|
|
},
|
|
remove: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
w.SharedData().UpdateQueries(r, func(queries url.Values) {
|
|
queries.Del(k)
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
FieldCookie: {
|
|
help: Help{
|
|
command: FieldCookie,
|
|
args: map[string]string{
|
|
"key": "the cookie key",
|
|
"value": "the cookie value",
|
|
},
|
|
},
|
|
validate: validatePreRequestKVTemplate,
|
|
builder: func(args any) *FieldHandler {
|
|
k, tmpl := args.(*keyValueTemplate).Unpack()
|
|
return &FieldHandler{
|
|
set: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
v, _, err := tmpl.ExpandVarsToString(w, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.SharedData().UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
|
|
for i, c := range cookies {
|
|
if c.Name == k {
|
|
cookies[i].Value = v
|
|
return cookies
|
|
}
|
|
}
|
|
return append(cookies, &http.Cookie{Name: k, Value: v})
|
|
})
|
|
return nil
|
|
},
|
|
add: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
v, _, err := tmpl.ExpandVarsToString(w, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.SharedData().UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
|
|
return append(cookies, &http.Cookie{Name: k, Value: v})
|
|
})
|
|
return nil
|
|
},
|
|
remove: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
w.SharedData().UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
|
|
index := -1
|
|
for i, c := range cookies {
|
|
if c.Name == k {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
if index != -1 {
|
|
if len(cookies) == 1 {
|
|
return []*http.Cookie{}
|
|
}
|
|
return append(cookies[:index], cookies[index+1:]...)
|
|
}
|
|
return cookies
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
FieldBody: {
|
|
help: Help{
|
|
command: FieldBody,
|
|
description: makeLines(
|
|
"Override the request body that will be sent to the upstream",
|
|
"The template supports the following variables:",
|
|
helpListItem("Request", "the request object"),
|
|
"",
|
|
"Example:",
|
|
helpExample(FieldBody, "HTTP STATUS: $req_method $req_path"),
|
|
),
|
|
args: map[string]string{
|
|
"template": "the body template",
|
|
},
|
|
},
|
|
validate: func(args []string) (phase PhaseFlag, parsedArgs any, err error) {
|
|
if len(args) != 1 {
|
|
return 0, nil, ErrExpectOneArg
|
|
}
|
|
phase = PhasePre
|
|
tmplReq, parsedArgs, err := validateTemplate(args[0], true)
|
|
phase |= tmplReq
|
|
return
|
|
},
|
|
builder: func(args any) *FieldHandler {
|
|
tmpl := args.(templateString)
|
|
return &FieldHandler{
|
|
set: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
if r.Body != nil {
|
|
r.Body.Close()
|
|
r.Body = nil
|
|
}
|
|
|
|
bufPool := w.BufPool()
|
|
b := bufPool.GetBuffer()
|
|
_, err := tmpl.ExpandVars(w, r, b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Body = ioutils.NewHookReadCloser(io.NopCloser(b), func() {
|
|
bufPool.PutBuffer(b)
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
FieldResponseBody: {
|
|
help: Help{
|
|
command: FieldResponseBody,
|
|
description: makeLines(
|
|
"Override the response body that will be sent to the client",
|
|
"The template supports the following variables:",
|
|
helpListItem("Request", "the request object"),
|
|
helpListItem("Response", "the response object"),
|
|
"",
|
|
"Example:",
|
|
helpExample(FieldResponseBody, "HTTP STATUS: $req_method $status_code"),
|
|
),
|
|
args: map[string]string{
|
|
"template": "the response body template",
|
|
},
|
|
},
|
|
validate: func(args []string) (phase PhaseFlag, parsedArgs any, err error) {
|
|
if len(args) != 1 {
|
|
return 0, nil, ErrExpectOneArg
|
|
}
|
|
phase = PhasePost
|
|
tmplReq, parsedArgs, err := validateTemplate(args[0], true)
|
|
phase |= tmplReq
|
|
return
|
|
},
|
|
builder: func(args any) *FieldHandler {
|
|
tmpl := args.(templateString)
|
|
return &FieldHandler{
|
|
set: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
w.ResetBody()
|
|
_, err := tmpl.ExpandVars(w, r, w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
FieldStatusCode: {
|
|
help: Help{
|
|
command: FieldStatusCode,
|
|
description: makeLines(
|
|
"Override the status code that will be sent to the client, e.g.:",
|
|
helpExample(FieldStatusCode, "200"),
|
|
),
|
|
args: map[string]string{
|
|
"code": "the status code",
|
|
},
|
|
},
|
|
validate: func(args []string) (phase PhaseFlag, parsedArgs any, err error) {
|
|
if len(args) != 1 {
|
|
return phase, nil, ErrExpectOneArg
|
|
}
|
|
phase = PhasePost
|
|
status, err := strconv.Atoi(args[0])
|
|
if err != nil {
|
|
return phase, nil, ErrInvalidArguments.With(err)
|
|
}
|
|
if status < 100 || status > 599 {
|
|
return phase, nil, ErrInvalidArguments.Withf("status code must be between 100 and 599, got %d", status)
|
|
}
|
|
return phase, status, nil
|
|
},
|
|
builder: func(args any) *FieldHandler {
|
|
status := args.(int)
|
|
return &FieldHandler{
|
|
set: func(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
w.WriteHeader(status)
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}
|