Skip to content
quarry Artifact [dbda0c7f54]
Login

Artifact [dbda0c7f54]

Artifact dbda0c7f549ade4cc8edabd1444b43fa1e52d9d9e5ce55a6370094a218f12dc3:


package s3

import (
	"context"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

// AWS's published vector for a presigned GET (SigV4 docs, "Query
// string parameters"): the one key, secret, region and instant whose
// answer is written down, so a wrong canonicalisation fails here and
// not against a steward's bucket.
func TestSignMatchesTheAWSVector(t *testing.T) {
	b := Bucket{Region: "us-east-1", AccessKey: "AKIAIOSFODNN7EXAMPLE",
		Secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}
	now := time.Date(2013, 5, 24, 0, 0, 0, 0, time.UTC)
	q := b.sign(http.MethodGet, "examplebucket.s3.amazonaws.com", "/test.txt", 86400*time.Second, now, nil)
	want := map[string]string{
		"X-Amz-Algorithm":     "AWS4-HMAC-SHA256",
		"X-Amz-Credential":    "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request",
		"X-Amz-Date":          "20130524T000000Z",
		"X-Amz-Expires":       "86400",
		"X-Amz-SignedHeaders": "host",
		"X-Amz-Signature":     "aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404",
	}
	for k, v := range want {
		if got := q.Get(k); got != v {
			t.Errorf("FAIL: %s = %q, want %q", k, got, v)
		}
	}
	t.Log("ok: the signature matches AWS's vector")
}

// Path style: the bucket is the first path segment, the key follows
// it with its own characters encoded the way the signature saw them,
// and every SigV4 parameter is on the query string.
func TestPresignIsPathStyle(t *testing.T) {
	b := Bucket{Endpoint: "https://acct.r2.cloudflarestorage.com", Name: "files", Region: "auto",
		AccessKey: "AK", Secret: "SK"}
	u, err := b.Presign(http.MethodPut, "attachments/ab cd", time.Minute, time.Now(),
		map[string]string{"Content-Type": "image/png", "content-length": "12"})
	if err != nil {
		t.Fatal(err)
	}
	if u.Host != "acct.r2.cloudflarestorage.com" || u.EscapedPath() != "/files/attachments/ab%20cd" {
		t.Errorf("FAIL: url %s", u)
	}
	q := u.Query()
	for _, k := range []string{"X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date", "X-Amz-Expires", "X-Amz-SignedHeaders", "X-Amz-Signature"} {
		if q.Get(k) == "" {
			t.Errorf("FAIL: %s missing from %s", k, u)
		}
	}
	if q.Get("X-Amz-SignedHeaders") != "content-length;content-type;host" {
		t.Errorf("FAIL: signed headers %q", q.Get("X-Amz-SignedHeaders"))
	}
	if strings.Contains(u.String(), "SK") {
		t.Error("FAIL: the secret is in the url")
	}
	if _, err := (Bucket{Endpoint: "ftp://x", Name: "b"}).Presign("GET", "k", time.Minute, time.Now(), nil); err == nil {
		t.Error("FAIL: a non-http endpoint presigned")
	}
	t.Log("ok: path-style url with every SigV4 parameter")
}

// The probe PUTs then DELETEs one object under attachments/, both
// presigned; the server's refusal comes back as the verb, the status
// and the service's code, never the secret.
func TestProbePutsThenDeletes(t *testing.T) {
	var verbs, keys []string
	var deleteStatus int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		verbs = append(verbs, r.Method)
		keys = append(keys, r.URL.Path)
		q := r.URL.Query()
		if q.Get("X-Amz-Algorithm") != "AWS4-HMAC-SHA256" || q.Get("X-Amz-Signature") == "" ||
			!strings.HasPrefix(q.Get("X-Amz-Credential"), "AK/") {
			http.Error(w, "unsigned", http.StatusBadRequest)
			return
		}
		if r.Method == http.MethodPut {
			body, _ := io.ReadAll(r.Body)
			if len(body) == 0 || r.Header.Get("Content-Type") != "text/plain" {
				http.Error(w, "no body", http.StatusBadRequest)
				return
			}
			w.WriteHeader(http.StatusOK)
			return
		}
		if deleteStatus != 0 {
			w.WriteHeader(deleteStatus)
			io.WriteString(w, `<?xml version="1.0"?><Error><Code>AccessDenied</Code><Message>no</Message></Error>`)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}))
	defer srv.Close()
	b := Bucket{Endpoint: srv.URL, Name: "files", Region: "auto", AccessKey: "AK", Secret: "topsecret"}
	if err := b.Probe(context.Background(), srv.Client(), time.Now()); err != nil {
		t.Fatalf("FAIL: probe: %v", err)
	}
	if len(verbs) != 2 || verbs[0] != "PUT" || verbs[1] != "DELETE" || keys[0] != keys[1] ||
		!strings.HasPrefix(keys[0], "/files/attachments/.probe-") {
		t.Fatalf("FAIL: requests %v %v", verbs, keys)
	}
	deleteStatus = http.StatusForbidden
	err := b.Probe(context.Background(), srv.Client(), time.Now())
	var pe *ProbeError
	if !errors.As(err, &pe) || pe.Verb != "DELETE" || pe.Status != 403 || pe.Code != "AccessDenied" {
		t.Fatalf("FAIL: want a DELETE 403 AccessDenied, got %v", err)
	}
	if strings.Contains(err.Error(), "topsecret") {
		t.Fatal("FAIL: the secret is in the error")
	}
	t.Logf("ok: PUT then DELETE of %s; refusal reads %q", keys[0], err)
}