sts.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. const debug = require('debug')('ali-oss:sts');
  2. const crypto = require('crypto');
  3. const querystring = require('querystring');
  4. const copy = require('copy-to');
  5. const AgentKeepalive = require('agentkeepalive');
  6. const is = require('is-type-of');
  7. const ms = require('humanize-ms');
  8. const urllib = require('urllib');
  9. const globalHttpAgent = new AgentKeepalive();
  10. function STS(options) {
  11. if (!(this instanceof STS)) {
  12. return new STS(options);
  13. }
  14. if (!options || !options.accessKeyId || !options.accessKeySecret) {
  15. throw new Error('require accessKeyId, accessKeySecret');
  16. }
  17. this.options = {
  18. endpoint: options.endpoint || 'https://sts.aliyuncs.com',
  19. format: 'JSON',
  20. apiVersion: '2015-04-01',
  21. sigMethod: 'HMAC-SHA1',
  22. sigVersion: '1.0',
  23. timeout: '60s'
  24. };
  25. copy(options).to(this.options);
  26. // support custom agent and urllib client
  27. if (this.options.urllib) {
  28. this.urllib = this.options.urllib;
  29. } else {
  30. this.urllib = urllib;
  31. this.agent = this.options.agent || globalHttpAgent;
  32. }
  33. }
  34. module.exports = STS;
  35. const proto = STS.prototype;
  36. /**
  37. * STS opertaions
  38. */
  39. proto.assumeRole = async function assumeRole(role, policy, expiration, session, options) {
  40. const opts = this.options;
  41. const params = {
  42. Action: 'AssumeRole',
  43. RoleArn: role,
  44. RoleSessionName: session || 'app',
  45. DurationSeconds: expiration || 3600,
  46. Format: opts.format,
  47. Version: opts.apiVersion,
  48. AccessKeyId: opts.accessKeyId,
  49. SignatureMethod: opts.sigMethod,
  50. SignatureVersion: opts.sigVersion,
  51. SignatureNonce: Math.random(),
  52. Timestamp: new Date().toISOString()
  53. };
  54. if (policy) {
  55. let policyStr;
  56. if (is.string(policy)) {
  57. try {
  58. policyStr = JSON.stringify(JSON.parse(policy));
  59. } catch (err) {
  60. throw new Error(`Policy string is not a valid JSON: ${err.message}`);
  61. }
  62. } else {
  63. policyStr = JSON.stringify(policy);
  64. }
  65. params.Policy = policyStr;
  66. }
  67. const signature = this._getSignature('POST', params, opts.accessKeySecret);
  68. params.Signature = signature;
  69. const reqUrl = opts.endpoint;
  70. const reqParams = {
  71. agent: this.agent,
  72. timeout: ms((options && options.timeout) || opts.timeout),
  73. method: 'POST',
  74. content: querystring.stringify(params),
  75. headers: {
  76. 'Content-Type': 'application/x-www-form-urlencoded'
  77. },
  78. ctx: options && options.ctx
  79. };
  80. const result = await this.urllib.request(reqUrl, reqParams);
  81. debug('response %s %s, got %s, headers: %j', reqParams.method, reqUrl, result.status, result.headers);
  82. if (Math.floor(result.status / 100) !== 2) {
  83. const err = await this._requestError(result);
  84. err.params = reqParams;
  85. throw err;
  86. }
  87. result.data = JSON.parse(result.data);
  88. return {
  89. res: result.res,
  90. credentials: result.data.Credentials
  91. };
  92. };
  93. proto._requestError = async function _requestError(result) {
  94. const err = new Error();
  95. err.status = result.status;
  96. try {
  97. const resp = (await JSON.parse(result.data)) || {};
  98. err.code = resp.Code;
  99. err.message = `${resp.Code}: ${resp.Message}`;
  100. err.requestId = resp.RequestId;
  101. } catch (e) {
  102. err.message = `UnknownError: ${String(result.data)}`;
  103. }
  104. return err;
  105. };
  106. proto._getSignature = function _getSignature(method, params, key) {
  107. const that = this;
  108. const canoQuery = Object.keys(params)
  109. .sort()
  110. .map(k => `${that._escape(k)}=${that._escape(params[k])}`)
  111. .join('&');
  112. const stringToSign = `${method.toUpperCase()}&${this._escape('/')}&${this._escape(canoQuery)}`;
  113. debug('string to sign: %s', stringToSign);
  114. let signature = crypto.createHmac('sha1', `${key}&`);
  115. signature = signature.update(stringToSign).digest('base64');
  116. debug('signature: %s', signature);
  117. return signature;
  118. };
  119. /**
  120. * Since `encodeURIComponent` doesn't encode '*', which causes
  121. * 'SignatureDoesNotMatch'. We need do it ourselves.
  122. */
  123. proto._escape = function _escape(str) {
  124. return encodeURIComponent(str)
  125. .replace(/!/g, '%21')
  126. .replace(/'/g, '%27')
  127. .replace(/\(/g, '%28')
  128. .replace(/\)/g, '%29')
  129. .replace(/\*/g, '%2A');
  130. };