diff --git a/es/ALiYunSDK.d.ts b/es/ALiYunSDK.d.ts new file mode 100644 index 0000000..35a3045 --- /dev/null +++ b/es/ALiYunSDK.d.ts @@ -0,0 +1,9 @@ +import { ALiYunInstance } from './service/ali/Ali'; +declare class ALiYunSDK { + aliMap: Record; + constructor(); + getInstance(accessKey: string, accessSecret: string): ALiYunInstance; +} +declare const SDK: ALiYunSDK; +export default SDK; +export { ALiYunInstance }; diff --git a/es/ALiYunSDK.js b/es/ALiYunSDK.js new file mode 100644 index 0000000..4be959c --- /dev/null +++ b/es/ALiYunSDK.js @@ -0,0 +1,20 @@ +import { ALiYunInstance } from './service/ali/Ali'; +class ALiYunSDK { + aliMap; //oss + constructor() { + this.aliMap = {}; + } + getInstance(accessKey, accessSecret) { + if (this.aliMap[accessKey]) { + return this.aliMap[accessKey]; + } + const instance = new ALiYunInstance(accessKey, accessSecret); + Object.assign(this.aliMap, { + [accessKey]: instance, + }); + return instance; + } +} +const SDK = new ALiYunSDK(); +export default SDK; +export { ALiYunInstance }; diff --git a/es/CTYunSDK.js b/es/CTYunSDK.js index 4f2bad0..342a580 100644 --- a/es/CTYunSDK.js +++ b/es/CTYunSDK.js @@ -1,6 +1,6 @@ import { CTYunInstance } from './service/ctyun/CTYun'; class CTYunSDK { - ctyunMap; + ctyunMap; //oss constructor() { this.ctyunMap = {}; } diff --git a/es/TencentYunSDK.d.ts b/es/TencentYunSDK.d.ts new file mode 100644 index 0000000..205fad2 --- /dev/null +++ b/es/TencentYunSDK.d.ts @@ -0,0 +1,9 @@ +import { TencentYunInstance } from './service/tencent/Tencent'; +declare class TencentYunSDK { + tencentMap: Record; + constructor(); + getInstance(accessKey: string, accessSecret: string): TencentYunInstance; +} +declare const SDK: TencentYunSDK; +export default SDK; +export { TencentYunInstance }; diff --git a/es/TencentYunSDK.js b/es/TencentYunSDK.js new file mode 100644 index 0000000..6e2271d --- /dev/null +++ b/es/TencentYunSDK.js @@ -0,0 +1,20 @@ +import { TencentYunInstance } from './service/tencent/Tencent'; +class TencentYunSDK { + tencentMap; //oss + constructor() { + this.tencentMap = {}; + } + getInstance(accessKey, accessSecret) { + if (this.tencentMap[accessKey]) { + return this.tencentMap[accessKey]; + } + const instance = new TencentYunInstance(accessKey, accessSecret); + Object.assign(this.tencentMap, { + [accessKey]: instance, + }); + return instance; + } +} +const SDK = new TencentYunSDK(); +export default SDK; +export { TencentYunInstance }; diff --git a/es/index.d.ts b/es/index.d.ts index 9db43d6..3f74fee 100644 --- a/es/index.d.ts +++ b/es/index.d.ts @@ -2,7 +2,9 @@ import WechatSDK, { WechatMpInstance, WechatPublicInstance, WechatWebInstance } import AmapSDK from './AmapSDK'; import QiniuSDK, { QiniuCloudInstance } from './QiniuSDK'; import SmsSdk, { TencentSmsInstance, AliSmsInstance, CTYunSmsInstance } from './SmsSdk'; -import CTYunSDk, { CTYunInstance } from './CTYunSDK'; +import CTYunSDK, { CTYunInstance } from './CTYunSDK'; +import ALiYunSDK, { ALiYunInstance } from './ALiYunSDK'; +import TencentYunSDK, { TencentYunInstance } from './TencentYunSDK'; export * from './service/amap/Amap'; -export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDk, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, QiniuCloudInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, }; +export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, }; export * from './types'; diff --git a/es/index.js b/es/index.js index c9cbae3..8a49dbe 100644 --- a/es/index.js +++ b/es/index.js @@ -2,7 +2,9 @@ import WechatSDK, { WechatMpInstance, WechatPublicInstance, WechatWebInstance } import AmapSDK from './AmapSDK'; import QiniuSDK, { QiniuCloudInstance } from './QiniuSDK'; import SmsSdk, { TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, } from './SmsSdk'; -import CTYunSDk, { CTYunInstance } from './CTYunSDK'; +import CTYunSDK, { CTYunInstance } from './CTYunSDK'; +import ALiYunSDK, { ALiYunInstance } from './ALiYunSDK'; +import TencentYunSDK, { TencentYunInstance } from './TencentYunSDK'; export * from './service/amap/Amap'; -export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDk, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, QiniuCloudInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, }; +export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, }; export * from './types'; diff --git a/es/service/ali/Ali.d.ts b/es/service/ali/Ali.d.ts index e69de29..f9d13c8 100644 --- a/es/service/ali/Ali.d.ts +++ b/es/service/ali/Ali.d.ts @@ -0,0 +1,18 @@ +import { ALiYunZone } from '../../types/ALiYun'; +import OSS from 'ali-oss'; +export declare class ALiYunInstance { + private accessKey; + private secretKey; + constructor(accessKey: string, secretKey: string); + getUploadInfo(bucket: string, zone: ALiYunZone, key?: string): { + key: string | undefined; + accessKey: string; + policy: string; + signature: string; + uploadHost: string; + bucket: string; + }; + private getSignInfo; + removeFile(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise; + isExistObject(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise; +} diff --git a/es/service/ali/Ali.js b/es/service/ali/Ali.js index 3918c74..bb4d3c0 100644 --- a/es/service/ali/Ali.js +++ b/es/service/ali/Ali.js @@ -1 +1,183 @@ -"use strict"; +import OSS from 'ali-oss'; +function getEndpoint(RegionID) { + return `oss-${RegionID}.aliyuncs.com`; +} +const ALiYun_ENDPOINT_LIST = { + 'cn-hangzhou': { + ul: getEndpoint('cn-hangzhou'), + }, + 'cn-shanghai': { + ul: getEndpoint('cn-shanghai'), + }, + 'cn-nanjing': { + ul: getEndpoint('cn-nanjing'), + }, + 'cn-fuzhou': { + ul: getEndpoint('cn-fuzhou'), + }, + 'cn-wuhan': { + ul: getEndpoint('cn-wuhan'), + }, + 'cn-qingdao': { + ul: getEndpoint('cn-qingdao'), + }, + 'cn-beijing': { + ul: getEndpoint('cn-beijing'), + }, + 'cn-zhangjiakou': { + ul: getEndpoint('cn-zhangjiakou'), + }, + 'cn-huhehaote': { + ul: getEndpoint('cn-huhehaote'), + }, + 'cn-wulanchabu': { + ul: getEndpoint('cn-wulanchabu'), + }, + 'cn-shenzhen': { + ul: getEndpoint('cn-shenzhen'), + }, + 'cn-heyuan': { + ul: getEndpoint('cn-heyuan'), + }, + 'cn-guangzhou': { + ul: getEndpoint('cn-guangzhou'), + }, + 'cn-chengdu': { + ul: getEndpoint('cn-chengdu'), + }, + 'cn-hongkong': { + ul: getEndpoint('cn-hongkong'), + }, + 'us-west-1': { + ul: getEndpoint('us-west-1'), + }, + 'us-east-1': { + ul: getEndpoint('us-east-1'), + }, + 'ap-northeast-1': { + ul: getEndpoint('ap-northeast-1'), + }, + 'ap-northeast-2': { + ul: getEndpoint('ap-northeast-2'), + }, + 'ap-southeast-1': { + ul: getEndpoint('ap-southeast-1'), + }, + 'ap-southeast-2': { + ul: getEndpoint('ap-southeast-2'), + }, + 'ap-southeast-3': { + ul: getEndpoint('ap-southeast-3'), + }, + 'ap-southeast-5': { + ul: getEndpoint('ap-southeast-5'), + }, + 'ap-southeast-6': { + ul: getEndpoint('ap-southeast-6'), + }, + 'ap-southeast-7': { + ul: getEndpoint('ap-southeast-7'), + }, + 'ap-south-1': { + ul: getEndpoint('ap-south-1'), + }, + 'eu-central-1': { + ul: getEndpoint('eu-central-1'), + }, + 'eu-west-1': { + ul: getEndpoint('eu-west-1'), + }, + 'me-east-1': { + ul: getEndpoint('me-east-1'), + }, + 'rg-china-mainland': { + ul: getEndpoint('rg-china-mainland'), + }, +}; +export class ALiYunInstance { + accessKey; + secretKey; + constructor(accessKey, secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + } + getUploadInfo(bucket, zone, key) { + try { + const signInfo = this.getSignInfo(bucket); + return { + key, + accessKey: this.accessKey, + policy: signInfo.policy, + signature: signInfo.signature, + uploadHost: `https://${bucket}.${ALiYun_ENDPOINT_LIST[zone].ul}`, + bucket, + }; + } + catch (err) { + throw err; + } + } + //https://help.aliyun.com/zh/oss/user-guide/form-upload?spm=a2c4g.11186623.0.0.22147f6b1UaGxF#6ee9b6b0be6on + getSignInfo(bucket) { + const client = new OSS({ + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + bucket: bucket, + }); + const now = new Date(); + now.setDate(now.getDate() + 1); + const policy = { + expiration: now.toISOString(), + conditions: [ + // 设置上传文件的大小限制。 + ['content-length-range', 0, 1048576000], + // 限制可上传的Bucket。 + { bucket: bucket }, + ], + }; + const data = client.calculatePostSignature(policy); + return { + policy: data.policy, + signature: data.Signature, + ossAccessKeyId: data.OSSAccessKeyId, + }; + } + async removeFile(srcBucket, zone, srcKey) { + const client = new OSS({ + // oss-cn-hangzhou填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 + region: `oss-${zone}`, + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + // 填写Bucket名称。 + bucket: srcBucket, + }); + try { + // 填写Object完整路径。Object完整路径中不能包含Bucket名称。 + const result = await client.delete(srcKey); + return result; + } + catch (error) { + throw error; + } + } + async isExistObject(srcBucket, zone, srcKey) { + const client = new OSS({ + // yourregion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 + region: `oss-${zone}`, + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + bucket: srcBucket, + }); + let result; + try { + result = await client.head(srcKey); + return true; + } + catch (error) { + if (error.code === 'NoSuchKey') { + return false; + } + throw error; + } + } +} diff --git a/es/service/ctyun/CTYun.d.ts b/es/service/ctyun/CTYun.d.ts index 2219b14..d2cdc87 100644 --- a/es/service/ctyun/CTYun.d.ts +++ b/es/service/ctyun/CTYun.d.ts @@ -1,6 +1,3 @@ -/// -/// -import { BinaryToTextEncoding } from 'crypto'; import { CTYunZone, ReqOptionProps } from '../../types/CTYun'; export declare class CTYunInstance { private accessKey; @@ -22,16 +19,10 @@ export declare class CTYunInstance { private hmacSha1; private urlSafeBase64Encode; removeFile(bucket: string, zone: CTYunZone, key: string): Promise; + isExistObject(srcBucket: string, zone: CTYunZone, srcKey: string): Promise; getAuthorization(reqOptions: ReqOptionProps, zone: CTYunZone): string; - signatureFn(reqOptions: ReqOptionProps, zone: CTYunZone): string | Buffer; - stringToSign(reqOptions: ReqOptionProps, zone: CTYunZone): string; - signedHeaders(headers: Record): string; - canonicalString(path: string, method: string, headers: Record): string; - canonicalHeaders(headers: Record): string; - canonicalHeaderValues(values: string): string; - getSigningKey(date: string, zone: CTYunZone): string | Buffer; - hmacSha256(key: string | Buffer, content: string | Buffer, digest?: BinaryToTextEncoding, fn?: string): string | Buffer; - each(object: any, iterFunction: (key: any, item: any) => any): void; - arrayEach(array: any, iterFunction: (item: any, key: any) => any): void; - isSignableHeader(key: string): boolean; + private getSignatureKey; + private buildCanonicalRequest; + private buildCanonicalQueryString; + private calculatePayloadHash; } diff --git a/es/service/ctyun/CTYun.js b/es/service/ctyun/CTYun.js index ea5ceff..3c94c7b 100644 --- a/es/service/ctyun/CTYun.js +++ b/es/service/ctyun/CTYun.js @@ -1,6 +1,6 @@ // import AWS from 'aws-sdk'; import crypto from 'crypto'; -import { OakNetworkException, } from 'oak-domain/lib/types/Exception'; +import { OakExternalException, OakNetworkException, } from 'oak-domain/lib/types/Exception'; const CTYun_ENDPOINT_LIST = { hazz: { ul: 'oos-hazz.ctyunapi.cn', @@ -44,8 +44,16 @@ const CTYun_ENDPOINT_LIST = { }; const serviceName = 's3'; const v4Identifier = 'aws4_request'; -const expiresHeader = "presigned-expires"; -const unsignableHeaders = ["authorization", "x-ctyun-data-location", "content-length", "user-agent", expiresHeader, "expect", "x-amzn-trace-id"]; +const expiresHeader = 'presigned-expires'; +const unsignableHeaders = [ + 'authorization', + 'x-ctyun-data-location', + 'content-length', + 'user-agent', + expiresHeader, + 'expect', + 'x-amzn-trace-id', +]; export class CTYunInstance { accessKey; secretKey; @@ -55,7 +63,6 @@ export class CTYunInstance { } getUploadInfo(bucket, zone, key) { try { - // const uploadToken = this.getToken(zone, bucket, actions); const signInfo = this.getSignInfo(bucket); return { key, @@ -73,30 +80,30 @@ export class CTYunInstance { getSignInfo(bucket) { // 对于policy里的expiration,我在天翼云的文档里没有找到具体的说明,但是这个字段不填入就会请求失败 // 设置一个明天过期的时间 - const now = new Date(); - now.setDate(now.getDate() + 1); - const tomorrow = now.toISOString(); + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 1); const policy = { - Version: "2012-10-17", - Statement: [{ - Effect: "Allow", - Action: ["oos:*"], - Resource: `arn:ctyun:oos:::${bucket} /*` - }], - expiration: tomorrow, - conditions: [{ + expiration: expiration.toISOString(), + conditions: [ + { bucket: bucket, - }, [ - "starts-with", - "$key", - "extraFile", - ]] + }, + ['starts-with', '$key', 'extraFile'], + ], + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['oos:*'], + Resource: `arn:ctyun:oos:::${bucket} /*`, + }, + ], }; const encodePolicy = this.urlSafeBase64Encode(JSON.stringify(policy)); const signature = this.hmacSha1(encodePolicy, this.secretKey); return { encodePolicy, - signature + signature, }; } base64ToUrlSafe(v) { @@ -111,146 +118,195 @@ export class CTYunInstance { const encoded = Buffer.from(jsonFlags).toString('base64'); return this.base64ToUrlSafe(encoded); } - // 当初就不应该封装天翼云,文档不全,找技术要签名代码还得自己去看源码,恶心 - // 下面的代码是根据天翼云生成签名源码改动得来 oos-js-sdk-6.2.js async removeFile(bucket, zone, key) { - const path = `/${bucket}/${key}`; - const host = `${CTYun_ENDPOINT_LIST[zone].ul}`; + const path = `/${key}`; + const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`; const url = `https://${host}${path}`; - const date = new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replace(/[:\-]|\.\d{3}/g, ""); + const payload = ''; + const method = 'DELETE'; + const service = 's3'; + const date = new Date() + .toISOString() + .replace(/\.\d{3}Z$/, 'Z') + .replace(/[:\-]|\.\d{3}/g, ''); const headers = { - "Content-Type": "application/octet-stream; charset=UTF-8", - "Host": "oos-hbwh.ctyunapi.cn", - "X-Amz-Content-Sha256": "UNSIGNED-PAYLOAD", - "X-Amz-User-Agent": "aws-sdk-js/2.296.0 callback" + 'Content-Type': 'application/xml; charset=utf-8', + Host: host, + 'x-amz-content-sha256': this.calculatePayloadHash(payload), + 'x-amz-date': date, }; - headers["X-Amz-Date"] = date; const reqOptions = { - path, - host, - date, headers, - method: "DELETE", + method: method, + payload: payload, + host, + path, + queryParameters: {}, + service, + date, }; const authorization = this.getAuthorization(reqOptions, zone); - headers['Authorization'] = authorization; + let response; try { - await fetch(url, { - method: 'DELETE', - headers, + response = await fetch(url, { + method, + headers: { + ...headers, + Authorization: authorization, + }, }); } catch (err) { throw new OakNetworkException(); } + if (response.status === 204) { + return; + } + const text = await response.text(); + throw new OakExternalException('ctyun', response.status.toString(), text, { + status: response.status, + }); + } + async isExistObject(srcBucket, zone, srcKey) { + const path = `/${srcKey}`; + const host = `${srcBucket}.${CTYun_ENDPOINT_LIST[zone].ul}`; + const url = `https://${host}${path}`; + const payload = ''; + const method = 'GET'; + const service = 's3'; + const date = new Date() + .toISOString() + .replace(/\.\d{3}Z$/, 'Z') + .replace(/[:\-]|\.\d{3}/g, ''); + const headers = { + Host: host, + 'x-amz-content-sha256': this.calculatePayloadHash(payload), + 'x-amz-date': date, + }; + const reqOptions = { + headers, + method: method, + payload: payload, + host, + path, + queryParameters: {}, + service, + date, + }; + const authorization = this.getAuthorization(reqOptions, zone); + let response; + try { + response = await fetch(url, { + method, + headers: { + ...headers, + Authorization: authorization, + }, + }); + } + catch (err) { + throw new OakNetworkException(); + } + if (response.status === 204) { + return true; + } + const text = await response.text(); + if (response.status === 404 && text.includes('NoSuchKey')) { + return false; + } + throw new OakExternalException('ctyun', response.status.toString(), text, { + status: response.status, + }); } getAuthorization(reqOptions, zone) { - const { headers, date } = reqOptions; - const parts = []; - const credString = [date.substring(0, 8), zone, serviceName, v4Identifier].join("/"); - parts.push("AWS4-HMAC-SHA256" + " Credential=" + this.accessKey + "/" + credString); - parts.push("SignedHeaders=" + this.signedHeaders(headers)); - parts.push("Signature=" + this.signatureFn(reqOptions, zone)); - return parts.join(", "); + const { headers, host, path, method, payload, queryParameters, service, date, } = reqOptions; + const payloadHash = this.calculatePayloadHash(payload); + // Step 2: Build canonical request + const { canonicalRequest, signedHeaders } = this.buildCanonicalRequest(method, path, queryParameters, headers, payloadHash); + // Step 3: Calculate canonical request hash + const canonicalRequestHash = crypto + .createHash('sha256') + .update(canonicalRequest) + .digest('hex'); + // Step 4: Build string to sign + const dateStamp = date.substr(0, 8); + const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`; + const stringToSign = [ + 'AWS4-HMAC-SHA256', + date, + credentialScope, + canonicalRequestHash, + ].join('\n'); + // Step 5: Get signing key + const signingKey = this.getSignatureKey(dateStamp, zone, service); + // Step 6: Calculate signature + const signature = crypto + .createHmac('sha256', signingKey) + .update(stringToSign) + .digest('hex'); + const authorizationHeader = [ + 'AWS4-HMAC-SHA256 Credential=' + + this.accessKey + + '/' + + credentialScope, + 'SignedHeaders=' + signedHeaders, + 'Signature=' + signature, + ].join(', '); + return authorizationHeader; } - signatureFn(reqOptions, zone) { - const { date } = reqOptions; - var signingKey = this.getSigningKey(date.substring(0, 8), zone); - return this.hmacSha256(signingKey, this.stringToSign(reqOptions, zone), "hex"); + getSignatureKey(dateStamp, zone, service) { + const kDate = crypto + .createHmac('sha256', `AWS4${this.secretKey}`) + .update(dateStamp) + .digest(); + const kRegion = crypto + .createHmac('sha256', kDate) + .update(zone) + .digest(); + const kService = crypto + .createHmac('sha256', kRegion) + .update(service) + .digest(); + const kSigning = crypto + .createHmac('sha256', kService) + .update('aws4_request') + .digest(); + return kSigning; } - stringToSign(reqOptions, zone) { - const { date, path, method, headers } = reqOptions; - var parts = []; - parts.push("AWS4-HMAC-SHA256"); - parts.push(date); - parts.push([date.substring(0, 8), zone, serviceName, v4Identifier].join("/")); - const canonicalStr = this.canonicalString(path, method, headers); - const buffer = Buffer.from(canonicalStr); - const encodeStr = crypto.createHash('sha256').update(buffer).digest('hex'); - parts.push(encodeStr); - return parts.join("\n"); + buildCanonicalRequest(method, path, queryParameters, headers, payloadHash) { + const canonicalQuerystring = this.buildCanonicalQueryString(queryParameters); + const canonicalHeaders = Object.keys(headers) + .sort() + .map((key) => `${key.toLowerCase()}:${headers[key].trim()}`) + .join('\n'); + const signedHeaders = Object.keys(headers) + .sort() + .map((key) => key.toLowerCase()) + .join(';'); + const canonicalRequest = [ + method, + path, + canonicalQuerystring, + canonicalHeaders, + '', + signedHeaders, + payloadHash, + ].join('\n'); + return { + canonicalRequest, + signedHeaders, + }; } - signedHeaders(headers) { - const keys = []; - this.each(headers, (key) => { - key = key.toLowerCase(); - if (this.isSignableHeader(key)) - keys.push(key); - }); - return keys.sort().join(";"); + buildCanonicalQueryString(queryParameters) { + const keys = Object.keys(queryParameters).sort(); + const canonicalQueryString = keys + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParameters[key])}`) + .join('&'); + return canonicalQueryString; } - canonicalString(path, method, headers) { - const parts = []; - parts.push(method); - parts.push(path); - parts.push(""); - parts.push(this.canonicalHeaders(headers) + "\n"); - parts.push(this.signedHeaders(headers)); - parts.push("UNSIGNED-PAYLOAD"); - return parts.join("\n"); - } - canonicalHeaders(headers) { - const headerarr = []; - this.each(headers, function (key, item) { - headerarr.push([key, item]); - }); - headerarr.sort(function (a, b) { - return a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1; - }); - const parts = []; - this.arrayEach(headerarr, (item) => { - let key = item[0].toLowerCase(); - if (this.isSignableHeader(key)) { - var value = item[1]; - if (typeof value === "undefined" || value === null || typeof value.toString !== "function") { - throw new Error("Header " + key + " contains invalid value"); - } - parts.push(key + ":" + this.canonicalHeaderValues(value.toString())); - } - }); - return parts.join("\n"); - } - canonicalHeaderValues(values) { - return values.replace(/\s+/g, " ").replace(/^\s+|\s+$/g, ""); - } - getSigningKey(date, zone) { - const kDate = this.hmacSha256("AWS4" + this.secretKey, date); - const kRegion = this.hmacSha256(kDate, zone); - const kService = this.hmacSha256(kRegion, serviceName); - var signingKey = this.hmacSha256(kService, v4Identifier); - return signingKey; - } - hmacSha256(key, content, digest, fn) { - if (!fn) - fn = "sha256"; - if (typeof content === "string") - content = Buffer.from(content); - if (!digest) { - return crypto.createHmac(fn, key).update(content).digest(); - } - return crypto.createHmac(fn, key).update(content).digest(digest); - } - each(object, iterFunction) { - for (let key in object) { - if (Object.prototype.hasOwnProperty.call(object, key)) { - const ret = iterFunction.call(this, key, object[key]); - if (Object.keys(ret).length === 0) - break; - } - } - } - arrayEach(array, iterFunction) { - for (let idx in array) { - if (Object.prototype.hasOwnProperty.call(array, idx)) { - const ret = iterFunction.call(this, array[idx], parseInt(idx, 10)); - if (Object.keys(ret).length === 0) - break; - } - } - } - isSignableHeader(key) { - if (key.toLowerCase().indexOf("x-amz-") === 0) - return true; - return unsignableHeaders.indexOf(key) < 0; + calculatePayloadHash(payload) { + const hash = crypto.createHash('sha256'); + hash.update(payload); + return hash.digest('hex'); } } diff --git a/es/service/tencent/Tencent.d.ts b/es/service/tencent/Tencent.d.ts new file mode 100644 index 0000000..523ef74 --- /dev/null +++ b/es/service/tencent/Tencent.d.ts @@ -0,0 +1,20 @@ +import { TencentYunZone } from '../../types/TencentYun'; +export declare class TencentYunInstance { + private accessKey; + private secretKey; + private COS; + constructor(accessKey: string, secretKey: string); + getUploadInfo(bucket: string, zone: TencentYunZone, key?: string): { + key: string | undefined; + accessKey: string; + policy: string; + signature: string; + uploadHost: string; + bucket: string; + keyTime: string; + algorithm: string; + }; + private getSignInfo; + removeFile(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise; + isExistObject(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise; +} diff --git a/es/service/tencent/Tencent.js b/es/service/tencent/Tencent.js new file mode 100644 index 0000000..d5a794e --- /dev/null +++ b/es/service/tencent/Tencent.js @@ -0,0 +1,144 @@ +import crypto from 'crypto'; +function getEndpoint(RegionID) { + return `cos-${RegionID}.myqcloud.com`; +} +const Tencent_ENDPOINT_LIST = { + 'ap-beijing': { + ul: getEndpoint('ap-nanjing'), + }, + 'ap-nanjing': { + ul: getEndpoint('ap-nanjing'), + }, + 'ap-shanghai': { + ul: getEndpoint('ap-shanghai'), + }, + 'ap-guangzhou': { + ul: getEndpoint('ap-guangzhou'), + }, + 'ap-chengdu': { + ul: getEndpoint('ap-chengdu'), + }, + 'ap-chongqing': { + ul: getEndpoint('ap-chongqing'), + }, + 'ap-shenzhen-fsi': { + ul: getEndpoint('ap-shenzhen-fsi'), + }, + 'ap-shanghai-fsi': { + ul: getEndpoint('ap-shanghai-fsi'), + }, + 'ap-beijing-fsi': { + ul: getEndpoint('ap-beijing-fsi'), + }, + 'ap-hongkong': { + ul: getEndpoint('ap-hongkong'), + }, + 'ap-singapore': { + ul: getEndpoint('ap-singapore'), + }, +}; +// TODO 腾讯云代码未验证 +export class TencentYunInstance { + accessKey; + secretKey; + COS; + constructor(accessKey, secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + this.COS = require('cos-wx-sdk-v5'); + } + getUploadInfo(bucket, zone, key) { + try { + const signInfo = this.getSignInfo(bucket); + return { + key, + accessKey: this.accessKey, + policy: signInfo.policy, + signature: signInfo.signature, + uploadHost: `https://${bucket}.${Tencent_ENDPOINT_LIST[zone].ul}`, + bucket, + keyTime: signInfo.keyTime, + algorithm: signInfo.algorithm, + }; + } + catch (err) { + throw err; + } + } + //https://cloud.tencent.com/document/product/436/7778 + getSignInfo(bucket) { + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 1); + const keyTime = `${Math.floor(Date.now() / 1000)};${Math.floor(expiration.getTime() / 1000)}`; + const algorithm = 'sha1'; + const policy = { + expiration: expiration.toISOString(), + conditions: [ + // 限制可上传的Bucket。 + { bucket: bucket }, + ['eq', '$x-cos-server-side-encryption', 'AES256'], + { 'q-sign-algorithm': algorithm }, + { 'q-ak': this.accessKey }, + { 'q-sign-time': keyTime }, + ], + }; + const encodedPolicy = Buffer.from(JSON.stringify(policy)).toString('base64'); + const signKey = crypto + .createHmac('sha1', this.secretKey) + .update(keyTime) + .digest(); + const stringToSign = crypto + .createHash('sha1') + .update(encodedPolicy) + .digest('hex'); + const signature = crypto + .createHmac('sha1', signKey) + .update(stringToSign) + .digest('hex'); + return { + policy: encodedPolicy, + signature, + keyTime, + algorithm, + }; + } + async removeFile(srcBucket, zone, srcKey) { + const client = new this.COS({ + SecretId: this.accessKey, + SecretKey: this.secretKey, + }); + try { + // 填写Object完整路径。Object完整路径中不能包含Bucket名称。 + const result = await client.deleteObject({ + Bucket: srcBucket, + Region: zone, + Key: srcKey, + }); + return result; + } + catch (error) { + throw error; + } + } + async isExistObject(srcBucket, zone, srcKey) { + const client = new this.COS({ + SecretId: this.accessKey, + SecretKey: this.secretKey, + }); + let result; + try { + result = await client.headObject({ + Bucket: srcBucket, + Region: zone, + Key: srcKey, + }); + return true; + } + catch (error) { + if (error.code === 'NoSuchKey') { + return false; + } + throw error; + } + } +} diff --git a/es/service/wechat/WechatMp.js b/es/service/wechat/WechatMp.js index 481f1e7..007a1ac 100644 --- a/es/service/wechat/WechatMp.js +++ b/es/service/wechat/WechatMp.js @@ -187,7 +187,7 @@ export class WechatMpInstance { const { type, media, filetype, filename } = options; const formData = new FormData(); formData.append('media', media, { - contentType: filetype, + contentType: filetype, // 微信识别需要 filename: filename, // 微信识别需要 }); const getLength = () => { @@ -332,7 +332,7 @@ export class WechatMpInstance { body: JSON.stringify({ jump_wxa: jump_wxa, is_expire: true, - expire_type: expireType, + expire_type: expireType, //默认是零,到期失效的 scheme 码失效类型,失效时间类型:0,失效间隔天数类型:1 expire_time: expiresAt, expire_interval: expireInterval, }), diff --git a/es/service/wechat/WechatPublic.js b/es/service/wechat/WechatPublic.js index 2b173e9..f79f3fe 100644 --- a/es/service/wechat/WechatPublic.js +++ b/es/service/wechat/WechatPublic.js @@ -511,8 +511,8 @@ export class WechatPublicInstance { const { type, media, description, filetype, filename, fileLength } = options; const formData = new FormData(); formData.append('media', media, { - contentType: filetype, - filename: filename, + contentType: filetype, // 微信识别需要 + filename: filename, // 微信识别需要 knownLength: fileLength, }); if (type === 'video') { @@ -535,8 +535,8 @@ export class WechatPublicInstance { const { media, filetype, filename, fileLength } = options; const formData = new FormData(); formData.append('media', media, { - contentType: filetype, - filename: filename, + contentType: filetype, // 微信识别需要 + filename: filename, // 微信识别需要 knownLength: fileLength, }); const headers = formData.getHeaders(); @@ -556,8 +556,8 @@ export class WechatPublicInstance { const { type, media, filetype, filename, fileLength } = options; const formData = new FormData(); formData.append('media', media, { - contentType: filetype, - filename: filename, + contentType: filetype, // 微信识别需要 + filename: filename, // 微信识别需要 knownLength: fileLength, }); const headers = formData.getHeaders(); diff --git a/es/types/ALiYun.d.ts b/es/types/ALiYun.d.ts new file mode 100644 index 0000000..6e11dc4 --- /dev/null +++ b/es/types/ALiYun.d.ts @@ -0,0 +1 @@ +export type ALiYunZone = 'cn-hangzhou' | 'cn-shanghai' | 'cn-nanjing' | 'cn-fuzhou' | 'cn-wuhan' | 'cn-qingdao' | 'cn-beijing' | 'cn-zhangjiakou' | 'cn-huhehaote' | 'cn-wulanchabu' | 'cn-shenzhen' | 'cn-heyuan' | 'cn-guangzhou' | 'cn-chengdu' | 'cn-hongkong' | 'us-west-1' | 'us-east-1' | 'ap-northeast-1' | 'ap-northeast-2' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-southeast-3' | 'ap-southeast-5' | 'ap-southeast-6' | 'ap-southeast-7' | 'ap-south-1' | 'eu-central-1' | 'eu-west-1' | 'me-east-1' | 'rg-china-mainland'; diff --git a/es/types/ALiYun.js b/es/types/ALiYun.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/es/types/ALiYun.js @@ -0,0 +1 @@ +export {}; diff --git a/es/types/CTYun.d.ts b/es/types/CTYun.d.ts index e46e11a..2bd3ebd 100644 --- a/es/types/CTYun.d.ts +++ b/es/types/CTYun.d.ts @@ -4,10 +4,13 @@ */ export type Action = '*' | 'PutObject' | 'GetObject' | 'DeleteObject' | 'ListBucket'; export type ReqOptionProps = { + payload: string; path: string; host: string; - date: string; headers: any; - method: 'DELETE' | "GET" | "POST" | "PUT"; + method: 'DELETE' | 'GET' | 'POST' | 'PUT' | 'HEAD'; + queryParameters: Record; + service: string; + date: string; }; export type CTYunZone = 'hazz' | 'lnsy' | 'sccd' | 'xjwlmq' | 'gslz' | 'sdqd' | 'gzgy' | 'hbwh' | 'xzls' | 'ahwh' | 'gdsz' | 'jssz' | 'sh2'; diff --git a/es/types/TencentYun.d.ts b/es/types/TencentYun.d.ts new file mode 100644 index 0000000..8da68bc --- /dev/null +++ b/es/types/TencentYun.d.ts @@ -0,0 +1 @@ +export type TencentYunZone = 'ap-beijing' | 'ap-nanjing' | 'ap-shanghai' | 'ap-guangzhou' | 'ap-chengdu' | 'ap-chongqing' | 'ap-shenzhen-fsi' | 'ap-shanghai-fsi' | 'ap-beijing-fsi' | 'ap-hongkong' | 'ap-singapore'; diff --git a/es/types/TencentYun.js b/es/types/TencentYun.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/es/types/TencentYun.js @@ -0,0 +1 @@ +export {}; diff --git a/es/types/index.d.ts b/es/types/index.d.ts index 0763376..182e689 100644 --- a/es/types/index.d.ts +++ b/es/types/index.d.ts @@ -1,3 +1,5 @@ export * from './Wechat'; export * from './Qiniu'; export * from './CTYun'; +export * from './ALiYun'; +export * from './TencentYun'; diff --git a/es/types/index.js b/es/types/index.js index 0763376..182e689 100644 --- a/es/types/index.js +++ b/es/types/index.js @@ -1,3 +1,5 @@ export * from './Wechat'; export * from './Qiniu'; export * from './CTYun'; +export * from './ALiYun'; +export * from './TencentYun'; diff --git a/lib/ALiYunSDK.d.ts b/lib/ALiYunSDK.d.ts new file mode 100644 index 0000000..35a3045 --- /dev/null +++ b/lib/ALiYunSDK.d.ts @@ -0,0 +1,9 @@ +import { ALiYunInstance } from './service/ali/Ali'; +declare class ALiYunSDK { + aliMap: Record; + constructor(); + getInstance(accessKey: string, accessSecret: string): ALiYunInstance; +} +declare const SDK: ALiYunSDK; +export default SDK; +export { ALiYunInstance }; diff --git a/lib/ALiYunSDK.js b/lib/ALiYunSDK.js new file mode 100644 index 0000000..fa2fb8d --- /dev/null +++ b/lib/ALiYunSDK.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ALiYunInstance = void 0; +const Ali_1 = require("./service/ali/Ali"); +Object.defineProperty(exports, "ALiYunInstance", { enumerable: true, get: function () { return Ali_1.ALiYunInstance; } }); +class ALiYunSDK { + aliMap; //oss + constructor() { + this.aliMap = {}; + } + getInstance(accessKey, accessSecret) { + if (this.aliMap[accessKey]) { + return this.aliMap[accessKey]; + } + const instance = new Ali_1.ALiYunInstance(accessKey, accessSecret); + Object.assign(this.aliMap, { + [accessKey]: instance, + }); + return instance; + } +} +const SDK = new ALiYunSDK(); +exports.default = SDK; diff --git a/lib/CTYunSDK.js b/lib/CTYunSDK.js index b75a368..3e953a7 100644 --- a/lib/CTYunSDK.js +++ b/lib/CTYunSDK.js @@ -4,7 +4,7 @@ exports.CTYunInstance = void 0; const CTYun_1 = require("./service/ctyun/CTYun"); Object.defineProperty(exports, "CTYunInstance", { enumerable: true, get: function () { return CTYun_1.CTYunInstance; } }); class CTYunSDK { - ctyunMap; + ctyunMap; //oss constructor() { this.ctyunMap = {}; } diff --git a/lib/TencentYunSDK.d.ts b/lib/TencentYunSDK.d.ts new file mode 100644 index 0000000..205fad2 --- /dev/null +++ b/lib/TencentYunSDK.d.ts @@ -0,0 +1,9 @@ +import { TencentYunInstance } from './service/tencent/Tencent'; +declare class TencentYunSDK { + tencentMap: Record; + constructor(); + getInstance(accessKey: string, accessSecret: string): TencentYunInstance; +} +declare const SDK: TencentYunSDK; +export default SDK; +export { TencentYunInstance }; diff --git a/lib/TencentYunSDK.js b/lib/TencentYunSDK.js new file mode 100644 index 0000000..00b5dc9 --- /dev/null +++ b/lib/TencentYunSDK.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TencentYunInstance = void 0; +const Tencent_1 = require("./service/tencent/Tencent"); +Object.defineProperty(exports, "TencentYunInstance", { enumerable: true, get: function () { return Tencent_1.TencentYunInstance; } }); +class TencentYunSDK { + tencentMap; //oss + constructor() { + this.tencentMap = {}; + } + getInstance(accessKey, accessSecret) { + if (this.tencentMap[accessKey]) { + return this.tencentMap[accessKey]; + } + const instance = new Tencent_1.TencentYunInstance(accessKey, accessSecret); + Object.assign(this.tencentMap, { + [accessKey]: instance, + }); + return instance; + } +} +const SDK = new TencentYunSDK(); +exports.default = SDK; diff --git a/lib/index.d.ts b/lib/index.d.ts index 9db43d6..3f74fee 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -2,7 +2,9 @@ import WechatSDK, { WechatMpInstance, WechatPublicInstance, WechatWebInstance } import AmapSDK from './AmapSDK'; import QiniuSDK, { QiniuCloudInstance } from './QiniuSDK'; import SmsSdk, { TencentSmsInstance, AliSmsInstance, CTYunSmsInstance } from './SmsSdk'; -import CTYunSDk, { CTYunInstance } from './CTYunSDK'; +import CTYunSDK, { CTYunInstance } from './CTYunSDK'; +import ALiYunSDK, { ALiYunInstance } from './ALiYunSDK'; +import TencentYunSDK, { TencentYunInstance } from './TencentYunSDK'; export * from './service/amap/Amap'; -export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDk, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, QiniuCloudInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, }; +export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, }; export * from './types'; diff --git a/lib/index.js b/lib/index.js index 39e356f..ef322e4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.CTYunSmsInstance = exports.AliSmsInstance = exports.TencentSmsInstance = exports.SmsSdk = exports.QiniuCloudInstance = exports.WechatWebInstance = exports.WechatPublicInstance = exports.WechatMpInstance = exports.CTYunInstance = exports.CTYunSDk = exports.WechatSDK = exports.QiniuSDK = exports.AmapSDK = void 0; +exports.CTYunSmsInstance = exports.AliSmsInstance = exports.TencentSmsInstance = exports.SmsSdk = exports.TencentYunInstance = exports.ALiYunInstance = exports.QiniuCloudInstance = exports.WechatWebInstance = exports.WechatPublicInstance = exports.WechatMpInstance = exports.CTYunInstance = exports.TencentYunSDK = exports.ALiYunSDK = exports.CTYunSDK = exports.WechatSDK = exports.QiniuSDK = exports.AmapSDK = void 0; const tslib_1 = require("tslib"); const WechatSDK_1 = tslib_1.__importStar(require("./WechatSDK")); exports.WechatSDK = WechatSDK_1.default; @@ -18,7 +18,13 @@ Object.defineProperty(exports, "TencentSmsInstance", { enumerable: true, get: fu Object.defineProperty(exports, "AliSmsInstance", { enumerable: true, get: function () { return SmsSdk_1.AliSmsInstance; } }); Object.defineProperty(exports, "CTYunSmsInstance", { enumerable: true, get: function () { return SmsSdk_1.CTYunSmsInstance; } }); const CTYunSDK_1 = tslib_1.__importStar(require("./CTYunSDK")); -exports.CTYunSDk = CTYunSDK_1.default; +exports.CTYunSDK = CTYunSDK_1.default; Object.defineProperty(exports, "CTYunInstance", { enumerable: true, get: function () { return CTYunSDK_1.CTYunInstance; } }); +const ALiYunSDK_1 = tslib_1.__importStar(require("./ALiYunSDK")); +exports.ALiYunSDK = ALiYunSDK_1.default; +Object.defineProperty(exports, "ALiYunInstance", { enumerable: true, get: function () { return ALiYunSDK_1.ALiYunInstance; } }); +const TencentYunSDK_1 = tslib_1.__importStar(require("./TencentYunSDK")); +exports.TencentYunSDK = TencentYunSDK_1.default; +Object.defineProperty(exports, "TencentYunInstance", { enumerable: true, get: function () { return TencentYunSDK_1.TencentYunInstance; } }); tslib_1.__exportStar(require("./service/amap/Amap"), exports); tslib_1.__exportStar(require("./types"), exports); diff --git a/lib/service/ali/Ali.d.ts b/lib/service/ali/Ali.d.ts index e69de29..f9d13c8 100644 --- a/lib/service/ali/Ali.d.ts +++ b/lib/service/ali/Ali.d.ts @@ -0,0 +1,18 @@ +import { ALiYunZone } from '../../types/ALiYun'; +import OSS from 'ali-oss'; +export declare class ALiYunInstance { + private accessKey; + private secretKey; + constructor(accessKey: string, secretKey: string); + getUploadInfo(bucket: string, zone: ALiYunZone, key?: string): { + key: string | undefined; + accessKey: string; + policy: string; + signature: string; + uploadHost: string; + bucket: string; + }; + private getSignInfo; + removeFile(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise; + isExistObject(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise; +} diff --git a/lib/service/ali/Ali.js b/lib/service/ali/Ali.js index 3918c74..82a3b91 100644 --- a/lib/service/ali/Ali.js +++ b/lib/service/ali/Ali.js @@ -1 +1,188 @@ "use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ALiYunInstance = void 0; +const tslib_1 = require("tslib"); +const ali_oss_1 = tslib_1.__importDefault(require("ali-oss")); +function getEndpoint(RegionID) { + return `oss-${RegionID}.aliyuncs.com`; +} +const ALiYun_ENDPOINT_LIST = { + 'cn-hangzhou': { + ul: getEndpoint('cn-hangzhou'), + }, + 'cn-shanghai': { + ul: getEndpoint('cn-shanghai'), + }, + 'cn-nanjing': { + ul: getEndpoint('cn-nanjing'), + }, + 'cn-fuzhou': { + ul: getEndpoint('cn-fuzhou'), + }, + 'cn-wuhan': { + ul: getEndpoint('cn-wuhan'), + }, + 'cn-qingdao': { + ul: getEndpoint('cn-qingdao'), + }, + 'cn-beijing': { + ul: getEndpoint('cn-beijing'), + }, + 'cn-zhangjiakou': { + ul: getEndpoint('cn-zhangjiakou'), + }, + 'cn-huhehaote': { + ul: getEndpoint('cn-huhehaote'), + }, + 'cn-wulanchabu': { + ul: getEndpoint('cn-wulanchabu'), + }, + 'cn-shenzhen': { + ul: getEndpoint('cn-shenzhen'), + }, + 'cn-heyuan': { + ul: getEndpoint('cn-heyuan'), + }, + 'cn-guangzhou': { + ul: getEndpoint('cn-guangzhou'), + }, + 'cn-chengdu': { + ul: getEndpoint('cn-chengdu'), + }, + 'cn-hongkong': { + ul: getEndpoint('cn-hongkong'), + }, + 'us-west-1': { + ul: getEndpoint('us-west-1'), + }, + 'us-east-1': { + ul: getEndpoint('us-east-1'), + }, + 'ap-northeast-1': { + ul: getEndpoint('ap-northeast-1'), + }, + 'ap-northeast-2': { + ul: getEndpoint('ap-northeast-2'), + }, + 'ap-southeast-1': { + ul: getEndpoint('ap-southeast-1'), + }, + 'ap-southeast-2': { + ul: getEndpoint('ap-southeast-2'), + }, + 'ap-southeast-3': { + ul: getEndpoint('ap-southeast-3'), + }, + 'ap-southeast-5': { + ul: getEndpoint('ap-southeast-5'), + }, + 'ap-southeast-6': { + ul: getEndpoint('ap-southeast-6'), + }, + 'ap-southeast-7': { + ul: getEndpoint('ap-southeast-7'), + }, + 'ap-south-1': { + ul: getEndpoint('ap-south-1'), + }, + 'eu-central-1': { + ul: getEndpoint('eu-central-1'), + }, + 'eu-west-1': { + ul: getEndpoint('eu-west-1'), + }, + 'me-east-1': { + ul: getEndpoint('me-east-1'), + }, + 'rg-china-mainland': { + ul: getEndpoint('rg-china-mainland'), + }, +}; +class ALiYunInstance { + accessKey; + secretKey; + constructor(accessKey, secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + } + getUploadInfo(bucket, zone, key) { + try { + const signInfo = this.getSignInfo(bucket); + return { + key, + accessKey: this.accessKey, + policy: signInfo.policy, + signature: signInfo.signature, + uploadHost: `https://${bucket}.${ALiYun_ENDPOINT_LIST[zone].ul}`, + bucket, + }; + } + catch (err) { + throw err; + } + } + //https://help.aliyun.com/zh/oss/user-guide/form-upload?spm=a2c4g.11186623.0.0.22147f6b1UaGxF#6ee9b6b0be6on + getSignInfo(bucket) { + const client = new ali_oss_1.default({ + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + bucket: bucket, + }); + const now = new Date(); + now.setDate(now.getDate() + 1); + const policy = { + expiration: now.toISOString(), + conditions: [ + // 设置上传文件的大小限制。 + ['content-length-range', 0, 1048576000], + // 限制可上传的Bucket。 + { bucket: bucket }, + ], + }; + const data = client.calculatePostSignature(policy); + return { + policy: data.policy, + signature: data.Signature, + ossAccessKeyId: data.OSSAccessKeyId, + }; + } + async removeFile(srcBucket, zone, srcKey) { + const client = new ali_oss_1.default({ + // oss-cn-hangzhou填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 + region: `oss-${zone}`, + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + // 填写Bucket名称。 + bucket: srcBucket, + }); + try { + // 填写Object完整路径。Object完整路径中不能包含Bucket名称。 + const result = await client.delete(srcKey); + return result; + } + catch (error) { + throw error; + } + } + async isExistObject(srcBucket, zone, srcKey) { + const client = new ali_oss_1.default({ + // yourregion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 + region: `oss-${zone}`, + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + bucket: srcBucket, + }); + let result; + try { + result = await client.head(srcKey); + return true; + } + catch (error) { + if (error.code === 'NoSuchKey') { + return false; + } + throw error; + } + } +} +exports.ALiYunInstance = ALiYunInstance; diff --git a/lib/service/ctyun/CTYun.d.ts b/lib/service/ctyun/CTYun.d.ts index 2219b14..d2cdc87 100644 --- a/lib/service/ctyun/CTYun.d.ts +++ b/lib/service/ctyun/CTYun.d.ts @@ -1,6 +1,3 @@ -/// -/// -import { BinaryToTextEncoding } from 'crypto'; import { CTYunZone, ReqOptionProps } from '../../types/CTYun'; export declare class CTYunInstance { private accessKey; @@ -22,16 +19,10 @@ export declare class CTYunInstance { private hmacSha1; private urlSafeBase64Encode; removeFile(bucket: string, zone: CTYunZone, key: string): Promise; + isExistObject(srcBucket: string, zone: CTYunZone, srcKey: string): Promise; getAuthorization(reqOptions: ReqOptionProps, zone: CTYunZone): string; - signatureFn(reqOptions: ReqOptionProps, zone: CTYunZone): string | Buffer; - stringToSign(reqOptions: ReqOptionProps, zone: CTYunZone): string; - signedHeaders(headers: Record): string; - canonicalString(path: string, method: string, headers: Record): string; - canonicalHeaders(headers: Record): string; - canonicalHeaderValues(values: string): string; - getSigningKey(date: string, zone: CTYunZone): string | Buffer; - hmacSha256(key: string | Buffer, content: string | Buffer, digest?: BinaryToTextEncoding, fn?: string): string | Buffer; - each(object: any, iterFunction: (key: any, item: any) => any): void; - arrayEach(array: any, iterFunction: (item: any, key: any) => any): void; - isSignableHeader(key: string): boolean; + private getSignatureKey; + private buildCanonicalRequest; + private buildCanonicalQueryString; + private calculatePayloadHash; } diff --git a/lib/service/ctyun/CTYun.js b/lib/service/ctyun/CTYun.js index 0b09c16..08c85cf 100644 --- a/lib/service/ctyun/CTYun.js +++ b/lib/service/ctyun/CTYun.js @@ -48,8 +48,16 @@ const CTYun_ENDPOINT_LIST = { }; const serviceName = 's3'; const v4Identifier = 'aws4_request'; -const expiresHeader = "presigned-expires"; -const unsignableHeaders = ["authorization", "x-ctyun-data-location", "content-length", "user-agent", expiresHeader, "expect", "x-amzn-trace-id"]; +const expiresHeader = 'presigned-expires'; +const unsignableHeaders = [ + 'authorization', + 'x-ctyun-data-location', + 'content-length', + 'user-agent', + expiresHeader, + 'expect', + 'x-amzn-trace-id', +]; class CTYunInstance { accessKey; secretKey; @@ -59,7 +67,6 @@ class CTYunInstance { } getUploadInfo(bucket, zone, key) { try { - // const uploadToken = this.getToken(zone, bucket, actions); const signInfo = this.getSignInfo(bucket); return { key, @@ -77,30 +84,30 @@ class CTYunInstance { getSignInfo(bucket) { // 对于policy里的expiration,我在天翼云的文档里没有找到具体的说明,但是这个字段不填入就会请求失败 // 设置一个明天过期的时间 - const now = new Date(); - now.setDate(now.getDate() + 1); - const tomorrow = now.toISOString(); + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 1); const policy = { - Version: "2012-10-17", - Statement: [{ - Effect: "Allow", - Action: ["oos:*"], - Resource: `arn:ctyun:oos:::${bucket} /*` - }], - expiration: tomorrow, - conditions: [{ + expiration: expiration.toISOString(), + conditions: [ + { bucket: bucket, - }, [ - "starts-with", - "$key", - "extraFile", - ]] + }, + ['starts-with', '$key', 'extraFile'], + ], + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['oos:*'], + Resource: `arn:ctyun:oos:::${bucket} /*`, + }, + ], }; const encodePolicy = this.urlSafeBase64Encode(JSON.stringify(policy)); const signature = this.hmacSha1(encodePolicy, this.secretKey); return { encodePolicy, - signature + signature, }; } base64ToUrlSafe(v) { @@ -115,147 +122,196 @@ class CTYunInstance { const encoded = Buffer.from(jsonFlags).toString('base64'); return this.base64ToUrlSafe(encoded); } - // 当初就不应该封装天翼云,文档不全,找技术要签名代码还得自己去看源码,恶心 - // 下面的代码是根据天翼云生成签名源码改动得来 oos-js-sdk-6.2.js async removeFile(bucket, zone, key) { - const path = `/${bucket}/${key}`; - const host = `${CTYun_ENDPOINT_LIST[zone].ul}`; + const path = `/${key}`; + const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`; const url = `https://${host}${path}`; - const date = new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replace(/[:\-]|\.\d{3}/g, ""); + const payload = ''; + const method = 'DELETE'; + const service = 's3'; + const date = new Date() + .toISOString() + .replace(/\.\d{3}Z$/, 'Z') + .replace(/[:\-]|\.\d{3}/g, ''); const headers = { - "Content-Type": "application/octet-stream; charset=UTF-8", - "Host": "oos-hbwh.ctyunapi.cn", - "X-Amz-Content-Sha256": "UNSIGNED-PAYLOAD", - "X-Amz-User-Agent": "aws-sdk-js/2.296.0 callback" + 'Content-Type': 'application/xml; charset=utf-8', + Host: host, + 'x-amz-content-sha256': this.calculatePayloadHash(payload), + 'x-amz-date': date, }; - headers["X-Amz-Date"] = date; const reqOptions = { - path, - host, - date, headers, - method: "DELETE", + method: method, + payload: payload, + host, + path, + queryParameters: {}, + service, + date, }; const authorization = this.getAuthorization(reqOptions, zone); - headers['Authorization'] = authorization; + let response; try { - await fetch(url, { - method: 'DELETE', - headers, + response = await fetch(url, { + method, + headers: { + ...headers, + Authorization: authorization, + }, }); } catch (err) { throw new Exception_1.OakNetworkException(); } + if (response.status === 204) { + return; + } + const text = await response.text(); + throw new Exception_1.OakExternalException('ctyun', response.status.toString(), text, { + status: response.status, + }); + } + async isExistObject(srcBucket, zone, srcKey) { + const path = `/${srcKey}`; + const host = `${srcBucket}.${CTYun_ENDPOINT_LIST[zone].ul}`; + const url = `https://${host}${path}`; + const payload = ''; + const method = 'GET'; + const service = 's3'; + const date = new Date() + .toISOString() + .replace(/\.\d{3}Z$/, 'Z') + .replace(/[:\-]|\.\d{3}/g, ''); + const headers = { + Host: host, + 'x-amz-content-sha256': this.calculatePayloadHash(payload), + 'x-amz-date': date, + }; + const reqOptions = { + headers, + method: method, + payload: payload, + host, + path, + queryParameters: {}, + service, + date, + }; + const authorization = this.getAuthorization(reqOptions, zone); + let response; + try { + response = await fetch(url, { + method, + headers: { + ...headers, + Authorization: authorization, + }, + }); + } + catch (err) { + throw new Exception_1.OakNetworkException(); + } + if (response.status === 204) { + return true; + } + const text = await response.text(); + if (response.status === 404 && text.includes('NoSuchKey')) { + return false; + } + throw new Exception_1.OakExternalException('ctyun', response.status.toString(), text, { + status: response.status, + }); } getAuthorization(reqOptions, zone) { - const { headers, date } = reqOptions; - const parts = []; - const credString = [date.substring(0, 8), zone, serviceName, v4Identifier].join("/"); - parts.push("AWS4-HMAC-SHA256" + " Credential=" + this.accessKey + "/" + credString); - parts.push("SignedHeaders=" + this.signedHeaders(headers)); - parts.push("Signature=" + this.signatureFn(reqOptions, zone)); - return parts.join(", "); + const { headers, host, path, method, payload, queryParameters, service, date, } = reqOptions; + const payloadHash = this.calculatePayloadHash(payload); + // Step 2: Build canonical request + const { canonicalRequest, signedHeaders } = this.buildCanonicalRequest(method, path, queryParameters, headers, payloadHash); + // Step 3: Calculate canonical request hash + const canonicalRequestHash = crypto_1.default + .createHash('sha256') + .update(canonicalRequest) + .digest('hex'); + // Step 4: Build string to sign + const dateStamp = date.substr(0, 8); + const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`; + const stringToSign = [ + 'AWS4-HMAC-SHA256', + date, + credentialScope, + canonicalRequestHash, + ].join('\n'); + // Step 5: Get signing key + const signingKey = this.getSignatureKey(dateStamp, zone, service); + // Step 6: Calculate signature + const signature = crypto_1.default + .createHmac('sha256', signingKey) + .update(stringToSign) + .digest('hex'); + const authorizationHeader = [ + 'AWS4-HMAC-SHA256 Credential=' + + this.accessKey + + '/' + + credentialScope, + 'SignedHeaders=' + signedHeaders, + 'Signature=' + signature, + ].join(', '); + return authorizationHeader; } - signatureFn(reqOptions, zone) { - const { date } = reqOptions; - var signingKey = this.getSigningKey(date.substring(0, 8), zone); - return this.hmacSha256(signingKey, this.stringToSign(reqOptions, zone), "hex"); + getSignatureKey(dateStamp, zone, service) { + const kDate = crypto_1.default + .createHmac('sha256', `AWS4${this.secretKey}`) + .update(dateStamp) + .digest(); + const kRegion = crypto_1.default + .createHmac('sha256', kDate) + .update(zone) + .digest(); + const kService = crypto_1.default + .createHmac('sha256', kRegion) + .update(service) + .digest(); + const kSigning = crypto_1.default + .createHmac('sha256', kService) + .update('aws4_request') + .digest(); + return kSigning; } - stringToSign(reqOptions, zone) { - const { date, path, method, headers } = reqOptions; - var parts = []; - parts.push("AWS4-HMAC-SHA256"); - parts.push(date); - parts.push([date.substring(0, 8), zone, serviceName, v4Identifier].join("/")); - const canonicalStr = this.canonicalString(path, method, headers); - const buffer = Buffer.from(canonicalStr); - const encodeStr = crypto_1.default.createHash('sha256').update(buffer).digest('hex'); - parts.push(encodeStr); - return parts.join("\n"); + buildCanonicalRequest(method, path, queryParameters, headers, payloadHash) { + const canonicalQuerystring = this.buildCanonicalQueryString(queryParameters); + const canonicalHeaders = Object.keys(headers) + .sort() + .map((key) => `${key.toLowerCase()}:${headers[key].trim()}`) + .join('\n'); + const signedHeaders = Object.keys(headers) + .sort() + .map((key) => key.toLowerCase()) + .join(';'); + const canonicalRequest = [ + method, + path, + canonicalQuerystring, + canonicalHeaders, + '', + signedHeaders, + payloadHash, + ].join('\n'); + return { + canonicalRequest, + signedHeaders, + }; } - signedHeaders(headers) { - const keys = []; - this.each(headers, (key) => { - key = key.toLowerCase(); - if (this.isSignableHeader(key)) - keys.push(key); - }); - return keys.sort().join(";"); + buildCanonicalQueryString(queryParameters) { + const keys = Object.keys(queryParameters).sort(); + const canonicalQueryString = keys + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParameters[key])}`) + .join('&'); + return canonicalQueryString; } - canonicalString(path, method, headers) { - const parts = []; - parts.push(method); - parts.push(path); - parts.push(""); - parts.push(this.canonicalHeaders(headers) + "\n"); - parts.push(this.signedHeaders(headers)); - parts.push("UNSIGNED-PAYLOAD"); - return parts.join("\n"); - } - canonicalHeaders(headers) { - const headerarr = []; - this.each(headers, function (key, item) { - headerarr.push([key, item]); - }); - headerarr.sort(function (a, b) { - return a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1; - }); - const parts = []; - this.arrayEach(headerarr, (item) => { - let key = item[0].toLowerCase(); - if (this.isSignableHeader(key)) { - var value = item[1]; - if (typeof value === "undefined" || value === null || typeof value.toString !== "function") { - throw new Error("Header " + key + " contains invalid value"); - } - parts.push(key + ":" + this.canonicalHeaderValues(value.toString())); - } - }); - return parts.join("\n"); - } - canonicalHeaderValues(values) { - return values.replace(/\s+/g, " ").replace(/^\s+|\s+$/g, ""); - } - getSigningKey(date, zone) { - const kDate = this.hmacSha256("AWS4" + this.secretKey, date); - const kRegion = this.hmacSha256(kDate, zone); - const kService = this.hmacSha256(kRegion, serviceName); - var signingKey = this.hmacSha256(kService, v4Identifier); - return signingKey; - } - hmacSha256(key, content, digest, fn) { - if (!fn) - fn = "sha256"; - if (typeof content === "string") - content = Buffer.from(content); - if (!digest) { - return crypto_1.default.createHmac(fn, key).update(content).digest(); - } - return crypto_1.default.createHmac(fn, key).update(content).digest(digest); - } - each(object, iterFunction) { - for (let key in object) { - if (Object.prototype.hasOwnProperty.call(object, key)) { - const ret = iterFunction.call(this, key, object[key]); - if (Object.keys(ret).length === 0) - break; - } - } - } - arrayEach(array, iterFunction) { - for (let idx in array) { - if (Object.prototype.hasOwnProperty.call(array, idx)) { - const ret = iterFunction.call(this, array[idx], parseInt(idx, 10)); - if (Object.keys(ret).length === 0) - break; - } - } - } - isSignableHeader(key) { - if (key.toLowerCase().indexOf("x-amz-") === 0) - return true; - return unsignableHeaders.indexOf(key) < 0; + calculatePayloadHash(payload) { + const hash = crypto_1.default.createHash('sha256'); + hash.update(payload); + return hash.digest('hex'); } } exports.CTYunInstance = CTYunInstance; diff --git a/lib/service/tencent/Tencent.d.ts b/lib/service/tencent/Tencent.d.ts new file mode 100644 index 0000000..523ef74 --- /dev/null +++ b/lib/service/tencent/Tencent.d.ts @@ -0,0 +1,20 @@ +import { TencentYunZone } from '../../types/TencentYun'; +export declare class TencentYunInstance { + private accessKey; + private secretKey; + private COS; + constructor(accessKey: string, secretKey: string); + getUploadInfo(bucket: string, zone: TencentYunZone, key?: string): { + key: string | undefined; + accessKey: string; + policy: string; + signature: string; + uploadHost: string; + bucket: string; + keyTime: string; + algorithm: string; + }; + private getSignInfo; + removeFile(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise; + isExistObject(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise; +} diff --git a/lib/service/tencent/Tencent.js b/lib/service/tencent/Tencent.js new file mode 100644 index 0000000..33a2537 --- /dev/null +++ b/lib/service/tencent/Tencent.js @@ -0,0 +1,149 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TencentYunInstance = void 0; +const tslib_1 = require("tslib"); +const crypto_1 = tslib_1.__importDefault(require("crypto")); +function getEndpoint(RegionID) { + return `cos-${RegionID}.myqcloud.com`; +} +const Tencent_ENDPOINT_LIST = { + 'ap-beijing': { + ul: getEndpoint('ap-nanjing'), + }, + 'ap-nanjing': { + ul: getEndpoint('ap-nanjing'), + }, + 'ap-shanghai': { + ul: getEndpoint('ap-shanghai'), + }, + 'ap-guangzhou': { + ul: getEndpoint('ap-guangzhou'), + }, + 'ap-chengdu': { + ul: getEndpoint('ap-chengdu'), + }, + 'ap-chongqing': { + ul: getEndpoint('ap-chongqing'), + }, + 'ap-shenzhen-fsi': { + ul: getEndpoint('ap-shenzhen-fsi'), + }, + 'ap-shanghai-fsi': { + ul: getEndpoint('ap-shanghai-fsi'), + }, + 'ap-beijing-fsi': { + ul: getEndpoint('ap-beijing-fsi'), + }, + 'ap-hongkong': { + ul: getEndpoint('ap-hongkong'), + }, + 'ap-singapore': { + ul: getEndpoint('ap-singapore'), + }, +}; +// TODO 腾讯云代码未验证 +class TencentYunInstance { + accessKey; + secretKey; + COS; + constructor(accessKey, secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + this.COS = require('cos-wx-sdk-v5'); + } + getUploadInfo(bucket, zone, key) { + try { + const signInfo = this.getSignInfo(bucket); + return { + key, + accessKey: this.accessKey, + policy: signInfo.policy, + signature: signInfo.signature, + uploadHost: `https://${bucket}.${Tencent_ENDPOINT_LIST[zone].ul}`, + bucket, + keyTime: signInfo.keyTime, + algorithm: signInfo.algorithm, + }; + } + catch (err) { + throw err; + } + } + //https://cloud.tencent.com/document/product/436/7778 + getSignInfo(bucket) { + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 1); + const keyTime = `${Math.floor(Date.now() / 1000)};${Math.floor(expiration.getTime() / 1000)}`; + const algorithm = 'sha1'; + const policy = { + expiration: expiration.toISOString(), + conditions: [ + // 限制可上传的Bucket。 + { bucket: bucket }, + ['eq', '$x-cos-server-side-encryption', 'AES256'], + { 'q-sign-algorithm': algorithm }, + { 'q-ak': this.accessKey }, + { 'q-sign-time': keyTime }, + ], + }; + const encodedPolicy = Buffer.from(JSON.stringify(policy)).toString('base64'); + const signKey = crypto_1.default + .createHmac('sha1', this.secretKey) + .update(keyTime) + .digest(); + const stringToSign = crypto_1.default + .createHash('sha1') + .update(encodedPolicy) + .digest('hex'); + const signature = crypto_1.default + .createHmac('sha1', signKey) + .update(stringToSign) + .digest('hex'); + return { + policy: encodedPolicy, + signature, + keyTime, + algorithm, + }; + } + async removeFile(srcBucket, zone, srcKey) { + const client = new this.COS({ + SecretId: this.accessKey, + SecretKey: this.secretKey, + }); + try { + // 填写Object完整路径。Object完整路径中不能包含Bucket名称。 + const result = await client.deleteObject({ + Bucket: srcBucket, + Region: zone, + Key: srcKey, + }); + return result; + } + catch (error) { + throw error; + } + } + async isExistObject(srcBucket, zone, srcKey) { + const client = new this.COS({ + SecretId: this.accessKey, + SecretKey: this.secretKey, + }); + let result; + try { + result = await client.headObject({ + Bucket: srcBucket, + Region: zone, + Key: srcKey, + }); + return true; + } + catch (error) { + if (error.code === 'NoSuchKey') { + return false; + } + throw error; + } + } +} +exports.TencentYunInstance = TencentYunInstance; diff --git a/lib/service/wechat/WechatMp.js b/lib/service/wechat/WechatMp.js index cf1bf70..5be8c56 100644 --- a/lib/service/wechat/WechatMp.js +++ b/lib/service/wechat/WechatMp.js @@ -191,7 +191,7 @@ class WechatMpInstance { const { type, media, filetype, filename } = options; const formData = new form_data_1.default(); formData.append('media', media, { - contentType: filetype, + contentType: filetype, // 微信识别需要 filename: filename, // 微信识别需要 }); const getLength = () => { @@ -336,7 +336,7 @@ class WechatMpInstance { body: JSON.stringify({ jump_wxa: jump_wxa, is_expire: true, - expire_type: expireType, + expire_type: expireType, //默认是零,到期失效的 scheme 码失效类型,失效时间类型:0,失效间隔天数类型:1 expire_time: expiresAt, expire_interval: expireInterval, }), diff --git a/lib/service/wechat/WechatPublic.js b/lib/service/wechat/WechatPublic.js index 1819f59..a598f53 100644 --- a/lib/service/wechat/WechatPublic.js +++ b/lib/service/wechat/WechatPublic.js @@ -515,8 +515,8 @@ class WechatPublicInstance { const { type, media, description, filetype, filename, fileLength } = options; const formData = new form_data_1.default(); formData.append('media', media, { - contentType: filetype, - filename: filename, + contentType: filetype, // 微信识别需要 + filename: filename, // 微信识别需要 knownLength: fileLength, }); if (type === 'video') { @@ -539,8 +539,8 @@ class WechatPublicInstance { const { media, filetype, filename, fileLength } = options; const formData = new form_data_1.default(); formData.append('media', media, { - contentType: filetype, - filename: filename, + contentType: filetype, // 微信识别需要 + filename: filename, // 微信识别需要 knownLength: fileLength, }); const headers = formData.getHeaders(); @@ -560,8 +560,8 @@ class WechatPublicInstance { const { type, media, filetype, filename, fileLength } = options; const formData = new form_data_1.default(); formData.append('media', media, { - contentType: filetype, - filename: filename, + contentType: filetype, // 微信识别需要 + filename: filename, // 微信识别需要 knownLength: fileLength, }); const headers = formData.getHeaders(); diff --git a/lib/types/ALiYun.d.ts b/lib/types/ALiYun.d.ts new file mode 100644 index 0000000..6e11dc4 --- /dev/null +++ b/lib/types/ALiYun.d.ts @@ -0,0 +1 @@ +export type ALiYunZone = 'cn-hangzhou' | 'cn-shanghai' | 'cn-nanjing' | 'cn-fuzhou' | 'cn-wuhan' | 'cn-qingdao' | 'cn-beijing' | 'cn-zhangjiakou' | 'cn-huhehaote' | 'cn-wulanchabu' | 'cn-shenzhen' | 'cn-heyuan' | 'cn-guangzhou' | 'cn-chengdu' | 'cn-hongkong' | 'us-west-1' | 'us-east-1' | 'ap-northeast-1' | 'ap-northeast-2' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-southeast-3' | 'ap-southeast-5' | 'ap-southeast-6' | 'ap-southeast-7' | 'ap-south-1' | 'eu-central-1' | 'eu-west-1' | 'me-east-1' | 'rg-china-mainland'; diff --git a/lib/types/ALiYun.js b/lib/types/ALiYun.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/lib/types/ALiYun.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/types/CTYun.d.ts b/lib/types/CTYun.d.ts index e46e11a..2bd3ebd 100644 --- a/lib/types/CTYun.d.ts +++ b/lib/types/CTYun.d.ts @@ -4,10 +4,13 @@ */ export type Action = '*' | 'PutObject' | 'GetObject' | 'DeleteObject' | 'ListBucket'; export type ReqOptionProps = { + payload: string; path: string; host: string; - date: string; headers: any; - method: 'DELETE' | "GET" | "POST" | "PUT"; + method: 'DELETE' | 'GET' | 'POST' | 'PUT' | 'HEAD'; + queryParameters: Record; + service: string; + date: string; }; export type CTYunZone = 'hazz' | 'lnsy' | 'sccd' | 'xjwlmq' | 'gslz' | 'sdqd' | 'gzgy' | 'hbwh' | 'xzls' | 'ahwh' | 'gdsz' | 'jssz' | 'sh2'; diff --git a/lib/types/TencentYun.d.ts b/lib/types/TencentYun.d.ts new file mode 100644 index 0000000..8da68bc --- /dev/null +++ b/lib/types/TencentYun.d.ts @@ -0,0 +1 @@ +export type TencentYunZone = 'ap-beijing' | 'ap-nanjing' | 'ap-shanghai' | 'ap-guangzhou' | 'ap-chengdu' | 'ap-chongqing' | 'ap-shenzhen-fsi' | 'ap-shanghai-fsi' | 'ap-beijing-fsi' | 'ap-hongkong' | 'ap-singapore'; diff --git a/lib/types/TencentYun.js b/lib/types/TencentYun.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/lib/types/TencentYun.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index 0763376..182e689 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -1,3 +1,5 @@ export * from './Wechat'; export * from './Qiniu'; export * from './CTYun'; +export * from './ALiYun'; +export * from './TencentYun'; diff --git a/lib/types/index.js b/lib/types/index.js index 51d4322..1e51252 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -4,3 +4,5 @@ const tslib_1 = require("tslib"); tslib_1.__exportStar(require("./Wechat"), exports); tslib_1.__exportStar(require("./Qiniu"), exports); tslib_1.__exportStar(require("./CTYun"), exports); +tslib_1.__exportStar(require("./ALiYun"), exports); +tslib_1.__exportStar(require("./TencentYun"), exports); diff --git a/package.json b/package.json index 0fff532..cc6eadc 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "license": "ISC", "devDependencies": { + "@types/ali-oss": "^6.16.11", "@types/node": "^20.6.1", "ts-node": "^10.9.1", "tslib": "^2.4.0", @@ -27,8 +28,10 @@ "dependencies": { "@alicloud/dysmsapi20170525": "^2.0.24", "@alicloud/pop-core": "^1.7.12", + "ali-oss": "^6.20.0", "aws-sdk": "^2.1499.0", "cheerio": "^1.0.0-rc.12", + "cos-wx-sdk-v5": "^1.7.1", "isomorphic-fetch": "^3.0.0", "oak-domain": "file:../oak-domain", "tencentcloud-sdk-nodejs": "^4.0.746", diff --git a/src/ALiYunSDK.ts b/src/ALiYunSDK.ts new file mode 100644 index 0000000..cd14883 --- /dev/null +++ b/src/ALiYunSDK.ts @@ -0,0 +1,26 @@ +import { ALiYunInstance } from './service/ali/Ali'; + +class ALiYunSDK { + aliMap: Record; //oss + + constructor() { + this.aliMap = {}; + } + + getInstance(accessKey: string, accessSecret: string) { + if (this.aliMap[accessKey]) { + return this.aliMap[accessKey]; + } + const instance = new ALiYunInstance(accessKey, accessSecret); + Object.assign(this.aliMap, { + [accessKey]: instance, + }); + return instance; + } +} + +const SDK = new ALiYunSDK(); +export default SDK; + +export { ALiYunInstance }; + diff --git a/src/CTYunSDK.ts b/src/CTYunSDK.ts index 10b2a48..22fa7af 100644 --- a/src/CTYunSDK.ts +++ b/src/CTYunSDK.ts @@ -1,16 +1,13 @@ import { CTYunInstance } from './service/ctyun/CTYun'; class CTYunSDK { - ctyunMap: Record; + ctyunMap: Record; //oss constructor() { this.ctyunMap = {}; } - getInstance( - accessKey: string, - accessSecret: string, - ) { + getInstance(accessKey: string, accessSecret: string) { if (this.ctyunMap[accessKey]) { return this.ctyunMap[accessKey]; } diff --git a/src/TencentYunSDK.ts b/src/TencentYunSDK.ts new file mode 100644 index 0000000..8ac3b7e --- /dev/null +++ b/src/TencentYunSDK.ts @@ -0,0 +1,26 @@ +import { TencentYunInstance } from './service/tencent/Tencent'; + +class TencentYunSDK { + tencentMap: Record; //oss + + constructor() { + this.tencentMap = {}; + } + + getInstance(accessKey: string, accessSecret: string) { + if (this.tencentMap[accessKey]) { + return this.tencentMap[accessKey]; + } + const instance = new TencentYunInstance(accessKey, accessSecret); + Object.assign(this.tencentMap, { + [accessKey]: instance, + }); + return instance; + } +} + +const SDK = new TencentYunSDK(); +export default SDK; + +export { TencentYunInstance }; + diff --git a/src/index.ts b/src/index.ts index ca5d08e..5bfae1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,19 +6,26 @@ import SmsSdk, { AliSmsInstance, CTYunSmsInstance, } from './SmsSdk'; -import CTYunSDk, { CTYunInstance } from './CTYunSDK'; +import CTYunSDK, { CTYunInstance } from './CTYunSDK'; +import ALiYunSDK, { ALiYunInstance } from './ALiYunSDK'; +import TencentYunSDK, { TencentYunInstance } from './TencentYunSDK'; export * from './service/amap/Amap'; export { AmapSDK, QiniuSDK, WechatSDK, - CTYunSDk, + CTYunSDK, + ALiYunSDK, + TencentYunSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, QiniuCloudInstance, + ALiYunInstance, + TencentYunInstance, + SmsSdk, TencentSmsInstance, AliSmsInstance, diff --git a/src/service/ali/Ali.ts b/src/service/ali/Ali.ts index e69de29..73f4dab 100644 --- a/src/service/ali/Ali.ts +++ b/src/service/ali/Ali.ts @@ -0,0 +1,198 @@ + +import { ALiYunZone } from '../../types/ALiYun'; +import OSS from 'ali-oss'; +import { + OakExternalException, + OakNetworkException, +} from 'oak-domain/lib/types/Exception'; + +function getEndpoint(RegionID: ALiYunZone) { + return `oss-${RegionID}.aliyuncs.com`; +} + +const ALiYun_ENDPOINT_LIST: Record = { + 'cn-hangzhou': { + ul: getEndpoint('cn-hangzhou'), + }, + 'cn-shanghai': { + ul: getEndpoint('cn-shanghai'), + }, + 'cn-nanjing': { + ul: getEndpoint('cn-nanjing'), + }, + 'cn-fuzhou': { + ul: getEndpoint('cn-fuzhou'), + }, + 'cn-wuhan': { + ul: getEndpoint('cn-wuhan'), + }, + 'cn-qingdao': { + ul: getEndpoint('cn-qingdao'), + }, + 'cn-beijing': { + ul: getEndpoint('cn-beijing'), + }, + 'cn-zhangjiakou': { + ul: getEndpoint('cn-zhangjiakou'), + }, + 'cn-huhehaote': { + ul: getEndpoint('cn-huhehaote'), + }, + 'cn-wulanchabu': { + ul: getEndpoint('cn-wulanchabu'), + }, + 'cn-shenzhen': { + ul: getEndpoint('cn-shenzhen'), + }, + 'cn-heyuan': { + ul: getEndpoint('cn-heyuan'), + }, + 'cn-guangzhou': { + ul: getEndpoint('cn-guangzhou'), + }, + 'cn-chengdu': { + ul: getEndpoint('cn-chengdu'), + }, + 'cn-hongkong': { + ul: getEndpoint('cn-hongkong'), + }, + + 'us-west-1': { + ul: getEndpoint('us-west-1'), + }, + 'us-east-1': { + ul: getEndpoint('us-east-1'), + }, + 'ap-northeast-1': { + ul: getEndpoint('ap-northeast-1'), + }, + 'ap-northeast-2': { + ul: getEndpoint('ap-northeast-2'), + }, + 'ap-southeast-1': { + ul: getEndpoint('ap-southeast-1'), + }, + 'ap-southeast-2': { + ul: getEndpoint('ap-southeast-2'), + }, + 'ap-southeast-3': { + ul: getEndpoint('ap-southeast-3'), + }, + 'ap-southeast-5': { + ul: getEndpoint('ap-southeast-5'), + }, + 'ap-southeast-6': { + ul: getEndpoint('ap-southeast-6'), + }, + 'ap-southeast-7': { + ul: getEndpoint('ap-southeast-7'), + }, + 'ap-south-1': { + ul: getEndpoint('ap-south-1'), + }, + 'eu-central-1': { + ul: getEndpoint('eu-central-1'), + }, + 'eu-west-1': { + ul: getEndpoint('eu-west-1'), + }, + 'me-east-1': { + ul: getEndpoint('me-east-1'), + }, + 'rg-china-mainland': { + ul: getEndpoint('rg-china-mainland'), + }, +}; + + +export class ALiYunInstance { + private accessKey: string; + private secretKey: string; + + constructor(accessKey: string, secretKey: string) { + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + getUploadInfo(bucket: string, zone: ALiYunZone, key?: string) { + try { + const signInfo = this.getSignInfo(bucket); + return { + key, + accessKey: this.accessKey, + policy: signInfo.policy, + signature: signInfo.signature, + uploadHost: `https://${bucket}.${ALiYun_ENDPOINT_LIST[zone].ul}`, + bucket, + }; + } catch (err) { + throw err; + } + } + + //https://help.aliyun.com/zh/oss/user-guide/form-upload?spm=a2c4g.11186623.0.0.22147f6b1UaGxF#6ee9b6b0be6on + private getSignInfo(bucket: string) { + const client = new OSS({ + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + bucket: bucket, + }); + const now = new Date(); + now.setDate(now.getDate() + 1); + const policy = { + expiration: now.toISOString(), + conditions: [ + // 设置上传文件的大小限制。 + ['content-length-range', 0, 1048576000], + // 限制可上传的Bucket。 + { bucket: bucket }, + ], + }; + const data = client.calculatePostSignature(policy); + + return { + policy: data.policy, + signature: data.Signature, + ossAccessKeyId: data.OSSAccessKeyId, + }; + } + + async removeFile(srcBucket: string, zone: ALiYunZone, srcKey: string) { + const client = new OSS({ + // oss-cn-hangzhou填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 + region: `oss-${zone}`, + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + // 填写Bucket名称。 + bucket: srcBucket, + }); + + try { + // 填写Object完整路径。Object完整路径中不能包含Bucket名称。 + const result = await client.delete(srcKey); + return result; + } catch (error) { + throw error; + } + } + + async isExistObject(srcBucket: string, zone: ALiYunZone, srcKey: string) { + const client = new OSS({ + // yourregion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 + region: `oss-${zone}`, + accessKeyId: this.accessKey, + accessKeySecret: this.secretKey, + bucket: srcBucket, + }); + let result: OSS.HeadObjectResult; + try { + result = await client.head(srcKey); + return true + } catch (error: any) { + if (error.code === 'NoSuchKey') { + return false; + } + throw error; + } + } +} \ No newline at end of file diff --git a/src/service/ctyun/CTYun.ts b/src/service/ctyun/CTYun.ts index f192714..977e790 100644 --- a/src/service/ctyun/CTYun.ts +++ b/src/service/ctyun/CTYun.ts @@ -5,7 +5,8 @@ import { OakExternalException, OakNetworkException, } from 'oak-domain/lib/types/Exception'; -const CTYun_ENDPOINT_LIST = { + +const CTYun_ENDPOINT_LIST: Record = { hazz: { ul: 'oos-hazz.ctyunapi.cn', }, @@ -47,24 +48,30 @@ const CTYun_ENDPOINT_LIST = { }, }; - const serviceName = 's3'; const v4Identifier = 'aws4_request'; -const expiresHeader = "presigned-expires"; -const unsignableHeaders = ["authorization", "x-ctyun-data-location", "content-length", "user-agent", expiresHeader, "expect", "x-amzn-trace-id"]; +const expiresHeader = 'presigned-expires'; +const unsignableHeaders = [ + 'authorization', + 'x-ctyun-data-location', + 'content-length', + 'user-agent', + expiresHeader, + 'expect', + 'x-amzn-trace-id', +]; export class CTYunInstance { private accessKey: string; private secretKey: string; - constructor(accessKey: string, secretKey: string,) { + constructor(accessKey: string, secretKey: string) { this.accessKey = accessKey; this.secretKey = secretKey; } getUploadInfo(bucket: string, zone: CTYunZone, key?: string) { try { - // const uploadToken = this.getToken(zone, bucket, actions); const signInfo = this.getSignInfo(bucket); return { key, @@ -82,30 +89,30 @@ export class CTYunInstance { getSignInfo(bucket: string) { // 对于policy里的expiration,我在天翼云的文档里没有找到具体的说明,但是这个字段不填入就会请求失败 // 设置一个明天过期的时间 - const now = new Date(); - now.setDate(now.getDate() + 1); - const tomorrow = now.toISOString(); + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 1); const policy = { - Version: "2012-10-17", - Statement: [{ - Effect: "Allow", - Action: ["oos:*"], - Resource: `arn:ctyun:oos:::${bucket} /*` - }], - expiration: tomorrow, - conditions: [{ - bucket: bucket, - }, [ - "starts-with", - "$key", - "extraFile", - ]] - } + expiration: expiration.toISOString(), + conditions: [ + { + bucket: bucket, + }, + ['starts-with', '$key', 'extraFile'], + ], + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['oos:*'], + Resource: `arn:ctyun:oos:::${bucket} /*`, + }, + ], + }; const encodePolicy = this.urlSafeBase64Encode(JSON.stringify(policy)); const signature = this.hmacSha1(encodePolicy, this.secretKey); return { encodePolicy, - signature + signature, }; } @@ -124,152 +131,252 @@ export class CTYunInstance { return this.base64ToUrlSafe(encoded); } - - // 当初就不应该封装天翼云,文档不全,找技术要签名代码还得自己去看源码,恶心 - // 下面的代码是根据天翼云生成签名源码改动得来 oos-js-sdk-6.2.js async removeFile(bucket: string, zone: CTYunZone, key: string) { - const path = `/${bucket}/${key}`; - const host = `${CTYun_ENDPOINT_LIST[zone].ul}`; + const path = `/${key}`; + const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`; const url = `https://${host}${path}`; - const date = new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replace(/[:\-]|\.\d{3}/g, ""); + const payload = ''; + const method = 'DELETE'; + const service = 's3'; + const date = new Date() + .toISOString() + .replace(/\.\d{3}Z$/, 'Z') + .replace(/[:\-]|\.\d{3}/g, ''); const headers: Record = { - "Content-Type": "application/octet-stream; charset=UTF-8", - "Host": "oos-hbwh.ctyunapi.cn", - "X-Amz-Content-Sha256": "UNSIGNED-PAYLOAD", - "X-Amz-User-Agent": "aws-sdk-js/2.296.0 callback" - } - headers["X-Amz-Date"] = date; + 'Content-Type': 'application/xml; charset=utf-8', + Host: host, + 'x-amz-content-sha256': this.calculatePayloadHash(payload), + 'x-amz-date': date, + }; + const reqOptions: ReqOptionProps = { - path, - host, - date, headers, - method: "DELETE", - } + method: method, + payload: payload, + host, + path, + queryParameters: {}, + service, + date, + }; const authorization = this.getAuthorization(reqOptions, zone); - headers['Authorization'] = authorization; + + let response: Response; try { - await fetch(url, { - method: 'DELETE', - headers, - }) + response = await fetch(url, { + method, + headers: { + ...headers, + Authorization: authorization, + }, + }); } catch (err) { throw new OakNetworkException(); } + if (response.status === 204) { + return; + } + const text = await response.text(); + throw new OakExternalException( + 'ctyun', + response.status.toString(), + text, + { + status: response.status, + } + ); } + async isExistObject(srcBucket: string, zone: CTYunZone, srcKey: string) { + const path = `/${srcKey}`; + const host = `${srcBucket}.${CTYun_ENDPOINT_LIST[zone].ul}`; + const url = `https://${host}${path}`; + const payload = ''; + const method = 'GET'; + const service = 's3'; + const date = new Date() + .toISOString() + .replace(/\.\d{3}Z$/, 'Z') + .replace(/[:\-]|\.\d{3}/g, ''); + const headers: Record = { + Host: host, + 'x-amz-content-sha256': this.calculatePayloadHash(payload), + 'x-amz-date': date, + }; + + const reqOptions: ReqOptionProps = { + headers, + method: method, + payload: payload, + host, + path, + queryParameters: {}, + service, + date, + }; + + const authorization = this.getAuthorization(reqOptions, zone); + let response: Response; + try { + response = await fetch(url, { + method, + headers: { + ...headers, + Authorization: authorization, + }, + }); + } catch (err) { + throw new OakNetworkException(); + } + if (response.status === 204) { + return true; + } + const text = await response.text(); + if (response.status === 404 && text.includes('NoSuchKey')) { + return false; + } + throw new OakExternalException( + 'ctyun', + response.status.toString(), + text, + { + status: response.status, + } + ); + } getAuthorization(reqOptions: ReqOptionProps, zone: CTYunZone) { - const { headers, date } = reqOptions; - const parts = []; - const credString = [date.substring(0, 8), zone, serviceName, v4Identifier].join("/"); - parts.push("AWS4-HMAC-SHA256" + " Credential=" + this.accessKey + "/" + credString); - parts.push("SignedHeaders=" + this.signedHeaders(headers)); - parts.push("Signature=" + this.signatureFn(reqOptions, zone)); - return parts.join(", "); + const { + headers, + host, + path, + method, + payload, + queryParameters, + service, + date, + } = reqOptions; + + const payloadHash = this.calculatePayloadHash(payload); + + // Step 2: Build canonical request + const { canonicalRequest, signedHeaders } = this.buildCanonicalRequest( + method, + path, + queryParameters, + headers, + payloadHash + ); + + // Step 3: Calculate canonical request hash + const canonicalRequestHash = crypto + .createHash('sha256') + .update(canonicalRequest) + .digest('hex'); + + // Step 4: Build string to sign + + const dateStamp = date.substr(0, 8); + const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`; + const stringToSign = [ + 'AWS4-HMAC-SHA256', + date, + credentialScope, + canonicalRequestHash, + ].join('\n'); + + // Step 5: Get signing key + const signingKey = this.getSignatureKey(dateStamp, zone, service); + + // Step 6: Calculate signature + const signature = crypto + .createHmac('sha256', signingKey) + .update(stringToSign) + .digest('hex'); + + const authorizationHeader = [ + 'AWS4-HMAC-SHA256 Credential=' + + this.accessKey + + '/' + + credentialScope, + 'SignedHeaders=' + signedHeaders, + 'Signature=' + signature, + ].join(', '); + + return authorizationHeader; } - signatureFn(reqOptions: ReqOptionProps, zone: CTYunZone) { - const { date } = reqOptions; - var signingKey = this.getSigningKey(date.substring(0, 8), zone); - return this.hmacSha256(signingKey, this.stringToSign(reqOptions, zone), "hex"); + private getSignatureKey(dateStamp: string, zone: string, service: string) { + const kDate = crypto + .createHmac('sha256', `AWS4${this.secretKey}`) + .update(dateStamp) + .digest(); + const kRegion = crypto + .createHmac('sha256', kDate) + .update(zone) + .digest(); + const kService = crypto + .createHmac('sha256', kRegion) + .update(service) + .digest(); + const kSigning = crypto + .createHmac('sha256', kService) + .update('aws4_request') + .digest(); + return kSigning; } - stringToSign(reqOptions: ReqOptionProps, zone: CTYunZone) { - const { date, path, method, headers } = reqOptions; - var parts = []; - parts.push("AWS4-HMAC-SHA256"); - parts.push(date); - parts.push([date.substring(0, 8), zone, serviceName, v4Identifier].join("/")); - const canonicalStr = this.canonicalString(path, method, headers); - const buffer = Buffer.from(canonicalStr); - const encodeStr = crypto.createHash('sha256').update(buffer).digest('hex'); - parts.push(encodeStr); - return parts.join("\n"); + private buildCanonicalRequest( + method: ReqOptionProps['method'], + path: string, + queryParameters: Record, + headers: Record, + payloadHash: string + ) { + const canonicalQuerystring = + this.buildCanonicalQueryString(queryParameters); + const canonicalHeaders = Object.keys(headers) + .sort() + .map((key) => `${key.toLowerCase()}:${headers[key].trim()}`) + .join('\n'); + const signedHeaders = Object.keys(headers) + .sort() + .map((key) => key.toLowerCase()) + .join(';'); + + const canonicalRequest = [ + method, + path, + canonicalQuerystring, + canonicalHeaders, + '', + signedHeaders, + payloadHash, + ].join('\n'); + + return { + canonicalRequest, + signedHeaders, + }; } - signedHeaders(headers: Record) { - const keys: string[] = []; - this.each(headers, (key) => { - key = key.toLowerCase(); - if (this.isSignableHeader(key)) keys.push(key); - }); - return keys.sort().join(";"); + private buildCanonicalQueryString(queryParameters: Record) { + const keys = Object.keys(queryParameters).sort(); + const canonicalQueryString = keys + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent( + queryParameters[key] + )}` + ) + .join('&'); + return canonicalQueryString; } - canonicalString(path: string, method: string, headers: Record) { - const parts = []; - parts.push(method); - parts.push(path); - parts.push(""); - parts.push(this.canonicalHeaders(headers) + "\n"); - parts.push(this.signedHeaders(headers)); - parts.push("UNSIGNED-PAYLOAD"); - return parts.join("\n"); + private calculatePayloadHash(payload: string) { + const hash = crypto.createHash('sha256'); + hash.update(payload); + return hash.digest('hex'); } - canonicalHeaders(headers: Record) { - const headerarr:string[][] = []; - this.each(headers, function (key, item) { - headerarr.push([key, item]); - }); - headerarr.sort(function (a, b) { - return a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1; - }); - const parts: string[] = []; - this.arrayEach(headerarr, (item) => { - let key = item[0].toLowerCase(); - if (this.isSignableHeader(key)) { - var value = item[1]; - if (typeof value === "undefined" || value === null || typeof value.toString !== "function") { - throw new Error("Header " + key + " contains invalid value"); - } - parts.push(key + ":" + this.canonicalHeaderValues(value.toString())); - } - }); - return parts.join("\n"); - } - canonicalHeaderValues(values: string) { - return values.replace(/\s+/g, " ").replace(/^\s+|\s+$/g, ""); - } - - getSigningKey(date: string, zone: CTYunZone) { - const kDate = this.hmacSha256("AWS4" + this.secretKey, date); - const kRegion = this.hmacSha256(kDate, zone); - const kService = this.hmacSha256(kRegion, serviceName); - var signingKey = this.hmacSha256(kService, v4Identifier); - return signingKey; - } - - hmacSha256(key: string | Buffer, content: string | Buffer, digest?: BinaryToTextEncoding, fn?: string) { - if (!fn) fn = "sha256"; - if (typeof content === "string") content = Buffer.from(content); - if (!digest) { - return crypto.createHmac(fn, key).update(content).digest(); - } - return crypto.createHmac(fn, key).update(content).digest(digest); - } - - each(object: any, iterFunction: (key: any, item: any) => any) { - for (let key in object) { - if (Object.prototype.hasOwnProperty.call(object, key)) { - const ret = iterFunction.call(this, key, object[key]); - if (Object.keys(ret).length === 0) break; - } - } - } - arrayEach(array: any, iterFunction: (item: any, key: any) => any) { - for (let idx in array) { - if (Object.prototype.hasOwnProperty.call(array, idx)) { - const ret = iterFunction.call(this, array[idx], parseInt(idx, 10)); - if (Object.keys(ret).length === 0) break; - } - } - } - - isSignableHeader(key: string) { - if (key.toLowerCase().indexOf("x-amz-") === 0) return true; - return unsignableHeaders.indexOf(key) < 0; - } - } diff --git a/src/service/tencent/Tencent.ts b/src/service/tencent/Tencent.ts new file mode 100644 index 0000000..1a914f2 --- /dev/null +++ b/src/service/tencent/Tencent.ts @@ -0,0 +1,176 @@ + +import crypto, { BinaryToTextEncoding } from 'crypto'; + +import { TencentYunZone } from '../../types/TencentYun'; +import { + OakExternalException, + OakNetworkException, +} from 'oak-domain/lib/types/Exception'; + + + + +function getEndpoint(RegionID: TencentYunZone) { + return `cos-${RegionID}.myqcloud.com`; +} + +const Tencent_ENDPOINT_LIST: Record = { + 'ap-beijing': { + ul: getEndpoint('ap-nanjing'), + }, + 'ap-nanjing': { + ul: getEndpoint('ap-nanjing'), + }, + 'ap-shanghai': { + ul: getEndpoint('ap-shanghai'), + }, + 'ap-guangzhou': { + ul: getEndpoint('ap-guangzhou'), + }, + 'ap-chengdu': { + ul: getEndpoint('ap-chengdu'), + }, + 'ap-chongqing': { + ul: getEndpoint('ap-chongqing'), + }, + 'ap-shenzhen-fsi': { + ul: getEndpoint('ap-shenzhen-fsi'), + }, + 'ap-shanghai-fsi': { + ul: getEndpoint('ap-shanghai-fsi'), + }, + 'ap-beijing-fsi': { + ul: getEndpoint('ap-beijing-fsi'), + }, + 'ap-hongkong': { + ul: getEndpoint('ap-hongkong'), + }, + 'ap-singapore': { + ul: getEndpoint('ap-singapore'), + }, +}; + +// TODO 腾讯云代码未验证 +export class TencentYunInstance { + private accessKey: string; + private secretKey: string; + + private COS; + + constructor(accessKey: string, secretKey: string) { + this.accessKey = accessKey; + this.secretKey = secretKey; + + this.COS = require('cos-wx-sdk-v5'); + } + + getUploadInfo(bucket: string, zone: TencentYunZone, key?: string) { + try { + const signInfo = this.getSignInfo(bucket); + return { + key, + accessKey: this.accessKey, + policy: signInfo.policy, + signature: signInfo.signature, + uploadHost: `https://${bucket}.${Tencent_ENDPOINT_LIST[zone].ul}`, + bucket, + keyTime: signInfo.keyTime, + algorithm: signInfo.algorithm, + }; + } catch (err) { + throw err; + } + } + + //https://cloud.tencent.com/document/product/436/7778 + private getSignInfo(bucket: string) { + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 1); + + const keyTime = `${Math.floor(Date.now() / 1000)};${Math.floor( + expiration.getTime() / 1000 + )}`; + const algorithm = 'sha1'; + + const policy = { + expiration: expiration.toISOString(), + conditions: [ + // 限制可上传的Bucket。 + { bucket: bucket }, + ['eq', '$x-cos-server-side-encryption', 'AES256'], + { 'q-sign-algorithm': algorithm }, + { 'q-ak': this.accessKey }, + { 'q-sign-time': keyTime }, + ], + }; + + const encodedPolicy = Buffer.from(JSON.stringify(policy)).toString( + 'base64' + ); + + const signKey = crypto + .createHmac('sha1', this.secretKey) + .update(keyTime) + .digest(); + const stringToSign = crypto + .createHash('sha1') + .update(encodedPolicy) + .digest('hex'); + + const signature = crypto + .createHmac('sha1', signKey) + .update(stringToSign) + .digest('hex'); + + return { + policy: encodedPolicy, + signature, + keyTime, + algorithm, + }; + } + + async removeFile(srcBucket: string, zone: TencentYunZone, srcKey: string) { + const client = new this.COS({ + SecretId: this.accessKey, + SecretKey: this.secretKey, + }); + + try { + // 填写Object完整路径。Object完整路径中不能包含Bucket名称。 + const result = await client.deleteObject({ + Bucket: srcBucket, + Region: zone, + Key: srcKey, + }); + return result; + } catch (error) { + throw error; + } + } + + async isExistObject( + srcBucket: string, + zone: TencentYunZone, + srcKey: string + ) { + const client = new this.COS({ + SecretId: this.accessKey, + SecretKey: this.secretKey, + }); + let result: any; + try { + result = await client.headObject({ + Bucket: srcBucket, + Region: zone, + Key: srcKey, + }); + return true; + } catch (error: any) { + if (error.code === 'NoSuchKey') { + return false; + } + throw error; + } + } +} \ No newline at end of file diff --git a/src/types/ALiYun.ts b/src/types/ALiYun.ts new file mode 100644 index 0000000..ccf69ff --- /dev/null +++ b/src/types/ALiYun.ts @@ -0,0 +1,35 @@ + + + + +export type ALiYunZone = + | 'cn-hangzhou' + | 'cn-shanghai' + | 'cn-nanjing' + | 'cn-fuzhou' + | 'cn-wuhan' + | 'cn-qingdao' + | 'cn-beijing' + | 'cn-zhangjiakou' + | 'cn-huhehaote' + | 'cn-wulanchabu' + | 'cn-shenzhen' + | 'cn-heyuan' + | 'cn-guangzhou' + | 'cn-chengdu' + | 'cn-hongkong' + | 'us-west-1' + | 'us-east-1' + | 'ap-northeast-1' + | 'ap-northeast-2' + | 'ap-southeast-1' + | 'ap-southeast-2' + | 'ap-southeast-3' + | 'ap-southeast-5' + | 'ap-southeast-6' + | 'ap-southeast-7' + | 'ap-south-1' + | 'eu-central-1' + | 'eu-west-1' + | 'me-east-1' + | 'rg-china-mainland'; diff --git a/src/types/CTYun.ts b/src/types/CTYun.ts index 923106c..4d63cb7 100644 --- a/src/types/CTYun.ts +++ b/src/types/CTYun.ts @@ -6,11 +6,14 @@ export type Action = '*' | 'PutObject' | 'GetObject' | 'DeleteObject' | 'ListBucket'; export type ReqOptionProps = { + payload: string; path: string; host: string; + headers: any; + method: 'DELETE' | 'GET' | 'POST' | 'PUT' | 'HEAD'; + queryParameters: Record; + service: string; date: string; - headers: any - method: 'DELETE' | "GET" | "POST" | "PUT", -} +}; export type CTYunZone = 'hazz' | 'lnsy' | 'sccd' | 'xjwlmq' | 'gslz' | 'sdqd' | 'gzgy' | 'hbwh' | 'xzls' | 'ahwh' | 'gdsz' | 'jssz' | 'sh2'; diff --git a/src/types/TencentYun.ts b/src/types/TencentYun.ts new file mode 100644 index 0000000..c75e7d6 --- /dev/null +++ b/src/types/TencentYun.ts @@ -0,0 +1,13 @@ + +export type TencentYunZone = + | 'ap-beijing' + | 'ap-nanjing' + | 'ap-shanghai' + | 'ap-guangzhou' + | 'ap-chengdu' + | 'ap-chongqing' + | 'ap-shenzhen-fsi' + | 'ap-shanghai-fsi' + | 'ap-beijing-fsi' + | 'ap-hongkong' + | 'ap-singapore'; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 09d60f9..9126479 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ export * from './Wechat'; export * from './Qiniu'; -export * from './CTYun'; \ No newline at end of file +export * from './CTYun'; +export * from './ALiYun'; +export * from './TencentYun'; \ No newline at end of file diff --git a/test/testCos.ts b/test/testCos.ts new file mode 100644 index 0000000..9208a95 --- /dev/null +++ b/test/testCos.ts @@ -0,0 +1,63 @@ +import { ALiYunInstance, ALiYunSDK, CTYunSDK } from '../src/index'; + +const accessKey = ''; +const accessSecret = ''; + + +//测试阿里云文件是否存在 +async function isExistFile() { + const instance = ALiYunSDK.getInstance(accessKey, accessSecret); + + const result = await instance.isExistObject( + 'bmzs-release', + 'cn-hangzhou', + 'extraFile/11ef425f-8c96-e3eb-8828-db68e1a0c102.png' + ); + + console.log(result); +} + +async function removeFile() { + const instance = ALiYunSDK.getInstance(accessKey, accessSecret); + + const result = await instance.removeFile( + 'bmzs-release', + 'cn-hangzhou', + 'extraFile/11ef425f-8c96-e3eb-8828-db68e1a0c102.png' + ); + console.log(result); +} + +// isExistFile(); +removeFile(); + +const accessKey2 = ''; +const accessSecret2 = ''; + +async function removeCTYunFile() { + const instance = CTYunSDK.getInstance(accessKey2, accessSecret2); + + const result = await instance.removeFile( + 'jiupai-oss-dev', + 'hbwh', + 'fff.jpg' + ); + console.log(result); +} + +async function isExistCTYunFile() { + const instance = CTYunSDK.getInstance(accessKey2, accessSecret2); + + const result = await instance.isExistObject( + 'jiupai-oss-dev', + 'hbwh', + 'fff.jpg' + ); + + console.log(result); +} + +// removeCTYunFile(); +// isExistCTYunFile(); + +