mesh/merge-gate head efd3d224e131 merged into trunk c9f978d3308b — pass, with a note: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head efd3d224e131 merged into trunk c9f978d3308b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
164 lines
6.6 KiB
Go
164 lines
6.6 KiB
Go
package objectlink
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Issue 1592: one helper signs and streams a store link (novox/hq ADR 0468 §3).
|
|
|
|
type roundTrip func(*http.Request) (*http.Response, error)
|
|
|
|
func (f roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
|
|
|
func hexSum(b []byte) string { s := sha256.Sum256(b); return hex.EncodeToString(s[:]) }
|
|
|
|
// 1592/AC1: AWS's documented query-presign example (a GET of test.txt, 86400 s, 2013-05-24) gives the documented signature.
|
|
func TestPresignMatchesAWSDocumentedExample(t *testing.T) {
|
|
now := time.Date(2013, 5, 24, 0, 0, 0, 0, time.UTC)
|
|
query := sign("GET", "examplebucket.s3.amazonaws.com", "/test.txt", "AKIAIOSFODNN7EXAMPLE",
|
|
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "us-east-1", now, 86400)
|
|
const want = "aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404"
|
|
if !strings.HasSuffix(query, "&X-Amz-Signature="+want) {
|
|
t.Fatalf("the signature differs from AWS's documented one: %s", query)
|
|
}
|
|
if !strings.Contains(query, "X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request") {
|
|
t.Errorf("the credential is not encoded as documented: %s", query)
|
|
}
|
|
}
|
|
|
|
// 1592/AC2: an Expires of 0, below 0 or above 600 s is refused; the signed host is the endpoint's host.
|
|
func TestPresignRefusesBadExpiresAndKeepsTheHost(t *testing.T) {
|
|
r := Request{Method: "PUT", Endpoint: "http://minio.internal:9000", Bucket: "files", Key: "a b/c.pdf",
|
|
AccessKey: "ak", SecretKey: "sk", Expires: 10 * time.Minute, Now: time.Unix(1700000000, 0)}
|
|
link, err := Presign(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
u, err := url.Parse(link)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if u.Host != "minio.internal:9000" || u.Path != "/files/a b/c.pdf" || u.Query().Get("X-Amz-Expires") != "600" ||
|
|
u.Query().Get("X-Amz-SignedHeaders") != "host" {
|
|
t.Errorf("the link is %s", link)
|
|
}
|
|
for _, e := range []time.Duration{0, -time.Second, 500 * time.Millisecond, 601 * time.Second, time.Hour} {
|
|
r.Expires = e
|
|
if _, err := Presign(r); err == nil {
|
|
t.Errorf("Expires %s was accepted", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1592/AC3: Put streams and answers size and SHA-256 as the server saw them, always with the declared Content-Length;
|
|
// an unknown size, one past the limit, or a body not ending as declared is refused; a 403 with an S3 error names its code.
|
|
func TestPutStreamsCountsAndRefuses(t *testing.T) {
|
|
var gotBody []byte
|
|
var lengths []int64
|
|
store := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotBody, _ = io.ReadAll(r.Body)
|
|
lengths = append(lengths, r.ContentLength)
|
|
if r.URL.Query().Get("deny") != "" {
|
|
w.WriteHeader(403)
|
|
io.WriteString(w, `<?xml version="1.0"?><Error><Code>SignatureDoesNotMatch</Code><Message>nope</Message></Error>`)
|
|
}
|
|
}))
|
|
defer store.Close()
|
|
data := bytes.Repeat([]byte("0123456789"), 100)
|
|
|
|
// A reader with no Len or Seek: still sent with the declared length.
|
|
got, err := Put(context.Background(), store.Client(), store.URL+"/b/k", io.MultiReader(bytes.NewReader(data)), 1000, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Size != int64(len(gotBody)) || got.SHA256 != hexSum(gotBody) || got.Size != 1000 {
|
|
t.Errorf("answered %+v, the server received %d bytes %s", got, len(gotBody), hexSum(gotBody))
|
|
}
|
|
if _, err := Put(context.Background(), store.Client(), store.URL+"/b/k", bytes.NewReader(data), 1000, 1000); err != nil {
|
|
t.Errorf("exactly the limit: %v", err)
|
|
}
|
|
for _, l := range lengths {
|
|
if l != 1000 {
|
|
t.Errorf("a request carried Content-Length %d, not the declared 1000", l)
|
|
}
|
|
}
|
|
if len(lengths) != 2 {
|
|
t.Fatalf("the server saw %d requests", len(lengths))
|
|
}
|
|
|
|
// Refused before any request: unknown (negative) size, and a size past the limit.
|
|
if _, err := Put(context.Background(), store.Client(), store.URL+"/b/k", bytes.NewReader(data), -1, 0); err == nil || !strings.Contains(err.Error(), "size") {
|
|
t.Errorf("an unknown size: %v", err)
|
|
}
|
|
if _, err := Put(context.Background(), store.Client(), store.URL+"/b/k", bytes.NewReader(data), 1000, 999); err == nil || !strings.Contains(err.Error(), "ADR 0468 §3") {
|
|
t.Errorf("a size past the limit: %v", err)
|
|
}
|
|
if len(lengths) != 2 {
|
|
t.Errorf("a refused call made a request: the server saw %d", len(lengths))
|
|
}
|
|
|
|
// A body longer or shorter than declared fails, and the store is not told it succeeded.
|
|
reads := &http.Client{Transport: roundTrip(func(r *http.Request) (*http.Response, error) {
|
|
_, err := io.Copy(io.Discard, r.Body)
|
|
if err == nil {
|
|
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(""))}, nil
|
|
}
|
|
return nil, err
|
|
})}
|
|
for name, declared := range map[string]int64{"longer than declared": 999, "shorter than declared": 1001} {
|
|
if _, err := Put(context.Background(), reads, store.URL+"/b/k", bytes.NewReader(data), declared, 0); err == nil {
|
|
t.Errorf("a body %s was accepted", name)
|
|
}
|
|
}
|
|
|
|
_, err = Put(context.Background(), store.Client(), store.URL+"/b/k?deny=1", bytes.NewReader(data), 1000, 0)
|
|
e, ok := err.(*StoreError)
|
|
if !ok || e.Status != 403 || e.Code != "SignatureDoesNotMatch" || !strings.Contains(e.Error(), "SignatureDoesNotMatch") {
|
|
t.Fatalf("the refusal is %v", err)
|
|
}
|
|
}
|
|
|
|
// 1592/AC4: Get writes the body and answers size and SHA-256; a wrong digest and a document past the limit are errors.
|
|
func TestGetStreamsChecksAndRefuses(t *testing.T) {
|
|
data := bytes.Repeat([]byte("abcdefghij"), 100)
|
|
store := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write(data)
|
|
}))
|
|
defer store.Close()
|
|
var out bytes.Buffer
|
|
got, err := Get(context.Background(), store.Client(), store.URL+"/b/k", &out, 0, hexSum(data))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Size != 1000 || got.SHA256 != hexSum(data) || !bytes.Equal(out.Bytes(), data) {
|
|
t.Errorf("answered %+v, wrote %d bytes", got, out.Len())
|
|
}
|
|
if _, err := Get(context.Background(), store.Client(), store.URL+"/b/k", io.Discard, 0, hexSum([]byte("other"))); err == nil {
|
|
t.Error("a wrong digest was accepted")
|
|
}
|
|
if _, err := Get(context.Background(), store.Client(), store.URL+"/b/k", io.Discard, 999, ""); err == nil {
|
|
t.Error("a document past the limit was accepted")
|
|
}
|
|
chunked := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.(http.Flusher).Flush()
|
|
w.Write(data)
|
|
}))
|
|
defer chunked.Close()
|
|
if _, err := Get(context.Background(), chunked.Client(), chunked.URL+"/b/k", io.Discard, 999, ""); err == nil {
|
|
t.Error("a chunked document past the limit was accepted")
|
|
}
|
|
if _, err := Get(context.Background(), chunked.Client(), chunked.URL+"/b/k", io.Discard, 1000, ""); err != nil {
|
|
t.Errorf("exactly the limit: %v", err)
|
|
}
|
|
}
|