oak-external-sdk/es/service/ctyun/CTYun.js

381 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// import AWS from 'aws-sdk';
import crypto from 'crypto';
import { OakExternalException, OakNetworkException, } from 'oak-domain/lib/types/Exception';
const CTYun_ENDPOINT_LIST = {
hazz: {
ul: 'oos-hazz.ctyunapi.cn',
},
lnsy: {
ul: 'oos-lnsy.ctyunapi.cn',
},
sccd: {
ul: 'oos-sccd.ctyunapi.cn',
},
xjwlmq: {
ul: 'oos-xjwlmq.ctyunapi.cn',
},
gslz: {
ul: 'oos-gslz.ctyunapi.cn',
},
sdqd: {
ul: 'oos-sdqd.ctyunapi.cn',
},
gzgy: {
ul: 'oos-gzgy.ctyunapi.cn',
},
hbwh: {
ul: 'oos-hbwh.ctyunapi.cn',
},
xzls: {
ul: 'oos-xzls.ctyunapi.cn',
},
ahwh: {
ul: 'oos-ahwh.ctyunapi.cn',
},
gdsz: {
ul: 'oos-gdsz.ctyunapi.cn',
},
jssz: {
ul: 'oos-jssz.ctyunapi.cn',
},
sh2: {
ul: 'oos-sh2.ctyunapi.cn',
},
};
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',
];
export class CTYunInstance {
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.encodePolicy,
signature: signInfo.signature,
uploadHost: `https://${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`,
bucket,
};
}
catch (err) {
throw err;
}
}
getSignInfo(bucket) {
// 对于policy里的expiration我在天翼云的文档里没有找到具体的说明但是这个字段不填入就会请求失败
// 设置一个明天过期的时间
const expiration = new Date();
expiration.setDate(expiration.getDate() + 1);
const policy = {
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,
};
}
base64ToUrlSafe(v) {
return v.replace(/\//g, '_').replace(/\+/g, '-');
}
hmacSha1(encodedFlags, secretKey) {
const hmac = crypto.createHmac('sha1', secretKey);
hmac.update(encodedFlags);
return hmac.digest('base64');
}
urlSafeBase64Encode(jsonFlags) {
const encoded = Buffer.from(jsonFlags).toString('base64');
return this.base64ToUrlSafe(encoded);
}
async removeFile(bucket, zone, key) {
const path = `/${key}`;
const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`;
const url = `https://${host}${path}`;
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/xml; charset=utf-8',
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;
}
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 || response.status === 200) {
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, 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;
}
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;
}
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,
};
}
buildCanonicalQueryString(queryParameters) {
const keys = Object.keys(queryParameters).sort();
const canonicalQueryString = keys
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParameters[key])}`)
.join('&');
return canonicalQueryString;
}
calculatePayloadHash(payload) {
const hash = crypto.createHash('sha256');
hash.update(payload);
return hash.digest('hex');
}
/**
* 获取预签名对象URL统一接口
* 生成天翼云对象的预签名访问URL兼容 AWS S3 Signature V4
*/
async presignObjectUrl(method, bucket, zone, key, options) {
const expiresIn = options?.expires || 3600;
const service = 's3';
// 对 key 进行 URI 编码(保留 /
const encodedKey = key
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/');
const path = `/${encodedKey}`;
const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`;
const baseUrl = `https://${host}${path}`;
// 生成 ISO8601 格式的日期
const date = new Date()
.toISOString()
.replace(/\.\d{3}Z$/, 'Z')
.replace(/[:\-]|\.\d{3}/g, '');
const dateStamp = date.substring(0, 8);
const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`;
// 构建查询参数(不含签名)
const queryParameters = {
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
'X-Amz-Credential': `${this.accessKey}/${credentialScope}`,
'X-Amz-Date': date,
'X-Amz-Expires': expiresIn.toString(),
'X-Amz-SignedHeaders': 'host',
};
// 构建 Canonical Request
// 对于预签名 URLpayload hash 使用 UNSIGNED-PAYLOAD
const { canonicalRequest } = this.buildCanonicalRequest(method, path, queryParameters, { host: host }, // 注意header 名称应为小写
'UNSIGNED-PAYLOAD');
// 计算 Canonical Request 的 hash
const canonicalRequestHash = crypto
.createHash('sha256')
.update(canonicalRequest)
.digest('hex');
// 构建 String to Sign
const stringToSign = [
'AWS4-HMAC-SHA256',
date,
credentialScope,
canonicalRequestHash,
].join('\n');
// 生成签名密钥并计算签名
const signingKey = this.getSignatureKey(dateStamp, zone, service);
const signature = crypto
.createHmac('sha256', signingKey)
.update(stringToSign)
.digest('hex');
// 将签名添加到查询参数
queryParameters['X-Amz-Signature'] = signature;
// 构建最终 URL
const queryString = this.buildCanonicalQueryString(queryParameters);
const url = `${baseUrl}?${queryString}`;
// PUT 方法返回需要的 headers
if (method === 'PUT' && options?.contentType) {
return {
url,
headers: {
'Content-Type': options.contentType,
},
};
}
return { url };
}
}