twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

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 }