From 788b42dba1ad1573d67cfc1fe91b27f758a367c2 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 5 Nov 2013 13:47:16 -0700 Subject: [PATCH] Trucking along with S3 signing --- common.go | 24 +++++++++++++++++-- common_test.go | 19 +++++++++++++++ s3.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ s3_test.go | 28 ++++++++++++++++++---- sign4.go | 3 +-- 5 files changed, 130 insertions(+), 8 deletions(-) diff --git a/common.go b/common.go index f11dc2c..65aae97 100644 --- a/common.go +++ b/common.go @@ -2,6 +2,7 @@ package awsauth import ( "crypto/hmac" + "crypto/sha1" "crypto/sha256" "fmt" "io/ioutil" @@ -15,12 +16,25 @@ import ( func serviceAndRegion(host string) (string, string) { var region, service string parts := strings.Split(host, ".") + service = parts[0] + if len(parts) >= 4 { - region = parts[1] + if parts[1] == "s3" { + region = parts[0] + service = parts[1] + } else { + region = parts[1] + } } else { - region = "us-east-1" // default. http://docs.aws.amazon.com/general/latest/gr/rande.html + if strings.HasPrefix(parts[0], "s3-") { + service = parts[0][:2] + region = parts[0][3:] + } else { + region = "us-east-1" // default. http://docs.aws.amazon.com/general/latest/gr/rande.html + } } + return service, region } @@ -51,6 +65,12 @@ func hmacSHA256(key []byte, content string) []byte { return mac.Sum(nil) } +func hmacSHA1(key []byte, content string) []byte { + mac := hmac.New(sha1.New, key) + mac.Write([]byte(content)) + return mac.Sum(nil) +} + func hashSHA256(content string) string { h := sha256.New() h.Write([]byte(content)) diff --git a/common_test.go b/common_test.go index 9c7202e..e6a2501 100644 --- a/common_test.go +++ b/common_test.go @@ -14,6 +14,18 @@ func TestCommonFunctions(t *testing.T) { s2, r2 := serviceAndRegion("iam.amazonaws.com") So(s2, ShouldEqual, "iam") So(r2, ShouldEqual, "us-east-1") + + s3, r3 := serviceAndRegion("bucketname.s3.amazonaws.com") + So(s3, ShouldEqual, "s3") + So(r3, ShouldEqual, "bucketname") + + s4, r4 := serviceAndRegion("s3.amazonaws.com") + So(s4, ShouldEqual, "s3") + So(r4, ShouldEqual, "us-east-1") + + s5, r5 := serviceAndRegion("s3-us-west-1.amazonaws.com") + So(s5, ShouldEqual, "s3") + So(r5, ShouldEqual, "us-west-1") }) Convey("SHA-256 hashes should be properly hex-encoded (base 16)", t, func() { @@ -33,6 +45,13 @@ func TestCommonFunctions(t *testing.T) { So(actual, ShouldResemble, expected) }) + + Convey("HMAC-SHA1 should be properly computed", func() { + expected := []byte{164, 77, 252, 0, 87, 109, 207, 110, 163, 75, 228, 122, 83, 255, 233, 237, 125, 206, 85, 70} + actual := hmacSHA1(key, contents) + + So(actual, ShouldResemble, expected) + }) }) Convey("Strings should be properly concatenated with a delimiter", t, func() { diff --git a/s3.go b/s3.go index 1afd1ac..b8a82cd 100644 --- a/s3.go +++ b/s3.go @@ -2,9 +2,73 @@ package awsauth import ( "fmt" + "net/http" + "sort" + "strings" + "time" ) func signatureS3(stringToSign string) string { fmt.Println("TODO") return "" } + +func canonicalAmzHeadersS3(req *http.Request) string { + var headers []string + + for header := range req.Header { + standardized := strings.ToLower(strings.TrimSpace(header)) + if strings.HasPrefix(standardized, "x-amz") { + headers = append(headers, standardized) + } + } + + sort.Strings(headers) + + for i, header := range headers { + headers[i] = header + ":" + strings.Replace(req.Header.Get(header), "\n", " ", -1) + } + + return strings.Join(headers, "\n") + "\n" +} + +func canonicalResourceS3(req *http.Request) string { + res := "" + + if isS3VirtualHostedStyle(req) { + _, bucketname := serviceAndRegion(req.Host) + res += "/" + bucketname + } + + res += req.URL.Path + + for _, subres := range strings.Split(subresourcesS3, ",") { + if strings.HasPrefix(req.URL.RawQuery, subres) { + res += "?" + subres + } + } + + return res +} + +func prepareRequestS3(req *http.Request) *http.Request { + // TODO: test + if req.URL.Path == "" { + req.URL.Path += "/" + } + return req +} + +func isS3VirtualHostedStyle(req *http.Request) bool { + service, _ := serviceAndRegion(req.Host) + return service == "s3" && strings.Count(req.Host, ".") == 3 +} + +func timestampS3() string { + return now().Format(timeFormatS3) +} + +const ( + timeFormatS3 = time.RFC1123Z + subresourcesS3 = "acl,lifecycle,location,logging,notification,partNumber,policy,requestPayment,torrent,uploadId,uploads,versionId,versioning,versions,website" +) diff --git a/s3_test.go b/s3_test.go index 5146d01..f955c99 100644 --- a/s3_test.go +++ b/s3_test.go @@ -1,14 +1,34 @@ package awsauth import ( - //. "github.com/smartystreets/goconvey/convey" - //"net/http" - //"net/url" + . "github.com/smartystreets/goconvey/convey" + "net/http" "testing" - //"time" ) func TestSignatureS3(t *testing.T) { // http://docs.aws.amazon.com/AmazonS3/2006-03-01/dev/RESTAuthentication.html + Convey("Given a GET request to Amazon S3", t, func() { + req, _ := http.NewRequest("GET", "https://bucketname.s3.amazonaws.com/photos/puppy.jpg", nil) + req.Header.Set("X-Amz-Meta-Something", "more foobar") + req.Header.Set("X-Amz-Date", "foobar") + req.Header.Set("X-Foobar", "nanoo-nanoo") + + Convey("The CanonicalizedAmzHeaders should be built properly", func() { + actual := canonicalAmzHeadersS3(req) + So(actual, ShouldEqual, expectedCanonAmzHeadersS3) + }) + + Convey("The CanonicalizedResource should be built properly", func() { + actual := canonicalResourceS3(req) + So(actual, ShouldEqual, expectedCanonResourceS3) + }) + }) } + +var ( + testCredS3 = &Credentials{"AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"} + expectedCanonAmzHeadersS3 = "x-amz-date:foobar\nx-amz-meta-something:more foobar\n" + expectedCanonResourceS3 = "/bucketname/photos/puppy.jpg" +) diff --git a/sign4.go b/sign4.go index c522188..5aa4463 100644 --- a/sign4.go +++ b/sign4.go @@ -76,8 +76,7 @@ func buildAuthHeaderV4(signature string, meta *metadata) string { } func timestampV4() string { - t := now().Format(timeFormatV4) - return t + return now().Format(timeFormatV4) } func tsDateV4(timestamp string) string {