signer.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const crypto = require("crypto");
  4. const utils_1 = require("./utils");
  5. const utils_lang_1 = require("./utils.lang");
  6. const keyvalue_1 = require("./keyvalue");
  7. const url_1 = require("url");
  8. const debug = require('util').debuglog('@cloudbase/signature');
  9. const isStream = require('is-stream');
  10. exports.signedParamsSeparator = ';';
  11. const HOST_KEY = 'host';
  12. const CONTENT_TYPE_KEY = 'content-type';
  13. var MIME;
  14. (function (MIME) {
  15. MIME["MULTIPART_FORM_DATA"] = "multipart/form-data";
  16. MIME["APPLICATION_JSON"] = "application/json";
  17. })(MIME || (MIME = {}));
  18. class Signer {
  19. constructor(credential, service, options = {}) {
  20. this.credential = credential;
  21. this.service = service;
  22. this.algorithm = 'TC3-HMAC-SHA256';
  23. this.options = options;
  24. }
  25. static camSafeUrlEncode(str) {
  26. return encodeURIComponent(str)
  27. .replace(/!/g, '%21')
  28. .replace(/'/g, '%27')
  29. .replace(/\(/g, '%28')
  30. .replace(/\)/g, '%29')
  31. .replace(/\*/g, '%2A');
  32. }
  33. /**
  34. * 将一个对象处理成 KeyValue 形式,嵌套的对象将会被处理成字符串,Key转换成小写字母
  35. * @param {Object} obj - 待处理的对象
  36. * @param {Object} options
  37. * @param {Boolean} options.enableBuffer
  38. */
  39. static formatKeyAndValue(obj, options = {}) {
  40. if (!utils_lang_1.isPlainObject(obj)) {
  41. return obj;
  42. }
  43. // enableValueToLowerCase:头部字段,要求小写,其他数据不需要小写,所以这里避免转小写
  44. const { multipart, enableValueToLowerCase = false, selectedKeys, filter } = options;
  45. const kv = {};
  46. Object.keys(obj || {}).forEach(key => {
  47. // NOTE: 客户端类型在服务端可能会丢失
  48. const lowercaseKey = Signer.camSafeUrlEncode(key.toLowerCase().trim());
  49. // 过滤 Key,服务端接收到的数据,可能含有未签名的 Key,通常是签名的时候被过滤掉的流,数据量可能会比较大
  50. // 所以这里提供一个过滤的判断,避免不必要的计算
  51. // istanbul ignore next
  52. if (Array.isArray(selectedKeys) && !selectedKeys.includes(lowercaseKey)) {
  53. return;
  54. }
  55. // istanbul ignore next
  56. if (typeof filter === 'function') {
  57. if (filter(key, obj[key], options)) {
  58. return;
  59. }
  60. }
  61. // istanbul ignore else
  62. if (key && obj[key] !== undefined) {
  63. if (lowercaseKey === CONTENT_TYPE_KEY) {
  64. // multipart/form-data; boundary=???
  65. if (obj[key].startsWith(MIME.MULTIPART_FORM_DATA)) {
  66. kv[lowercaseKey] = MIME.MULTIPART_FORM_DATA;
  67. }
  68. else {
  69. kv[lowercaseKey] = obj[key];
  70. }
  71. return;
  72. }
  73. if (isStream(obj[key])) {
  74. // 这里如果是个文件流,在发送的时候可以识别
  75. // 服务端接收到数据之后传到这里判断不出来的
  76. // 所以会进入后边的逻辑
  77. return;
  78. }
  79. else if (utils_1.isNodeEnv() && Buffer.isBuffer(obj[key])) {
  80. if (multipart) {
  81. kv[lowercaseKey] = obj[key];
  82. }
  83. else {
  84. kv[lowercaseKey] = enableValueToLowerCase
  85. ? utils_1.stringify(obj[key]).trim().toLowerCase()
  86. : utils_1.stringify(obj[key]).trim();
  87. }
  88. }
  89. else {
  90. kv[lowercaseKey] = enableValueToLowerCase
  91. ? utils_1.stringify(obj[key]).trim().toLowerCase()
  92. : utils_1.stringify(obj[key]).trim();
  93. }
  94. }
  95. });
  96. return kv;
  97. }
  98. static calcParamsHash(params, keys = null, options = {}) {
  99. debug(params, 'calcParamsHash');
  100. if (utils_lang_1.isString(params)) {
  101. return utils_1.sha256hash(params);
  102. }
  103. // 只关心业务参数,不关心以什么类型的 Content-Type 传递的
  104. // 所以 application/json multipart/form-data 计算方式是相同的
  105. keys = keys || keyvalue_1.SortedKeyValue.kv(params).keys();
  106. const hash = crypto.createHash('sha256');
  107. for (const key of keys) {
  108. // istanbul ignore next
  109. if (!params[key]) {
  110. continue;
  111. }
  112. // istanbul ignore next
  113. if (isStream(params[key])) {
  114. continue;
  115. }
  116. // string && buffer
  117. hash.update(`&${key}=`);
  118. hash.update(params[key]);
  119. hash.update('\r\n');
  120. }
  121. return hash.digest(options.encoding || 'hex');
  122. }
  123. /**
  124. * 计算签名信息
  125. * @param {string} method - Http Verb:GET/get POST/post 区分大小写
  126. * @param {string} url - 地址:http://abc.org/api/v1?a=1&b=2
  127. * @param {Object} headers - 需要签名的头部字段
  128. * @param {string} params - 请求参数
  129. * @param {number} [timestamp] - 签名时间戳
  130. * @param {object} [options] - 可选参数
  131. */
  132. tc3sign(method, url, headers, params, timestamp, options = {}) {
  133. timestamp = timestamp || utils_1.second();
  134. const urlInfo = url_1.parse(url);
  135. const formatedHeaders = Signer.formatKeyAndValue(headers, {
  136. enableValueToLowerCase: true
  137. });
  138. const headerKV = keyvalue_1.SortedKeyValue.kv(formatedHeaders);
  139. const signedHeaders = headerKV.keys();
  140. const canonicalHeaders = headerKV.toString(':', '\n') + '\n';
  141. const { enableHostCheck = true, enableContentTypeCheck = true } = options;
  142. if (enableHostCheck && headerKV.get(HOST_KEY) !== urlInfo.host) {
  143. throw new TypeError(`host:${urlInfo.host} in url must be equals to host:${headerKV.get('host')} in headers`);
  144. }
  145. if (enableContentTypeCheck && !headerKV.get(CONTENT_TYPE_KEY)) {
  146. throw new TypeError(`${CONTENT_TYPE_KEY} field must in headers`);
  147. }
  148. const multipart = headerKV.get(CONTENT_TYPE_KEY).startsWith(MIME.MULTIPART_FORM_DATA);
  149. const formatedParams = method.toUpperCase() === 'GET' ? '' : Signer.formatKeyAndValue(params, {
  150. multipart
  151. });
  152. const paramKV = keyvalue_1.SortedKeyValue.kv(formatedParams);
  153. const signedParams = paramKV.keys();
  154. const hashedPayload = Signer.calcParamsHash(formatedParams, null);
  155. const signedUrl = url.replace(/^https?:/, '').split('?')[0];
  156. const canonicalRequest = `${method}\n${signedUrl}\n${urlInfo.query || ''}\n${canonicalHeaders}\n${signedHeaders.join(';')}\n${hashedPayload}`;
  157. debug(canonicalRequest, 'canonicalRequest\n\n');
  158. const date = utils_1.formateDate(timestamp);
  159. const service = this.service;
  160. const algorithm = this.algorithm;
  161. const credentialScope = `${date}/${service}/tc3_request`;
  162. const stringToSign = `${algorithm}\n${timestamp}\n${credentialScope}\n${utils_1.sha256hash(canonicalRequest)}`;
  163. debug(stringToSign, 'stringToSign\n\n');
  164. const secretDate = utils_1.sha256hmac(date, `TC3${this.credential.secretKey}`);
  165. const secretService = utils_1.sha256hmac(service, secretDate);
  166. const secretSigning = utils_1.sha256hmac('tc3_request', secretService);
  167. const signature = utils_1.sha256hmac(stringToSign, secretSigning, 'hex');
  168. debug(secretDate.toString('hex'), 'secretDate');
  169. debug(secretService.toString('hex'), 'secretService');
  170. debug(secretSigning.toString('hex'), 'secretSigning');
  171. debug(signature, 'signature');
  172. const { withSignedParams = false } = options;
  173. return {
  174. // 需注意该字段长度
  175. // https://stackoverflow.com/questions/686217/maximum-on-http-header-values
  176. // https://www.tutorialspoint.com/What-is-the-maximum-size-of-HTTP-header-values
  177. authorization: `${algorithm} Credential=${this.credential.secretId}/${credentialScope},${withSignedParams ? ` SignedParams=${signedParams.join(';')},` : ''} SignedHeaders=${signedHeaders.join(';')}, Signature=${signature}`,
  178. signedParams,
  179. signedHeaders,
  180. signature,
  181. timestamp,
  182. multipart
  183. };
  184. }
  185. }
  186. exports.Signer = Signer;