1package v2
2
3import (
4	"crypto/hmac"
5	"crypto/sha256"
6	"encoding/base64"
7	"errors"
8	"fmt"
9	"net/http"
10	"net/url"
11	"sort"
12	"strings"
13	"time"
14
15	"github.com/aws/aws-sdk-go/aws"
16	"github.com/aws/aws-sdk-go/aws/credentials"
17	"github.com/aws/aws-sdk-go/aws/request"
18)
19
20var (
21	errInvalidMethod = errors.New("v2 signer only handles HTTP POST")
22)
23
24const (
25	signatureVersion = "2"
26	signatureMethod  = "HmacSHA256"
27	timeFormat       = "2006-01-02T15:04:05Z"
28)
29
30type signer struct {
31	// Values that must be populated from the request
32	Request     *http.Request
33	Time        time.Time
34	Credentials *credentials.Credentials
35	Debug       aws.LogLevelType
36	Logger      aws.Logger
37
38	Query        url.Values
39	stringToSign string
40	signature    string
41}
42
43// SignRequestHandler is a named request handler the SDK will use to sign
44// service client request with using the V4 signature.
45var SignRequestHandler = request.NamedHandler{
46	Name: "v2.SignRequestHandler", Fn: SignSDKRequest,
47}
48
49// SignSDKRequest requests with signature version 2.
50//
51// Will sign the requests with the service config's Credentials object
52// Signing is skipped if the credentials is the credentials.AnonymousCredentials
53// object.
54func SignSDKRequest(req *request.Request) {
55	// If the request does not need to be signed ignore the signing of the
56	// request if the AnonymousCredentials object is used.
57	if req.Config.Credentials == credentials.AnonymousCredentials {
58		return
59	}
60
61	if req.HTTPRequest.Method != "POST" && req.HTTPRequest.Method != "GET" {
62		// The V2 signer only supports GET and POST
63		req.Error = errInvalidMethod
64		return
65	}
66
67	v2 := signer{
68		Request:     req.HTTPRequest,
69		Time:        req.Time,
70		Credentials: req.Config.Credentials,
71		Debug:       req.Config.LogLevel.Value(),
72		Logger:      req.Config.Logger,
73	}
74
75	req.Error = v2.Sign()
76
77	if req.Error != nil {
78		return
79	}
80
81	if req.HTTPRequest.Method == "POST" {
82		// Set the body of the request based on the modified query parameters
83		req.SetStringBody(v2.Query.Encode())
84
85		// Now that the body has changed, remove any Content-Length header,
86		// because it will be incorrect
87		req.HTTPRequest.ContentLength = 0
88		req.HTTPRequest.Header.Del("Content-Length")
89	} else {
90		req.HTTPRequest.URL.RawQuery = v2.Query.Encode()
91	}
92}
93
94func (v2 *signer) Sign() error {
95	credValue, err := v2.Credentials.Get()
96	if err != nil {
97		return err
98	}
99
100	if v2.Request.Method == "POST" {
101		// Parse the HTTP request to obtain the query parameters that will
102		// be used to build the string to sign. Note that because the HTTP
103		// request will need to be modified, the PostForm and Form properties
104		// are reset to nil after parsing.
105		v2.Request.ParseForm()
106		v2.Query = v2.Request.PostForm
107		v2.Request.PostForm = nil
108		v2.Request.Form = nil
109	} else {
110		v2.Query = v2.Request.URL.Query()
111	}
112
113	// Set new query parameters
114	v2.Query.Set("AWSAccessKeyId", credValue.AccessKeyID)
115	v2.Query.Set("SignatureVersion", signatureVersion)
116	v2.Query.Set("SignatureMethod", signatureMethod)
117	v2.Query.Set("Timestamp", v2.Time.UTC().Format(timeFormat))
118	if credValue.SessionToken != "" {
119		v2.Query.Set("SecurityToken", credValue.SessionToken)
120	}
121
122	// in case this is a retry, ensure no signature present
123	v2.Query.Del("Signature")
124
125	method := v2.Request.Method
126	host := v2.Request.URL.Host
127	path := v2.Request.URL.Path
128	if path == "" {
129		path = "/"
130	}
131
132	// obtain all of the query keys and sort them
133	queryKeys := make([]string, 0, len(v2.Query))
134	for key := range v2.Query {
135		queryKeys = append(queryKeys, key)
136	}
137	sort.Strings(queryKeys)
138
139	// build URL-encoded query keys and values
140	queryKeysAndValues := make([]string, len(queryKeys))
141	for i, key := range queryKeys {
142		k := strings.Replace(url.QueryEscape(key), "+", "%20", -1)
143		v := strings.Replace(url.QueryEscape(v2.Query.Get(key)), "+", "%20", -1)
144		queryKeysAndValues[i] = k + "=" + v
145	}
146
147	// join into one query string
148	query := strings.Join(queryKeysAndValues, "&")
149
150	// build the canonical string for the V2 signature
151	v2.stringToSign = strings.Join([]string{
152		method,
153		host,
154		path,
155		query,
156	}, "\n")
157
158	hash := hmac.New(sha256.New, []byte(credValue.SecretAccessKey))
159	hash.Write([]byte(v2.stringToSign))
160	v2.signature = base64.StdEncoding.EncodeToString(hash.Sum(nil))
161	v2.Query.Set("Signature", v2.signature)
162
163	if v2.Debug.Matches(aws.LogDebugWithSigning) {
164		v2.logSigningInfo()
165	}
166
167	return nil
168}
169
170const logSignInfoMsg = `DEBUG: Request Signature:
171---[ STRING TO SIGN ]--------------------------------
172%s
173---[ SIGNATURE ]-------------------------------------
174%s
175-----------------------------------------------------`
176
177func (v2 *signer) logSigningInfo() {
178	msg := fmt.Sprintf(logSignInfoMsg, v2.stringToSign, v2.Query.Get("Signature"))
179	v2.Logger.Log(msg)
180}
181