Skip to content

support cloudbox #1355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ fengmk2 <[email protected]> (https://github.com/fengmk2)
Yan Qing <[email protected]> (https://github.com/zensh)
mars-coder <[email protected]> (https://github.com/mars-coder)
Jacky Tang <[email protected]> (https://github.com/jackytck)

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ options:
- [retryMax] {Number}, used by auto retry send request count when request error is net error or timeout. **_NOTE:_** Not support `put` with stream, `putStream`, `append` with stream because the stream can only be consumed once
- [maxSockets] {Number} Maximum number of sockets to allow per host. Default is infinity
- [authorizationV4] {Boolean} Use V4 signature. Default is false.
- [cloudBoxId] {String} the CloudBox ID you want to access. When configuring this option, please set the endpoint option to the CloudBox endpoint and set the authorizationV4 option to true.

example:

Expand Down
7 changes: 5 additions & 2 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,19 @@ proto.authorization = function authorization(method, resource, subres, headers)
* @api private
*/
proto.authorizationV4 = function authorizationV4(method, requestParams, bucketName, objectName, additionalHeaders) {
const { cloudBoxId } = this.options;
const signRegion = cloudBoxId === undefined ? getStandardRegion(this.options.region) : cloudBoxId;
return signUtils.authorizationV4(
this.options.accessKeyId,
this.options.accessKeySecret,
getStandardRegion(this.options.region),
signRegion,
method,
requestParams,
bucketName,
objectName,
additionalHeaders,
this.options.headerEncoding
this.options.headerEncoding,
cloudBoxId
);
};

Expand Down
16 changes: 12 additions & 4 deletions lib/common/object/signatureUrlV4.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@ const proto = exports;
* @param {string[]} [additionalHeaders]
*/
proto.signatureUrlV4 = async function signatureUrlV4(method, expires, request, objectName, additionalHeaders) {
const { cloudBoxId } = this.options;
const product = signHelper.getProduct(cloudBoxId);
const signRegion = signHelper.getSignRegion(cloudBoxId, getStandardRegion(this.options.region));
const headers = (request && request.headers) || {};
const queries = Object.assign({}, (request && request.queries) || {});
const date = new Date();
const formattedDate = dateFormat(date, "UTC:yyyymmdd'T'HHMMss'Z'");
const onlyDate = formattedDate.split('T')[0];
const fixedAdditionalHeaders = signHelper.fixAdditionalHeaders(additionalHeaders);
const region = getStandardRegion(this.options.region);

if (fixedAdditionalHeaders.length > 0) {
queries['x-oss-additional-headers'] = fixedAdditionalHeaders.join(';');
}
queries['x-oss-credential'] = signHelper.getCredential(onlyDate, region, this.options.accessKeyId);
queries['x-oss-credential'] = signHelper.getCredential(onlyDate, signRegion, this.options.accessKeyId, product);
queries['x-oss-date'] = formattedDate;
queries['x-oss-expires'] = expires;
queries['x-oss-signature-version'] = 'OSS4-HMAC-SHA256';
Expand All @@ -54,9 +56,15 @@ proto.signatureUrlV4 = async function signatureUrlV4(method, expires, request, o
objectName,
fixedAdditionalHeaders
);
const stringToSign = signHelper.getStringToSign(region, formattedDate, canonicalRequest);
const stringToSign = signHelper.getStringToSign(signRegion, formattedDate, canonicalRequest, product);

queries['x-oss-signature'] = signHelper.getSignatureV4(this.options.accessKeySecret, onlyDate, region, stringToSign);
queries['x-oss-signature'] = signHelper.getSignatureV4(
this.options.accessKeySecret,
onlyDate,
signRegion,
stringToSign,
product
);

const signedUrl = urlUtil.parse(
this._getReqUrl({
Expand Down
44 changes: 36 additions & 8 deletions lib/common/signUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ const qs = require('qs');
const { lowercaseKeyHeader } = require('./utils/lowercaseKeyHeader');
const { encodeString } = require('./utils/encodeString');

/**
*
* @param {string} [cloudBoxId]
* @return {string}
*/
exports.getProduct = function getProduct(cloudBoxId) {
if (cloudBoxId === undefined) return 'oss';
return 'oss-cloudbox';
};
/**
*
* @param {string} [cloudBoxId]
* @param {string} region
* @return {string}
*/
exports.getSignRegion = function getSignRegion(cloudBoxId, region) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必填项在前,可选项在后

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

js没有必填概念吧,都要填的,只不过cloudBoxId为undefined

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

遵守函数定义的规范,你的JS doc写的是{string} [cloudBoxId],可选项放在前面了,如果两个都是必填,cloudBoxId的类型要增加undefined,且不能是可选

if (cloudBoxId === undefined) return region;
return cloudBoxId;
};

/**
*
* @param {String} resourcePath
Expand Down Expand Up @@ -201,13 +221,14 @@ exports.getCredential = function getCredential(date, region, accessKeyId, produc
* @param {string} region Standard region, e.g. cn-hangzhou
* @param {string} date ISO8601 UTC:yyyymmdd'T'HHMMss'Z'
* @param {string} canonicalRequest
* @param {string} product
* @returns {string}
*/
exports.getStringToSign = function getStringToSign(region, date, canonicalRequest) {
exports.getStringToSign = function getStringToSign(region, date, canonicalRequest, product) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

product改成可选项,product = 'oss',下面就不用写product || 'oss'了

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修改

const stringToSign = [
'OSS4-HMAC-SHA256',
date, // TimeStamp
this.getCredential(date.split('T')[0], region), // Scope
this.getCredential(date.split('T')[0], region, undefined, product || 'oss'), // Scope
crypto.createHash('sha256').update(canonicalRequest).digest('hex') // Hashed Canonical Request
];

Expand All @@ -219,12 +240,16 @@ exports.getStringToSign = function getStringToSign(region, date, canonicalReques
* @param {string} date yyyymmdd
* @param {string} region Standard region, e.g. cn-hangzhou
* @param {string} stringToSign
* @param {string} product
* @returns {string}
*/
exports.getSignatureV4 = function getSignatureV4(accessKeySecret, date, region, stringToSign) {
exports.getSignatureV4 = function getSignatureV4(accessKeySecret, date, region, stringToSign, product) {
const signingDate = crypto.createHmac('sha256', `aliyun_v4${accessKeySecret}`).update(date).digest();
const signingRegion = crypto.createHmac('sha256', signingDate).update(region).digest();
const signingOss = crypto.createHmac('sha256', signingRegion).update('oss').digest();
const signingOss = crypto
.createHmac('sha256', signingRegion)
.update(product || 'oss')
.digest();
const signingKey = crypto.createHmac('sha256', signingOss).update('aliyun_v4_request').digest();
const signatureValue = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');

Expand All @@ -243,6 +268,7 @@ exports.getSignatureV4 = function getSignatureV4(accessKeySecret, date, region,
* @param {string} [objectName]
* @param {string[]} [additionalHeaders]
* @param {string} [headerEncoding='utf-8']
* @param {string} [cloudBoxId]
* @returns {string}
*/
exports.authorizationV4 = function authorizationV4(
Expand All @@ -254,8 +280,10 @@ exports.authorizationV4 = function authorizationV4(
bucketName,
objectName,
additionalHeaders,
headerEncoding = 'utf-8'
headerEncoding = 'utf-8',
cloudBoxId
) {
const product = this.getProduct(cloudBoxId);
const fixedAdditionalHeaders = this.fixAdditionalHeaders(additionalHeaders);
const fixedHeaders = {};
Object.entries(request.headers).forEach(v => {
Expand All @@ -272,13 +300,13 @@ exports.authorizationV4 = function authorizationV4(
objectName,
fixedAdditionalHeaders
);
const stringToSign = this.getStringToSign(region, date, canonicalRequest);
const stringToSign = this.getStringToSign(region, date, canonicalRequest, product);
const onlyDate = date.split('T')[0];
const signatureValue = this.getSignatureV4(accessKeySecret, onlyDate, region, stringToSign);
const signatureValue = this.getSignatureV4(accessKeySecret, onlyDate, region, stringToSign, product);
const additionalHeadersValue =
fixedAdditionalHeaders.length > 0 ? `AdditionalHeaders=${fixedAdditionalHeaders.join(';')},` : '';

return `OSS4-HMAC-SHA256 Credential=${this.getCredential(onlyDate, region, accessKeyId)},${additionalHeadersValue}Signature=${signatureValue}`;
return `OSS4-HMAC-SHA256 Credential=${this.getCredential(onlyDate, region, accessKeyId, product)},${additionalHeadersValue}Signature=${signatureValue}`;
};

/**
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 2 additions & 8 deletions test/browser/browser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { getCredential } = require('../../lib/common/signUtils');
const { getStandardRegion } = require('../../lib/common/utils/getStandardRegion');
const { parseRestoreInfo } = require('../../lib/common/utils/parseRestoreInfo');
const { policy2Str } = require('../../lib/common/utils/policy2Str');
const { conditions } = require('../config');

let ossConfig;

Expand Down Expand Up @@ -67,14 +68,7 @@ describe('browser', () => {
await cleanBucket(store);
});

[
{
authorizationV4: false
},
{
authorizationV4: true
}
].forEach((moreConfigs, index) => {
conditions.forEach((moreConfigs, index) => {
describe(`test browser in iterate ${index}`, () => {
describe('stsTokenFreshTime', () => {
it('init stsTokenFreshTime', () => {
Expand Down
8 changes: 8 additions & 0 deletions test/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ config.oss = {
accessKeySecret: env.ALI_SDK_OSS_SECRET,
accountId: env.ALI_SDK_STS_ROLE.match(/^acs:ram::(\d+):role/i)[1], // Obtain the main account ID through roleRan
region: env.ALI_SDK_OSS_REGION,
cloudBoxId: env.ALI_CLOUD_BOX_ID,
endpoint: env.ALI_CLOUD_ENDPOINT,
authorizationV4: env.ALI_CLOUD_BOX_ID ? true : undefined,
// endpoint: env.ONCI ? `https://${USWEST}.aliyuncs.com` : undefined,
maxSocket: 50
};
Expand All @@ -22,5 +25,10 @@ config.sts = {
// callbackServer: env.ALI_SDK_CALLBACK_IP
};

config.conditions = [
{ authorizationV4: true },
env.ALI_CLOUD_BOX_ID === undefined ? { authorizationV4: false } : undefined
].filter(item => item !== undefined);

config.metaSyncTime = env.ONCI ? '1s' : '1000ms';
config.timeout = '120s';
36 changes: 26 additions & 10 deletions test/node/bucket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,14 @@ const { default: ResourceManager, ListResourceGroupsRequest } = require('@aliclo
const { Config: OpenConfig } = require('@alicloud/openapi-client');
const { RuntimeOptions } = require('@alicloud/tea-util');

const { oss: config, metaSyncTime, timeout } = require('../config');
const { oss: config, conditions, metaSyncTime, timeout } = require('../config');

describe('test/bucket.test.js', () => {
const { prefix, includesConf } = utils;
let store;
let bucket;
const { accountId } = config;
[
{
authorizationV4: false
},
{
authorizationV4: true
}
].forEach((moreConfigs, idx) => {
conditions.forEach((moreConfigs, idx) => {
describe(`test bucket in iterate ${idx}`, () => {
before(async () => {
store = oss({ ...config, ...moreConfigs });
Expand Down Expand Up @@ -64,7 +57,11 @@ describe('test/bucket.test.js', () => {
name = `ali-oss-test-putbucket-${prefix.replace(/[/.]/g, '-')}${idx}`;
// just for archive bucket test
archvieBucket = `ali-oss-archive-bucket-${prefix.replace(/[/.]/g, '-')}${idx}`;
await store.putBucket(archvieBucket, { StorageClass: 'Archive', timeout });
// cloudbox only support standard
await store.putBucket(archvieBucket, {
StorageClass: store.options.cloudBoxId ? 'Standard' : 'Archive',
timeout
});
});

it('should create a new bucket', async () => {
Expand All @@ -80,6 +77,7 @@ describe('test/bucket.test.js', () => {
});

it('should create an archive bucket', async () => {
if (store.options.cloudBoxId) return;
await utils.sleep(ms(metaSyncTime));
const result2 = await store.listBuckets(
{},
Expand Down Expand Up @@ -130,6 +128,7 @@ describe('test/bucket.test.js', () => {

describe('getBucketInfo', () => {
it('it should return correct bucketInfo when bucket exist', async () => {
if (store.options.cloudBoxId) return;
const result = await store.getBucketInfo(bucket);
assert.equal(result.res.status, 200);

Expand All @@ -138,6 +137,7 @@ describe('test/bucket.test.js', () => {
assert.equal(result.bucket.IntranetEndpoint, `${config.region}-internal.aliyuncs.com`);
assert.equal(result.bucket.AccessControlList.Grant, 'private');
assert.equal(result.bucket.StorageClass, 'Standard');
assert.equal(result.bucket.StorageClass, 'Standard');
});

it('it should return NoSuchBucketError when bucket not exist', async () => {
Expand Down Expand Up @@ -247,6 +247,7 @@ describe('test/bucket.test.js', () => {
});

it('should list buckets by subres', async () => {
if (store.options.cloudBoxId) return; // 云盒不支持tag
const tag = {
a: '1',
b: '2'
Expand All @@ -270,6 +271,7 @@ describe('test/bucket.test.js', () => {
});

it('should list buckets by group id', async () => {
if (store.options.cloudBoxId) return; // 云盒不支持group
const { accessKeyId, accessKeySecret } = config;
const openConfig = new OpenConfig({
accessKeyId,
Expand Down Expand Up @@ -472,11 +474,13 @@ describe('test/bucket.test.js', () => {
describe('putBucketCORS(), getBucketCORS(), deleteBucketCORS()', () => {
afterEach(async () => {
// delete it
if (store.options.cloudBoxId) return;
const result = await store.deleteBucketCORS(bucket, { timeout });
assert.equal(result.res.status, 204);
});

it('should create, get and delete the cors', async () => {
if (store.options.cloudBoxId) return; // 云盒不支持跨域
const rules = [
{
allowedOrigin: '*',
Expand All @@ -503,6 +507,7 @@ describe('test/bucket.test.js', () => {
});

it('should overwrite cors', async () => {
if (store.options.cloudBoxId) return;
const rules1 = [
{
allowedOrigin: '*',
Expand Down Expand Up @@ -578,6 +583,7 @@ describe('test/bucket.test.js', () => {
});

it('should throw error when rules not exist', async () => {
if (store.options.cloudBoxId) return;
try {
await store.getBucketCORS(bucket);
throw new Error('should not run');
Expand All @@ -588,6 +594,9 @@ describe('test/bucket.test.js', () => {
});

describe('putBucketRequestPayment(), getBucketRequestPayment()', () => {
before(function () {
if (store.options.cloudBoxId) this.skip(); // 云盒不支持请求着付费
});
it('should create, get the request payment', async () => {
try {
await store.putBucketRequestPayment(bucket, 'Requester');
Expand All @@ -608,6 +617,9 @@ describe('test/bucket.test.js', () => {
});

describe('getBucketTags() putBucketTags() deleteBucketTags()', () => {
before(function () {
if (store.options.cloudBoxId) this.skip(); // 云盒不支持tag
});
it('should get the tags of bucket', async () => {
try {
const result = await store.getBucketTags(bucket);
Expand Down Expand Up @@ -870,6 +882,7 @@ describe('test/bucket.test.js', () => {
});

it('should put the lifecycle with Transition', async () => {
if (store.options.cloudBoxId) return; // 云盒不支持tag
const res1 = await store.putBucketLifecycle(bucket, [
{
id: 'transition',
Expand Down Expand Up @@ -1538,6 +1551,9 @@ describe('test/bucket.test.js', () => {
});
});
describe('inventory()', () => {
before(function () {
if (store.options.cloudBoxId) this.skip(); // 云盒不支持清单
});
const field = [
'Size',
'LastModifiedDate',
Expand Down
Loading