aws4.js (11172B)
1 var aws4 = exports, 2 url = require('url'), 3 querystring = require('querystring'), 4 crypto = require('crypto'), 5 lru = require('./lru'), 6 credentialsCache = lru(1000) 7 8 // http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html 9 10 function hmac(key, string, encoding) { 11 return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding) 12 } 13 14 function hash(string, encoding) { 15 return crypto.createHash('sha256').update(string, 'utf8').digest(encoding) 16 } 17 18 // This function assumes the string has already been percent encoded 19 function encodeRfc3986(urlEncodedString) { 20 return urlEncodedString.replace(/[!'()*]/g, function(c) { 21 return '%' + c.charCodeAt(0).toString(16).toUpperCase() 22 }) 23 } 24 25 function encodeRfc3986Full(str) { 26 return encodeRfc3986(encodeURIComponent(str)) 27 } 28 29 // request: { path | body, [host], [method], [headers], [service], [region] } 30 // credentials: { accessKeyId, secretAccessKey, [sessionToken] } 31 function RequestSigner(request, credentials) { 32 33 if (typeof request === 'string') request = url.parse(request) 34 35 var headers = request.headers = (request.headers || {}), 36 hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host) 37 38 this.request = request 39 this.credentials = credentials || this.defaultCredentials() 40 41 this.service = request.service || hostParts[0] || '' 42 this.region = request.region || hostParts[1] || 'us-east-1' 43 44 // SES uses a different domain from the service name 45 if (this.service === 'email') this.service = 'ses' 46 47 if (!request.method && request.body) 48 request.method = 'POST' 49 50 if (!headers.Host && !headers.host) { 51 headers.Host = request.hostname || request.host || this.createHost() 52 53 // If a port is specified explicitly, use it as is 54 if (request.port) 55 headers.Host += ':' + request.port 56 } 57 if (!request.hostname && !request.host) 58 request.hostname = headers.Host || headers.host 59 60 this.isCodeCommitGit = this.service === 'codecommit' && request.method === 'GIT' 61 } 62 63 RequestSigner.prototype.matchHost = function(host) { 64 var match = (host || '').match(/([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com(\.cn)?$/) 65 var hostParts = (match || []).slice(1, 3) 66 67 // ES's hostParts are sometimes the other way round, if the value that is expected 68 // to be region equals ‘es’ switch them back 69 // e.g. search-cluster-name-aaaa00aaaa0aaa0aaaaaaa0aaa.us-east-1.es.amazonaws.com 70 if (hostParts[1] === 'es') 71 hostParts = hostParts.reverse() 72 73 return hostParts 74 } 75 76 // http://docs.aws.amazon.com/general/latest/gr/rande.html 77 RequestSigner.prototype.isSingleRegion = function() { 78 // Special case for S3 and SimpleDB in us-east-1 79 if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return true 80 81 return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts'] 82 .indexOf(this.service) >= 0 83 } 84 85 RequestSigner.prototype.createHost = function() { 86 var region = this.isSingleRegion() ? '' : 87 (this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this.region, 88 service = this.service === 'ses' ? 'email' : this.service 89 return service + region + '.amazonaws.com' 90 } 91 92 RequestSigner.prototype.prepareRequest = function() { 93 this.parsePath() 94 95 var request = this.request, headers = request.headers, query 96 97 if (request.signQuery) { 98 99 this.parsedPath.query = query = this.parsedPath.query || {} 100 101 if (this.credentials.sessionToken) 102 query['X-Amz-Security-Token'] = this.credentials.sessionToken 103 104 if (this.service === 's3' && !query['X-Amz-Expires']) 105 query['X-Amz-Expires'] = 86400 106 107 if (query['X-Amz-Date']) 108 this.datetime = query['X-Amz-Date'] 109 else 110 query['X-Amz-Date'] = this.getDateTime() 111 112 query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256' 113 query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString() 114 query['X-Amz-SignedHeaders'] = this.signedHeaders() 115 116 } else { 117 118 if (!request.doNotModifyHeaders && !this.isCodeCommitGit) { 119 if (request.body && !headers['Content-Type'] && !headers['content-type']) 120 headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8' 121 122 if (request.body && !headers['Content-Length'] && !headers['content-length']) 123 headers['Content-Length'] = Buffer.byteLength(request.body) 124 125 if (this.credentials.sessionToken && !headers['X-Amz-Security-Token'] && !headers['x-amz-security-token']) 126 headers['X-Amz-Security-Token'] = this.credentials.sessionToken 127 128 if (this.service === 's3' && !headers['X-Amz-Content-Sha256'] && !headers['x-amz-content-sha256']) 129 headers['X-Amz-Content-Sha256'] = hash(this.request.body || '', 'hex') 130 131 if (headers['X-Amz-Date'] || headers['x-amz-date']) 132 this.datetime = headers['X-Amz-Date'] || headers['x-amz-date'] 133 else 134 headers['X-Amz-Date'] = this.getDateTime() 135 } 136 137 delete headers.Authorization 138 delete headers.authorization 139 } 140 } 141 142 RequestSigner.prototype.sign = function() { 143 if (!this.parsedPath) this.prepareRequest() 144 145 if (this.request.signQuery) { 146 this.parsedPath.query['X-Amz-Signature'] = this.signature() 147 } else { 148 this.request.headers.Authorization = this.authHeader() 149 } 150 151 this.request.path = this.formatPath() 152 153 return this.request 154 } 155 156 RequestSigner.prototype.getDateTime = function() { 157 if (!this.datetime) { 158 var headers = this.request.headers, 159 date = new Date(headers.Date || headers.date || new Date) 160 161 this.datetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, '') 162 163 // Remove the trailing 'Z' on the timestamp string for CodeCommit git access 164 if (this.isCodeCommitGit) this.datetime = this.datetime.slice(0, -1) 165 } 166 return this.datetime 167 } 168 169 RequestSigner.prototype.getDate = function() { 170 return this.getDateTime().substr(0, 8) 171 } 172 173 RequestSigner.prototype.authHeader = function() { 174 return [ 175 'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(), 176 'SignedHeaders=' + this.signedHeaders(), 177 'Signature=' + this.signature(), 178 ].join(', ') 179 } 180 181 RequestSigner.prototype.signature = function() { 182 var date = this.getDate(), 183 cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(), 184 kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey) 185 if (!kCredentials) { 186 kDate = hmac('AWS4' + this.credentials.secretAccessKey, date) 187 kRegion = hmac(kDate, this.region) 188 kService = hmac(kRegion, this.service) 189 kCredentials = hmac(kService, 'aws4_request') 190 credentialsCache.set(cacheKey, kCredentials) 191 } 192 return hmac(kCredentials, this.stringToSign(), 'hex') 193 } 194 195 RequestSigner.prototype.stringToSign = function() { 196 return [ 197 'AWS4-HMAC-SHA256', 198 this.getDateTime(), 199 this.credentialString(), 200 hash(this.canonicalString(), 'hex'), 201 ].join('\n') 202 } 203 204 RequestSigner.prototype.canonicalString = function() { 205 if (!this.parsedPath) this.prepareRequest() 206 207 var pathStr = this.parsedPath.path, 208 query = this.parsedPath.query, 209 headers = this.request.headers, 210 queryStr = '', 211 normalizePath = this.service !== 's3', 212 decodePath = this.service === 's3' || this.request.doNotEncodePath, 213 decodeSlashesInPath = this.service === 's3', 214 firstValOnly = this.service === 's3', 215 bodyHash 216 217 if (this.service === 's3' && this.request.signQuery) { 218 bodyHash = 'UNSIGNED-PAYLOAD' 219 } else if (this.isCodeCommitGit) { 220 bodyHash = '' 221 } else { 222 bodyHash = headers['X-Amz-Content-Sha256'] || headers['x-amz-content-sha256'] || 223 hash(this.request.body || '', 'hex') 224 } 225 226 if (query) { 227 var reducedQuery = Object.keys(query).reduce(function(obj, key) { 228 if (!key) return obj 229 obj[encodeRfc3986Full(key)] = !Array.isArray(query[key]) ? query[key] : 230 (firstValOnly ? query[key][0] : query[key]) 231 return obj 232 }, {}) 233 var encodedQueryPieces = [] 234 Object.keys(reducedQuery).sort().forEach(function(key) { 235 if (!Array.isArray(reducedQuery[key])) { 236 encodedQueryPieces.push(key + '=' + encodeRfc3986Full(reducedQuery[key])) 237 } else { 238 reducedQuery[key].map(encodeRfc3986Full).sort() 239 .forEach(function(val) { encodedQueryPieces.push(key + '=' + val) }) 240 } 241 }) 242 queryStr = encodedQueryPieces.join('&') 243 } 244 if (pathStr !== '/') { 245 if (normalizePath) pathStr = pathStr.replace(/\/{2,}/g, '/') 246 pathStr = pathStr.split('/').reduce(function(path, piece) { 247 if (normalizePath && piece === '..') { 248 path.pop() 249 } else if (!normalizePath || piece !== '.') { 250 if (decodePath) piece = decodeURIComponent(piece).replace(/\+/g, ' ') 251 path.push(encodeRfc3986Full(piece)) 252 } 253 return path 254 }, []).join('/') 255 if (pathStr[0] !== '/') pathStr = '/' + pathStr 256 if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/') 257 } 258 259 return [ 260 this.request.method || 'GET', 261 pathStr, 262 queryStr, 263 this.canonicalHeaders() + '\n', 264 this.signedHeaders(), 265 bodyHash, 266 ].join('\n') 267 } 268 269 RequestSigner.prototype.canonicalHeaders = function() { 270 var headers = this.request.headers 271 function trimAll(header) { 272 return header.toString().trim().replace(/\s+/g, ' ') 273 } 274 return Object.keys(headers) 275 .sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 }) 276 .map(function(key) { return key.toLowerCase() + ':' + trimAll(headers[key]) }) 277 .join('\n') 278 } 279 280 RequestSigner.prototype.signedHeaders = function() { 281 return Object.keys(this.request.headers) 282 .map(function(key) { return key.toLowerCase() }) 283 .sort() 284 .join(';') 285 } 286 287 RequestSigner.prototype.credentialString = function() { 288 return [ 289 this.getDate(), 290 this.region, 291 this.service, 292 'aws4_request', 293 ].join('/') 294 } 295 296 RequestSigner.prototype.defaultCredentials = function() { 297 var env = process.env 298 return { 299 accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY, 300 secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY, 301 sessionToken: env.AWS_SESSION_TOKEN, 302 } 303 } 304 305 RequestSigner.prototype.parsePath = function() { 306 var path = this.request.path || '/' 307 308 // S3 doesn't always encode characters > 127 correctly and 309 // all services don't encode characters > 255 correctly 310 // So if there are non-reserved chars (and it's not already all % encoded), just encode them all 311 if (/[^0-9A-Za-z;,/?:@&=+$\-_.!~*'()#%]/.test(path)) { 312 path = encodeURI(decodeURI(path)) 313 } 314 315 var queryIx = path.indexOf('?'), 316 query = null 317 318 if (queryIx >= 0) { 319 query = querystring.parse(path.slice(queryIx + 1)) 320 path = path.slice(0, queryIx) 321 } 322 323 this.parsedPath = { 324 path: path, 325 query: query, 326 } 327 } 328 329 RequestSigner.prototype.formatPath = function() { 330 var path = this.parsedPath.path, 331 query = this.parsedPath.query 332 333 if (!query) return path 334 335 // Services don't support empty query string keys 336 if (query[''] != null) delete query[''] 337 338 return path + '?' + encodeRfc3986(querystring.stringify(query)) 339 } 340 341 aws4.RequestSigner = RequestSigner 342 343 aws4.sign = function(request, credentials) { 344 return new RequestSigner(request, credentials).sign() 345 }