Skip to content

Commit

Permalink
aws/signer/v4: Add support for URL.EscapedPath to signer (#885)
Browse files Browse the repository at this point in the history
Adds support for the URL.EscapedPath method added in Go1.5. This allows
you to hint to the signer and Go HTTP client what the escaped form of the
request's URI path will be. This is needed when using the AWS v4 Signer
outside of the context of the SDK on http.Requests you manage.

Also adds documentation to the signer that pre-escaping of the URI path
is needed, and suggestions how how to do this.

aws/signer/v4 TestStandaloneSign test function is an example
using the request signer outside of the SDK.

Fix #866
  • Loading branch information
jasdel authored Oct 10, 2016
1 parent 0206986 commit d6072f0
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 20 deletions.
40 changes: 40 additions & 0 deletions aws/signer/v4/functional_1_4_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
40 changes: 40 additions & 0 deletions aws/signer/v4/functional_1_5_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
39 changes: 39 additions & 0 deletions aws/signer/v4/functional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}
24 changes: 24 additions & 0 deletions aws/signer/v4/uri_path.go
Original file line number Diff line number Diff line change
@@ -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
}
24 changes: 24 additions & 0 deletions aws/signer/v4/uri_path_1_4.go
Original file line number Diff line number Diff line change
@@ -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
}
87 changes: 69 additions & 18 deletions aws/signer/v4/v4.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//
// "//<hostname>/<path>"
//
// // 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 (
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -151,6 +202,8 @@ type signingCtx struct {
ExpireTime time.Duration
SignedHeaderVals http.Header

DisableURIPathEscaping bool

credValues credentials.Value
isPresign bool
formattedTime string
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
2 changes: 0 additions & 2 deletions aws/signer/v4/v4_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package v4

import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit d6072f0

Please sign in to comment.