| index 75663ed4c..5defbec0e 100644 |
| --- a/internal/lease/leasetest/s3server_test.go |
| +++ b/internal/lease/leasetest/s3server_test.go |
| @@ -2,6 +2,8 @@ package leasetest |
| |
| import ( |
| "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| "errors" |
| "io" |
| "net/http" |
| @@ -119,3 +121,108 @@ func TestS3ServerCopyOfCurrentVersion(t *testing.T) { |
| t.Fatal("a failed precondition copied") |
| } |
| } |
| + |
| +// Real S3 rejects a PUT whose body is not the length the request declares, |
| +// or whose sha256 is not the one the request signed, and stores nothing. The |
| +// fake must do the same, or a client that sends a short or altered body looks |
| +// like it succeeded (a transport can hand the server a partial body before it |
| +// fails). |
| +func TestS3ServerValidatesPutBody(t *testing.T) { |
| + sum := sha256.Sum256([]byte("abcdefghij")) |
| + good := hex.EncodeToString(sum[:]) |
| + wrong := hex.EncodeToString(make([]byte, 32)) |
| + put := func(h http.Handler, key, body string, declared int64, sha string, extra map[string]string) *httptest.ResponseRecorder { |
| + req := httptest.NewRequest(http.MethodPut, "/data/"+key, strings.NewReader(body)) |
| + req.ContentLength = declared |
| + if sha != "" { |
| + req.Header.Set("x-amz-content-sha256", sha) |
| + } |
| + for k, v := range extra { |
| + req.Header.Set(k, v) |
| + } |
| + rec := httptest.NewRecorder() |
| + h.ServeHTTP(rec, req) |
| + return rec |
| + } |
| + for _, c := range []struct { |
| + name string |
| + body string |
| + declared int64 |
| + sha string |
| + code string // empty: accepted |
| + }{ |
| + {"exact", "abcdefghij", 10, good, ""}, |
| + {"unsigned payload", "abcdefghij", 10, "UNSIGNED-PAYLOAD", ""}, |
| + {"no sha header", "abcdefghij", 10, "", ""}, |
| + {"unknown length", "abcdefghij", -1, good, ""}, |
| + {"short body", "abc", 10, good, "IncompleteBody"}, |
| + {"short body unsigned", "abc", 10, "UNSIGNED-PAYLOAD", "IncompleteBody"}, |
| + {"long body", "abcdefghijkl", 10, "", "IncompleteBody"}, |
| + {"wrong sha", "abcdefghij", 10, wrong, "XAmzContentSHA256Mismatch"}, |
| + {"altered body", "abcdefghiX", 10, good, "XAmzContentSHA256Mismatch"}, |
| + } { |
| + for _, createOnly := range []bool{false, true} { |
| + m := NewVersionedBucket() |
| + m.AddBucket("data") |
| + h, _ := NewS3Server(m) |
| + extra := map[string]string{} |
| + if createOnly { |
| + extra["If-None-Match"] = "*" |
| + } |
| + rec := put(h, "k", c.body, c.declared, c.sha, extra) |
| + _, stored := m.Current("data", "k") |
| + if c.code == "" { |
| + if rec.Code != 200 || !stored { |
| + t.Errorf("%s (create-only %v): %d, stored %v", c.name, createOnly, rec.Code, stored) |
| + } |
| + continue |
| + } |
| + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "<Code>"+c.code+"</Code>") { |
| + t.Errorf("%s (create-only %v): %d %q, want 400 %s", c.name, createOnly, rec.Code, rec.Body.String(), c.code) |
| + } |
| + if stored { |
| + t.Errorf("%s (create-only %v): a rejected PUT stored the body", c.name, createOnly) |
| + } |
| + } |
| + } |
| +} |
| + |
| +// A copy carries no body: its Content-Length must be what arrives and its |
| +// signed sha256 (when a digest) the hash of nothing. A copy that fails the |
| +// check stores nothing. |
| +func TestS3ServerValidatesCopyBody(t *testing.T) { |
| + empty := sha256.Sum256(nil) |
| + m := NewVersionedBucket() |
| + m.AddBucket("src") |
| + m.AddBucket("dst") |
| + m.Put("src", "a", []byte("body")) |
| + h, _ := NewS3Server(m) |
| + copyWith := func(dst, body string, declared int64, sha string) *httptest.ResponseRecorder { |
| + req := httptest.NewRequest(http.MethodPut, "/dst/"+dst, strings.NewReader(body)) |
| + req.ContentLength = declared |
| + req.Header.Set("x-amz-copy-source", "/src/a") |
| + if sha != "" { |
| + req.Header.Set("x-amz-content-sha256", sha) |
| + } |
| + rec := httptest.NewRecorder() |
| + h.ServeHTTP(rec, req) |
| + return rec |
| + } |
| + if rec := copyWith("ok", "", 0, hex.EncodeToString(empty[:])); rec.Code != 200 { |
| + t.Fatalf("a well-formed copy: %d %q", rec.Code, rec.Body.String()) |
| + } |
| + if rec := copyWith("ok2", "", 0, "UNSIGNED-PAYLOAD"); rec.Code != 200 { |
| + t.Fatalf("an unsigned-payload copy: %d %q", rec.Code, rec.Body.String()) |
| + } |
| + if rec := copyWith("long", "xyz", 0, ""); rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "<Code>IncompleteBody</Code>") { |
| + t.Fatalf("a copy with a body longer than declared: %d %q", rec.Code, rec.Body.String()) |
| + } |
| + if rec := copyWith("sha", "", 0, hex.EncodeToString(make([]byte, 32))); rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "<Code>XAmzContentSHA256Mismatch</Code>") { |
| + t.Fatalf("a copy with the wrong sha256: %d %q", rec.Code, rec.Body.String()) |
| + } |
| + for _, k := range []string{"long", "sha"} { |
| + if _, ok := m.Current("dst", k); ok { |
| + t.Fatalf("a rejected copy stored %q", k) |
| + } |
| + } |
| +} |