rpc.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. 'use strict';
  2. const assert = require('assert');
  3. const httpx = require('httpx');
  4. const kitx = require('kitx');
  5. const JSON = require('json-bigint');
  6. const helper = require('./helper');
  7. function firstLetterUpper(str) {
  8. return str.slice(0, 1).toUpperCase() + str.slice(1);
  9. }
  10. function formatParams(params) {
  11. var keys = Object.keys(params);
  12. var newParams = {};
  13. for (var i = 0; i < keys.length; i++) {
  14. var key = keys[i];
  15. newParams[firstLetterUpper(key)] = params[key];
  16. }
  17. return newParams;
  18. }
  19. function timestamp() {
  20. var date = new Date();
  21. var YYYY = date.getUTCFullYear();
  22. var MM = kitx.pad2(date.getUTCMonth() + 1);
  23. var DD = kitx.pad2(date.getUTCDate());
  24. var HH = kitx.pad2(date.getUTCHours());
  25. var mm = kitx.pad2(date.getUTCMinutes());
  26. var ss = kitx.pad2(date.getUTCSeconds());
  27. // 删除掉毫秒部分
  28. return `${YYYY}-${MM}-${DD}T${HH}:${mm}:${ss}Z`;
  29. }
  30. function encode(str) {
  31. var result = encodeURIComponent(str);
  32. return result.replace(/!/g, '%21')
  33. .replace(/'/g, '%27')
  34. .replace(/\(/g, '%28')
  35. .replace(/\)/g, '%29')
  36. .replace(/\*/g, '%2A');
  37. }
  38. function replaceRepeatList(target, key, repeat) {
  39. for (var i = 0; i < repeat.length; i++) {
  40. var item = repeat[i];
  41. if (item && typeof item === 'object') {
  42. const keys = Object.keys(item);
  43. for (var j = 0; j < keys.length; j++) {
  44. target[`${key}.${i + 1}.${keys[j]}`] = item[keys[j]];
  45. }
  46. } else {
  47. target[`${key}.${i + 1}`] = item;
  48. }
  49. }
  50. }
  51. function flatParams(params) {
  52. var target = {};
  53. var keys = Object.keys(params);
  54. for (let i = 0; i < keys.length; i++) {
  55. var key = keys[i];
  56. var value = params[key];
  57. if (Array.isArray(value)) {
  58. replaceRepeatList(target, key, value);
  59. } else {
  60. target[key] = value;
  61. }
  62. }
  63. return target;
  64. }
  65. function normalize(params) {
  66. var list = [];
  67. var flated = flatParams(params);
  68. var keys = Object.keys(flated).sort();
  69. for (let i = 0; i < keys.length; i++) {
  70. var key = keys[i];
  71. var value = flated[key];
  72. list.push([encode(key), encode(value)]); //push []
  73. }
  74. return list;
  75. }
  76. function canonicalize(normalized) {
  77. var fields = [];
  78. for (var i = 0; i < normalized.length; i++) {
  79. var [key, value] = normalized[i];
  80. fields.push(key + '=' + value);
  81. }
  82. return fields.join('&');
  83. }
  84. class RPCClient {
  85. constructor(config, verbose) {
  86. assert(config, 'must pass "config"');
  87. assert(config.endpoint, 'must pass "config.endpoint"');
  88. if (!config.endpoint.startsWith('https://') &&
  89. !config.endpoint.startsWith('http://')) {
  90. throw new Error(`"config.endpoint" must starts with 'https://' or 'http://'.`);
  91. }
  92. assert(config.apiVersion, 'must pass "config.apiVersion"');
  93. assert(config.accessKeyId, 'must pass "config.accessKeyId"');
  94. var accessKeySecret = config.secretAccessKey || config.accessKeySecret;
  95. assert(accessKeySecret, 'must pass "config.accessKeySecret"');
  96. if (config.endpoint.endsWith('/')) {
  97. config.endpoint = config.endpoint.slice(0, -1);
  98. }
  99. this.endpoint = config.endpoint;
  100. this.apiVersion = config.apiVersion;
  101. this.accessKeyId = config.accessKeyId;
  102. this.accessKeySecret = accessKeySecret;
  103. this.securityToken = config.securityToken;
  104. this.verbose = verbose === true;
  105. // 非 codes 里的值,将抛出异常
  106. this.codes = new Set([200, '200', 'OK', 'Success']);
  107. if (config.codes) {
  108. // 合并 codes
  109. for (var elem of config.codes) {
  110. this.codes.add(elem);
  111. }
  112. }
  113. this.opts = config.opts || {};
  114. var httpModule = this.endpoint.startsWith('https://')
  115. ? require('https') : require('http');
  116. this.keepAliveAgent = new httpModule.Agent({
  117. keepAlive: true,
  118. keepAliveMsecs: 3000
  119. });
  120. }
  121. request(action, params = {}, opts = {}) {
  122. // 1. compose params and opts
  123. opts = Object.assign({
  124. headers: {
  125. 'x-sdk-client': helper.DEFAULT_CLIENT,
  126. 'user-agent': helper.DEFAULT_UA,
  127. 'x-acs-action': action,
  128. 'x-acs-version': this.apiVersion
  129. }
  130. }, this.opts, opts);
  131. // format action until formatAction is false
  132. if (opts.formatAction !== false) {
  133. action = firstLetterUpper(action);
  134. }
  135. // format params until formatParams is false
  136. if (opts.formatParams !== false) {
  137. params = formatParams(params);
  138. }
  139. var defaults = this._buildParams();
  140. params = Object.assign({Action: action}, defaults, params);
  141. // 2. caculate signature
  142. var method = (opts.method || 'GET').toUpperCase();
  143. var normalized = normalize(params);
  144. var canonicalized = canonicalize(normalized);
  145. // 2.1 get string to sign
  146. var stringToSign = `${method}&${encode('/')}&${encode(canonicalized)}`;
  147. // 2.2 get signature
  148. const key = this.accessKeySecret + '&';
  149. var signature = kitx.sha1(stringToSign, key, 'base64');
  150. // add signature
  151. normalized.push(['Signature', encode(signature)]);
  152. // 3. generate final url
  153. const url = opts.method === 'POST' ? `${this.endpoint}/` : `${this.endpoint}/?${canonicalize(normalized)}`;
  154. // 4. send request
  155. var entry = {
  156. url: url,
  157. request: null,
  158. response: null
  159. };
  160. if (opts && !opts.agent) {
  161. opts.agent = this.keepAliveAgent;
  162. }
  163. if (opts.method === 'POST') {
  164. opts.headers = opts.headers || {};
  165. opts.headers['content-type'] = 'application/x-www-form-urlencoded';
  166. opts.data = canonicalize(normalized);
  167. }
  168. return httpx.request(url, opts).then((response) => {
  169. entry.request = {
  170. headers: response.req.getHeaders ? response.req.getHeaders() : response.req._headers
  171. };
  172. entry.response = {
  173. statusCode: response.statusCode,
  174. headers: response.headers
  175. };
  176. return httpx.read(response);
  177. }).then((buffer) => {
  178. var json = JSON.parse(buffer);
  179. if (json.Code && !this.codes.has(json.Code)) {
  180. var err = new Error(`${json.Message}, URL: ${url}`);
  181. err.name = json.Code + 'Error';
  182. err.data = json;
  183. err.code = json.Code;
  184. err.url = url;
  185. err.entry = entry;
  186. return Promise.reject(err);
  187. }
  188. if (this.verbose) {
  189. return [json, entry];
  190. }
  191. return json;
  192. });
  193. }
  194. _buildParams() {
  195. var defaultParams = {
  196. Format: 'JSON',
  197. SignatureMethod: 'HMAC-SHA1',
  198. SignatureNonce: kitx.makeNonce(),
  199. SignatureVersion: '1.0',
  200. Timestamp: timestamp(),
  201. AccessKeyId: this.accessKeyId,
  202. Version: this.apiVersion,
  203. };
  204. if (this.securityToken) {
  205. defaultParams.SecurityToken = this.securityToken;
  206. }
  207. return defaultParams;
  208. }
  209. }
  210. module.exports = RPCClient;