Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changing how signing and presigning works #526

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions aws/request/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,22 @@ type Request struct {
Handlers Handlers

Retryer
Time time.Time
ExpireTime time.Duration
Operation *Operation
HTTPRequest *http.Request
HTTPResponse *http.Response
Body io.ReadSeeker
BodyStart int64 // offset from beginning of Body that the request body starts
Params interface{}
Error error
Data interface{}
RequestID string
RetryCount int
Retryable *bool
RetryDelay time.Duration
Time time.Time
ExpireTime time.Duration
Operation *Operation
HTTPRequest *http.Request
HTTPResponse *http.Response
Body io.ReadSeeker
BodyStart int64 // offset from beginning of Body that the request body starts
Params interface{}
Error error
Data interface{}
RequestID string
RetryCount int
Retryable *bool
RetryDelay time.Duration
NotHoist bool
SignedHeaderVals http.Header

built bool
}
Expand Down Expand Up @@ -137,13 +139,26 @@ func (r *Request) SetReaderBody(reader io.ReadSeeker) {
// if the signing fails.
func (r *Request) Presign(expireTime time.Duration) (string, error) {
r.ExpireTime = expireTime
r.NotHoist = false
r.Sign()
if r.Error != nil {
return "", r.Error
}
return r.HTTPRequest.URL.String(), nil
}

// PresignRequest behaves just like presign, but hoists all headers and signs them.
// Also returns the signed hash back to the user
func (r *Request) PresignRequest(expireTime time.Duration) (string, http.Header, error) {
r.ExpireTime = expireTime
r.NotHoist = true
r.Sign()
if r.Error != nil {
return "", nil, r.Error
}
return r.HTTPRequest.URL.String(), r.SignedHeaderVals, nil
}

func debugLogReqError(r *Request, stage string, retrying bool, err error) {
if !r.Config.LogLevel.Matches(aws.LogDebugWithRequestErrors) {
return
Expand Down
39 changes: 37 additions & 2 deletions private/signer/v4/functional_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v4_test

import (
"net/http"
"net/url"
"testing"
"time"
Expand All @@ -26,8 +27,8 @@ func TestPresignHandler(t *testing.T) {
assert.NoError(t, err)

expectedDate := "19700101T000000Z"
expectedHeaders := "host;x-amz-acl"
expectedSig := "7edcb4e3a1bf12f4989018d75acbe3a7f03df24bd6f3112602d59fc551f0e4e2"
expectedHeaders := "content-disposition;host;x-amz-acl"
expectedSig := "2d76a414208c0eac2a23ef9c834db9635ecd5a0fbb447a00ad191f82d854f55b"
expectedCred := "AKID/19700101/mock-region/s3/aws4_request"

u, _ := url.Parse(urlstr)
Expand All @@ -40,3 +41,37 @@ func TestPresignHandler(t *testing.T) {

assert.NotContains(t, urlstr, "+") // + encoded as %20
}

func TestPresignRequest(t *testing.T) {
svc := s3.New(unit.Session)
req, _ := svc.PutObjectRequest(&s3.PutObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("key"),
ContentDisposition: aws.String("a+b c$d"),
ACL: aws.String("public-read"),
})
req.Time = time.Unix(0, 0)
urlstr, headers, err := req.PresignRequest(5 * time.Minute)

assert.NoError(t, err)

expectedDate := "19700101T000000Z"
expectedHeaders := "content-disposition;host;x-amz-acl"
expectedSig := "2d76a414208c0eac2a23ef9c834db9635ecd5a0fbb447a00ad191f82d854f55b"
expectedCred := "AKID/19700101/mock-region/s3/aws4_request"
expectedHeaderMap := http.Header{
"x-amz-acl": []string{"public-read"},
"content-disposition": []string{"a+b c$d"},
}

u, _ := url.Parse(urlstr)
urlQ := u.Query()
assert.Equal(t, expectedSig, urlQ.Get("X-Amz-Signature"))
assert.Equal(t, expectedCred, urlQ.Get("X-Amz-Credential"))
assert.Equal(t, expectedHeaders, urlQ.Get("X-Amz-SignedHeaders"))
assert.Equal(t, expectedDate, urlQ.Get("X-Amz-Date"))
assert.Equal(t, expectedHeaderMap, headers)
assert.Equal(t, "300", urlQ.Get("X-Amz-Expires"))

assert.NotContains(t, urlstr, "+") // + encoded as %20
}
82 changes: 82 additions & 0 deletions private/signer/v4/header_rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package v4

import (
"net/http"
"strings"
)

// validator houses a set of rule needed for validation of a
// string value
type rules []rule

// rule interface allows for more flexible rules and just simply
// checks whether or not a value adheres to that rule
type rule interface {
IsValid(value string) bool
}

// IsValid will iterate through all rules and see if any rules
// apply to the value and supports nested rules
func (r rules) IsValid(value string) bool {
for _, rule := range r {
if rule.IsValid(value) {
return true
}
}
return false
}

// mapRule generic rule for maps
type mapRule map[string]struct{}

// IsValid for the map rule satisfies whether it exists in the map
func (m mapRule) IsValid(value string) bool {
_, ok := m[value]
return ok
}

// whitelist is a generic rule for whitelisting
type whitelist struct {
rule
}

// IsValid for whitelist checks if the value is within the whitelist
func (w whitelist) IsValid(value string) bool {
return w.rule.IsValid(value)
}

// blacklist is a generic rule for blacklisting
type blacklist struct {
rule
}

// IsValid for whitelist checks if the value is within the whitelist
func (b blacklist) IsValid(value string) bool {
return !b.rule.IsValid(value)
}

type patterns []string

// IsValid for patterns checks each pattern and returns if a match has
// been found
func (p patterns) IsValid(value string) bool {
for _, pattern := range p {
if strings.HasPrefix(http.CanonicalHeaderKey(value), pattern) {
return true
}
}
return false
}

// inclusiveRules rules allow for rules to depend on one another
type inclusiveRules []rule

// IsValid will return true if all rules are true
func (r inclusiveRules) IsValid(value string) bool {
for _, rule := range r {
if !rule.IsValid(value) {
return false
}
}
return true
}
57 changes: 57 additions & 0 deletions private/signer/v4/header_rules_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package v4

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRuleCheckWhitelist(t *testing.T) {
w := whitelist{
mapRule{
"Cache-Control": struct{}{},
},
}

assert.True(t, w.IsValid("Cache-Control"))
assert.False(t, w.IsValid("Cache-"))
}

func TestRuleCheckBlacklist(t *testing.T) {
b := blacklist{
mapRule{
"Cache-Control": struct{}{},
},
}

assert.False(t, b.IsValid("Cache-Control"))
assert.True(t, b.IsValid("Cache-"))
}

func TestRuleCheckPattern(t *testing.T) {
p := patterns{"X-Amz-Meta-"}

assert.True(t, p.IsValid("X-Amz-Meta-"))
assert.True(t, p.IsValid("X-Amz-Meta-Star"))
assert.False(t, p.IsValid("Cache-"))
}

func TestRuleComplexWhitelist(t *testing.T) {
w := rules{
whitelist{
mapRule{
"Cache-Control": struct{}{},
},
},
patterns{"X-Amz-Meta-"},
}

r := rules{
inclusiveRules{patterns{"X-Amz-"}, blacklist{w}},
}

assert.True(t, r.IsValid("X-Amz-Blah"))
assert.False(t, r.IsValid("X-Amz-Meta-"))
assert.False(t, r.IsValid("X-Amz-Meta-Star"))
assert.False(t, r.IsValid("Cache-Control"))
}
Loading