diff --git a/aws/signer/v4/functional_1_4_test.go b/aws/signer/v4/functional_1_4_test.go new file mode 100644 index 00000000000..e559838b848 --- /dev/null +++ b/aws/signer/v4/functional_1_4_test.go @@ -0,0 +1,40 @@ +// +build !go1.5 + +package v4_test + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws/signer/v4" + "github.com/aws/aws-sdk-go/awstesting/unit" + "github.com/stretchr/testify/assert" +) + +func TestStandaloneSign(t *testing.T) { + creds := unit.Session.Config.Credentials + signer := v4.NewSigner(creds) + + for _, c := range standaloneSignCases { + host := fmt.Sprintf("%s.%s.%s.amazonaws.com", + c.SubDomain, c.Region, c.Service) + + req, err := http.NewRequest("GET", fmt.Sprintf("https://%s", host), nil) + assert.NoError(t, err) + + req.URL.Path = c.OrigURI + req.URL.RawQuery = c.OrigQuery + req.URL.Opaque = fmt.Sprintf("//%s%s", host, c.EscapedURI) + opaqueURI := req.URL.Opaque + + _, err = signer.Sign(req, nil, c.Service, c.Region, time.Unix(0, 0)) + assert.NoError(t, err) + + actual := req.Header.Get("Authorization") + assert.Equal(t, c.ExpSig, actual) + assert.Equal(t, c.OrigURI, req.URL.Path) + assert.Equal(t, opaqueURI, req.URL.Opaque) + } +} diff --git a/aws/signer/v4/functional_1_5_test.go b/aws/signer/v4/functional_1_5_test.go new file mode 100644 index 00000000000..6f0d549d529 --- /dev/null +++ b/aws/signer/v4/functional_1_5_test.go @@ -0,0 +1,40 @@ +// +build go1.5 + +package v4_test + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws/signer/v4" + "github.com/aws/aws-sdk-go/awstesting/unit" + "github.com/stretchr/testify/assert" +) + +func TestStandaloneSign(t *testing.T) { + creds := unit.Session.Config.Credentials + signer := v4.NewSigner(creds) + + for _, c := range standaloneSignCases { + host := fmt.Sprintf("https://%s.%s.%s.amazonaws.com", + c.SubDomain, c.Region, c.Service) + + req, err := http.NewRequest("GET", host, nil) + assert.NoError(t, err) + + // URL.EscapedPath() will be used by the signer to get the + // escaped form of the request's URI path. + req.URL.Path = c.OrigURI + req.URL.RawQuery = c.OrigQuery + + _, err = signer.Sign(req, nil, c.Service, c.Region, time.Unix(0, 0)) + assert.NoError(t, err) + + actual := req.Header.Get("Authorization") + assert.Equal(t, c.ExpSig, actual) + assert.Equal(t, c.OrigURI, req.URL.Path) + assert.Equal(t, c.EscapedURI, req.URL.EscapedPath()) + } +} diff --git a/aws/signer/v4/functional_test.go b/aws/signer/v4/functional_test.go index e4329c6249a..f86293a6077 100644 --- a/aws/signer/v4/functional_test.go +++ b/aws/signer/v4/functional_test.go @@ -7,11 +7,28 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/signer/v4" "github.com/aws/aws-sdk-go/awstesting/unit" "github.com/aws/aws-sdk-go/service/s3" "github.com/stretchr/testify/assert" ) +var standaloneSignCases = []struct { + OrigURI string + OrigQuery string + Region, Service, SubDomain string + ExpSig string + EscapedURI string +}{ + { + OrigURI: `/logs-*/_search`, + OrigQuery: `pretty=true`, + Region: "us-west-2", Service: "es", SubDomain: "hostname-clusterkey", + EscapedURI: `/logs-%2A/_search`, + ExpSig: `AWS4-HMAC-SHA256 Credential=AKID/19700101/us-west-2/es/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=79d0760751907af16f64a537c1242416dacf51204a7dd5284492d15577973b91`, + }, +} + func TestPresignHandler(t *testing.T) { svc := s3.New(unit.Session) req, _ := svc.PutObjectRequest(&s3.PutObjectInput{ @@ -75,3 +92,25 @@ func TestPresignRequest(t *testing.T) { assert.NotContains(t, urlstr, "+") // + encoded as %20 } + +func TestStandaloneSign_CustomURIEscape(t *testing.T) { + var expectSig = `AWS4-HMAC-SHA256 Credential=AKID/19700101/us-east-1/es/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=6601e883cc6d23871fd6c2a394c5677ea2b8c82b04a6446786d64cd74f520967` + + creds := unit.Session.Config.Credentials + signer := v4.NewSigner(creds, func(s *v4.Signer) { + s.DisableURIPathEscaping = true + }) + + host := "https://subdomain.us-east-1.es.amazonaws.com" + req, err := http.NewRequest("GET", host, nil) + assert.NoError(t, err) + + req.URL.Path = `/log-*/_search` + req.URL.Opaque = "//subdomain.us-east-1.es.amazonaws.com/log-%2A/_search" + + _, err = signer.Sign(req, nil, "es", "us-east-1", time.Unix(0, 0)) + assert.NoError(t, err) + + actual := req.Header.Get("Authorization") + assert.Equal(t, expectSig, actual) +} diff --git a/aws/signer/v4/uri_path.go b/aws/signer/v4/uri_path.go new file mode 100644 index 00000000000..bd082e9d1f7 --- /dev/null +++ b/aws/signer/v4/uri_path.go @@ -0,0 +1,24 @@ +// +build go1.5 + +package v4 + +import ( + "net/url" + "strings" +) + +func getURIPath(u *url.URL) string { + var uri string + + if len(u.Opaque) > 0 { + uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/") + } else { + uri = u.EscapedPath() + } + + if len(uri) == 0 { + uri = "/" + } + + return uri +} diff --git a/aws/signer/v4/uri_path_1_4.go b/aws/signer/v4/uri_path_1_4.go new file mode 100644 index 00000000000..796604121ce --- /dev/null +++ b/aws/signer/v4/uri_path_1_4.go @@ -0,0 +1,24 @@ +// +build !go1.5 + +package v4 + +import ( + "net/url" + "strings" +) + +func getURIPath(u *url.URL) string { + var uri string + + if len(u.Opaque) > 0 { + uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/") + } else { + uri = u.Path + } + + if len(uri) == 0 { + uri = "/" + } + + return uri +} diff --git a/aws/signer/v4/v4.go b/aws/signer/v4/v4.go index eb79ded9321..986530b4019 100644 --- a/aws/signer/v4/v4.go +++ b/aws/signer/v4/v4.go @@ -2,6 +2,48 @@ // // Provides request signing for request that need to be signed with // AWS V4 Signatures. +// +// Standalone Signer +// +// Generally using the signer outside of the SDK should not require any additional +// logic when using Go v1.5 or higher. The signer does this by taking advantage +// of the URL.EscapedPath method. If your request URI requires additional escaping +// you many need to use the URL.Opaque to define what the raw URI should be sent +// to the service as. +// +// The signer will first check the URL.Opaque field, and use its value if set. +// The signer does require the URL.Opaque field to be set in the form of: +// +// "///" +// +// // e.g. +// "//example.com/some/path" +// +// The leading "//" and hostname are required or the URL.Opaque escaping will +// not work correctly. +// +// If URL.Opaque is not set the signer will fallback to the URL.EscapedPath() +// method and using the returned value. If you're using Go v1.4 you must set +// URL.Opaque if the URI path needs escaping. If URL.Opaque is not set with +// Go v1.5 the signer will fallback to URL.Path. +// +// AWS v4 signature validation requires that the canonical string's URI path +// element must be the URI escaped form of the HTTP request's path. +// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html +// +// The Go HTTP client will perform escaping automatically on the request. Some +// of these escaping may cause signature validation errors because the HTTP +// request differs from the URI path or query that the signature was generated. +// https://golang.org/pkg/net/url/#URL.EscapedPath +// +// Because of this, it is recommended that when using the signer outside of the +// SDK that explicitly escaping the request prior to being signed is preferable, +// and will help prevent signature validation errors. This can be done by setting +// the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then +// call URL.EscapedPath() if Opaque is not set. +// +// Test `TestStandaloneSign` provides a complete example of using the signer +// outside of the SDK and pre-escaping the URI path. package v4 import ( @@ -120,6 +162,15 @@ type Signer struct { // request's query string. DisableHeaderHoisting bool + // Disables the automatic escaping of the URI path of the request for the + // siganture's canonical string's path. For services that do not need additional + // escaping then use this to disable the signer escaping the path. + // + // S3 is an example of a service that does not need additional escaping. + // + // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + DisableURIPathEscaping bool + // currentTimeFn returns the time value which represents the current time. // This value should only be used for testing. If it is nil the default // time.Now will be used. @@ -151,6 +202,8 @@ type signingCtx struct { ExpireTime time.Duration SignedHeaderVals http.Header + DisableURIPathEscaping bool + credValues credentials.Value isPresign bool formattedTime string @@ -236,14 +289,15 @@ func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, regi } ctx := &signingCtx{ - Request: r, - Body: body, - Query: r.URL.Query(), - Time: signTime, - ExpireTime: exp, - isPresign: exp != 0, - ServiceName: service, - Region: region, + Request: r, + Body: body, + Query: r.URL.Query(), + Time: signTime, + ExpireTime: exp, + isPresign: exp != 0, + ServiceName: service, + Region: region, + DisableURIPathEscaping: v4.DisableURIPathEscaping, } if ctx.isRequestSigned() { @@ -354,6 +408,10 @@ func signSDKRequestWithCurrTime(req *request.Request, curTimeFn func() time.Time v4.Logger = req.Config.Logger v4.DisableHeaderHoisting = req.NotHoist v4.currentTimeFn = curTimeFn + if name == "s3" { + // S3 service should not have any escaping applied + v4.DisableURIPathEscaping = true + } }) signingTime := req.Time @@ -510,17 +568,10 @@ func (ctx *signingCtx) buildCanonicalHeaders(r rule, header http.Header) { func (ctx *signingCtx) buildCanonicalString() { ctx.Request.URL.RawQuery = strings.Replace(ctx.Query.Encode(), "+", "%20", -1) - uri := ctx.Request.URL.Opaque - if uri != "" { - uri = "/" + strings.Join(strings.Split(uri, "/")[3:], "/") - } else { - uri = ctx.Request.URL.Path - } - if uri == "" { - uri = "/" - } - if ctx.ServiceName != "s3" { + uri := getURIPath(ctx.Request.URL) + + if !ctx.DisableURIPathEscaping { uri = rest.EscapePath(uri, false) } diff --git a/aws/signer/v4/v4_test.go b/aws/signer/v4/v4_test.go index fec1db18ba1..cf7a9acbc29 100644 --- a/aws/signer/v4/v4_test.go +++ b/aws/signer/v4/v4_test.go @@ -2,7 +2,6 @@ package v4 import ( "bytes" - "fmt" "io" "io/ioutil" "net/http" @@ -217,7 +216,6 @@ func TestIgnorePreResignRequestWithValidCreds(t *testing.T) { SignSDKRequest(r) sig := r.HTTPRequest.URL.Query().Get("X-Amz-Signature") - fmt.Println(sig) signSDKRequestWithCurrTime(r, func() time.Time { // Simulate one second has passed so that signature's date changes // when it is resigned.