You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

357 lines
11 KiB

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