From 71f575390a03f01e64134d77087646a0b42e58ed Mon Sep 17 00:00:00 2001 From: nanhe Date: Wed, 4 Dec 2024 09:38:54 +0800 Subject: [PATCH] feat: support credentials providers --- .github/workflows/node.js.yml | 1 + lib/roa.js | 61 +++++++++++++++++++++++++------ lib/rpc.js | 68 ++++++++++++++++++++++++++--------- test/roa.test.js | 20 +++++++++++ test/rpc.test.js | 20 +++++++++++ 5 files changed, 143 insertions(+), 27 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f2882e0..3d34f9c 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -15,6 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: node-version: [8.x, 10.x, 12.x, 14.x, 16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ diff --git a/lib/roa.js b/lib/roa.js index 050f700..ab44860 100644 --- a/lib/roa.js +++ b/lib/roa.js @@ -114,15 +114,30 @@ class ROAClient { throw new Error(`"config.endpoint" must starts with 'https://' or 'http://'.`); } assert(config.apiVersion, 'must pass "config.apiVersion"'); - assert(config.accessKeyId, 'must pass "config.accessKeyId"'); - assert(config.accessKeySecret, 'must pass "config.accessKeySecret"'); + if (config.credentialsProvider) { + if (typeof config.credentialsProvider.getCredentials !== 'function') { + throw new Error(`must pass "config.credentialsProvider" with function "getCredentials()"`); + } + this.credentialsProvider = config.credentialsProvider; + } else { + assert(config.accessKeyId, 'must pass "config.accessKeyId"'); + assert(config.accessKeySecret, 'must pass "config.accessKeySecret"'); + this.accessKeyId = config.accessKeyId; + this.accessKeySecret = config.accessKeySecret; + this.securityToken = config.securityToken; + this.credentialsProvider = { + getCredentials: async () => { + return { + accessKeyId: config.accessKeyId, + accessKeySecret: config.accessKeySecret, + securityToken: config.securityToken, + }; + } + }; + } this.endpoint = config.endpoint; - this.apiVersion = config.apiVersion; - this.accessKeyId = config.accessKeyId; - this.accessKeySecret = config.accessKeySecret; - this.securityToken = config.securityToken; this.host = url.parse(this.endpoint).hostname; this.opts = config.opts; var httpModule = this.endpoint.startsWith('https://') ? require('https') : require('http'); @@ -163,9 +178,31 @@ class ROAClient { } async request(method, uriPattern, query = {}, body = '', headers = {}, opts = {}) { + const credentials = await this.credentialsProvider.getCredentials(); + + const now = new Date(); + var defaultHeaders = { + accept: 'application/json', + date: now.toGMTString(), + host: this.host, + 'x-acs-signature-nonce': kitx.makeNonce(), + 'x-acs-version': this.apiVersion, + 'user-agent': helper.DEFAULT_UA, + 'x-sdk-client': helper.DEFAULT_CLIENT + }; + if (credentials && credentials.accessKeyId && credentials.accessKeySecret) { + defaultHeaders['x-acs-signature-method'] = 'HMAC-SHA1'; + defaultHeaders['x-acs-signature-version'] = '1.0'; + if (credentials.securityToken) { + defaultHeaders['x-acs-accesskey-id'] = credentials.accessKeyId; + defaultHeaders['x-acs-security-token'] = credentials.securityToken; + } + } + + var mixHeaders = Object.assign(defaultHeaders, keyLowerify(headers)); + var postBody = null; - var mixHeaders = Object.assign(this.buildHeaders(), keyLowerify(headers)); postBody = Buffer.from(body, 'utf8'); mixHeaders['content-md5'] = kitx.md5(postBody, 'base64'); mixHeaders['content-length'] = postBody.length; @@ -175,9 +212,13 @@ class ROAClient { url += `?${querystring.stringify(query)}`; } - const stringToSign = buildStringToSign(method, uriPattern, mixHeaders, query); - debug('stringToSign: %s', stringToSign); - mixHeaders['authorization'] = this.buildAuthorization(stringToSign); + if (credentials && credentials.accessKeyId && credentials.accessKeySecret) { + const stringToSign = buildStringToSign(method, uriPattern, mixHeaders, query); + debug('stringToSign: %s', stringToSign); + const utf8Buff = Buffer.from(stringToSign, 'utf8'); + const signature = kitx.sha1(utf8Buff, credentials.accessKeySecret, 'base64'); + mixHeaders['authorization'] = `acs ${credentials.accessKeyId}:${signature}`; + } const options = Object.assign({ method, diff --git a/lib/rpc.js b/lib/rpc.js index c0702df..3467914 100644 --- a/lib/rpc.js +++ b/lib/rpc.js @@ -111,9 +111,29 @@ class RPCClient { throw new Error(`"config.endpoint" must starts with 'https://' or 'http://'.`); } assert(config.apiVersion, 'must pass "config.apiVersion"'); - assert(config.accessKeyId, 'must pass "config.accessKeyId"'); - var accessKeySecret = config.secretAccessKey || config.accessKeySecret; - assert(accessKeySecret, 'must pass "config.accessKeySecret"'); + if (config.credentialsProvider) { + if (typeof config.credentialsProvider.getCredentials !== 'function') { + throw new Error(`must pass "config.credentialsProvider" with function "getCredentials()"`); + } + this.credentialsProvider = config.credentialsProvider; + } else { + assert(config.accessKeyId, 'must pass "config.accessKeyId"'); + var accessKeySecret = config.secretAccessKey || config.accessKeySecret; + assert(accessKeySecret, 'must pass "config.accessKeySecret"'); + this.accessKeyId = config.accessKeyId; + this.accessKeySecret = accessKeySecret; + this.securityToken = config.securityToken; + this.credentialsProvider = { + getCredentials: async () => { + return { + accessKeyId: config.accessKeyId, + accessKeySecret: accessKeySecret, + securityToken: config.securityToken, + }; + } + }; + } + if (config.endpoint.endsWith('/')) { config.endpoint = config.endpoint.slice(0, -1); @@ -121,9 +141,6 @@ class RPCClient { this.endpoint = config.endpoint; this.apiVersion = config.apiVersion; - this.accessKeyId = config.accessKeyId; - this.accessKeySecret = accessKeySecret; - this.securityToken = config.securityToken; this.verbose = verbose === true; // 非 codes 里的值,将抛出异常 this.codes = new Set([200, '200', 'OK', 'Success', 'success']); @@ -145,6 +162,7 @@ class RPCClient { } async request(action, params = {}, opts = {}) { + const credentials = await this.credentialsProvider.getCredentials(); // 1. compose params and opts opts = Object.assign({ headers: { @@ -164,20 +182,36 @@ class RPCClient { if (opts.formatParams !== false) { params = formatParams(params); } - const defaults = this._buildParams(); - params = Object.assign({Action: action}, defaults, params); + const defaultParams = { + Format: 'JSON', + Timestamp: timestamp(), + Version: this.apiVersion, + }; + if (credentials && credentials.accessKeyId && credentials.accessKeySecret) { + defaultParams.SignatureMethod = 'HMAC-SHA1'; + defaultParams.SignatureVersion = '1.0'; + defaultParams.SignatureNonce = kitx.makeNonce(); + defaultParams.AccessKeyId = credentials.accessKeyId; + if (credentials.accessKeySecret) { + defaultParams.SecurityToken = credentials.securityToken; + } + } + params = Object.assign({ Action: action }, defaultParams, params); - // 2. caculate signature const method = (opts.method || 'GET').toUpperCase(); const normalized = normalize(params); - const canonicalized = canonicalize(normalized); - // 2.1 get string to sign - const stringToSign = `${method}&${encode('/')}&${encode(canonicalized)}`; - // 2.2 get signature - const key = this.accessKeySecret + '&'; - const signature = kitx.sha1(stringToSign, key, 'base64'); - // add signature - normalized.push(['Signature', encode(signature)]); + // 2. caculate signature + if (credentials && credentials.accessKeyId && credentials.accessKeySecret) { + const canonicalized = canonicalize(normalized); + // 2.1 get string to sign + const stringToSign = `${method}&${encode('/')}&${encode(canonicalized)}`; + // 2.2 get signature + const key = credentials.accessKeySecret + '&'; + const signature = kitx.sha1(stringToSign, key, 'base64'); + // add signature + normalized.push(['Signature', encode(signature)]); + } + // 3. generate final url const url = opts.method === 'POST' ? `${this.endpoint}/` : `${this.endpoint}/?${canonicalize(normalized)}`; // 4. send request diff --git a/test/roa.test.js b/test/roa.test.js index b6c6c70..1167059 100644 --- a/test/roa.test.js +++ b/test/roa.test.js @@ -44,6 +44,13 @@ describe('roa core', function () { apiVersion: '1.0' }); }).to.throwException(/must pass "config\.accessKeyId"/); + expect(function () { + new ROAClient({ + endpoint: 'http://ecs.aliyuncs.com/', + apiVersion: '1.0', + credentialsProvider: null + }); + }).to.throwException(/must pass "config\.accessKeyId"/); }); it('should pass into "config.accessKeySecret"', function () { @@ -56,6 +63,19 @@ describe('roa core', function () { }).to.throwException(/must pass "config\.accessKeySecret"/); }); + it('should pass into "config.credentialsProvider" with getCredentials()', function () { + expect(function () { + new ROAClient({ + endpoint: 'http://ecs.aliyuncs.com/', + apiVersion: '1.0', + credentialsProvider: { + accessKeyId: 'test', + accessKeySecret: 'test', + } + }); + }).to.throwException(/must pass "config\.credentialsProvider" with function "getCredentials\(\)"/); + }); + it('should ok with http protocol', function () { const client = new ROAClient({ endpoint: 'http://ecs.aliyuncs.com/', diff --git a/test/rpc.test.js b/test/rpc.test.js index 4db23e0..9ca66ff 100644 --- a/test/rpc.test.js +++ b/test/rpc.test.js @@ -44,6 +44,13 @@ describe('rpc core', function () { apiVersion: '1.0' }); }).to.throwException(/must pass "config\.accessKeyId"/); + expect(function () { + new RPCClient({ + endpoint: 'http://ecs.aliyuncs.com/', + apiVersion: '1.0', + credentialsProvider: null + }); + }).to.throwException(/must pass "config\.accessKeyId"/); }); it('should pass into "config.accessKeySecret"', function () { @@ -56,6 +63,19 @@ describe('rpc core', function () { }).to.throwException(/must pass "config\.accessKeySecret"/); }); + it('should pass into "config.credentialsProvider" with getCredentials()', function () { + expect(function () { + new RPCClient({ + endpoint: 'http://ecs.aliyuncs.com/', + apiVersion: '1.0', + credentialsProvider: { + accessKeyId: 'test', + accessKeySecret: 'test', + } + }); + }).to.throwException(/must pass "config\.credentialsProvider" with function "getCredentials\(\)"/); + }); + it('should ok with http endpoint', function () { const client = new RPCClient({ endpoint: 'http://ecs.aliyuncs.com',