fix(channel): 渠道适配器与出站工具的上游响应体/带凭据 URL 不再进日志与错误(M41)
接 M38 的 18 条真泄漏里「渠道适配器 + 出站工具」这一族(#10 #11 #12 #13 #15 #16)。 只动日志与 error 文字:发送/接收、解析口径、重试次数一个都没改。 - httpclient.do:非 2xx 不再把 512B 响应体原文拼进 error,改 trace.UpstreamError (状态/体积/sha256 前缀)+ 信封 code/message(RedactText + 200 rune);error 里的 target 改 trace.HostOf,并剥掉 *url.Error 外壳(它带整条地址) - telegram doJSON:4xx 只解 error_code/description(清洗 + 截断),体其余字段不拼; 传输错误只留来源 —— Bot API 把 token 写在地址路径里 - weixin_ilink doJSON:4096B 体 ⇒ trace.UpstreamError,rawURL ⇒ mediafetch.HostOf; uploadOnce 剥掉预签名上传地址(encrypted_query_param),重试日志补来源主机 - qq_obv11 connectClient 成功 INFO:改走同包已有的 maskAccessTokenQuery (OneBot 惯例把 ?access_token= 写进 remote_url) - plugins/tools/io http 拦截日志:改记 trace.HostOf + 原因码,不记原 URL、也不记错误 原文(解析失败时它包着 *url.Error) 每条都有能变异打红的用例:QQ/iLink 预签名地址的哨兵、三处 4xx 体哨兵、 user:pass@ 与 query 凭据;八处变异逐条验证变红。
This commit is contained in:
1 parent
46a6da13e0
commit
0f9251b983
11 files changed
+726
-29
No files matched your search
@@ -4,11 +4,15 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||
)
|
||||
|
||||
// botResponse is the shared envelope Telegram returns for every method: ok plus
|
||||
@@ -20,6 +24,15 @@ type botResponse struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
}
|
||||
|
||||
const (
|
||||
// maxErrorBodyBytes caps how much of a non-200 reply is read before classifying it.
|
||||
maxErrorBodyBytes = 512
|
||||
// maxDescriptionRunes caps the description once it enters an error. Telegram's
|
||||
// description is upstream free text (it can echo the parameter we sent), so it
|
||||
// is cleaned and bounded instead of being passed through verbatim.
|
||||
maxDescriptionRunes = 200
|
||||
)
|
||||
|
||||
// doJSON posts a JSON payload to a Bot API method and decodes the envelope. The
|
||||
// caller is responsible for decoding Result; a non-OK reply is returned as
|
||||
// ErrBadResponse wrapped with the API description.
|
||||
@@ -39,17 +52,15 @@ func (a *Adapter) doJSON(ctx context.Context, method string, payload any) (*botR
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("telegram: %s call: %w", method, err)
|
||||
// 只留来源:Bot API 把 token 写在地址路径里(/bot<token>/<method>),而
|
||||
// *url.Error 会把整条地址拼进错误串 —— 原样 `%w` 等于把 bot token 写进日志。
|
||||
return nil, fmt.Errorf("telegram: %s call %s: %w", method, trace.HostOf(a.endpoint(method)), callCause(err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
desc := strings.TrimSpace(string(body))
|
||||
if desc != "" {
|
||||
return nil, fmt.Errorf("telegram: %s: status %d: %s: %w", method, resp.StatusCode, desc, ErrBadResponse)
|
||||
}
|
||||
return nil, fmt.Errorf("telegram: %s: status %d: %w", method, resp.StatusCode, ErrBadResponse)
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodyBytes))
|
||||
return nil, badResponseError(method, resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var out botResponse
|
||||
@@ -203,3 +214,44 @@ func parseReplyToID(s string) int64 {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// badResponseError 把一次非 200 的回复收敛成错误:**只**取 error_code 与
|
||||
// description。响应体其余字段(以及 Telegram 回显的请求参数)一个字都不拼 ——
|
||||
// 旧实现把 512B 体原文当 desc 拼进来,等于把上游报文写进调用方的日志。
|
||||
//
|
||||
// description 是上游自由文本(例如 setWebhook 失败时会回显我们给的地址),因此过
|
||||
// trace.RedactText 清洗凭据形状并按 rune 截断后才出镜。
|
||||
func badResponseError(method string, status int, body []byte) error {
|
||||
var env botResponse
|
||||
_ = json.Unmarshal(body, &env)
|
||||
parts := []string{fmt.Sprintf("telegram: %s: status %d", method, status)}
|
||||
if env.ErrorCode != 0 {
|
||||
parts = append(parts, fmt.Sprintf("error_code %d", env.ErrorCode))
|
||||
}
|
||||
if desc := boundedDescription(env.Description); desc != "" {
|
||||
parts = append(parts, desc)
|
||||
}
|
||||
return fmt.Errorf("%s: %w", strings.Join(parts, ": "), ErrBadResponse)
|
||||
}
|
||||
|
||||
// boundedDescription 是 Bot API description 进入错误前的唯一处理:压平空白、清洗
|
||||
// 凭据形状,再按 rune 截断(宁可丢细节,不可把一段回显整段带出来)。
|
||||
func boundedDescription(s string) string {
|
||||
flat := strings.Join(strings.Fields(s), " ")
|
||||
flat = trace.RedactText(flat)
|
||||
if r := []rune(flat); len(r) > maxDescriptionRunes {
|
||||
return string(r[:maxDescriptionRunes]) + "…"
|
||||
}
|
||||
return flat
|
||||
}
|
||||
|
||||
// callCause 剥掉 net/http 的 *url.Error 外壳:它会把完整地址拼进错误串,而 Bot API
|
||||
// 的地址路径里就写着 bot token。剥壳只丢地址,内层哨兵链(context 取消等)原样保留。
|
||||
// 与 internal/channel/mediafetch 的 sanitizeErr 同一口径(那份未导出,且在别包里)。
|
||||
func callCause(err error) error {
|
||||
var uerr *url.Error
|
||||
if errors.As(err, &uerr) && uerr.Err != nil {
|
||||
return uerr.Err
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in new issue
Block a user