126 lines
4.0 KiB
Go
126 lines
4.0 KiB
Go
package local
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/storage"
|
|
)
|
|
|
|
func TestSignedPayloadRanges(t *testing.T) {
|
|
store, err := New(t.TempDir(), 1<<20)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data := "%PDF-1.4\n" + strings.Repeat("0123456789", 100) + "\n%%EOF\n"
|
|
ctx := context.Background()
|
|
if _, err = store.Put(ctx, "private/document", strings.NewReader(data), int64(len(data)), "application/pdf"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signer, _ := storage.NewSigner(store, "https://files.example/read", []byte(strings.Repeat("s", 32)))
|
|
link, err := signer.PresignGetWithOptions(ctx, "private/document", 30*time.Minute, storage.ReadOptions{Disposition: storage.DispositionInline, FileName: "示例.pdf"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, tc := range []struct {
|
|
method, span string
|
|
status int
|
|
want string
|
|
}{
|
|
{"GET", "", 200, data}, {"HEAD", "", 200, ""}, {"HEAD", "bytes=0-3", 200, ""},
|
|
{"GET", "bytes=0-7", 206, data[:8]}, {"GET", "bytes=20-", 206, data[20:]}, {"GET", "bytes=-6", 206, data[len(data)-6:]},
|
|
{"GET", "bytes=1000-999999", 206, data[1000:]}, {"GET", "bytes=999999-", 416, ""}, {"GET", "bytes=bad", 416, ""},
|
|
} {
|
|
t.Run(tc.method+tc.span, func(t *testing.T) {
|
|
r := httptest.NewRequest(tc.method, link.URL, nil)
|
|
if tc.span != "" {
|
|
r.Header.Set("Range", tc.span)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
signer.ServeHTTP(w, r)
|
|
if w.Code != tc.status {
|
|
t.Fatal(w.Code)
|
|
}
|
|
if tc.status != 416 && w.Body.String() != tc.want {
|
|
t.Fatal("wrong payload slice")
|
|
}
|
|
if tc.status == 206 && (w.Header().Get("Content-Length") != strconv.Itoa(len(tc.want)) || !strings.HasSuffix(w.Header().Get("Content-Range"), "/"+strconv.Itoa(len(data)))) {
|
|
t.Fatal(w.Header())
|
|
}
|
|
if tc.status == 200 && w.Header().Get("Content-Length") != strconv.Itoa(len(data)) {
|
|
t.Fatal("metadata included in length")
|
|
}
|
|
if tc.span == "bytes=999999-" && w.Header().Get("Content-Range") != "bytes */"+strconv.Itoa(len(data)) {
|
|
t.Fatal(w.Header())
|
|
}
|
|
if w.Header().Get("Cache-Control") != "private, no-store" {
|
|
t.Fatal("cache policy")
|
|
}
|
|
if tc.status != 416 {
|
|
kind, params, err := mime.ParseMediaType(w.Header().Get("Content-Disposition"))
|
|
if err != nil || kind != "inline" || params["filename"] != "示例.pdf" || w.Header().Get("Content-Type") != "application/pdf" {
|
|
t.Fatal(w.Header(), err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
r := httptest.NewRequest("GET", link.URL, nil)
|
|
r.Header.Set("Range", "bytes=0-3,10-13")
|
|
w := httptest.NewRecorder()
|
|
signer.ServeHTTP(w, r)
|
|
kind, params, err := mime.ParseMediaType(w.Header().Get("Content-Type"))
|
|
if err != nil || w.Code != 206 || kind != "multipart/byteranges" {
|
|
t.Fatal(w.Header(), err)
|
|
}
|
|
reader := multipart.NewReader(w.Body, params["boundary"])
|
|
for _, want := range []string{data[:4], data[10:14]} {
|
|
p, err := reader.NextPart()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := io.ReadAll(p)
|
|
if err != nil || string(got) != want {
|
|
t.Fatal("multipart bytes", err)
|
|
}
|
|
}
|
|
if _, err := reader.NextPart(); err != io.EOF {
|
|
t.Fatal(err)
|
|
}
|
|
// Random access cannot expose the managed header and still observes context.
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
object, err := store.Get(ctx, "private/document")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer object.Body.Close()
|
|
seeker := object.Body.(io.ReadSeeker)
|
|
if size, err := seeker.Seek(0, io.SeekEnd); err != nil || size != int64(len(data)) {
|
|
t.Fatal(size, err)
|
|
}
|
|
if _, err := seeker.Seek(-1, io.SeekStart); err == nil {
|
|
t.Fatal("seek into header")
|
|
}
|
|
if _, err := seeker.Seek(0, io.SeekStart); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prefix := make([]byte, 8)
|
|
if _, err := io.ReadFull(seeker, prefix); err != nil || string(prefix) != data[:8] {
|
|
t.Fatal("metadata exposed", err)
|
|
}
|
|
cancel()
|
|
if _, err := seeker.Seek(0, io.SeekStart); err != context.Canceled {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := seeker.Read(prefix); err != context.Canceled {
|
|
t.Fatal(err)
|
|
}
|
|
}
|