feat: agent 本机 SimAdmin 登录与会话维持;中继设备侧限流与 agent Retry-After
- runtime.NodeAuth:agent 自查 /api/auth/status,需要时自行 POST /api/auth/login 并持 simadmin_session Cookie 转发;错误密码进入 60s 冷却(对齐控制台 ensureAuthenticated), 节点回 401/403 即失效重探,SimAdmin 重启无需重启 agent - 节点密码进 agent 配置(nodePassword),永不经中继、永不下发到手机;登录路径仍被策略 挡在电话可达面之外(TestAuthPathsAreUnreachable) - 新拒绝分类 node_auth_failed:与 upstream_error 区分,手机可提示'该节点需重新配置' - 中继:/v1/request 每设备固定窗口限流(默认 30/分钟),未鉴权调用不消耗预算 - transport:解析 Retry-After 并入退避;main loop 对 429 直接遵从中继指示 - contracts/envelope.schema.json 补 InnerResponse.outcome 字段 - 设计文档与 README 同步:WSS 改为长轮询的实现修订、响应方向已临时×临时、P2/P3 状态
This commit is contained in:
@@ -172,7 +172,12 @@ func TestAClockedRequestIsClassifiedAsExpiredNotReplay(t *testing.T) {
|
||||
|
||||
func TestAuthPathsAreUnreachable(t *testing.T) {
|
||||
f := newFixture(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("the login endpoint must never be dialed on behalf of a phone")
|
||||
// The agent's own status probe is allowed here (and answers "unprotected", so no login
|
||||
// happens); what must never be dialed is a login on behalf of a phone.
|
||||
if r.URL.Path != "/api/auth/status" {
|
||||
t.Error("the login endpoint must never be dialed on behalf of a phone")
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
for index, path := range []string{"/api/auth/login", "/api/login", "/api/logout"} {
|
||||
_, refusal := f.executor.Execute(context.Background(),
|
||||
@@ -189,7 +194,10 @@ func TestAuthPathsAreUnreachable(t *testing.T) {
|
||||
func TestADangerousWriteNeedsAMatchingIntent(t *testing.T) {
|
||||
calls := 0
|
||||
f := newFixture(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
// The auth probe reaches the local service too; only the intended path is counted.
|
||||
if r.URL.Path == "/api/sms/send" {
|
||||
calls++
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`{"sent":true}`))
|
||||
}))
|
||||
@@ -328,10 +336,12 @@ func TestAResponseExactlyAtTheLimitIsForwarded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUnreachableLocalServiceIsAnUpstreamError(t *testing.T) {
|
||||
func TestAnUnreachableLocalServiceIsANodeAuthError(t *testing.T) {
|
||||
pol := testPolicy(t)
|
||||
// A port with nothing on it: the loopback dial fails, which is a node problem rather than a
|
||||
// policy problem, and the refusal class must say so.
|
||||
// A port with nothing on it: the authentication probe fails first, which is a node problem
|
||||
// rather than a policy problem. It is classified as node_auth_failed rather than
|
||||
// upstream_error because nothing of the phone's command was ever attempted, and the phone's
|
||||
// remedy ("check that SimAdmin is running on this node") is the operator's, not the user's.
|
||||
executor, err := NewExecutor(pol, Config{Port: 1, Timeout: 500 * time.Millisecond, Audit: NullAudit()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -339,8 +349,8 @@ func TestAnUnreachableLocalServiceIsAnUpstreamError(t *testing.T) {
|
||||
_, refusal := executor.Execute(context.Background(),
|
||||
envelopeAt("nd_test0001", "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", uint64(time.Now().Unix())),
|
||||
request("GET", "/api/health", nil))
|
||||
if refusal == nil || refusal.Class != ClassUpstream {
|
||||
t.Fatalf("got %v, want %s", refusal, ClassUpstream)
|
||||
if refusal == nil || refusal.Class != ClassNodeAuth {
|
||||
t.Fatalf("got %v, want %s", refusal, ClassNodeAuth)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user