|
|
"use strict";
|
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
const crypto = require("crypto");
|
|
|
const utils_1 = require("./utils");
|
|
|
const utils_lang_1 = require("./utils.lang");
|
|
|
const keyvalue_1 = require("./keyvalue");
|
|
|
const url_1 = require("url");
|
|
|
const debug = require('util').debuglog('@cloudbase/signature');
|
|
|
const isStream = require('is-stream');
|
|
|
exports.signedParamsSeparator = ';';
|
|
|
const HOST_KEY = 'host';
|
|
|
const CONTENT_TYPE_KEY = 'content-type';
|
|
|
var MIME;
|
|
|
(function (MIME) {
|
|
|
MIME["MULTIPART_FORM_DATA"] = "multipart/form-data";
|
|
|
MIME["APPLICATION_JSON"] = "application/json";
|
|
|
})(MIME || (MIME = {}));
|
|
|
class Signer {
|
|
|
constructor(credential, service, options = {}) {
|
|
|
this.credential = credential;
|
|
|
this.service = service;
|
|
|
this.algorithm = 'TC3-HMAC-SHA256';
|
|
|
this.options = options;
|
|
|
}
|
|
|
static camSafeUrlEncode(str) {
|
|
|
return encodeURIComponent(str)
|
|
|
.replace(/!/g, '%21')
|
|
|
.replace(/'/g, '%27')
|
|
|
.replace(/\(/g, '%28')
|
|
|
.replace(/\)/g, '%29')
|
|
|
.replace(/\*/g, '%2A');
|
|
|
}
|
|
|
/**
|
|
|
* 将一个对象处理成 KeyValue 形式,嵌套的对象将会被处理成字符串,Key转换成小写字母
|
|
|
* @param {Object} obj - 待处理的对象
|
|
|
* @param {Object} options
|
|
|
* @param {Boolean} options.enableBuffer
|
|
|
*/
|
|
|
static formatKeyAndValue(obj, options = {}) {
|
|
|
if (!utils_lang_1.isPlainObject(obj)) {
|
|
|
return obj;
|
|
|
}
|
|
|
// enableValueToLowerCase:头部字段,要求小写,其他数据不需要小写,所以这里避免转小写
|
|
|
const { multipart, enableValueToLowerCase = false, selectedKeys, filter } = options;
|
|
|
const kv = {};
|
|
|
Object.keys(obj || {}).forEach(key => {
|
|
|
// NOTE: 客户端类型在服务端可能会丢失
|
|
|
const lowercaseKey = Signer.camSafeUrlEncode(key.toLowerCase().trim());
|
|
|
// 过滤 Key,服务端接收到的数据,可能含有未签名的 Key,通常是签名的时候被过滤掉的流,数据量可能会比较大
|
|
|
// 所以这里提供一个过滤的判断,避免不必要的计算
|
|
|
// istanbul ignore next
|
|
|
if (Array.isArray(selectedKeys) && !selectedKeys.includes(lowercaseKey)) {
|
|
|
return;
|
|
|
}
|
|
|
// istanbul ignore next
|
|
|
if (typeof filter === 'function') {
|
|
|
if (filter(key, obj[key], options)) {
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
// istanbul ignore else
|
|
|
if (key && obj[key] !== undefined) {
|
|
|
if (lowercaseKey === CONTENT_TYPE_KEY) {
|
|
|
// multipart/form-data; boundary=???
|
|
|
if (obj[key].startsWith(MIME.MULTIPART_FORM_DATA)) {
|
|
|
kv[lowercaseKey] = MIME.MULTIPART_FORM_DATA;
|
|
|
}
|
|
|
else {
|
|
|
kv[lowercaseKey] = obj[key];
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
if (isStream(obj[key])) {
|
|
|
// 这里如果是个文件流,在发送的时候可以识别
|
|
|
// 服务端接收到数据之后传到这里判断不出来的
|
|
|
// 所以会进入后边的逻辑
|
|
|
return;
|
|
|
}
|
|
|
else if (utils_1.isNodeEnv() && Buffer.isBuffer(obj[key])) {
|
|
|
if (multipart) {
|
|
|
kv[lowercaseKey] = obj[key];
|
|
|
}
|
|
|
else {
|
|
|
kv[lowercaseKey] = enableValueToLowerCase
|
|
|
? utils_1.stringify(obj[key]).trim().toLowerCase()
|
|
|
: utils_1.stringify(obj[key]).trim();
|
|
|
}
|
|
|
}
|
|
|
else {
|
|
|
kv[lowercaseKey] = enableValueToLowerCase
|
|
|
? utils_1.stringify(obj[key]).trim().toLowerCase()
|
|
|
: utils_1.stringify(obj[key]).trim();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
return kv;
|
|
|
}
|
|
|
static calcParamsHash(params, keys = null, options = {}) {
|
|
|
debug(params, 'calcParamsHash');
|
|
|
if (utils_lang_1.isString(params)) {
|
|
|
return utils_1.sha256hash(params);
|
|
|
}
|
|
|
// 只关心业务参数,不关心以什么类型的 Content-Type 传递的
|
|
|
// 所以 application/json multipart/form-data 计算方式是相同的
|
|
|
keys = keys || keyvalue_1.SortedKeyValue.kv(params).keys();
|
|
|
const hash = crypto.createHash('sha256');
|
|
|
for (const key of keys) {
|
|
|
// istanbul ignore next
|
|
|
if (!params[key]) {
|
|
|
continue;
|
|
|
}
|
|
|
// istanbul ignore next
|
|
|
if (isStream(params[key])) {
|
|
|
continue;
|
|
|
}
|
|
|
// string && buffer
|
|
|
hash.update(`&${key}=`);
|
|
|
hash.update(params[key]);
|
|
|
hash.update('\r\n');
|
|
|
}
|
|
|
return hash.digest(options.encoding || 'hex');
|
|
|
}
|
|
|
/**
|
|
|
* 计算签名信息
|
|
|
* @param {string} method - Http Verb:GET/get POST/post 区分大小写
|
|
|
* @param {string} url - 地址:http://abc.org/api/v1?a=1&b=2
|
|
|
* @param {Object} headers - 需要签名的头部字段
|
|
|
* @param {string} params - 请求参数
|
|
|
* @param {number} [timestamp] - 签名时间戳
|
|
|
* @param {object} [options] - 可选参数
|
|
|
*/
|
|
|
tc3sign(method, url, headers, params, timestamp, options = {}) {
|
|
|
timestamp = timestamp || utils_1.second();
|
|
|
const urlInfo = url_1.parse(url);
|
|
|
const formatedHeaders = Signer.formatKeyAndValue(headers, {
|
|
|
enableValueToLowerCase: true
|
|
|
});
|
|
|
const headerKV = keyvalue_1.SortedKeyValue.kv(formatedHeaders);
|
|
|
const signedHeaders = headerKV.keys();
|
|
|
const canonicalHeaders = headerKV.toString(':', '\n') + '\n';
|
|
|
const { enableHostCheck = true, enableContentTypeCheck = true } = options;
|
|
|
if (enableHostCheck && headerKV.get(HOST_KEY) !== urlInfo.host) {
|
|
|
throw new TypeError(`host:${urlInfo.host} in url must be equals to host:${headerKV.get('host')} in headers`);
|
|
|
}
|
|
|
if (enableContentTypeCheck && !headerKV.get(CONTENT_TYPE_KEY)) {
|
|
|
throw new TypeError(`${CONTENT_TYPE_KEY} field must in headers`);
|
|
|
}
|
|
|
const multipart = headerKV.get(CONTENT_TYPE_KEY).startsWith(MIME.MULTIPART_FORM_DATA);
|
|
|
const formatedParams = method.toUpperCase() === 'GET' ? '' : Signer.formatKeyAndValue(params, {
|
|
|
multipart
|
|
|
});
|
|
|
const paramKV = keyvalue_1.SortedKeyValue.kv(formatedParams);
|
|
|
const signedParams = paramKV.keys();
|
|
|
const hashedPayload = Signer.calcParamsHash(formatedParams, null);
|
|
|
const signedUrl = url.replace(/^https?:/, '').split('?')[0];
|
|
|
const canonicalRequest = `${method}\n${signedUrl}\n${urlInfo.query || ''}\n${canonicalHeaders}\n${signedHeaders.join(';')}\n${hashedPayload}`;
|
|
|
debug(canonicalRequest, 'canonicalRequest\n\n');
|
|
|
const date = utils_1.formateDate(timestamp);
|
|
|
const service = this.service;
|
|
|
const algorithm = this.algorithm;
|
|
|
const credentialScope = `${date}/${service}/tc3_request`;
|
|
|
const stringToSign = `${algorithm}\n${timestamp}\n${credentialScope}\n${utils_1.sha256hash(canonicalRequest)}`;
|
|
|
debug(stringToSign, 'stringToSign\n\n');
|
|
|
const secretDate = utils_1.sha256hmac(date, `TC3${this.credential.secretKey}`);
|
|
|
const secretService = utils_1.sha256hmac(service, secretDate);
|
|
|
const secretSigning = utils_1.sha256hmac('tc3_request', secretService);
|
|
|
const signature = utils_1.sha256hmac(stringToSign, secretSigning, 'hex');
|
|
|
debug(secretDate.toString('hex'), 'secretDate');
|
|
|
debug(secretService.toString('hex'), 'secretService');
|
|
|
debug(secretSigning.toString('hex'), 'secretSigning');
|
|
|
debug(signature, 'signature');
|
|
|
const { withSignedParams = false } = options;
|
|
|
return {
|
|
|
// 需注意该字段长度
|
|
|
// https://stackoverflow.com/questions/686217/maximum-on-http-header-values
|
|
|
// https://www.tutorialspoint.com/What-is-the-maximum-size-of-HTTP-header-values
|
|
|
authorization: `${algorithm} Credential=${this.credential.secretId}/${credentialScope},${withSignedParams ? ` SignedParams=${signedParams.join(';')},` : ''} SignedHeaders=${signedHeaders.join(';')}, Signature=${signature}`,
|
|
|
signedParams,
|
|
|
signedHeaders,
|
|
|
signature,
|
|
|
timestamp,
|
|
|
multipart
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
exports.Signer = Signer;
|