You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
10 KiB
351 lines
10 KiB
const crypto = require('crypto');
|
|
const is = require('is-type-of');
|
|
const qs = require('qs');
|
|
const { lowercaseKeyHeader } = require('./utils/lowercaseKeyHeader');
|
|
const { encodeString } = require('./utils/encodeString');
|
|
|
|
/**
|
|
*
|
|
* @param {String} resourcePath
|
|
* @param {Object} parameters
|
|
* @return
|
|
*/
|
|
exports.buildCanonicalizedResource = function buildCanonicalizedResource(resourcePath, parameters) {
|
|
let canonicalizedResource = `${resourcePath}`;
|
|
let separatorString = '?';
|
|
|
|
if (is.string(parameters) && parameters.trim() !== '') {
|
|
canonicalizedResource += separatorString + parameters;
|
|
} else if (is.array(parameters)) {
|
|
parameters.sort();
|
|
canonicalizedResource += separatorString + parameters.join('&');
|
|
} else if (parameters) {
|
|
const processFunc = key => {
|
|
canonicalizedResource += separatorString + key;
|
|
if (parameters[key] || parameters[key] === 0) {
|
|
canonicalizedResource += `=${parameters[key]}`;
|
|
}
|
|
separatorString = '&';
|
|
};
|
|
Object.keys(parameters).sort().forEach(processFunc);
|
|
}
|
|
|
|
return canonicalizedResource;
|
|
};
|
|
|
|
/**
|
|
* @param {String} method
|
|
* @param {String} resourcePath
|
|
* @param {Object} request
|
|
* @param {String} expires
|
|
* @return {String} canonicalString
|
|
*/
|
|
exports.buildCanonicalString = function canonicalString(method, resourcePath, request, expires) {
|
|
request = request || {};
|
|
const headers = lowercaseKeyHeader(request.headers);
|
|
const OSS_PREFIX = 'x-oss-';
|
|
const ossHeaders = [];
|
|
const headersToSign = {};
|
|
|
|
let signContent = [
|
|
method.toUpperCase(),
|
|
headers['content-md5'] || '',
|
|
headers['content-type'],
|
|
expires || headers['x-oss-date']
|
|
];
|
|
|
|
Object.keys(headers).forEach(key => {
|
|
const lowerKey = key.toLowerCase();
|
|
if (lowerKey.indexOf(OSS_PREFIX) === 0) {
|
|
headersToSign[lowerKey] = String(headers[key]).trim();
|
|
}
|
|
});
|
|
|
|
Object.keys(headersToSign)
|
|
.sort()
|
|
.forEach(key => {
|
|
ossHeaders.push(`${key}:${headersToSign[key]}`);
|
|
});
|
|
|
|
signContent = signContent.concat(ossHeaders);
|
|
|
|
signContent.push(this.buildCanonicalizedResource(resourcePath, request.parameters));
|
|
|
|
return signContent.join('\n');
|
|
};
|
|
|
|
/**
|
|
* @param {String} accessKeySecret
|
|
* @param {String} canonicalString
|
|
*/
|
|
exports.computeSignature = function computeSignature(accessKeySecret, canonicalString, headerEncoding = 'utf-8') {
|
|
const signature = crypto.createHmac('sha1', accessKeySecret);
|
|
return signature.update(Buffer.from(canonicalString, headerEncoding)).digest('base64');
|
|
};
|
|
|
|
/**
|
|
* @param {String} accessKeyId
|
|
* @param {String} accessKeySecret
|
|
* @param {String} canonicalString
|
|
*/
|
|
exports.authorization = function authorization(accessKeyId, accessKeySecret, canonicalString, headerEncoding) {
|
|
return `OSS ${accessKeyId}:${this.computeSignature(accessKeySecret, canonicalString, headerEncoding)}`;
|
|
};
|
|
|
|
/**
|
|
* @param {string[]} [additionalHeaders]
|
|
* @returns {string[]}
|
|
*/
|
|
exports.fixAdditionalHeaders = additionalHeaders => {
|
|
if (!additionalHeaders) {
|
|
return [];
|
|
}
|
|
|
|
const OSS_PREFIX = 'x-oss-';
|
|
|
|
return [...new Set(additionalHeaders.map(v => v.toLowerCase()))]
|
|
.filter(v => {
|
|
return v !== 'content-type' && v !== 'content-md5' && !v.startsWith(OSS_PREFIX);
|
|
})
|
|
.sort();
|
|
};
|
|
|
|
/**
|
|
* @param {string} method
|
|
* @param {Object} request
|
|
* @param {Object} request.headers
|
|
* @param {Object} [request.queries]
|
|
* @param {string} [bucketName]
|
|
* @param {string} [objectName]
|
|
* @param {string[]} [additionalHeaders] additional headers after deduplication, lowercase and sorting
|
|
* @returns {string}
|
|
*/
|
|
exports.getCanonicalRequest = function getCanonicalRequest(method, request, bucketName, objectName, additionalHeaders) {
|
|
const headers = lowercaseKeyHeader(request.headers);
|
|
const queries = request.queries || {};
|
|
const OSS_PREFIX = 'x-oss-';
|
|
|
|
if (objectName && !bucketName) {
|
|
throw Error('Please ensure that bucketName is passed into getCanonicalRequest.');
|
|
}
|
|
|
|
const signContent = [
|
|
method.toUpperCase(), // HTTP Verb
|
|
encodeString(`/${bucketName ? `${bucketName}/` : ''}${objectName || ''}`).replace(/%2F/g, '/') // Canonical URI
|
|
];
|
|
|
|
// Canonical Query String
|
|
signContent.push(
|
|
qs.stringify(queries, {
|
|
encoder: encodeString,
|
|
sort: (a, b) => a.localeCompare(b),
|
|
strictNullHandling: true
|
|
})
|
|
);
|
|
|
|
// Canonical Headers
|
|
if (additionalHeaders) {
|
|
additionalHeaders.forEach(v => {
|
|
if (!Object.prototype.hasOwnProperty.call(headers, v)) {
|
|
throw Error(`Can't find additional header ${v} in request headers.`);
|
|
}
|
|
});
|
|
}
|
|
|
|
const tempHeaders = new Set(additionalHeaders);
|
|
|
|
Object.keys(headers).forEach(v => {
|
|
if (v === 'content-type' || v === 'content-md5' || v.startsWith(OSS_PREFIX)) {
|
|
tempHeaders.add(v);
|
|
}
|
|
});
|
|
|
|
const canonicalHeaders = `${[...tempHeaders]
|
|
.sort()
|
|
.map(v => `${v}:${is.string(headers[v]) ? headers[v].trim() : headers[v]}\n`)
|
|
.join('')}`;
|
|
|
|
signContent.push(canonicalHeaders);
|
|
|
|
// Additional Headers
|
|
if (additionalHeaders.length > 0) {
|
|
signContent.push(additionalHeaders.join(';'));
|
|
} else {
|
|
signContent.push('');
|
|
}
|
|
|
|
// Hashed Payload
|
|
signContent.push(headers['x-oss-content-sha256'] || 'UNSIGNED-PAYLOAD');
|
|
|
|
return signContent.join('\n');
|
|
};
|
|
|
|
/**
|
|
* @param {string} region Standard region, e.g. cn-hangzhou
|
|
* @param {string} date ISO8601 UTC:yyyymmdd'T'HHMMss'Z'
|
|
* @param {string} canonicalRequest
|
|
* @returns {string}
|
|
*/
|
|
exports.getStringToSign = function getStringToSign(region, date, canonicalRequest) {
|
|
const stringToSign = [
|
|
'OSS4-HMAC-SHA256',
|
|
date, // TimeStamp
|
|
`${date.split('T')[0]}/${region}/oss/aliyun_v4_request`, // Scope
|
|
crypto.createHash('sha256').update(canonicalRequest).digest('hex') // Hashed Canonical Request
|
|
];
|
|
|
|
return stringToSign.join('\n');
|
|
};
|
|
|
|
/**
|
|
* @param {String} accessKeySecret
|
|
* @param {string} date yyyymmdd
|
|
* @param {string} region Standard region, e.g. cn-hangzhou
|
|
* @param {string} stringToSign
|
|
* @returns {string}
|
|
*/
|
|
exports.getSignatureV4 = function getSignatureV4(accessKeySecret, date, region, stringToSign) {
|
|
const signingDate = crypto.createHmac('sha256', `aliyun_v4${accessKeySecret}`).update(date).digest();
|
|
const signingRegion = crypto.createHmac('sha256', signingDate).update(region).digest();
|
|
const signingOss = crypto.createHmac('sha256', signingRegion).update('oss').digest();
|
|
const signingKey = crypto.createHmac('sha256', signingOss).update('aliyun_v4_request').digest();
|
|
const signatureValue = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
|
|
|
|
return signatureValue;
|
|
};
|
|
|
|
/**
|
|
* @param {String} accessKeyId
|
|
* @param {String} accessKeySecret
|
|
* @param {string} region Standard region, e.g. cn-hangzhou
|
|
* @param {string} method
|
|
* @param {Object} request
|
|
* @param {Object} request.headers
|
|
* @param {Object} [request.queries]
|
|
* @param {string} [bucketName]
|
|
* @param {string} [objectName]
|
|
* @param {string[]} [additionalHeaders]
|
|
* @param {string} [headerEncoding='utf-8']
|
|
* @returns {string}
|
|
*/
|
|
exports.authorizationV4 = function authorizationV4(
|
|
accessKeyId,
|
|
accessKeySecret,
|
|
region,
|
|
method,
|
|
request,
|
|
bucketName,
|
|
objectName,
|
|
additionalHeaders,
|
|
headerEncoding = 'utf-8'
|
|
) {
|
|
const fixedAdditionalHeaders = this.fixAdditionalHeaders(additionalHeaders);
|
|
const fixedHeaders = {};
|
|
Object.entries(request.headers).forEach(v => {
|
|
fixedHeaders[v[0]] = is.string(v[1]) ? Buffer.from(v[1], headerEncoding).toString() : v[1];
|
|
});
|
|
const date = fixedHeaders['x-oss-date'] || (request.queries && request.queries['x-oss-date']);
|
|
const canonicalRequest = this.getCanonicalRequest(
|
|
method,
|
|
{
|
|
headers: fixedHeaders,
|
|
queries: request.queries
|
|
},
|
|
bucketName,
|
|
objectName,
|
|
fixedAdditionalHeaders
|
|
);
|
|
const stringToSign = this.getStringToSign(region, date, canonicalRequest);
|
|
const onlyDate = date.split('T')[0];
|
|
const signatureValue = this.getSignatureV4(accessKeySecret, onlyDate, region, stringToSign);
|
|
const additionalHeadersValue =
|
|
fixedAdditionalHeaders.length > 0 ? `AdditionalHeaders=${fixedAdditionalHeaders.join(';')},` : '';
|
|
|
|
return `OSS4-HMAC-SHA256 Credential=${accessKeyId}/${onlyDate}/${region}/oss/aliyun_v4_request,${additionalHeadersValue}Signature=${signatureValue}`;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {String} accessKeySecret
|
|
* @param {Object} options
|
|
* @param {String} resource
|
|
* @param {Number} expires
|
|
*/
|
|
exports._signatureForURL = function _signatureForURL(accessKeySecret, options = {}, resource, expires, headerEncoding) {
|
|
const headers = {};
|
|
const { subResource = {} } = options;
|
|
|
|
if (options.process) {
|
|
const processKeyword = 'x-oss-process';
|
|
subResource[processKeyword] = options.process;
|
|
}
|
|
|
|
if (options.trafficLimit) {
|
|
const trafficLimitKey = 'x-oss-traffic-limit';
|
|
subResource[trafficLimitKey] = options.trafficLimit;
|
|
}
|
|
|
|
if (options.response) {
|
|
Object.keys(options.response).forEach(k => {
|
|
const key = `response-${k.toLowerCase()}`;
|
|
subResource[key] = options.response[k];
|
|
});
|
|
}
|
|
|
|
Object.keys(options).forEach(key => {
|
|
const lowerKey = key.toLowerCase();
|
|
const value = options[key];
|
|
if (lowerKey.indexOf('x-oss-') === 0) {
|
|
headers[lowerKey] = value;
|
|
} else if (lowerKey.indexOf('content-md5') === 0) {
|
|
headers[key] = value;
|
|
} else if (lowerKey.indexOf('content-type') === 0) {
|
|
headers[key] = value;
|
|
}
|
|
});
|
|
|
|
if (Object.prototype.hasOwnProperty.call(options, 'security-token')) {
|
|
subResource['security-token'] = options['security-token'];
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(options, 'callback')) {
|
|
const json = {
|
|
callbackUrl: encodeURI(options.callback.url),
|
|
callbackBody: options.callback.body
|
|
};
|
|
if (options.callback.host) {
|
|
json.callbackHost = options.callback.host;
|
|
}
|
|
if (options.callback.contentType) {
|
|
json.callbackBodyType = options.callback.contentType;
|
|
}
|
|
if (options.callback.callbackSNI) {
|
|
json.callbackSNI = options.callback.callbackSNI;
|
|
}
|
|
subResource.callback = Buffer.from(JSON.stringify(json)).toString('base64');
|
|
|
|
if (options.callback.customValue) {
|
|
const callbackVar = {};
|
|
Object.keys(options.callback.customValue).forEach(key => {
|
|
callbackVar[`x:${key}`] = options.callback.customValue[key];
|
|
});
|
|
subResource['callback-var'] = Buffer.from(JSON.stringify(callbackVar)).toString('base64');
|
|
}
|
|
}
|
|
|
|
const canonicalString = this.buildCanonicalString(
|
|
options.method,
|
|
resource,
|
|
{
|
|
headers,
|
|
parameters: subResource
|
|
},
|
|
expires.toString()
|
|
);
|
|
|
|
return {
|
|
Signature: this.computeSignature(accessKeySecret, canonicalString, headerEncoding),
|
|
subResource
|
|
};
|
|
};
|