1 | var AWS = require('../core');
|
2 | var v4Credentials = require('./v4_credentials');
|
3 | var inherit = AWS.util.inherit;
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | var expiresHeader = 'presigned-expires';
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | AWS.Signers.V4 = inherit(AWS.Signers.RequestSigner, {
|
14 | constructor: function V4(request, serviceName, options) {
|
15 | AWS.Signers.RequestSigner.call(this, request);
|
16 | this.serviceName = serviceName;
|
17 | options = options || {};
|
18 | this.signatureCache = typeof options.signatureCache === 'boolean' ? options.signatureCache : true;
|
19 | this.operation = options.operation;
|
20 | this.signatureVersion = options.signatureVersion;
|
21 | },
|
22 |
|
23 | algorithm: 'AWS4-HMAC-SHA256',
|
24 |
|
25 | addAuthorization: function addAuthorization(credentials, date) {
|
26 | var datetime = AWS.util.date.iso8601(date).replace(/[:\-]|\.\d{3}/g, '');
|
27 |
|
28 | if (this.isPresigned()) {
|
29 | this.updateForPresigned(credentials, datetime);
|
30 | } else {
|
31 | this.addHeaders(credentials, datetime);
|
32 | }
|
33 |
|
34 | this.request.headers['Authorization'] =
|
35 | this.authorization(credentials, datetime);
|
36 | },
|
37 |
|
38 | addHeaders: function addHeaders(credentials, datetime) {
|
39 | this.request.headers['X-Amz-Date'] = datetime;
|
40 | if (credentials.sessionToken) {
|
41 | this.request.headers['x-amz-security-token'] = credentials.sessionToken;
|
42 | }
|
43 | },
|
44 |
|
45 | updateForPresigned: function updateForPresigned(credentials, datetime) {
|
46 | var credString = this.credentialString(datetime);
|
47 | var qs = {
|
48 | 'X-Amz-Date': datetime,
|
49 | 'X-Amz-Algorithm': this.algorithm,
|
50 | 'X-Amz-Credential': credentials.accessKeyId + '/' + credString,
|
51 | 'X-Amz-Expires': this.request.headers[expiresHeader],
|
52 | 'X-Amz-SignedHeaders': this.signedHeaders()
|
53 | };
|
54 |
|
55 | if (credentials.sessionToken) {
|
56 | qs['X-Amz-Security-Token'] = credentials.sessionToken;
|
57 | }
|
58 |
|
59 | if (this.request.headers['Content-Type']) {
|
60 | qs['Content-Type'] = this.request.headers['Content-Type'];
|
61 | }
|
62 | if (this.request.headers['Content-MD5']) {
|
63 | qs['Content-MD5'] = this.request.headers['Content-MD5'];
|
64 | }
|
65 | if (this.request.headers['Cache-Control']) {
|
66 | qs['Cache-Control'] = this.request.headers['Cache-Control'];
|
67 | }
|
68 |
|
69 |
|
70 | AWS.util.each.call(this, this.request.headers, function(key, value) {
|
71 | if (key === expiresHeader) return;
|
72 | if (this.isSignableHeader(key)) {
|
73 | var lowerKey = key.toLowerCase();
|
74 |
|
75 | if (lowerKey.indexOf('x-amz-meta-') === 0) {
|
76 | qs[lowerKey] = value;
|
77 | } else if (lowerKey.indexOf('x-amz-') === 0) {
|
78 | qs[key] = value;
|
79 | }
|
80 | }
|
81 | });
|
82 |
|
83 | var sep = this.request.path.indexOf('?') >= 0 ? '&' : '?';
|
84 | this.request.path += sep + AWS.util.queryParamsToString(qs);
|
85 | },
|
86 |
|
87 | authorization: function authorization(credentials, datetime) {
|
88 | var parts = [];
|
89 | var credString = this.credentialString(datetime);
|
90 | parts.push(this.algorithm + ' Credential=' +
|
91 | credentials.accessKeyId + '/' + credString);
|
92 | parts.push('SignedHeaders=' + this.signedHeaders());
|
93 | parts.push('Signature=' + this.signature(credentials, datetime));
|
94 | return parts.join(', ');
|
95 | },
|
96 |
|
97 | signature: function signature(credentials, datetime) {
|
98 | var signingKey = v4Credentials.getSigningKey(
|
99 | credentials,
|
100 | datetime.substr(0, 8),
|
101 | this.request.region,
|
102 | this.serviceName,
|
103 | this.signatureCache
|
104 | );
|
105 | return AWS.util.crypto.hmac(signingKey, this.stringToSign(datetime), 'hex');
|
106 | },
|
107 |
|
108 | stringToSign: function stringToSign(datetime) {
|
109 | var parts = [];
|
110 | parts.push('AWS4-HMAC-SHA256');
|
111 | parts.push(datetime);
|
112 | parts.push(this.credentialString(datetime));
|
113 | parts.push(this.hexEncodedHash(this.canonicalString()));
|
114 | return parts.join('\n');
|
115 | },
|
116 |
|
117 | canonicalString: function canonicalString() {
|
118 | var parts = [], pathname = this.request.pathname();
|
119 | if (this.serviceName !== 's3' && this.signatureVersion !== 's3v4') pathname = AWS.util.uriEscapePath(pathname);
|
120 |
|
121 | parts.push(this.request.method);
|
122 | parts.push(pathname);
|
123 | parts.push(this.request.search());
|
124 | parts.push(this.canonicalHeaders() + '\n');
|
125 | parts.push(this.signedHeaders());
|
126 | parts.push(this.hexEncodedBodyHash());
|
127 | return parts.join('\n');
|
128 | },
|
129 |
|
130 | canonicalHeaders: function canonicalHeaders() {
|
131 | var headers = [];
|
132 | AWS.util.each.call(this, this.request.headers, function (key, item) {
|
133 | headers.push([key, item]);
|
134 | });
|
135 | headers.sort(function (a, b) {
|
136 | return a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1;
|
137 | });
|
138 | var parts = [];
|
139 | AWS.util.arrayEach.call(this, headers, function (item) {
|
140 | var key = item[0].toLowerCase();
|
141 | if (this.isSignableHeader(key)) {
|
142 | var value = item[1];
|
143 | if (typeof value === 'undefined' || value === null || typeof value.toString !== 'function') {
|
144 | throw AWS.util.error(new Error('Header ' + key + ' contains invalid value'), {
|
145 | code: 'InvalidHeader'
|
146 | });
|
147 | }
|
148 | parts.push(key + ':' +
|
149 | this.canonicalHeaderValues(value.toString()));
|
150 | }
|
151 | });
|
152 | return parts.join('\n');
|
153 | },
|
154 |
|
155 | canonicalHeaderValues: function canonicalHeaderValues(values) {
|
156 | return values.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
|
157 | },
|
158 |
|
159 | signedHeaders: function signedHeaders() {
|
160 | var keys = [];
|
161 | AWS.util.each.call(this, this.request.headers, function (key) {
|
162 | key = key.toLowerCase();
|
163 | if (this.isSignableHeader(key)) keys.push(key);
|
164 | });
|
165 | return keys.sort().join(';');
|
166 | },
|
167 |
|
168 | credentialString: function credentialString(datetime) {
|
169 | return v4Credentials.createScope(
|
170 | datetime.substr(0, 8),
|
171 | this.request.region,
|
172 | this.serviceName
|
173 | );
|
174 | },
|
175 |
|
176 | hexEncodedHash: function hash(string) {
|
177 | return AWS.util.crypto.sha256(string, 'hex');
|
178 | },
|
179 |
|
180 | hexEncodedBodyHash: function hexEncodedBodyHash() {
|
181 | var request = this.request;
|
182 | if (this.isPresigned() && this.serviceName === 's3' && !request.body) {
|
183 | return 'UNSIGNED-PAYLOAD';
|
184 | } else if (request.headers['X-Amz-Content-Sha256']) {
|
185 | return request.headers['X-Amz-Content-Sha256'];
|
186 | } else {
|
187 | return this.hexEncodedHash(this.request.body || '');
|
188 | }
|
189 | },
|
190 |
|
191 | unsignableHeaders: [
|
192 | 'authorization',
|
193 | 'content-type',
|
194 | 'content-length',
|
195 | 'user-agent',
|
196 | expiresHeader,
|
197 | 'expect',
|
198 | 'x-amzn-trace-id'
|
199 | ],
|
200 |
|
201 | isSignableHeader: function isSignableHeader(key) {
|
202 | if (key.toLowerCase().indexOf('x-amz-') === 0) return true;
|
203 | return this.unsignableHeaders.indexOf(key) < 0;
|
204 | },
|
205 |
|
206 | isPresigned: function isPresigned() {
|
207 | return this.request.headers[expiresHeader] ? true : false;
|
208 | }
|
209 |
|
210 | });
|
211 |
|
212 |
|
213 |
|
214 |
|
215 | module.exports = AWS.Signers.V4;
|