package weixin_ilink // 本文件钉住 iLink 出站侧的两条出镜边界(M41 #10/#11 那一族): // // - **预签名上传地址不进日志与错误**:uploadURL 由 getuploadurl 签发, // `encrypted_query_param` 就是下载凭据;而 net/http 的 *url.Error 会把整条地址 // 拼进错误串,那条错误要经 `cdn upload attempt failed, retrying` 与 send.go 的 // `outbound media item skipped` 两处落日志。 // - **非 2xx 的响应体原文不进错误**:iLink 在 errmsg 里回显请求内容,旧实现把 4096B // 体原文拼进 error。 // // 两条都是「改回去就变红」的形状:断言的是错误文本与日志文本本身。 import ( "bytes" "context" "net" "net/http" "net/http/httptest" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // closedLoopbackAddr 返回一个**刚被关掉**的 127.0.0.1 端口地址。用它而不是拨一个 // 域名:这里要的是「连接被拒」这条确定的传输层失败 —— 只有传输层失败才会让 // net/http 包出带完整地址的 *url.Error,那正是要钉住的泄漏形态(不需要 DNS、不需要 // 外网、失败是立刻的)。 func closedLoopbackAddr(t *testing.T) string { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } addr := ln.Addr().String() if cerr := ln.Close(); cerr != nil { t.Fatalf("close listener: %v", cerr) } return addr } // newLeakTestAdapter 构造一个只用来发起出站调用的适配器:日志写进返回的 buffer, // 便于断言「日志里搜不到哨兵」。 func newLeakTestAdapter(t *testing.T) (*Adapter, *bytes.Buffer) { t.Helper() buf := &bytes.Buffer{} ad, err := New( Config{BaseURL: "http://127.0.0.1:9/", BotToken: "tok"}, nil, logger.New(logger.Options{Out: buf, Level: "debug"}), ) if err != nil { t.Fatalf("New: %v", err) } return ad, buf } // TestUploadCiphertext_PresignedURLStaysOutOfLogs 重试日志与最终错误里都不得出现 // 预签名地址的签名参数。 func TestUploadCiphertext_PresignedURLStaysOutOfLogs(t *testing.T) { t.Parallel() const secret = "PRESIGNED-ENCRYPTED-PARAM-SECRET" ad, logs := newLeakTestAdapter(t) uploadURL := "http://" + closedLoopbackAddr(t) + "/upload?encrypted_query_param=" + secret + "&filekey=abc" _, err := ad.uploadCiphertext(context.Background(), uploadURL, []byte("cipher")) if err == nil { t.Fatal("连接被拒却报成功") } if strings.Contains(err.Error(), secret) { t.Fatalf("预签名地址进了错误(会被 send.go 落日志):%v", err) } if !strings.Contains(err.Error(), "dial tcp") { t.Fatalf("剥掉地址时把原因一起丢了:%v", err) } logged := logs.String() if strings.Contains(logged, secret) { t.Fatalf("预签名地址进了日志:\n%s", logged) } // 日志确实被写到了(否则上面的断言是空跑):重试行必须出现,且带来源主机。 if !strings.Contains(logged, "cdn upload attempt failed") { t.Fatalf("没有重试日志,测试没覆盖到目标路径:\n%s", logged) } if !strings.Contains(logged, "host=http://127.0.0.1") { t.Fatalf("重试日志缺少来源主机(诊断面不能一起丢):\n%s", logged) } } // TestDoJSON_StatusErrorDoesNotEchoBodySentinel 非 2xx 的体原文不得进错误。 func TestDoJSON_StatusErrorDoesNotEchoBodySentinel(t *testing.T) { t.Parallel() const sentinel = "SENTINEL-ILINK-4XX" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"ret":-1,"errcode":-14,"errmsg":"` + sentinel + `"}`)) })) defer srv.Close() ad, _ := New(Config{BaseURL: srv.URL, BotToken: "tok"}, srv.Client(), logger.New(logger.Options{Out: &bytes.Buffer{}, Level: "debug"})) var out map[string]any err := ad.doJSON(context.Background(), http.MethodPost, ad.apiURL("/ilink/bot/getupdates"), nil, &out) if err == nil { t.Fatal("500 却报成功") } if strings.Contains(err.Error(), sentinel) { t.Fatalf("响应体原文进了错误:%v", err) } if !strings.Contains(err.Error(), "status=500") || !strings.Contains(err.Error(), "body_bytes=") { t.Fatalf("状态码/体积丢了(诊断面不能一起丢):%v", err) } if !strings.Contains(err.Error(), "127.0.0.1") { t.Fatalf("错误里没有来源主机:%v", err) } } // TestDoJSON_TransportErrorOmitsRequestURL 传输层失败时,错误里不得出现请求地址。 func TestDoJSON_TransportErrorOmitsRequestURL(t *testing.T) { t.Parallel() const secret = "QUERY-ENCRYPTED-PARAM-SECRET" ad, _ := newLeakTestAdapter(t) rawURL := "http://" + closedLoopbackAddr(t) + "/ilink/bot/getupdates?encrypted_query_param=" + secret err := ad.doJSON(context.Background(), http.MethodPost, rawURL, nil, nil) if err == nil { t.Fatal("连接被拒却报成功") } if strings.Contains(err.Error(), secret) { t.Fatalf("请求地址的签名参数进了错误:%v", err) } if !strings.Contains(err.Error(), "dial tcp") { t.Fatalf("剥掉地址时把原因一起丢了:%v", err) } } // TestUploadCiphertext_LogLevelIsWarn 钉住级别:重试行是 WARN(默认 INFO 级别下可见), // 不是只有 debug 才看得到的取证行 —— 这条线要在生产默认级别下就安全。 func TestUploadCiphertext_RetryLogIsWarnAndCarriesHost(t *testing.T) { t.Parallel() const secret = "PRESIGNED-SECRET-2" buf := &bytes.Buffer{} ad, err := New(Config{BaseURL: "http://127.0.0.1:9/", BotToken: "tok"}, nil, logger.New(logger.Options{Out: buf, Level: "info"})) if err != nil { t.Fatalf("New: %v", err) } uploadURL := "http://" + closedLoopbackAddr(t) + "/upload?encrypted_query_param=" + secret if _, uerr := ad.uploadCiphertext(context.Background(), uploadURL, []byte("x")); uerr == nil { t.Fatal("连接被拒却报成功") } logged := buf.String() if !strings.Contains(logged, "level=WARN") { t.Fatalf("重试行不是 WARN 级(默认 INFO 下会看不见):\n%s", logged) } if strings.Contains(logged, secret) { t.Fatalf("预签名地址进了日志:\n%s", logged) } }