新增响应过滤插件

This commit is contained in:
Liujian
2025-12-19 18:06:10 +08:00
parent 9cae36a935
commit e5a5ffda91
8 changed files with 398 additions and 40 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

10
go.mod
View File

@@ -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

View File

@@ -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()

57
utils/json_path.go Normal file
View File

@@ -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
}