feat(channels): 适配器入站内联附件字节(telegram/kook/qq_obv11/weixin_ilink)
- 新增 internal/channel/mediafetch:来源白名单(deny-by-default,支持 *.suffix) + 逐跳重定向校验 + 边读边限 + 超时;只回来源主机,绝不回完整 URL - 四渠道各自 media.go 接通入站内联:只从平台载荷给出的引用取回字节 (telegram file_id→getFile→file_path 形态校验;kook CDN;qq 文件段 url; weixin cdnURL + AES-ECB 解密) - 常量全部复用既有来源:documents.ExtAllowed / DefaultMaxBytes / MaxPerMessage - channelbridge 诚实提示文案改为对「渠道只给远程引用」与「代取失败」两种成因都成立 - 18 个测试函数(含四渠道「正文里的 URL ⇒ 0 次命中」红线反证、端到端进模型可见视图) - 变异验证:去掉尺寸上限 ⇒ 四渠道红(含修正后不再是假绿的 weixin 超量用例)
This commit is contained in:
1 parent
46caa724f2
commit
3be70f8232
22 files changed
+2601
-19
No files matched your search
@@ -9,6 +9,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/mediafetch"
|
||||
)
|
||||
|
||||
// ErrKeyTooShort 在入站密钥推导无法给出所需分组长度的 AES 密钥时返回。
|
||||
@@ -90,3 +93,53 @@ func (a *Adapter) fetchBody(ctx context.Context, rawURL string) ([]byte, error)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// inlineInboundMedia 把受支持格式的文件附件取回、解密并内联到消息上(切片 IM-1)。
|
||||
// 失败不抛出:该附件保持无字节,由 channelbridge 的入站摄取照既有口径给模型一行
|
||||
// 诚实提示(design/24 §4.4)——本切片绝不为了「让文件能读」而把失败吞掉。
|
||||
func (a *Adapter) inlineInboundMedia(ctx context.Context, msg *adapter.UnifiedMessage) {
|
||||
mediafetch.Inline(ctx, a.log, msg, ilinkFetcher{a: a})
|
||||
}
|
||||
|
||||
// ilinkFetcher 实现 mediafetch.Fetcher:平台给的是 CDN **相对引用**(少数时候是绝对
|
||||
// URL),两者都先经 `cdnURL` 归一,再按来源白名单取回,最后剥掉 AES-ECB 层 ——
|
||||
// 与既有 `DownloadMedia` 同一条平台口径,只是多了一层白名单与独立的体积上限。
|
||||
type ilinkFetcher struct{ a *Adapter }
|
||||
|
||||
// Fetch 取回并解密一份微信 iLink 附件。体积上限与超时都不在这里写数:前者由
|
||||
// mediafetch 的默认值(`documents.DefaultMaxBytes`)兜底,后者是具名常量
|
||||
// `mediafetch.DefaultTimeout`(**不是** `MaxMediaSize`:那是媒体通道自己的历史上限,
|
||||
// 与本切片的文档上限刻意解耦)。
|
||||
func (f ilinkFetcher) Fetch(ctx context.Context, m adapter.Media) ([]byte, error) {
|
||||
key, err := deriveInboundKey(f.a.cfg.BotToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weixin_ilink: derive inbound key: %w", err)
|
||||
}
|
||||
ciphertext, err := mediafetch.Fetch(ctx, mediafetch.Request{
|
||||
URL: f.a.cdnURL(m.URL),
|
||||
Allow: f.a.assetAllowlist(),
|
||||
Header: f.a.inboundHeaders(),
|
||||
Client: f.a.httpClient,
|
||||
Timeout: mediafetch.DefaultTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weixin_ilink: fetch media: %w", err)
|
||||
}
|
||||
plaintext, err := aesECBDecrypt(key, ciphertext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weixin_ilink: decrypt media: %w", err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// assetAllowlist 给出入站附件取回的来源白名单:**只有已配置的 CDN 根**(默认
|
||||
// `https://novac2c.cdn.weixin.qq.com/c2c` 的来源)。`cdnURL` 只把平台给的相对引用
|
||||
// 拼到它之后,但**绝对 URL 会被原样放行**(见 TestCDNURL_JoinsRelativeAndPassesAbsolute),
|
||||
// 因此白名单是这里唯一能挡住「对端伪造绝对引用把我们引到别的主机」的门。
|
||||
func (a *Adapter) assetAllowlist() mediafetch.Allowlist {
|
||||
origin, err := mediafetch.OriginOf(a.cfg.CDNBaseURL)
|
||||
if err != nil {
|
||||
return mediafetch.NewAllowlist()
|
||||
}
|
||||
return mediafetch.NewAllowlist(origin)
|
||||
}
|
||||
Reference in new issue
Block a user