Skip to content

Commit

Permalink
feat: support signature v4 (#1277)
Browse files Browse the repository at this point in the history
* feat: support signature v4

* fix: operator is not support in node v10 and v12

* fix: error in canonicalHeaders and queries

* fix: hasOwn in node v10

* test: modify bucket name

* fix: signature v4 url

---------

Co-authored-by: zhengzuoyu.zzy <zhengzuoyu.zzy@alibaba-inc.com>
  • Loading branch information
YunZZY and zhengzuoyu.zzy authored Jan 18, 2024
1 parent bd723bb commit 8bbe9b1
Show file tree
Hide file tree
Showing 33 changed files with 13,624 additions and 26,944 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
}
],
'no-buffer-constructor': [2],
'no-void': 'warn',
'comma-dangle': [0],
'import/prefer-default-export': [0]
}
Expand Down
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ options:
- [proxy] {String | Object}, proxy agent uri or options, default is null.
- [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.

example:

Expand Down Expand Up @@ -435,6 +436,42 @@ for (let i = 0; i <= store.options.retryMax; i++) {
}
```

6. use V4 signature, and use optional additionalHeaders option which type is a string array, and the values inside need to be included in the header.

```js
const OSS = require('ali-oss');

const store = new OSS({
accessKeyId: 'your access key',
accessKeySecret: 'your access secret',
bucket: 'your bucket name',
region: 'oss-cn-hangzhou',
authorizationV4: true
});

try {
const bucketInfo = await store.getBucketInfo('your bucket name');
console.log(bucketInfo);
} catch (e) {
console.log(e);
}

try {
const putObjectResult = await store.put('your bucket name', 'your object name', {
headers: {
// The headers of this request
'header1': 'value1',
'header2': 'value2'
},
// The keys of the request headers that need to be calculated into the V4 signature. Please ensure that these additional headers are included in the request headers.
additionalHeaders: ['additional header1', 'additional header2']
});
console.log(putObjectResult);
} catch (e) {
console.log(e);
}
```

## Bucket Operations

### .listBuckets(query[, options])
Expand Down Expand Up @@ -2739,6 +2776,54 @@ const url = await store.asyncSignatureUrl('ossdemo.png', {
console.log(url);
```
### .signatureUrlV4(method, expires[, request, objectName, additionalHeaders])
Generate a signed URL for V4 of an OSS resource and share the URL to allow authorized third-party users to access the resource.
parameters:
- method {string} the HTTP method
- expires {number} the signed URL will expire after the set number of seconds
- [request] {Object} optional request parameters
- [headers] {Object} headers of http requests, please make sure these request headers are set during the actual request
- [queries] {Object} queries of the signed URL, please ensure that if the query only has key, the value is set to null
- [objectName] {string} object name
- [additionalHeaders] {string[]} the keys of the request headers that need to be calculated into the V4 signature, please ensure that these additional headers are included in the request headers
Success will return signature url.
example:
```js
// GetObject
const getObjectUrl = await store.signatureUrlV4('GET', 60, undefined, 'your obejct name');
console.log(getObjectUrl);
// --------------------------------------------------
const getObjectUrl = await store.signatureUrlV4('GET', 60, {
headers: {
'Cache-Control': 'no-cache'
},
queries: {
versionId: 'version ID of your object'
}
}, 'your obejct name', ['Cache-Control']);
console.log(getObjectUrl);
// -------------------------------------------------
// PutObject
const putObejctUrl = await store.signatureUrlV4('PUT', 60, undefined, 'your obejct name');
console.log(putObejctUrl);
// --------------------------------------------------
const putObejctUrl = await store.signatureUrlV4('PUT', 60, {
headers: {
'Content-Type': 'text/plain',
'Content-MD5': 'xxx',
'Content-Length': 1
}
}, 'your obejct name', ['Content-Length']);
console.log(putObejctUrl);
```
### .putACL(name, acl[, options])
Set object's ACL.
Expand Down
1 change: 1 addition & 0 deletions lib/browser/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ proto._bucketRequestParams = function _bucketRequestParams(method, bucket, subre
method,
bucket,
subres,
additionalHeaders: options && options.additionalHeaders,
timeout: options && options.timeout,
ctx: options && options.ctx
};
Expand Down
29 changes: 29 additions & 0 deletions lib/browser/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { getReqUrl } = require('../common/client/getReqUrl');
const { setSTSToken } = require('../common/utils/setSTSToken');
const { retry } = require('../common/utils/retry');
const { isFunction } = require('../common/utils/isFunction');
const { getStandardRegion } = require('../common/utils/getStandardRegion');

const globalHttpAgent = new AgentKeepalive();

Expand Down Expand Up @@ -191,6 +192,34 @@ proto.authorization = function authorization(method, resource, subres, headers)
);
};

/**
* get authorization header v4
*
* @param {string} method
* @param {Object} requestParams
* @param {Object} requestParams.headers
* @param {(string|string[]|Object)} [requestParams.queries]
* @param {string} [bucketName]
* @param {string} [objectName]
* @param {string[]} [additionalHeaders]
* @return {string}
*
* @api private
*/
proto.authorizationV4 = function authorizationV4(method, requestParams, bucketName, objectName, additionalHeaders) {
return signUtils.authorizationV4(
this.options.accessKeyId,
this.options.accessKeySecret,
getStandardRegion(this.options.region),
method,
requestParams,
bucketName,
objectName,
additionalHeaders,
this.options.headerEncoding
);
};

