Files

111 lines
4.6 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 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)
}
}