fix(agent): 手机已经放弃的请求不再执行
中继答 504 并说"可能已经执行了,重试前请先确认"——这句话诚实的前提是它保持为真。 批次是串行处理的,所以满邮箱里第八个信封可以在它被签名之后 8 个上游超时才轮到; 那时手机早已被告知失败、用户可能已经重试。再执行它,就把一次诚实的"未知"变成了 一条重复发出的短信。 现在 agent 在动任何东西之前先比对自己的截止时间(策略里的 requestTtlSeconds), 超龄一律以 expired 拒绝并落审计。判定放在新鲜度检查之后、策略判定之前:它比签名 更便宜,而且一个过期请求无论路径是否在白名单里都不该执行。 为了让这条规则**可达**,策略新增一条断言:TTL 不得大于时钟偏移窗口。否则新鲜度 检查会先触发,这条规则永远不会被执行——而"永不触发的规则"是出事时才会被发现坏了 的那种东西。 单个上游调用的超时也移进了策略(agentUpstreamTimeoutSeconds)。"中继期限必须长于 一次上游调用"这个比较,与二进制里的常量是比不了的;现在两侧从同一个文件读,策略 加载时就能拒绝一组自相矛盾的数值。原来那个 20s 常量保留为默认值的来源,不再硬编码。 测试两项:一个请求在期限内被转发、一个超龄请求被拒且**没有被转发**(计数按路径, 否则首次转发前的 /api/auth/status 探测会让断言以错误的理由通过);另一项断言策略 本身满足上述两个关系。变异验证:关掉判定,第一项失败。 Go 全包、Node 119 项、Swift 101 项通过。
This commit is contained in:
@@ -91,7 +91,10 @@ func NewExecutor(pol *policy.Policy, cfg Config) (*Executor, error) {
|
||||
}
|
||||
timeout := cfg.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 20 * time.Second
|
||||
// From the policy, not a constant here. The relay's deadline is compared against this number
|
||||
// when the policy is validated, and a comparison against a constant inside a binary is not one
|
||||
// anything can check.
|
||||
timeout = time.Duration(pol.Limits.AgentUpstreamTimeoutSeconds) * time.Second
|
||||
}
|
||||
client, err := egress.NewClient(target, timeout)
|
||||
if err != nil {
|
||||
@@ -145,6 +148,21 @@ func (e *Executor) Execute(ctx context.Context, envelope wire.Envelope, inner wi
|
||||
if err := Freshness(envelope.TS, uint64(now.Unix()), e.pol.Limits.ClockSkewSeconds); err != nil {
|
||||
return Result{}, e.refuse(envelope, inner, ClassExpired, err)
|
||||
}
|
||||
// The phone's own deadline, checked before anything is executed.
|
||||
//
|
||||
// The relay answers 504 with "it may or may not have executed it; check the node before retrying",
|
||||
// which is honest — but it is only honest while it stays true. A batch is processed serially, so
|
||||
// the eighth envelope of a full mailbox can begin up to eight upstream timeouts after it was
|
||||
// signed; by then the phone has been told the request failed and the user may already have retried.
|
||||
// Executing it at that point turns an honest "unknown" into a duplicate SMS.
|
||||
//
|
||||
// The relay's deadline is the authority on when that happened, because the relay is the party that
|
||||
// starts the clock. Its value is therefore carried in the policy and asserted to be no larger than
|
||||
// the clock-skew window, which is what makes the freshness check above unable to mask this one.
|
||||
if age := int64(now.Unix()) - int64(envelope.TS); age > int64(e.pol.Limits.RequestTTLSeconds) {
|
||||
return Result{}, e.refuse(envelope, inner, ClassExpired,
|
||||
fmt.Errorf("%w: signed %ds ago, deadline is %ds", ErrExpired, age, e.pol.Limits.RequestTTLSeconds))
|
||||
}
|
||||
if e.replay.Seen(envelope.Req, now) {
|
||||
return Result{}, e.refuse(envelope, inner, ClassReplay, fmt.Errorf("%w: %s", ErrReplay, envelope.Req))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user