mirror of
https://github.com/eolinker/apinto
synced 2025-12-24 13:28:15 +08:00
新增响应过滤插件
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
126
drivers/plugins/response-filter/filter.go
Normal file
126
drivers/plugins/response-filter/filter.go
Normal 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
|
||||
}
|
||||
133
drivers/plugins/response-filter/rule.go
Normal file
133
drivers/plugins/response-filter/rule.go
Normal 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
10
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
|
||||
|
||||
@@ -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
57
utils/json_path.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user