181 lines
5.8 KiB
Go
181 lines
5.8 KiB
Go
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, "<Error><Code>NoSuchKey</Code></Error>")
|
|
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")
|
|
}
|
|
}
|