Files
ttstd-infinite-canvas/backend/internal/storage/signing_test.go

113 lines
4.5 KiB
Go
Raw 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 storage
import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
)
// 展示地址的签名时间由调用方对齐;同一时间窗内重复签发必须得到逐字节相同的 URL,
// 否则浏览器 HTTP 缓存(以完整 URL 为键)在每次刷新画布时都会失效并重新下载对象。
func TestDisplayURLIsDeterministicForAlignedSigningTime(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
server := httptest.NewServer(http.NotFoundHandler())
defer server.Close()
signedAt := time.Now().UTC().Truncate(time.Hour)
expiresAt := signedAt.Add(24 * time.Hour)
cases := map[string]Settings{
"s3": {Provider: s3Provider, Region: "cn-nb1", Endpoint: server.URL, Bucket: "bucket", AccessKeyID: "access-id", AccessKeySecret: "secret-value"},
"qiniu": {Provider: qiniuKodoProvider, Endpoint: "https://up-z0.qiniup.com", Bucket: "bucket", AccessKeyID: "access-id", AccessKeySecret: "secret-value"},
"tencent": {Provider: tencentCOSProvider, Endpoint: "https://cos.ap-guangzhou.myqcloud.com", Bucket: "bucket-1250000000", AccessKeyID: "access-id", AccessKeySecret: "secret-value"},
"aliyun": {Provider: aliyunOSSProvider, Endpoint: "https://oss-cn-test.aliyuncs.com", Bucket: "bucket", AccessKeyID: "access-id", AccessKeySecret: "secret-value"},
}
for name, setting := range cases {
t.Run(name, func(t *testing.T) {
first, err := SignedOriginObjectDisplayURL(setting, "users/u-1/image/a b.png", signedAt, expiresAt)
if err != nil {
t.Fatal(err)
}
time.Sleep(1100 * time.Millisecond) // 跨过秒级时间戳,证明结果不依赖 time.Now。
second, err := SignedOriginObjectDisplayURL(setting, "users/u-1/image/a b.png", signedAt, expiresAt)
if err != nil {
t.Fatal(err)
}
if first != second {
t.Fatalf("display URL is not stable:\n%s\n%s", first, second)
}
query := mustParseQuery(t, first)
if got := query.Get("response-cache-control"); got != "private, max-age=86400, immutable" {
t.Fatalf("response-cache-control = %q in %s", got, first)
}
if strings.Contains(first, "secret-value") {
t.Fatal("signed URL leaked secret key")
}
switch name {
case "s3", "qiniu":
if query.Get("X-Amz-Date") != signedAt.Format("20060102T150405Z") || query.Get("X-Amz-Expires") != "86400" {
t.Fatalf("SigV4 window = %s/%s", query.Get("X-Amz-Date"), query.Get("X-Amz-Expires"))
}
case "tencent":
window := strings.Join([]string{itoa(signedAt.Unix()), itoa(expiresAt.Unix())}, ";")
if query.Get("q-sign-time") != window || query.Get("q-key-time") != window {
t.Fatalf("COS window = %s", first)
}
}
})
}
}
func TestOneShotURLsKeepCurrentTimeSigningWithoutCacheOverride(t *testing.T) {
setting := Settings{Provider: qiniuKodoProvider, Endpoint: "https://up-z0.qiniup.com", Bucket: "bucket", AccessKeyID: "access-id", AccessKeySecret: "secret-value"}
value, err := SignedOriginObjectDownloadURL(setting, "users/u-1/video/clip.mp4", time.Now().Add(time.Hour), "镜头.mp4")
if err != nil {
t.Fatal(err)
}
query := mustParseQuery(t, value)
if query.Get("response-cache-control") != "" || !strings.HasPrefix(query.Get("response-content-disposition"), "attachment") {
t.Fatalf("download URL = %s", value)
}
signedAt, err := time.Parse("20060102T150405Z", query.Get("X-Amz-Date"))
if err != nil || time.Since(signedAt) > time.Minute {
t.Fatalf("download URL must be signed at the current time, got %q", query.Get("X-Amz-Date"))
}
}
func TestPublicCDNObjectDownloadURLForcesAttachmentOnlyOnDownloadURL(t *testing.T) {
setting := Settings{CDNBaseURL: "https://media.example.com"}
value, err := PublicCDNObjectDownloadURL(setting, "users/u-1/image/test image.png", "下载图片.png")
if err != nil {
t.Fatal(err)
}
parsed, err := url.Parse(value)
if err != nil {
t.Fatal(err)
}
if parsed.Host != "media.example.com" || parsed.Path != "/users/u-1/image/test image.png" || !strings.HasPrefix(parsed.Query().Get("response-content-disposition"), "attachment") {
t.Fatalf("download URL = %q", value)
}
inline, err := OssCDNObjectURL(setting.CDNBaseURL, "users/u-1/image/test image.png")
if err != nil {
t.Fatal(err)
}
if parsedInline, err := url.Parse(inline); err != nil || parsedInline.RawQuery != "" {
t.Fatalf("inline CDN URL = %q, %v", inline, err)
}
}
func mustParseQuery(t *testing.T, raw string) url.Values {
t.Helper()
parsed, err := url.Parse(raw)
if err != nil {
t.Fatal(err)
}
return parsed.Query()
}
func itoa(value int64) string {
return strconv.FormatInt(value, 10)
}