| 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 && 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} date yyyymmdd | |
|  * @param {string} region Standard region, e.g. cn-hangzhou | |
|  * @param {string} [accessKeyId] Access Key ID | |
|  * @param {string} [product] Product name, default is oss | |
|  * @returns {string} | |
|  */ | |
| exports.getCredential = function getCredential(date, region, accessKeyId, product = 'oss') { | |
|   const tempCredential = `${date}/${region}/${product}/aliyun_v4_request`; | |
| 
 | |
|   if (accessKeyId) { | |
|     return `${accessKeyId}/${tempCredential}`; | |
|   } | |
| 
 | |
|   return tempCredential; | |
| }; | |
| 
 | |
| /** | |
|  * @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 | |
|     this.getCredential(date.split('T')[0], region), // 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=${this.getCredential(onlyDate, region, accessKeyId)},${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 | |
|   }; | |
| };
 |