// 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 // 对于预签名 URL,payload 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 }; } }