mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-20 18:27:36 +00:00
设计经双视角架构评审裁决(零行为变更,特征化测试先行锁定): - payment/provider:factory switch → 包内私有构造器注册表(init 自注册); unknown-key 文案与 ApplicationError 透传逐字节等价(前端 i18n 依赖) - internal/gatewayhook:协议无关 pre-flight 钩子链(panic 隔离、error 默认 fail-open、首 Blocked 短路、空链零分配);Decision→格式化保留在各调用点 - 8 个 HTTP moderation 调用点收敛经链(各点入参表达式/格式化函数逐一保留), WebSocket 两点按裁决保持原路径;moderation 为 Wire 装配的核心钩子 - 新增 7 个拦截格式特征化测试(5 种 HTTP 格式差异 + WS turn-2 + fail-open) 实施后对抗审计零 P0/P1,突变实验 4/4 被测试捕获;bench allocs 全部零增加。
83 lines
2.4 KiB
Go
83 lines
2.4 KiB
Go
package gatewayhook
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// Chain 按固定顺序执行 pre-flight 钩子。
|
||
//
|
||
// 执行语义(SEAM-DESIGN 裁决 H 链层三件必补):
|
||
// 1. 每钩子 recover():panic 记 error 日志后按 fail-open 继续下一钩子;
|
||
// 2. 钩子返回 error 默认 fail-open:记日志后继续下一钩子(与内容审核现状一致);
|
||
// 3. 首个 Blocked Decision 即短路返回;非 Blocked 的 Decision 视为放行并继续。
|
||
//
|
||
// Run 永不向调用方返回 error:返回 nil 即放行。
|
||
type Chain struct {
|
||
hooks []PreFlightHook
|
||
log *zap.Logger
|
||
}
|
||
|
||
// NewChain 构造钩子链。log 传 nil 时延迟回退到进程全局 logger;
|
||
// nil 钩子被忽略。
|
||
func NewChain(log *zap.Logger, hooks ...PreFlightHook) *Chain {
|
||
filtered := make([]PreFlightHook, 0, len(hooks))
|
||
for _, hook := range hooks {
|
||
if hook != nil {
|
||
filtered = append(filtered, hook)
|
||
}
|
||
}
|
||
return &Chain{hooks: filtered, log: log}
|
||
}
|
||
|
||
// IsEmpty 报告链上是否没有任何钩子(nil 链视为空链)。
|
||
// 调用侧可据此零开销跳过钩子请求的构造。
|
||
func (c *Chain) IsEmpty() bool {
|
||
return c == nil || len(c.hooks) == 0
|
||
}
|
||
|
||
// Run 按序执行钩子,返回首个 Blocked Decision;全部放行(或链为空)返回 nil。
|
||
func (c *Chain) Run(ctx context.Context, req *Request) *Decision {
|
||
if c.IsEmpty() {
|
||
return nil
|
||
}
|
||
for _, hook := range c.hooks {
|
||
if decision := c.runHook(ctx, hook, req); decision != nil && decision.Blocked {
|
||
return decision
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// runHook 执行单个钩子并隔离其 panic / error(均按 fail-open 降级为放行)。
|
||
func (c *Chain) runHook(ctx context.Context, hook PreFlightHook, req *Request) (decision *Decision) {
|
||
hookID := ""
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
c.logger().Error("gatewayhook.hook_panic",
|
||
zap.String("hook_id", hookID),
|
||
zap.Any("panic_value", r),
|
||
zap.Stack("stack"))
|
||
decision = nil // fail-open:panic 钩子视为放行
|
||
}
|
||
}()
|
||
hookID = hook.HookID()
|
||
d, err := hook.CheckPreFlight(ctx, req)
|
||
if err != nil {
|
||
c.logger().Warn("gatewayhook.hook_error",
|
||
zap.String("hook_id", hookID),
|
||
zap.Error(err))
|
||
return nil // fail-open:钩子自身故障视为放行
|
||
}
|
||
return d
|
||
}
|
||
|
||
func (c *Chain) logger() *zap.Logger {
|
||
if c != nil && c.log != nil {
|
||
return c.log
|
||
}
|
||
return logger.L()
|
||
}
|