313 lines
9.6 KiB
JavaScript
313 lines
9.6 KiB
JavaScript
// 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) {
|
||
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');
|
||
}
|
||
}
|