/**
* request oss server
* @param {Object} params
Expand Down
2 changes: 2 additions & 0 deletions lib/browser/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ merge(proto, require('../common/object/getObjectUrl'));
merge(proto, require('../common/object/generateObjectUrl'));
merge(proto, require('../common/object/signatureUrl'));
merge(proto, require('../common/object/asyncSignatureUrl'));
merge(proto, require('../common/object/signatureUrlV4'));

proto.putMeta = async function putMeta(name, meta, options) {
const copyResult = await this.copy(name, name, {
Expand Down Expand Up @@ -340,6 +341,7 @@ proto._objectRequestParams = function _objectRequestParams(method, name, options
bucket: this.options.bucket,
method,
subres: options && options.subres,
additionalHeaders: options && options.additionalHeaders,
timeout: options && options.timeout,
ctx: options && options.ctx
};
Expand Down
1 change: 1 addition & 0 deletions lib/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ proto._bucketRequestParams = function _bucketRequestParams(method, bucket, subre
method,
bucket,
subres,
additionalHeaders: options && options.additionalHeaders,
timeout: options && options.timeout,
ctx: options && options.ctx
};
Expand Down
29 changes: 29 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const { getReqUrl } = require('./common/client/getReqUrl');
const { setSTSToken } = require('./common/utils/setSTSToken');
const { retry } = require('./common/utils/retry');
const { isFunction } = require('./common/utils/isFunction');
const { getStandardRegion } = require('./common/utils/getStandardRegion');

const globalHttpAgent = new AgentKeepalive();
const globalHttpsAgent = new HttpsAgentKeepalive();
Expand Down Expand Up @@ -157,6 +158,34 @@ proto.authorization = function authorization(method, resource, subres, headers)
);
};

/**
* get authorization header v4
*
* @param {string} method
* @param {Object} requestParams
* @param {Object} requestParams.headers
* @param {Object} [requestParams.queries]
* @param {string} [bucketName]
* @param {string} [objectName]
* @param {string[]} [additionalHeaders]
* @return {string}
*
* @api private
*/
proto.authorizationV4 = function authorizationV4(method, requestParams, bucketName, objectName, additionalHeaders) {
return signUtils.authorizationV4(
this.options.accessKeyId,
this.options.accessKeySecret,
getStandardRegion(this.options.region),
method,
requestParams,
bucketName,
objectName,
additionalHeaders,
this.options.headerEncoding
);
};

/**
* request oss server
* @param {Object} params
Expand Down
3 changes: 2 additions & 1 deletion lib/common/client/initOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ module.exports = function (options) {
headerEncoding: 'utf-8',
refreshSTSToken: null,
refreshSTSTokenInterval: 60000 * 5,
retryMax: 0
retryMax: 0,
authorizationV4: false // 启用v4签名,默认关闭
},
options
);
Expand Down
1 change: 1 addition & 0 deletions lib/common/object/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ merge(proto, require('./generateObjectUrl'));
merge(proto, require('./getObjectUrl'));
merge(proto, require('./signatureUrl'));
merge(proto, require('./asyncSignatureUrl'));
merge(proto, require('./SignatureUrlV4'));
70 changes: 70 additions & 0 deletions lib/common/object/signatureUrlV4.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const dateFormat = require('dateformat');
const urlUtil = require('url');

const signHelper = require('../../common/signUtils');
const { setSTSToken } = require('../utils/setSTSToken');
const { isFunction } = require('../utils/isFunction');
const { getStandardRegion } = require('../utils/getStandardRegion');

const proto = exports;

/**
* signatureUrlV4
*
* @param {string} method
* @param {number} expires
* @param {Object} [request]
* @param {Object} [request.headers]
* @param {Object} [request.queries]
* @param {string} [objectName]
* @param {string[]} [additionalHeaders]
*/
proto.signatureUrlV4 = async function signatureUrlV4(method, expires, request, objectName, additionalHeaders) {
const headers = (request && request.headers) || {};
const queries = { ...((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'] = `${this.options.accessKeyId}/${onlyDate}/${region}/oss/aliyun_v4_request`;
queries['x-oss-date'] = formattedDate;
queries['x-oss-expires'] = expires;
queries['x-oss-signature-version'] = 'OSS4-HMAC-SHA256';

if (this.options.stsToken && isFunction(this.options.refreshSTSToken)) {
await setSTSToken.call(this);
}

if (this.options.stsToken) {
queries['x-oss-security-token'] = this.options.stsToken;
}

const canonicalRequest = signHelper.getCanonicalRequest(
method,
{
headers,
queries
},
this.options.bucket,
objectName,
fixedAdditionalHeaders
);
const stringToSign = signHelper.getStringToSign(region, formattedDate, canonicalRequest);

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

const signedUrl = urlUtil.parse(
this._getReqUrl({
bucket: this.options.bucket,
object: objectName
})
);
signedUrl.query = { ...queries };

return signedUrl.format();
};
Loading

0 comments on commit 8bbe9b1

Please sign in to comment.