package main import ( "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/sha256" "encoding/base64" "image" "image/png" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "github.com/example/xingyu/internal/testusers" cos "github.com/tencentyun/cos-go-sdk-v5" ) func TestEncryptedConfigurationCompatibility(t *testing.T) { seal := func(key string) string { digest := sha256.Sum256([]byte(key)) block, _ := aes.NewCipher(digest[:]) gcm, _ := cipher.NewGCM(block) nonce := make([]byte, gcm.NonceSize()) return "enc:v1:" + base64.RawStdEncoding.EncodeToString(gcm.Seal(nonce, nonce, []byte("test-only-value"), nil)) } for _, encoded := range []string{seal("config-key"), seal("jwt-key:integration-config")} { plain, err := decrypt(encoded, "config-key", "jwt-key") if err != nil || plain != "test-only-value" { t.Fatal("decryption compatibility failed") } } if _, err := decrypt(seal("unrelated-key"), "config-key", "jwt-key"); err == nil { t.Fatal("accepted wrong key") } if _, err := decrypt("enc:v1:bad!", "config-key", "jwt-key"); err == nil { t.Fatal("accepted malformed ciphertext") } } func TestAssetsAreScopedAndContentAddressed(t *testing.T) { dir := t.TempDir() var buf bytes.Buffer if err := png.Encode(&buf, image.NewRGBA(image.Rect(0, 0, 256, 256))); err != nil { t.Fatal(err) } for _, name := range testusers.Avatars() { if err := os.WriteFile(filepath.Join(dir, name), buf.Bytes(), 0600); err != nil { t.Fatal(err) } } assets, err := loadAssets(dir, "media", "https://example.cos.ap-guangzhou.myqcloud.com/") if err != nil || len(assets) != 10 { t.Fatalf("count=%d err=%v", len(assets), err) } seen := map[string]bool{} for _, a := range assets { if seen[a.Key] || !strings.HasPrefix(a.Key, "media/test-users/"+testusers.Batch+"/") || !strings.Contains(a.Key, a.SHA256[:20]) { t.Fatal("unscoped or nonunique object key") } if !strings.HasSuffix(a.ThumbnailKey, "-thumb.jpg") || !strings.HasSuffix(a.ThumbnailURL, "-thumb.jpg") || len(a.ThumbnailData) == 0 { t.Fatal("missing deterministic thumbnail object") } config, format, decodeErr := image.DecodeConfig(bytes.NewReader(a.ThumbnailData)) if decodeErr != nil || format != "jpeg" || config.Width > 256 || config.Height > 256 { t.Fatal("invalid generated thumbnail", decodeErr) } seen[a.Key] = true } for _, prefix := range []string{"../existing", "/root", "media//test", "media/../test"} { if _, err := loadAssets(dir, prefix, "https://example.com"); err == nil { t.Errorf("accepted prefix %q", prefix) } } for _, raw := range []string{"http://example.com", "https://user:pass@example.com", "https://example.com?signature=temporary", "/uploads"} { if _, err := validHTTPS(raw); err == nil { t.Errorf("accepted unsafe base %q", raw) } } } func TestUploadIsVerifiedAndRepeatableWithoutOverwriting(t *testing.T) { data := []byte("test-png-content") var stored []byte puts, aclWrites := 0, 0 publicReadable := true server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/batch/image.png" { t.Errorf("unexpected path %s", r.URL.Path) w.WriteHeader(404) return } if r.Method == "PUT" && r.URL.Query().Has("acl") { aclWrites++ publicReadable = true w.WriteHeader(200) return } if r.Method == "PUT" { puts++ if r.Header.Get("x-cos-forbid-overwrite") != "true" { t.Error("missing overwrite protection") } stored, _ = io.ReadAll(r.Body) w.WriteHeader(200) return } if stored == nil { w.WriteHeader(404) _, _ = io.WriteString(w, "NoSuchKey") return } if r.Header.Get("Authorization") == "" && !publicReadable { w.WriteHeader(403) return } w.Header().Set("Content-Type", "image/png") _, _ = w.Write(stored) })) defer server.Close() u, _ := url.Parse(server.URL) c := cos.NewClient(&cos.BaseURL{BucketURL: u}, &http.Client{Transport: &cos.AuthorizationTransport{SecretID: "test-id", SecretKey: "test-key"}}) c.Conf.EnableCRC = false makeAsset := func() *asset { return &asset{File: "test.png", Key: "batch/image.png", URL: server.URL + "/batch/image.png", Data: data} } a := makeAsset() if err := upload(context.Background(), c, server.Client(), a); err != nil { t.Fatal(err) } if puts != 1 || !a.Uploaded || aclWrites != 0 { t.Fatal("unexpected new upload state") } if err := upload(context.Background(), c, server.Client(), makeAsset()); err != nil { t.Fatal(err) } if puts != 1 { t.Fatal("repeat upload wrote the object again") } stored = []byte("unrelated-object") if err := upload(context.Background(), c, server.Client(), makeAsset()); err == nil || puts != 1 { t.Fatal("overwrote a conflicting object") } stored = nil publicReadable = false a = makeAsset() if err := upload(context.Background(), c, server.Client(), a); err != nil { t.Fatal(err) } if aclWrites != 1 || !a.PublicReadSet { t.Fatal("new public avatar ACL was not scoped to the object") } } func TestPublicCheckRejectsUnreachableOrChangedImage(t *testing.T) { status := http.StatusForbidden server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "" || r.URL.RawQuery != "" { t.Error("public check must be anonymous and unsigned") } w.Header().Set("Content-Type", "image/png") w.WriteHeader(status) _, _ = w.Write([]byte("different")) })) defer server.Close() a := &asset{URL: server.URL, Data: []byte("original")} if code, err := publicCheck(context.Background(), server.Client(), a); err == nil || code != 403 { t.Fatal("accepted inaccessible image") } status = http.StatusOK if _, err := publicCheck(context.Background(), server.Client(), a); err == nil { t.Fatal("accepted different image content") } }