Files
AskaEth 4b4173dd42 fix(channel): M41 r1 复审三条 —— telegram 构造分支、QQ 改 MaskQuery、删两处死上限
P2-1 telegram doJSON 的**请求构造**失败分支:http.NewRequestWithContext 解析失败的
err 同样是 *url.Error(里面是整条 /bot<token>/… 地址),而 poll.go 的 long poll
failed 会原样落日志。改成与传输层分支同形:trace.HostOf(endpoint) + callCause。

P2-2 qq_obv11 connectClient 的成功 INFO 从 maskAccessTokenQuery 换成
trace.MaskQuery:后者认得 access%5Ftoken / token / api_key / 签名参数 / userinfo,
且保留 EscapedPath(路径可读)与不截断。maskAccessTokenQuery 仍被
sanitizeConnectReason 使用,未成死代码。

P2-3 删掉 maxEnvelopeMessageRunes / maxDescriptionRunes(各 200):trace.RedactText
内部已 truncate 到 maxSummaryRunes=80,这两层永不命中,注释口径与实际不符。改为
直接 return trace.RedactText(压平空白),注释写明 80 rune 由 RedactText 负责。

用例(全部变异打红,逐条验证):telegram 构造分支 token、QQ 八形态凭据矩阵
(含四种 maskAccessTokenQuery 漏掉的形态 + 路径可读反向断言)、两处 80 rune 上限。
2026-10-26 13:58:19 +08:00

152 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package qq_obv11
// 本文件钉住 ws+client 连接成功那条 INFO 的出镜边界(M41 #15):remote_url 可以带
// 凭据 —— OneBot 惯例是 `?access_token=`,但真实配置里也见过 `?token=`、`?api_key=`、
// 签名参数,以及 `user:pass@`。原样 INFO 就是把凭据写进日志。
//
// 打码走 trace.MaskQuery(同包 maskAccessTokenQuery 只认 access_token 一个键名,复审
// 形态矩阵里 `access%5Ftoken` / `token` / `api_key` / 签名 / userinfo 五种形态都不认)。
// 这条 INFO 的全部诊断价值是「连到了哪个端点」,因此路径必须原样可读 —— 每个用例都
// 反向断言这一点。
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// wsServerForLogTest 起一个只说「握手 + 保持连接」的假 OneBot 服务端。它不解析路径与
// 查询串,因此下面每条形态都能连得上,测的纯粹是日志字段。
func wsServerForLogTest(t *testing.T) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgradeWS(w, r)
if err != nil {
return
}
defer conn.Close()
for {
if _, _, err := conn.ReadMessage(); err != nil {
return
}
}
}))
t.Cleanup(srv.Close)
return srv
}
// connectAndLog 用给定的远端 URL 连一次,返回这段时间的全部日志文本。
func connectAndLog(t *testing.T, remote string) string {
t.Helper()
logs, logger := logCaptureSync()
a := NewAdapter(Config{Mode: "client", RemoteURL: remote}, WithLogger(logger))
if err := a.Connect(context.Background()); err != nil {
t.Fatalf("Connect(%q): %v", remote, err)
}
t.Cleanup(func() { _ = a.Disconnect(context.Background()) })
logged := logs.text()
if !strings.Contains(logged, "connected to onebot ws") {
t.Fatalf("没有连接成功日志,测试没覆盖到目标路径:\n%s", logged)
}
return logged
}
// TestConnectClientInfoLogMasksCredentials 是「凭据形态 × 打码」的矩阵:每种形态都要求
// 凭据搜不到、打码标记在场、路径仍可读。
func TestConnectClientInfoLogMasksCredentials(t *testing.T) {
srv := wsServerForLogTest(t)
hostport := strings.TrimPrefix(srv.URL, "http://")
const secret = "SECRET-VALUE-1234"
const path = "/onebot/v11/ws"
cases := []struct {
name string
url string
// leaked 是**不得出现**的子串(明文凭据的形态)。
leaked string
// masked 是**必须出现**的打码结果片段,兼作「这条日志确实带了该字段」的反向断言。
masked string
}{
{
name: "access_token_query",
url: "ws://" + hostport + path + "?access_token=" + secret,
leaked: secret,
masked: "?access_token=***",
},
{
name: "access_token_after_other_query",
url: "ws://" + hostport + path + "?compress=0&access_token=" + secret,
leaked: secret,
masked: "&access_token=***",
},
{
name: "percent_encoded_key",
url: "ws://" + hostport + path + "?access%5Ftoken=" + secret,
leaked: secret,
masked: "?access%5Ftoken=***",
},
{
name: "generic_token_key",
url: "ws://" + hostport + path + "?token=" + secret,
leaked: secret,
masked: "?token=***",
},
{
name: "api_key_key",
url: "ws://" + hostport + path + "?api_key=" + secret,
leaked: secret,
masked: "?api_key=***",
},
{
name: "signature_key",
url: "ws://" + hostport + path + "?X-Signature=" + secret,
leaked: secret,
masked: "?X-Signature=***",
},
{
name: "fragment_token",
url: "ws://" + hostport + path + "#access_token=" + secret,
leaked: secret,
masked: "#access_token=***",
},
{
name: "userinfo",
url: "ws://apiuser:" + secret + "@" + hostport + path,
leaked: secret,
masked: "ws://[redacted]@" + hostport + path,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
logged := connectAndLog(t, tc.url)
t.Logf("before=%q after=%s", tc.url, urlField(logged))
if strings.Contains(logged, tc.leaked) {
t.Fatalf("凭据明文进了 INFO 日志:\n%s", logged)
}
if !strings.Contains(logged, tc.masked) {
t.Fatalf("日志里没有打码结果 %q:\n%s", tc.masked, logged)
}
// 反向断言:路径必须还在 —— 否则「连到了哪个端点」这条诊断就没了。
if !strings.Contains(logged, path) {
t.Fatalf("日志丢了路径 %q:\n%s", path, logged)
}
})
}
}
// urlField 摘出日志里 `url=…` 字段的值(渲染后可能带引号),只用于测试输出。
func urlField(logged string) string {
for _, line := range strings.Split(logged, "\n") {
if !strings.Contains(line, "connected to onebot ws") {
continue
}
if i := strings.Index(line, " url="); i >= 0 {
return line[i+1:]
}
}
return ""
}