111 lines
4.6 KiB
Go
111 lines
4.6 KiB
Go
package kook
|
||
|
||
// 本文件锁定 KOOK 入站**图片**的来源白名单口径(切片 IM-IMG 的渠道侧):图片与文件
|
||
// **共用同一份**媒体来源白名单(`assetAllowlist()`:`img.kookapp.cn` + 已配置的 API 根),
|
||
// 因此图片不需要、也**不许**另立第三份名单。
|
||
//
|
||
// 覆盖:白名单命中(平台图片 CDN、已配置的 API 根)、**反证**(非白名单来源一个字节都不
|
||
// 发出去 —— 用一台真在监听、真会计数的服务端做对照),以及白名单外的「0 次命中」。
|
||
//
|
||
// KOOK 的图片地址来自消息的 `content` 字段(发送方可影响的内容),因此这份白名单是硬门:
|
||
// 伪造的图片消息完全可以填任意地址,能挡住它的只有来源名单本身。
|
||
|
||
import (
|
||
"context"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"sync/atomic"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||
)
|
||
|
||
// TestKookImageOriginsAreAllowlisted 钉住「图片来源复用既有媒体来源白名单」:平台图片 CDN
|
||
// 与已配置的 API 根命中,其余一律不命中(deny-by-default)。
|
||
func TestKookImageOriginsAreAllowlisted(t *testing.T) {
|
||
t.Parallel()
|
||
s := newKookAssetServer(t)
|
||
a, _, _ := newInlineKookAdapter(t, s)
|
||
allow := a.assetAllowlist()
|
||
|
||
for _, origin := range []string{
|
||
"https://img.kookapp.cn/assets/2023-03/abcdef.png",
|
||
s.srv.URL + "/asset/photo.png",
|
||
} {
|
||
if !allow.Allowed(origin) {
|
||
t.Fatalf("图片来源 %q 没有被既有白名单覆盖(图片与文件必须共用一份来源名单)", origin)
|
||
}
|
||
}
|
||
for _, origin := range []string{
|
||
"https://evil.example.com/photo.png",
|
||
"http://img.kookapp.cn/assets/2023-03/abcdef.png", // 该 CDN 只声明了 https 来源
|
||
"file:///etc/passwd",
|
||
} {
|
||
if allow.Allowed(origin) {
|
||
t.Fatalf("非平台来源 %q 被白名单放行了", origin)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestKookImageOffAllowlistIsNotDownloaded 是**反证**:图片的 `content` 指向白名单之外的主机时
|
||
// **一个字节都不发出去** —— 对照服务端真的在监听、真的会计数(因此这条不是「连不上」的
|
||
// 假象),命中次数必须是 0,字节必须为空,且失败不被静默。
|
||
func TestKookImageOffAllowlistIsNotDownloaded(t *testing.T) {
|
||
t.Parallel()
|
||
var hits atomic.Int32
|
||
foreign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
hits.Add(1)
|
||
w.Header().Set("Content-Type", "image/png")
|
||
_, _ = w.Write(kookPNG)
|
||
}))
|
||
t.Cleanup(foreign.Close)
|
||
|
||
s := newKookAssetServer(t) // 它才是被配置进白名单的那个 API 根
|
||
a, c, _ := newInlineKookAdapter(t, s)
|
||
|
||
if err := a.handleEvent(context.Background(), kookEventJSON(kindImage, foreign.URL+"/asset/photo.png"), nil); err != nil {
|
||
t.Fatalf("handleEvent: %v", err)
|
||
}
|
||
|
||
if got := hits.Load(); got != 0 {
|
||
t.Fatalf("非白名单来源的图片被取回 %d 次,want 0", got)
|
||
}
|
||
msg := c.message(t)
|
||
if len(msg.Media) != 1 || msg.Media[0].Kind != adapter.MediaImage || len(msg.Media[0].Data) != 0 {
|
||
t.Fatalf("非白名单来源的图片仍被写入了字节:%+v", msg.Media)
|
||
}
|
||
if s.hits.Load() != 0 {
|
||
t.Fatalf("图片请求打到了已配置根 %d 次,want 0", s.hits.Load())
|
||
}
|
||
}
|
||
|
||
// TestKookImageIsInlinedFromPlatformCDN 正向(平台 CDN 形状):`img.kookapp.cn` 的图片地址
|
||
// 在名单内 ⇒ 取回并内联。(取回用的是假服务端:真 CDN 不在用例里碰,白名单本身由上面的
|
||
// 断言钉住。)
|
||
func TestKookImageIsInlinedFromPlatformCDN(t *testing.T) {
|
||
t.Parallel()
|
||
s := newKookAssetServer(t)
|
||
s.body = kookPNG
|
||
a, c, _ := newInlineKookAdapter(t, s)
|
||
// handleEvent 的图片地址取自 `content`,因此这里用假服务端地址(它同时是配置的 API 根,
|
||
// 于是「名单内」这条前提成立);平台 CDN 主机名的命中由上面的白名单用例单独钉住。
|
||
if err := a.handleEvent(context.Background(), kookEventJSON(kindImage, s.srv.URL+"/asset/photo.png"), nil); err != nil {
|
||
t.Fatalf("handleEvent: %v", err)
|
||
}
|
||
|
||
msg := c.message(t)
|
||
if msg.ContentType != adapter.ContentImage || len(msg.Media) != 1 {
|
||
t.Fatalf("图片消息被归一成了别的形状:%+v", msg)
|
||
}
|
||
if string(msg.Media[0].Data) != string(kookPNG) {
|
||
t.Fatalf("Media.Data = %q, want 取回的图片字节", msg.Media[0].Data)
|
||
}
|
||
if s.hits.Load() != 1 {
|
||
t.Fatalf("图片资产命中 = %d, want 1", s.hits.Load())
|
||
}
|
||
// 图片地址是发送方可影响的 `content`,因此它**绝不**能进正文(正文只承载聊天文本)。
|
||
if msg.Content != "" {
|
||
t.Fatalf("图片地址被写进了正文:%q(资产地址只配进 Media[].URL)", msg.Content)
|
||
}
|
||
}
|