/
sign.go
208 lines (182 loc) · 5.8 KB
/
sign.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package s3sig
import (
"os"
"url"
"http"
"time"
"sort"
"strings"
"encoding/base64"
"crypto/hmac"
)
var amzQueryParams = map[string]bool{
"acl": true,
"location": true,
"logging": true,
"notification": true,
"partNumber": true,
"policy": true,
"requestPayment": true,
"torrent": true,
"uploadId": true,
"uploads": true,
"versionId": true,
"versioning": true,
"versions": true,
"website": true,
"response-content-type": true,
"response-content-language": true,
"response-expires": true,
"response-cache-control": true,
"response-content-disposition": true,
"response-content-encoding": true,
}
func canonicalizedResource(url *url.URL) string {
var res string
// Strip any port declaration (443/80/8080/...)
host := first(strings.SplitN(url.Host, ":", 2))
if strings.HasSuffix(host, ".amazonaws.com") {
// Hostname bucket style, ignore (s3-eu-west.|s3.)amazonaws.com
parts := strings.SplitN(host, ".", -1)
if len(parts) > 3 {
res = res + "/" + strings.Join(parts[:len(parts)-3], ".")
}
} else if len(host) > 0 {
// CNAME bucket style
res = res + "/" + host
} else {
// Bucket as root element in path already
}
// RawPath will include the bucket if not in the host
res = res + strings.SplitN(url.RawPath, "?", 2)[0]
// Include a sorted list of query parameters that have
// special meaning to aws. These should stay decoded for
// the canonical resource.
var amz []string
for key, values := range url.Query() {
if amzQueryParams[key] {
for _, value := range values {
if value != "" {
amz = append(amz, key+"="+value)
} else {
amz = append(amz, key)
}
}
}
}
if len(amz) > 0 {
sort.Strings(amz)
res = res + "?" + strings.Join(amz, "&")
}
// All done.
return res
}
func first(s []string) string {
if len(s) > 0 {
return s[0]
}
return ""
}
/*
Creates the StringToSign string for either query string
or Authorization header based authentication.
*/
func StringToSign(method string, url *url.URL, requestHeaders http.Header, expires string) string {
// Positional headers are optional but should be captured
var contentMD5, contentType, date, amzDate string
var headers []string
// Build the named, and capture the positional headers
for name, values := range requestHeaders {
name = strings.ToLower(name)
switch name {
case "date":
date = first(values)
case "content-type":
contentType = first(values)
case "content-md5":
contentMD5 = first(values)
default:
if strings.HasPrefix(name, "x-amz-") {
// Capture the x-amz-date header
// Note: undefined behavior if there are more than
// one of these headers
if name == "x-amz-date" {
amzDate = first(values)
}
// Assuming any rfc822 unfolding has happened already
headers = append(headers, name+":"+strings.Join(values, ",")+"\n")
}
}
}
sort.Strings(headers)
// overrideDate is used for query string "expires" auth
// and is a unix timestamp
switch {
case expires != "":
date = expires
case amzDate != "":
date = ""
default:
// Don't break referential transparency here by injecting
// the date when the Date is empty. Rather we assume the
// caller knows what she is doing.
}
return method + "\n" +
contentMD5 + "\n" +
contentType + "\n" +
date + "\n" +
strings.Join(headers, "") +
canonicalizedResource(url)
}
// Returns the signature to be used in the query string or Authorization header
func Signature(secret, toSign string) string {
// Signature = Base64( HMAC-SHA1( UTF-8-Encoding-Of( YourSecretAccessKeyID, StringToSign ) ) );
// Need to confirm what encoding go strings are when converted to []byte
hmac := hmac.NewSHA1([]byte(secret))
hmac.Write([]byte(toSign))
return base64.StdEncoding.EncodeToString(hmac.Sum())
}
func Authorization(req *http.Request, key, secret string) string {
return "AWS " + key + ":" + Signature(secret, StringToSign(req.Method, req.URL, req.Header, ""))
}
// Assumes no custom headers are sent so only needs access to a URL.
// If you plan on sending x-amz-* headers with a query string authorization
// you can use Signature(secret, StringToSign(url, headers, expires)) instead
// Returns an url.URL struct constructed from the Raw URL with the AWS
// query parameters appended at the end.
// Assumes any fragments are not included in url.Raw
func URL(url *url.URL, key, secret, method, expires string) (*url.URL, os.Error) {
sig := Signature(secret, StringToSign(method, url, http.Header{}, expires))
raw := url.Raw
parts := strings.SplitN(raw, "?", 2)
params := parts[1:]
params = append(params, "AWSAccessKeyId="+key)
params = append(params, "Expires="+expires)
params = append(params, "Signature="+sig)
signed := strings.Join(append(parts[:1], strings.Join(params, "&")), "?")
return url.Parse(signed)
}
// Authorizes an http.Request pointer in place by in-place replacing the
// header of the provided request:
//
// Authorization: AWS ACCOUNT SIGNATURE
//
// If the x-amz-date and Date headers are missing, this adds UTC current
// time in RFC1123 format inplace to the Date header:
//
// Date: Mon, 02 Jan 2006 15:04:05 UTC
//
// If the Host does not appear in the req.URL, then it will be assigned
// from req.Host
func Authorize(req *http.Request, key, secret string) {
var header string
if req.URL.Host == "" {
req.URL.Host = req.Host
}
if header = req.Header.Get("Date"); len(header) == 0 {
if header = req.Header.Get("X-Amz-Date"); len(header) == 0 {
req.Header.Set("Date", time.UTC().Format(time.RFC1123))
}
}
req.Header.Set("Authorization", Authorization(req, key, secret))
}