增加复制粘贴交互
This commit is contained in:
1 parent
4195525dc7
commit
deb9cc5bcf
14 files changed
+1144
-31
No files matched your search
@@ -0,0 +1,261 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"image"
|
||||
"image/gif"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type importFetcher func(context.Context, string) (Blob, error)
|
||||
|
||||
func (f importFetcher) Fetch(ctx context.Context, u string) (Blob, error) { return f(ctx, u) }
|
||||
|
||||
func TestImportImageStoresRecognizedBytesAsUpload(t *testing.T) {
|
||||
pngData, jpegData, webpData := imageFormatFixtures(t)
|
||||
var gifBuffer bytes.Buffer
|
||||
if err := gif.Encode(&gifBuffer, image.NewRGBA(image.Rect(0, 0, 2, 2)), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name, ext, mime string
|
||||
data []byte
|
||||
}{
|
||||
{"png", ".png", "image/png", pngData},
|
||||
{"jpeg", ".jpg", "image/jpeg", jpegData},
|
||||
{"webp", ".webp", "image/webp", webpData},
|
||||
{"gif", ".gif", "image/gif", gifBuffer.Bytes()},
|
||||
{"bmp", ".bmp", "image/bmp", importBMP()},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cat, store := &memoryCatalog{}, &memoryBlobs{}
|
||||
svc := NewService(cat, store, importFetcher(func(context.Context, string) (Blob, error) {
|
||||
return Blob{Body: io.NopCloser(bytes.NewReader(tc.data)), ContentType: "text/html", Size: int64(len(tc.data))}, nil
|
||||
}), nil, func(prefix string) string { return prefix + "-1" })
|
||||
asset, err := svc.ImportImage(context.Background(), PlatformScope("owner-a"), "https://images.test/picture.svg?secret=signed")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if asset.OwnerID != "owner-a" || asset.Source != SourceUpload || asset.Kind != KindImage || asset.Name != "导入图片"+tc.ext || asset.Metadata["contentType"] != tc.mime || store.putContentType != tc.mime || !bytes.Equal(store.putBody, tc.data) || len(cat.assets) != 1 {
|
||||
t.Fatalf("asset=%#v store=%q", asset, store.putContentType)
|
||||
}
|
||||
if bytes.Contains([]byte(asset.URL), []byte("signed")) || asset.Metadata["importedFrom"] != nil {
|
||||
t.Fatalf("remote URL persisted: %#v", asset)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportImageRejectsInvalidInputWithoutCreatingRecord(t *testing.T) {
|
||||
pngData, _, _ := imageFormatFixtures(t)
|
||||
for _, tc := range []struct {
|
||||
name, url string
|
||||
data []byte
|
||||
want error
|
||||
}{
|
||||
{"svg", "https://images.test/a.svg", []byte(`<svg xmlns="http://www.w3.org/2000/svg"/>`), ErrImportImageUnsupported},
|
||||
{"html", "https://images.test/a.png", []byte("<html>login</html>"), ErrImportImageUnsupported},
|
||||
{"fake png", "https://images.test/a.png", []byte("not png"), ErrImportImageUnsupported},
|
||||
{"truncated png", "https://images.test/a.png", pngData[:32], ErrImportImageUnsupported},
|
||||
{"userinfo", "https://user:password@images.test/a.png", pngData, ErrImportImageURL},
|
||||
{"protocol", "file:///etc/passwd", pngData, ErrImportImageURL},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cat, store := &memoryCatalog{}, &memoryBlobs{}
|
||||
svc := NewService(cat, store, importFetcher(func(context.Context, string) (Blob, error) {
|
||||
return Blob{Body: io.NopCloser(bytes.NewReader(tc.data)), Size: int64(len(tc.data))}, nil
|
||||
}), nil, nil)
|
||||
if _, err := svc.ImportImage(context.Background(), PlatformScope("owner"), tc.url); !errors.Is(err, tc.want) {
|
||||
t.Fatalf("err=%v want=%v", err, tc.want)
|
||||
}
|
||||
if len(cat.assets) != 0 || store.putKey != "" {
|
||||
t.Fatalf("persisted on rejection: %#v %q", cat.assets, store.putKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportImageBoundsAndCompensation(t *testing.T) {
|
||||
pngData, _, _ := imageFormatFixtures(t)
|
||||
cat, store := &memoryCatalog{createErr: errors.New("db down")}, &memoryBlobs{}
|
||||
svc := NewService(cat, store, importFetcher(func(context.Context, string) (Blob, error) {
|
||||
return Blob{Body: io.NopCloser(bytes.NewReader(pngData)), Size: int64(len(pngData))}, nil
|
||||
}), nil, nil)
|
||||
if _, err := svc.ImportImage(context.Background(), PlatformScope("owner"), "https://images.test/a.png"); !errors.Is(err, cat.createErr) || len(store.deleted) != 1 {
|
||||
t.Fatalf("err=%v deleted=%v", err, store.deleted)
|
||||
}
|
||||
cat2, store2 := &memoryCatalog{}, &memoryBlobs{}
|
||||
svc2 := NewService(cat2, store2, importFetcher(func(context.Context, string) (Blob, error) {
|
||||
return Blob{Body: io.NopCloser(bytes.NewReader(pngData)), Size: maxImportImageBytes + 1}, nil
|
||||
}), nil, nil)
|
||||
if _, err := svc2.ImportImage(context.Background(), PlatformScope("owner"), "https://images.test/a.png"); !errors.Is(err, ErrRemoteTooLarge) || len(cat2.assets) != 0 {
|
||||
t.Fatalf("err=%v records=%d", err, len(cat2.assets))
|
||||
}
|
||||
cat3, store3 := &memoryCatalog{}, &memoryBlobs{}
|
||||
svc3 := NewService(cat3, store3, importFetcher(func(context.Context, string) (Blob, error) {
|
||||
return Blob{Body: io.NopCloser(io.LimitReader(&repeatingReader{value: 'x'}, maxImportImageBytes+1)), Size: -1}, nil
|
||||
}), nil, nil)
|
||||
if _, err := svc3.ImportImage(context.Background(), PlatformScope("owner"), "https://images.test/a.png"); !errors.Is(err, ErrRemoteTooLarge) || len(cat3.assets) != 0 || store3.putKey != "" {
|
||||
t.Fatalf("unknown length error=%v records=%d key=%q", err, len(cat3.assets), store3.putKey)
|
||||
}
|
||||
cat4, store4 := &memoryCatalog{}, &failingImportBlobStore{memoryBlobs: &memoryBlobs{}}
|
||||
svc4 := NewService(cat4, store4, importFetcher(func(context.Context, string) (Blob, error) {
|
||||
return Blob{Body: io.NopCloser(bytes.NewReader(pngData)), Size: int64(len(pngData))}, nil
|
||||
}), nil, nil)
|
||||
if _, err := svc4.ImportImage(context.Background(), PlatformScope("owner"), "https://images.test/a.png"); !errors.Is(err, store4.failure) || len(cat4.assets) != 0 {
|
||||
t.Fatalf("put failure error=%v records=%d", err, len(cat4.assets))
|
||||
}
|
||||
}
|
||||
|
||||
type repeatingReader struct{ value byte }
|
||||
|
||||
func (r *repeatingReader) Read(p []byte) (int, error) {
|
||||
for i := range p {
|
||||
p[i] = r.value
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type failingImportBlobStore struct {
|
||||
*memoryBlobs
|
||||
failure error
|
||||
}
|
||||
|
||||
func (s *failingImportBlobStore) Put(context.Context, string, io.Reader, int64, string) (StoredObject, error) {
|
||||
if s.failure == nil {
|
||||
s.failure = errors.New("object store failed")
|
||||
}
|
||||
return StoredObject{}, s.failure
|
||||
}
|
||||
|
||||
func TestImportImageURLLengthAndSafeFetcherError(t *testing.T) {
|
||||
pngData, _, _ := imageFormatFixtures(t)
|
||||
remoteCalled := false
|
||||
remote := importFetcher(func(_ context.Context, _ string) (Blob, error) {
|
||||
remoteCalled = true
|
||||
return Blob{Body: io.NopCloser(bytes.NewReader(pngData)), Size: int64(len(pngData))}, nil
|
||||
})
|
||||
svc := NewService(&memoryCatalog{}, &memoryBlobs{}, remote, nil, nil)
|
||||
base := "https://images.test/a.png?signature="
|
||||
allowed := base + strings.Repeat("x", maxImportImageURLBytes-len(base))
|
||||
if _, err := svc.ImportImage(context.Background(), PlatformScope("owner"), allowed); err != nil || !remoteCalled {
|
||||
t.Fatalf("8192-byte URL err=%v called=%v", err, remoteCalled)
|
||||
}
|
||||
remoteCalled = false
|
||||
if _, err := svc.ImportImage(context.Background(), PlatformScope("owner"), allowed+"x"); !errors.Is(err, ErrImportImageURL) || remoteCalled {
|
||||
t.Fatalf("overlong URL err=%v called=%v", err, remoteCalled)
|
||||
}
|
||||
secret := "https://images.test/a.png?signature=secret-value"
|
||||
svc = NewService(&memoryCatalog{}, &memoryBlobs{}, importFetcher(func(context.Context, string) (Blob, error) { return Blob{}, errors.New("GET " + secret + ": timeout") }), nil, nil)
|
||||
if _, err := svc.ImportImage(context.Background(), PlatformScope("owner"), secret); !errors.Is(err, ErrImportImageUnavailable) || strings.Contains(err.Error(), "secret-value") {
|
||||
t.Fatalf("unsafe fetch error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportImageRejectsPrivateDestinationsAndPrivateRedirectWithProductionPolicy(t *testing.T) {
|
||||
privateDialer := &importRedirectDialer{}
|
||||
policy := NewPublicDestinationPolicy(nil, privateDialer)
|
||||
fetcher, err := NewPublicHTTPRemoteFetcher(time.Second, maxImportImageBytes, policy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, target := range []string{"http://127.0.0.1/a.png", "http://169.254.169.254/latest/meta-data", "http://[::1]/a.png"} {
|
||||
cat := &memoryCatalog{}
|
||||
svc := NewService(cat, &memoryBlobs{}, fetcher, nil, nil)
|
||||
if _, err := svc.ImportImage(context.Background(), PlatformScope("owner"), target); !errors.Is(err, ErrImportImageUnavailable) || len(cat.assets) != 0 {
|
||||
t.Fatalf("private target %q err=%v assets=%d", target, err, len(cat.assets))
|
||||
}
|
||||
}
|
||||
if privateDialer.calls != 0 {
|
||||
t.Fatalf("private targets reached dialer %d times", privateDialer.calls)
|
||||
}
|
||||
dialer := &importRedirectDialer{}
|
||||
policy = NewPublicDestinationPolicy(nil, dialer)
|
||||
fetcher, err = NewPublicHTTPRemoteFetcher(time.Second, maxImportImageBytes, policy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cat := &memoryCatalog{}
|
||||
svc := NewService(cat, &memoryBlobs{}, fetcher, nil, nil)
|
||||
if _, err := svc.ImportImage(context.Background(), PlatformScope("owner"), "http://8.8.8.8/a.png?signature=secret"); !errors.Is(err, ErrImportImageUnavailable) || len(cat.assets) != 0 || dialer.calls != 1 {
|
||||
t.Fatalf("private redirect err=%v assets=%d dial calls=%d", err, len(cat.assets), dialer.calls)
|
||||
}
|
||||
}
|
||||
|
||||
type importRedirectDialer struct{ calls int }
|
||||
|
||||
func (d *importRedirectDialer) DialContext(context.Context, string, string) (net.Conn, error) {
|
||||
d.calls++
|
||||
client, server := net.Pipe()
|
||||
go func() {
|
||||
_, _ = io.WriteString(server, "HTTP/1.1 302 Found\r\nLocation: http://169.254.169.254/private\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
|
||||
_ = server.Close()
|
||||
}()
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func TestImportImageRejectsFakeExtendedWebP(t *testing.T) {
|
||||
// Valid canvas metadata with an unrelated trailing chunk is not an image.
|
||||
webp := make([]byte, 12)
|
||||
copy(webp, "RIFF")
|
||||
copy(webp[8:], "WEBP")
|
||||
webp = appendWebPChunk(webp, "VP8X", []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||
webp = appendWebPChunk(webp, "EXIF", []byte("fake"))
|
||||
binary.LittleEndian.PutUint32(webp[4:8], uint32(len(webp)-8))
|
||||
if validWebP(webp) {
|
||||
t.Fatal("VP8X without image payload accepted")
|
||||
}
|
||||
webp = appendWebPChunk(webp, "ANMF", []byte("fake"))
|
||||
binary.LittleEndian.PutUint32(webp[4:8], uint32(len(webp)-8))
|
||||
if validWebP(webp) {
|
||||
t.Fatal("animation payload accepted")
|
||||
}
|
||||
_, _, simple := imageFormatFixtures(t)
|
||||
valid := make([]byte, 12)
|
||||
copy(valid, "RIFF")
|
||||
copy(valid[8:], "WEBP")
|
||||
valid = appendWebPChunk(valid, "VP8X", []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||
valid = append(valid, simple[12:]...)
|
||||
binary.LittleEndian.PutUint32(valid[4:8], uint32(len(valid)-8))
|
||||
if !validWebP(valid) {
|
||||
t.Fatal("valid extended still WebP was rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportImagePixelLimit(t *testing.T) {
|
||||
if !validImageDimensions(5000, 5000) || validImageDimensions(5001, 5000) || validImageDimensions(0, 1) {
|
||||
t.Fatal("pixel limit does not enforce 25 million positive pixels")
|
||||
}
|
||||
}
|
||||
|
||||
func appendWebPChunk(b []byte, name string, data []byte) []byte {
|
||||
b = append(b, []byte(name)...)
|
||||
var size [4]byte
|
||||
binary.LittleEndian.PutUint32(size[:], uint32(len(data)))
|
||||
b = append(b, size[:]...)
|
||||
b = append(b, data...)
|
||||
if len(data)&1 != 0 {
|
||||
b = append(b, 0)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func importBMP() []byte {
|
||||
b := make([]byte, 70)
|
||||
copy(b, "BM")
|
||||
binary.LittleEndian.PutUint32(b[2:6], uint32(len(b)))
|
||||
binary.LittleEndian.PutUint32(b[10:14], 54)
|
||||
binary.LittleEndian.PutUint32(b[14:18], 40)
|
||||
binary.LittleEndian.PutUint32(b[18:22], 2)
|
||||
binary.LittleEndian.PutUint32(b[22:26], 2)
|
||||
binary.LittleEndian.PutUint16(b[26:28], 1)
|
||||
binary.LittleEndian.PutUint16(b[28:30], 24)
|
||||
return b
|
||||
}
|
||||
Reference in new issue
Block a user