roa.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. 'use strict';
  2. const assert = require('assert');
  3. const url = require('url');
  4. const querystring = require('querystring');
  5. const kitx = require('kitx');
  6. const httpx = require('httpx');
  7. const xml2js = require('xml2js');
  8. const JSON = require('json-bigint');
  9. const debug = require('debug')('roa');
  10. const helper = require('./helper');
  11. function filter(value) {
  12. return value.replace(/[\t\n\r\f]/g, ' ');
  13. }
  14. function keyLowerify(headers) {
  15. const keys = Object.keys(headers);
  16. const newHeaders = {};
  17. for (let i = 0; i < keys.length; i++) {
  18. const key = keys[i];
  19. newHeaders[key.toLowerCase()] = headers[key];
  20. }
  21. return newHeaders;
  22. }
  23. function getCanonicalizedHeaders(headers) {
  24. const prefix = 'x-acs-';
  25. const keys = Object.keys(headers);
  26. const canonicalizedKeys = [];
  27. for (let i = 0; i < keys.length; i++) {
  28. const key = keys[i];
  29. if (key.startsWith(prefix)) {
  30. canonicalizedKeys.push(key);
  31. }
  32. }
  33. canonicalizedKeys.sort();
  34. var result = '';
  35. for (let i = 0; i < canonicalizedKeys.length; i++) {
  36. const key = canonicalizedKeys[i];
  37. result += `${key}:${filter(headers[key]).trim()}\n`;
  38. }
  39. return result;
  40. }
  41. function getCanonicalizedResource(uriPattern, query) {
  42. const keys = Object.keys(query).sort();
  43. if (keys.length === 0) {
  44. return uriPattern;
  45. }
  46. var result = [];
  47. for (var i = 0; i < keys.length; i++) {
  48. const key = keys[i];
  49. result.push(`${key}=${query[key]}`);
  50. }
  51. return `${uriPattern}?${result.join('&')}`;
  52. }
  53. function buildStringToSign(method, uriPattern, headers, query) {
  54. const accept = headers['accept'];
  55. const contentMD5 = headers['content-md5'] || '';
  56. const contentType = headers['content-type'] || '';
  57. const date = headers['date'] || '';
  58. const header = `${method}\n${accept}\n${contentMD5}\n${contentType}\n${date}\n`;
  59. const canonicalizedHeaders = getCanonicalizedHeaders(headers);
  60. const canonicalizedResource = getCanonicalizedResource(uriPattern, query);
  61. return `${header}${canonicalizedHeaders}${canonicalizedResource}`;
  62. }
  63. function parseXML(xml) {
  64. const parser = new xml2js.Parser({
  65. // explicitArray: false
  66. });
  67. return new Promise((resolve, reject) => {
  68. parser.parseString(xml, (err, result) => {
  69. if (err) {
  70. return reject(err);
  71. }
  72. resolve(result);
  73. });
  74. });
  75. }
  76. class ACSError extends Error {
  77. constructor(err) {
  78. const message = err.Message[0];
  79. const code = err.Code[0];
  80. const hostid = err.HostId[0];
  81. const requestid = err.RequestId[0];
  82. super(`${message} hostid: ${hostid}, requestid: ${requestid}`);
  83. this.code = code;
  84. }
  85. }
  86. class ROAClient {
  87. constructor(config) {
  88. assert(config, 'must pass "config"');
  89. assert(config.endpoint, 'must pass "config.endpoint"');
  90. if (!config.endpoint.startsWith('https://') &&
  91. !config.endpoint.startsWith('http://')) {
  92. throw new Error(`"config.endpoint" must starts with 'https://' or 'http://'.`);
  93. }
  94. assert(config.apiVersion, 'must pass "config.apiVersion"');
  95. assert(config.accessKeyId, 'must pass "config.accessKeyId"');
  96. assert(config.accessKeySecret, 'must pass "config.accessKeySecret"');
  97. this.endpoint = config.endpoint;
  98. this.apiVersion = config.apiVersion;
  99. this.accessKeyId = config.accessKeyId;
  100. this.accessKeySecret = config.accessKeySecret;
  101. this.securityToken = config.securityToken;
  102. this.host = url.parse(this.endpoint).hostname;
  103. this.opts = config.opts;
  104. var httpModule = this.endpoint.startsWith('https://') ? require('https') : require('http');
  105. this.keepAliveAgent = new httpModule.Agent({
  106. keepAlive: true,
  107. keepAliveMsecs: 3000
  108. });
  109. }
  110. buildHeaders() {
  111. const now = new Date();
  112. var defaultHeaders = {
  113. accept: 'application/json',
  114. date: now.toGMTString(),
  115. host: this.host,
  116. 'x-acs-signature-nonce': kitx.makeNonce(),
  117. 'x-acs-signature-method': 'HMAC-SHA1',
  118. 'x-acs-signature-version': '1.0',
  119. 'x-acs-version': this.apiVersion,
  120. 'user-agent': helper.DEFAULT_UA,
  121. 'x-sdk-client': helper.DEFAULT_CLIENT
  122. };
  123. if (this.securityToken) {
  124. defaultHeaders['x-acs-accesskey-id'] = this.accessKeyId;
  125. defaultHeaders['x-acs-security-token'] = this.securityToken;
  126. }
  127. return defaultHeaders;
  128. }
  129. signature(stringToSign) {
  130. const utf8Buff = Buffer.from(stringToSign, 'utf8');
  131. return kitx.sha1(utf8Buff, this.accessKeySecret, 'base64');
  132. }
  133. buildAuthorization(stringToSign) {
  134. return `acs ${this.accessKeyId}:${this.signature(stringToSign)}`;
  135. }
  136. request(method, uriPattern, query = {}, body = '', headers = {}, opts = {}) {
  137. var postBody = null;
  138. var mixHeaders = Object.assign(this.buildHeaders(), keyLowerify(headers));
  139. postBody = Buffer.from(body, 'utf8');
  140. mixHeaders['content-md5'] = kitx.md5(postBody, 'base64');
  141. mixHeaders['content-length'] = postBody.length;
  142. var url = `${this.endpoint}${uriPattern}`;
  143. if (Object.keys(query).length) {
  144. url += `?${querystring.stringify(query)}`;
  145. }
  146. const stringToSign = buildStringToSign(method, uriPattern, mixHeaders, query);
  147. debug('stringToSign: %s', stringToSign);
  148. mixHeaders['authorization'] = this.buildAuthorization(stringToSign);
  149. const options = Object.assign({
  150. method,
  151. agent: this.keepAliveAgent,
  152. headers: mixHeaders,
  153. data: postBody
  154. }, this.opts, opts);
  155. return httpx.request(url, options).then((response) => {
  156. return httpx.read(response, 'utf8').then((body) => {
  157. // Retrun raw body
  158. if (opts.rawBody) {
  159. return body;
  160. }
  161. const contentType = response.headers['content-type'] || '';
  162. // JSON
  163. if (contentType.startsWith('application/json')) {
  164. const statusCode = response.statusCode;
  165. if (statusCode === 204) {
  166. return body;
  167. }
  168. var result;
  169. try {
  170. result = JSON.parse(body);
  171. } catch (err) {
  172. err.name = 'FormatError';
  173. err.message = 'parse response to json error';
  174. err.body = body;
  175. throw err;
  176. }
  177. if (statusCode >= 400) {
  178. const errorMessage = result.Message || result.errorMsg || '';
  179. const errorCode = result.Code || result.errorCode || '';
  180. const requestId = result.RequestId || '';
  181. var err = new Error(`code: ${statusCode}, ${errorMessage}, requestid: ${requestId}`);
  182. err.name = `${errorCode}Error`;
  183. err.statusCode = statusCode;
  184. err.result = result;
  185. err.code = errorCode;
  186. throw err;
  187. }
  188. return result;
  189. }
  190. if (contentType.startsWith('text/xml')) {
  191. return parseXML(body).then((result) => {
  192. if (result.Error) {
  193. throw new ACSError(result.Error);
  194. }
  195. return result;
  196. });
  197. }
  198. return body;
  199. });
  200. });
  201. }
  202. put(path, query, body, headers, options) {
  203. return this.request('PUT', path, query, body, headers, options);
  204. }
  205. post(path, query, body, headers, options) {
  206. return this.request('POST', path, query, body, headers, options);
  207. }
  208. get(path, query, headers, options) {
  209. return this.request('GET', path, query, '', headers, options);
  210. }
  211. delete(path, query, headers, options) {
  212. return this.request('DELETE', path, query, '', headers, options);
  213. }
  214. }
  215. module.exports = ROAClient;