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 }; };