From e5a5ffda91440605584da16c07a4dcffe985f905 Mon Sep 17 00:00:00 2001 From: Liujian <824010343@qq.com> Date: Fri, 19 Dec 2025 18:06:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=93=8D=E5=BA=94=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/plugins/response-filter/config.go | 27 +++- drivers/plugins/response-filter/executor.go | 24 ++-- drivers/plugins/response-filter/factory.go | 54 +++++--- drivers/plugins/response-filter/filter.go | 126 +++++++++++++++++++ drivers/plugins/response-filter/rule.go | 133 ++++++++++++++++++++ go.mod | 10 +- node/http-context/header.go | 7 ++ utils/json_path.go | 57 +++++++++ 8 files changed, 398 insertions(+), 40 deletions(-) create mode 100644 drivers/plugins/response-filter/filter.go create mode 100644 drivers/plugins/response-filter/rule.go create mode 100644 utils/json_path.go diff --git a/drivers/plugins/response-filter/config.go b/drivers/plugins/response-filter/config.go index 98fbae94..3ab2508d 100644 --- a/drivers/plugins/response-filter/config.go +++ b/drivers/plugins/response-filter/config.go @@ -1,6 +1,29 @@ package response_filter +import ( + "fmt" + "github.com/eolinker/eosc" +) + type Config struct { - BodyFilter []string `json:"body_filter" label:"响应体过滤字段"` - HeaderFilter []string `json:"header_filter" label:"响应头过滤字段"` + BodyFilter []string `json:"body_filter" label:"响应体过滤字段"` + HeaderFilter []string `json:"header_filter" label:"响应头过滤字段"` + HeaderFilterType string `json:"header_filter_type" label:"响应头过滤类型" enum:"black,white" default:"black"` + BodyFilterType string `json:"body_filter_type" label:"响应体过滤类型" enum:"black,white" default:"black"` +} + +func check(conf *Config, workers map[eosc.RequireId]eosc.IWorker) error { + if conf.HeaderFilterType == "" { + conf.HeaderFilterType = "black" + } + if conf.HeaderFilterType != "white" && conf.HeaderFilterType != "black" { + return fmt.Errorf("header_filter_type must be white or black") + } + if conf.BodyFilterType == "" { + conf.BodyFilterType = "black" + } + if conf.BodyFilterType != "white" && conf.BodyFilterType != "black" { + return fmt.Errorf("body_filter_type must be white or black") + } + return nil } diff --git a/drivers/plugins/response-filter/executor.go b/drivers/plugins/response-filter/executor.go index efb9cb29..e3245d6b 100644 --- a/drivers/plugins/response-filter/executor.go +++ b/drivers/plugins/response-filter/executor.go @@ -1,9 +1,6 @@ package response_filter import ( - "github.com/ohler55/ojg/jp" - "github.com/ohler55/ojg/oj" - "github.com/eolinker/apinto/drivers" "github.com/eolinker/eosc" "github.com/eolinker/eosc/eocontext" @@ -16,8 +13,7 @@ var _ eosc.IWorker = (*executor)(nil) type executor struct { drivers.WorkerBase - bodyFilter []jp.Expr - headerFilter []string + filters []IFilter } func (e *executor) DoFilter(ctx eocontext.EoContext, next eocontext.IChain) (err error) { @@ -32,23 +28,17 @@ func (e *executor) DoHttpFilter(ctx http_service.IHttpContext, next eocontext.IC return err } } - body := ctx.Response().GetBody() - n, err := oj.Parse(body) - if err != nil { - return err - } - for _, filter := range e.bodyFilter { - filter.Del(n) - } - body, err = oj.Marshal(n) - ctx.Response().SetBody(body) - for _, filter := range e.headerFilter { - ctx.Response().DelHeader(filter) + for _, filter := range e.filters { + err = filter.Filter(ctx) + if err != nil { + return err + } } return nil } func (e *executor) Destroy() { + e.filters = nil return } diff --git a/drivers/plugins/response-filter/factory.go b/drivers/plugins/response-filter/factory.go index 1c9d817c..9d74d886 100644 --- a/drivers/plugins/response-filter/factory.go +++ b/drivers/plugins/response-filter/factory.go @@ -1,15 +1,14 @@ package response_filter import ( - "strings" - "github.com/eolinker/apinto/drivers" "github.com/eolinker/eosc" - "github.com/ohler55/ojg/jp" ) const ( - Name = "response_filter" + Name = "response_filter" + WhiteFilterType = "white" + BlackFilterType = "black" ) func Register(register eosc.IExtenderDriverRegister) { @@ -17,26 +16,47 @@ func Register(register eosc.IExtenderDriverRegister) { } func NewFactory() eosc.IExtenderDriverFactory { - return drivers.NewFactory[Config](Create) + return drivers.NewFactory[Config](Create, check) } func Create(id, name string, conf *Config, workers map[eosc.RequireId]eosc.IWorker) (eosc.IWorker, error) { - bodyFilter := make([]jp.Expr, 0, len(conf.BodyFilter)) - for _, filter := range conf.BodyFilter { - key := filter - if !strings.HasPrefix(key, "$.") { - key = "$." + key + filters := make([]IFilter, 0, 4) + if len(conf.BodyFilter) > 0 { + switch conf.BodyFilterType { + case WhiteFilterType: + filter, err := NewBodyWhiteFilter(conf.BodyFilter) + if err != nil { + return nil, err + } + filters = append(filters, filter) + case BlackFilterType: + filter, err := NewBodyBlackFilter(conf.BodyFilter) + if err != nil { + return nil, err + } + filters = append(filters, filter) } - expr, err := jp.ParseString(filter) - if err != nil { - return nil, err + } + + if len(conf.HeaderFilter) > 0 { + switch conf.HeaderFilterType { + case WhiteFilterType: + filter, err := NewHeaderWhiteFilter(conf.HeaderFilter) + if err != nil { + return nil, err + } + filters = append(filters, filter) + case BlackFilterType: + filter, err := NewHeaderBlackFilter(conf.HeaderFilter) + if err != nil { + return nil, err + } + filters = append(filters, filter) } - bodyFilter = append(bodyFilter, expr) } return &executor{ - WorkerBase: drivers.Worker(id, name), - bodyFilter: bodyFilter, - headerFilter: conf.HeaderFilter, + WorkerBase: drivers.Worker(id, name), + filters: filters, }, nil } diff --git a/drivers/plugins/response-filter/filter.go b/drivers/plugins/response-filter/filter.go new file mode 100644 index 00000000..fad0616c --- /dev/null +++ b/drivers/plugins/response-filter/filter.go @@ -0,0 +1,126 @@ +package response_filter + +import ( + "fmt" + http_service "github.com/eolinker/eosc/eocontext/http-context" + "github.com/ohler55/ojg/jp" + "github.com/ohler55/ojg/oj" + "strings" +) + +func removeDuplicateStrings(input []string) []string { + seen := make(map[string]struct{}) + result := make([]string, 0, len(input)) + for _, str := range input { + if str == "" { + continue + } + if _, exists := seen[str]; !exists { + seen[str] = struct{}{} + result = append(result, str) + } + } + + return result +} + +type IFilter interface { + Filter(ctx http_service.IHttpContext) error +} + +func NewBodyWhiteFilter(keys []string) (IFilter, error) { + rules, err := SafeCompile(removeDuplicateStrings(keys)) + if err != nil { + return nil, err + } + return &BodyWhiteFilter{rules: rules}, nil +} + +type BodyWhiteFilter struct { + rules []CompiledRule +} + +func (b *BodyWhiteFilter) Filter(ctx http_service.IHttpContext) error { + newBody, err := Extract(string(ctx.Response().GetBody()), b.rules) + if err != nil { + return fmt.Errorf("failed to generate new body: %v", err) + } + ctx.Response().SetBody([]byte(newBody)) + return nil +} + +func NewBodyBlackFilter(keys []string) (IFilter, error) { + es, err := newExprSlice(removeDuplicateStrings(keys)) + if err != nil { + return nil, err + } + return &BodyBlackFilter{es: es}, nil +} + +type BodyBlackFilter struct { + es []jp.Expr +} + +func (b *BodyBlackFilter) Filter(ctx http_service.IHttpContext) error { + body := ctx.Response().GetBody() + n, err := oj.Parse(body) + if err != nil { + return err + } + for _, filter := range b.es { + filter.Del(n) + } + body, err = oj.Marshal(n) + ctx.Response().SetBody(body) + return nil +} + +func NewHeaderWhiteFilter(keys []string) (IFilter, error) { + return &HeaderWhiteFilter{keys: removeDuplicateStrings(keys)}, nil +} + +type HeaderWhiteFilter struct { + keys []string +} + +func (h *HeaderWhiteFilter) Filter(ctx http_service.IHttpContext) error { + header := ctx.Response().Headers() + ctx.Response().HeaderReset() + for _, key := range h.keys { + if value := header.Get(key); value != "" { + ctx.Response().SetHeader(key, value) + } + } + return nil +} + +func NewHeaderBlackFilter(keys []string) (IFilter, error) { + return &HeaderBlackFilter{keys: removeDuplicateStrings(keys)}, nil +} + +type HeaderBlackFilter struct { + keys []string +} + +func (h *HeaderBlackFilter) Filter(ctx http_service.IHttpContext) error { + for _, key := range h.keys { + ctx.Response().DelHeader(key) + } + return nil +} + +func newExprSlice(rules []string) ([]jp.Expr, error) { + es := make([]jp.Expr, 0, len(rules)) + for _, filter := range rules { + key := filter + if !strings.HasPrefix(key, "$.") { + key = "$." + key + } + expr, err := jp.ParseString(filter) + if err != nil { + return nil, err + } + es = append(es, expr) + } + return es, nil +} diff --git a/drivers/plugins/response-filter/rule.go b/drivers/plugins/response-filter/rule.go new file mode 100644 index 00000000..7666e5f1 --- /dev/null +++ b/drivers/plugins/response-filter/rule.go @@ -0,0 +1,133 @@ +package response_filter + +import ( + "fmt" + "github.com/eolinker/apinto/utils" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + "strings" +) + +type CompiledRule struct { + IsArray bool + BasePath string // 如 d.data + SubPath string // 如 id / a.b + FieldName string // 最终字段名 +} + +func SafeCompile(paths []string) ([]CompiledRule, error) { + valid := make([]string, 0) + + for _, p := range paths { + if err := utils.ValidateJSONPath(p); err != nil { + return nil, fmt.Errorf("invalid jsonpath %s: %w", p, err) + } + valid = append(valid, p) + } + + return CompileRules(valid) +} + +func lastKey(path string) string { + if !strings.Contains(path, ".") { + return path + } + parts := strings.Split(path, ".") + return parts[len(parts)-1] +} + +func CompileRules(paths []string) ([]CompiledRule, error) { + rules := make([]CompiledRule, 0) + + for _, p := range paths { + p = strings.TrimSpace(p) + if !strings.HasPrefix(p, "$.") { + return nil, fmt.Errorf("invalid path: %s", p) + } + + p = strings.TrimPrefix(p, "$.") + + if strings.Contains(p, "[*]") { + parts := strings.SplitN(p, "[*].", 2) + rules = append(rules, CompiledRule{ + IsArray: true, + BasePath: parts[0], + SubPath: parts[1], + FieldName: lastKey(parts[1]), + }) + } else { + rules = append(rules, CompiledRule{ + IsArray: false, + BasePath: p, + }) + } + } + + return rules, nil +} + +func applyNormal(dst *string, src string, rule CompiledRule) error { + val := gjson.Get(src, rule.BasePath) + if val.Exists() { + var err error + *dst, err = sjson.Set(*dst, rule.BasePath, val.Value()) + return err + } + return nil +} + +func applyArray(dst *string, src string, base string, rules []CompiledRule) error { + arr := gjson.Get(src, base) + if !arr.IsArray() { + return nil + } + + result := make([]map[string]interface{}, 0) + + arr.ForEach(func(_, item gjson.Result) bool { + obj := map[string]interface{}{} + for _, r := range rules { + v := item.Get(r.SubPath) + if v.Exists() { + obj[r.FieldName] = v.Value() + } + } + if len(obj) > 0 { + result = append(result, obj) + } + return true + }) + + var err error + *dst, err = sjson.Set(*dst, base, result) + return err +} + +func Extract(src string, rules []CompiledRule) (string, error) { + dst := "{}" + + // 1️⃣ 普通字段 + for _, r := range rules { + if !r.IsArray { + if err := applyNormal(&dst, src, r); err != nil { + return "", err + } + } + } + + // 2️⃣ 数组字段(按 basePath 分组) + group := map[string][]CompiledRule{} + for _, r := range rules { + if r.IsArray { + group[r.BasePath] = append(group[r.BasePath], r) + } + } + + for base, rs := range group { + if err := applyArray(&dst, src, base, rs); err != nil { + return "", err + } + } + + return dst, nil +} diff --git a/go.mod b/go.mod index c7c1f631..c2a23ef8 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/clbanning/mxj v1.8.4 github.com/coocood/freecache v1.2.2 github.com/dubbogo/gost v1.13.1 - github.com/eolinker/eosc v0.21.2 + github.com/eolinker/eosc v0.21.3 github.com/fasthttp/websocket v1.5.0 github.com/fullstorydev/grpcurl v1.8.7 github.com/google/uuid v1.4.0 @@ -22,7 +22,7 @@ require ( github.com/lestrrat-go/jwx v1.2.28 github.com/nacos-group/nacos-sdk-go/v2 v2.2.3 github.com/nsqio/go-nsq v1.1.0 - github.com/ohler55/ojg v1.25.1 + github.com/ohler55/ojg v1.27.0 github.com/pkg/sftp v1.13.4 github.com/pkoukk/tiktoken-go v0.1.7 github.com/polarismesh/polaris-go v1.1.0 @@ -44,6 +44,8 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.2.1 + github.com/tidwall/gjson v1.18.0 + github.com/tidwall/sjson v1.2.5 github.com/tjfoc/gmsm v1.4.1 github.com/xdg-go/scram v1.1.0 ) @@ -105,6 +107,8 @@ require ( github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect github.com/shirou/gopsutil/v3 v3.22.2 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/numcpus v0.4.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -205,5 +209,3 @@ require ( ) replace github.com/soheilhy/cmux v0.1.5 => github.com/hmzzrcs/cmux v0.1.6 - -//replace github.com/eolinker/eosc => ../eosc diff --git a/node/http-context/header.go b/node/http-context/header.go index c34c9865..196dde6a 100644 --- a/node/http-context/header.go +++ b/node/http-context/header.go @@ -162,6 +162,13 @@ func (r *ResponseHeader) Headers() http.Header { return r.cache.Clone() } +func (r *ResponseHeader) HeaderReset() { + r.locker.RLock() + defer r.locker.RUnlock() + r.cache = http.Header{} + r.header.Reset() +} + func (r *ResponseHeader) SetHeader(key, value string) { r.locker.Lock() defer r.locker.Unlock() diff --git a/utils/json_path.go b/utils/json_path.go new file mode 100644 index 00000000..bd280be4 --- /dev/null +++ b/utils/json_path.go @@ -0,0 +1,57 @@ +package utils + +import ( + "fmt" + "regexp" + "strings" +) + +var ( + allowedPattern = regexp.MustCompile(`^\$\.[a-zA-Z0-9_.*\[\]]+$`) + maxDepth = 6 + maxArrayCount = 3 + maxPathLength = 100 +) + +func ValidateJSONPath(path string) error { + path = strings.TrimSpace(path) + + // 1️⃣ 长度限制 + if len(path) == 0 || len(path) > maxPathLength { + return fmt.Errorf("invalid path length") + } + + // 2️⃣ 字符白名单 + if !allowedPattern.MatchString(path) { + return fmt.Errorf("invalid characters in path") + } + + // 3️⃣ 必须 $. 开头 + if !strings.HasPrefix(path, "$.") { + return fmt.Errorf("path must start with $.") + } + + // 4️⃣ 禁止递归 + if strings.Contains(path, "..") { + return fmt.Errorf("recursive path not allowed") + } + + // 5️⃣ 深度限制 + depth := strings.Count(path, ".") + if depth > maxDepth { + return fmt.Errorf("path depth exceeds limit") + } + + // 6️⃣ 数组节点限制 + arrayCount := strings.Count(path, "[*]") + if arrayCount > maxArrayCount { + return fmt.Errorf("too many array selectors") + } + + // 7️⃣ 非法组合 + if strings.Contains(path, "[") && !strings.Contains(path, "[*]") { + return fmt.Errorf("only [*] array selector allowed") + } + + return nil +}