| const debug = require('debug')('ali-oss:sts'); | |
| const crypto = require('crypto'); | |
| const querystring = require('querystring'); | |
| const copy = require('copy-to'); | |
| const AgentKeepalive = require('agentkeepalive'); | |
| const is = require('is-type-of'); | |
| const ms = require('humanize-ms'); | |
| const urllib = require('urllib'); | |
| 
 | |
| const globalHttpAgent = new AgentKeepalive(); | |
| 
 | |
| function STS(options) { | |
|   if (!(this instanceof STS)) { | |
|     return new STS(options); | |
|   } | |
| 
 | |
|   if (!options || !options.accessKeyId || !options.accessKeySecret) { | |
|     throw new Error('require accessKeyId, accessKeySecret'); | |
|   } | |
| 
 | |
|   this.options = { | |
|     endpoint: options.endpoint || 'https://sts.aliyuncs.com', | |
|     format: 'JSON', | |
|     apiVersion: '2015-04-01', | |
|     sigMethod: 'HMAC-SHA1', | |
|     sigVersion: '1.0', | |
|     timeout: '60s' | |
|   }; | |
|   copy(options).to(this.options); | |
| 
 | |
|   // support custom agent and urllib client | |
|   if (this.options.urllib) { | |
|     this.urllib = this.options.urllib; | |
|   } else { | |
|     this.urllib = urllib; | |
|     this.agent = this.options.agent || globalHttpAgent; | |
|   } | |
| } | |
| 
 | |
| module.exports = STS; | |
| 
 | |
| const proto = STS.prototype; | |
| 
 | |
| /** | |
|  * STS opertaions | |
|  */ | |
| 
 | |
| proto.assumeRole = async function assumeRole(role, policy, expiration, session, options) { | |
|   const opts = this.options; | |
|   const params = { | |
|     Action: 'AssumeRole', | |
|     RoleArn: role, | |
|     RoleSessionName: session || 'app', | |
|     DurationSeconds: expiration || 3600, | |
| 
 | |
|     Format: opts.format, | |
|     Version: opts.apiVersion, | |
|     AccessKeyId: opts.accessKeyId, | |
|     SignatureMethod: opts.sigMethod, | |
|     SignatureVersion: opts.sigVersion, | |
|     SignatureNonce: Math.random(), | |
|     Timestamp: new Date().toISOString() | |
|   }; | |
| 
 | |
|   if (policy) { | |
|     let policyStr; | |
|     if (is.string(policy)) { | |
|       try { | |
|         policyStr = JSON.stringify(JSON.parse(policy)); | |
|       } catch (err) { | |
|         throw new Error(`Policy string is not a valid JSON: ${err.message}`); | |
|       } | |
|     } else { | |
|       policyStr = JSON.stringify(policy); | |
|     } | |
|     params.Policy = policyStr; | |
|   } | |
| 
 | |
|   const signature = this._getSignature('POST', params, opts.accessKeySecret); | |
|   params.Signature = signature; | |
| 
 | |
|   const reqUrl = opts.endpoint; | |
|   const reqParams = { | |
|     agent: this.agent, | |
|     timeout: ms((options && options.timeout) || opts.timeout), | |
|     method: 'POST', | |
|     content: querystring.stringify(params), | |
|     headers: { | |
|       'Content-Type': 'application/x-www-form-urlencoded' | |
|     }, | |
|     ctx: options && options.ctx | |
|   }; | |
| 
 | |
|   const result = await this.urllib.request(reqUrl, reqParams); | |
|   debug('response %s %s, got %s, headers: %j', reqParams.method, reqUrl, result.status, result.headers); | |
| 
 | |
|   if (Math.floor(result.status / 100) !== 2) { | |
|     const err = await this._requestError(result); | |
|     err.params = reqParams; | |
|     throw err; | |
|   } | |
|   result.data = JSON.parse(result.data); | |
| 
 | |
|   return { | |
|     res: result.res, | |
|     credentials: result.data.Credentials | |
|   }; | |
| }; | |
| 
 | |
| proto._requestError = async function _requestError(result) { | |
|   const err = new Error(); | |
|   err.status = result.status; | |
| 
 | |
|   try { | |
|     const resp = (await JSON.parse(result.data)) || {}; | |
|     err.code = resp.Code; | |
|     err.message = `${resp.Code}: ${resp.Message}`; | |
|     err.requestId = resp.RequestId; | |
|   } catch (e) { | |
|     err.message = `UnknownError: ${String(result.data)}`; | |
|   } | |
| 
 | |
|   return err; | |
| }; | |
| 
 | |
| proto._getSignature = function _getSignature(method, params, key) { | |
|   const that = this; | |
|   const canoQuery = Object.keys(params) | |
|     .sort() | |
|     .map(k => `${that._escape(k)}=${that._escape(params[k])}`) | |
|     .join('&'); | |
| 
 | |
|   const stringToSign = `${method.toUpperCase()}&${this._escape('/')}&${this._escape(canoQuery)}`; | |
| 
 | |
|   debug('string to sign: %s', stringToSign); | |
| 
 | |
|   let signature = crypto.createHmac('sha1', `${key}&`); | |
|   signature = signature.update(stringToSign).digest('base64'); | |
| 
 | |
|   debug('signature: %s', signature); | |
| 
 | |
|   return signature; | |
| }; | |
| 
 | |
| /** | |
|  * Since `encodeURIComponent` doesn't encode '*', which causes | |
|  * 'SignatureDoesNotMatch'. We need do it ourselves. | |
|  */ | |
| proto._escape = function _escape(str) { | |
|   return encodeURIComponent(str) | |
|     .replace(/!/g, '%21') | |
|     .replace(/'/g, '%27') | |
|     .replace(/\(/g, '%28') | |
|     .replace(/\)/g, '%29') | |
|     .replace(/\*/g, '%2A'); | |
| };
 |