Skip to content

Commit 660cc85

Browse files
authored
Merge pull request #286 from nodevault/copilot/fix-issue-285
Restore backward-compatible requestOptions for axios migration
2 parents 850e5b9 + 8c23701 commit 660cc85

8 files changed

Lines changed: 285 additions & 17 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,10 @@ const vault = require('node-vault')({
251251
});
252252
```
253253

254-
The `requestOptions` object is passed through to the underlying HTTP library
255-
([postman-request](https://www.npmjs.com/package/postman-request)) for every request. You can
256-
use it to configure any supported request option, including `agentOptions`, custom `headers`,
257-
or a custom `agent`.
254+
The `requestOptions` object supports TLS/SSL options (`ca`, `cert`, `key`, `passphrase`,
255+
`agentOptions`, `strictSSL`) as well as `timeout`, `httpsAgent`, and `httpAgent`. TLS options
256+
are mapped to an `https.Agent` and applied to every request. You can also pass native
257+
[axios](https://axios-http.com/) request options such as custom `headers`.
258258

259259
You can also pass request options per-call to any method:
260260

example/auth.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ process.env.DEBUG = 'node-vault'; // switch on debug mode
44

55
const vault = require('./../src/index')();
66

7-
const options = {
8-
requestOptions: {
9-
followAllRedirects: true,
10-
},
11-
};
7+
// Note: axios follows redirects by default (up to 5).
8+
// Use maxRedirects in requestOptions to customise this behaviour.
9+
const options = {};
1210

1311
vault.auths(options)
1412
.then(console.log)

example/pass_request_options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
process.env.DEBUG = 'node-vault'; // switch on debug mode
44

55
// Pass request options at initialization time.
6-
// These options are forwarded to postman-request for every request.
6+
// TLS options (ca, cert, key, passphrase, agentOptions) are mapped to an
7+
// https.Agent and forwarded to axios for every request.
78
const vault = require('./../src/index')({
89
requestOptions: {
910
agentOptions: {

index.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ declare namespace NodeVault {
1111
[p: string]: any;
1212
}
1313

14+
/** Backward-compatible TLS options from the former request/postman-request API. */
15+
interface TlsOptions {
16+
ca?: string | Buffer | Array<string | Buffer>;
17+
cert?: string | Buffer | Array<string | Buffer>;
18+
key?: string | Buffer | Array<string | Buffer>;
19+
passphrase?: string;
20+
pfx?: string | Buffer | Array<string | Buffer>;
21+
strictSSL?: boolean;
22+
agentOptions?: { [p: string]: any };
23+
timeout?: number;
24+
}
25+
1426
interface RequestOption extends Option {
1527
path: string;
1628
method: string;
@@ -149,7 +161,7 @@ declare namespace NodeVault {
149161
noCustomHTTPVerbs?: boolean;
150162
pathPrefix?: string;
151163
token?: string;
152-
requestOptions?: AxiosRequestConfig;
164+
requestOptions?: AxiosRequestConfig & TlsOptions;
153165
}
154166
}
155167

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-vault",
3-
"version": "0.11.0",
3+
"version": "0.11.1",
44
"description": "Javascript client for HashiCorp's Vault",
55
"main": "./src/index.js",
66
"scripts": {

src/index.js

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,60 @@ module.exports = (config = {}) => {
4343
if (config['request-promise'])
4444
return config['request-promise'].defaults(rpDefaults);
4545

46-
const httpsAgent = rpDefaults.strictSSL === false
47-
? new https.Agent({ rejectUnauthorized: false })
46+
const baseAgentOptions = {};
47+
if (rpDefaults.strictSSL === false) {
48+
baseAgentOptions.rejectUnauthorized = false;
49+
}
50+
51+
// Properties that map to https.Agent options for backward compatibility
52+
// with the former postman-request / request library API.
53+
const tlsOptionKeys = ['ca', 'cert', 'key', 'passphrase', 'pfx'];
54+
55+
// Build the default agent from base options + config.requestOptions TLS
56+
// settings so the common case (no per-call overrides) reuses one agent.
57+
const configReqOpts = config.requestOptions || {};
58+
const defaultAgentOpts = { ...baseAgentOptions };
59+
let hasDefaultTls = Object.keys(baseAgentOptions).length > 0;
60+
61+
tlsOptionKeys.forEach((prop) => {
62+
if (configReqOpts[prop] !== undefined) {
63+
defaultAgentOpts[prop] = configReqOpts[prop];
64+
hasDefaultTls = true;
65+
}
66+
});
67+
if (configReqOpts.agentOptions !== undefined) {
68+
Object.assign(defaultAgentOpts, configReqOpts.agentOptions);
69+
hasDefaultTls = true;
70+
}
71+
if (configReqOpts.strictSSL !== undefined) {
72+
defaultAgentOpts.rejectUnauthorized = configReqOpts.strictSSL !== false;
73+
hasDefaultTls = true;
74+
}
75+
76+
const defaultHttpsAgent = hasDefaultTls
77+
? new https.Agent(defaultAgentOpts)
4878
: undefined;
4979

5080
const instance = axios.create({
5181
// Accept all HTTP status codes (equivalent to request's simple: false)
5282
// so that vault response handling logic can process non-2xx responses.
5383
validateStatus: () => true,
54-
...(httpsAgent ? { httpsAgent } : {}),
84+
...(defaultHttpsAgent ? { httpsAgent: defaultHttpsAgent } : {}),
5585
...(rpDefaults.timeout ? { timeout: rpDefaults.timeout } : {}),
5686
});
5787

88+
// Snapshot config-level TLS references so we can detect per-call overrides.
89+
const configTlsSnapshot = {};
90+
tlsOptionKeys.forEach((prop) => {
91+
if (configReqOpts[prop] !== undefined) configTlsSnapshot[prop] = configReqOpts[prop];
92+
});
93+
if (configReqOpts.agentOptions !== undefined) {
94+
configTlsSnapshot.agentOptions = configReqOpts.agentOptions;
95+
}
96+
if (configReqOpts.strictSSL !== undefined) {
97+
configTlsSnapshot.strictSSL = configReqOpts.strictSSL;
98+
}
99+
58100
return function requestWrapper(options) {
59101
const axiosOptions = {
60102
method: options.method,
@@ -66,6 +108,46 @@ module.exports = (config = {}) => {
66108
axiosOptions.data = options.json;
67109
}
68110

111+
// Forward axios-native options when provided directly.
112+
if (options.timeout !== undefined) {
113+
axiosOptions.timeout = options.timeout;
114+
}
115+
if (options.httpAgent !== undefined) {
116+
axiosOptions.httpAgent = options.httpAgent;
117+
}
118+
119+
// Only create a per-request httpsAgent when per-call TLS options
120+
// differ from the config defaults already baked into the instance.
121+
let hasOverride = false;
122+
const perRequestAgentOpts = {};
123+
124+
tlsOptionKeys.forEach((prop) => {
125+
if (options[prop] !== undefined) {
126+
perRequestAgentOpts[prop] = options[prop];
127+
if (options[prop] !== configTlsSnapshot[prop]) hasOverride = true;
128+
}
129+
});
130+
131+
if (options.agentOptions !== undefined) {
132+
Object.assign(perRequestAgentOpts, options.agentOptions);
133+
if (options.agentOptions !== configTlsSnapshot.agentOptions) hasOverride = true;
134+
}
135+
136+
if (options.strictSSL !== undefined) {
137+
perRequestAgentOpts.rejectUnauthorized = options.strictSSL !== false;
138+
if (options.strictSSL !== configTlsSnapshot.strictSSL) hasOverride = true;
139+
}
140+
141+
if (hasOverride) {
142+
axiosOptions.httpsAgent = new https.Agent({
143+
...defaultAgentOpts,
144+
...perRequestAgentOpts,
145+
});
146+
} else if (options.httpsAgent !== undefined) {
147+
// Allow passing a pre-built httpsAgent directly (e.g. for proxies).
148+
axiosOptions.httpsAgent = options.httpsAgent;
149+
}
150+
69151
return instance(axiosOptions).then((response) => {
70152
let requestPath;
71153
try {

test/unit.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,4 +856,179 @@ describe('node-vault', () => {
856856
});
857857
});
858858
});
859+
860+
describe('axios TLS options forwarding', () => {
861+
const https = require('https');
862+
const axios = require('axios');
863+
let axiosInstanceStub;
864+
let axiosCreateStub;
865+
let agentSpy;
866+
867+
beforeEach(() => {
868+
// Stub axios.create to return a controllable instance stub
869+
axiosInstanceStub = sinon.stub().resolves({
870+
status: 200,
871+
data: {},
872+
});
873+
axiosCreateStub = sinon.stub(axios, 'create').returns(axiosInstanceStub);
874+
agentSpy = sinon.spy(https, 'Agent');
875+
});
876+
877+
afterEach(() => {
878+
sinon.restore();
879+
});
880+
881+
it('should create default httpsAgent with ca option from config.requestOptions', () => {
882+
index({
883+
endpoint: 'http://localhost:8200',
884+
token: '123',
885+
requestOptions: {
886+
ca: 'my-custom-ca-cert',
887+
},
888+
});
889+
agentSpy.should.have.been.called();
890+
const agentArgs = agentSpy.lastCall.args[0];
891+
expect(agentArgs).to.have.property('ca', 'my-custom-ca-cert');
892+
expect(axiosCreateStub.lastCall.args[0]).to.have.property('httpsAgent');
893+
});
894+
895+
it('should create default httpsAgent with cert and key from config.requestOptions', () => {
896+
index({
897+
endpoint: 'http://localhost:8200',
898+
token: '123',
899+
requestOptions: {
900+
cert: 'client-cert',
901+
key: 'client-key',
902+
passphrase: 'secret',
903+
},
904+
});
905+
agentSpy.should.have.been.called();
906+
const agentArgs = agentSpy.lastCall.args[0];
907+
expect(agentArgs).to.have.property('cert', 'client-cert');
908+
expect(agentArgs).to.have.property('key', 'client-key');
909+
expect(agentArgs).to.have.property('passphrase', 'secret');
910+
});
911+
912+
it('should create default httpsAgent from agentOptions in config.requestOptions', () => {
913+
index({
914+
endpoint: 'http://localhost:8200',
915+
token: '123',
916+
requestOptions: {
917+
agentOptions: {
918+
securityOptions: 'SSL_OP_NO_SSLv3',
919+
cert: 'agent-cert',
920+
},
921+
},
922+
});
923+
agentSpy.should.have.been.called();
924+
const agentArgs = agentSpy.lastCall.args[0];
925+
expect(agentArgs).to.have.property('securityOptions', 'SSL_OP_NO_SSLv3');
926+
expect(agentArgs).to.have.property('cert', 'agent-cert');
927+
});
928+
929+
it('should allow per-call TLS options to override config.requestOptions', () => {
930+
const vault = index({
931+
endpoint: 'http://localhost:8200',
932+
token: '123',
933+
requestOptions: {
934+
ca: 'default-ca',
935+
},
936+
});
937+
return vault.read('secret/hello', { ca: 'override-ca' }).then(() => {
938+
// Per-call override creates a new per-request agent
939+
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
940+
expect(axiosCallArg).to.have.property('httpsAgent');
941+
expect(axiosCallArg.httpsAgent).to.be.an.instanceOf(https.Agent);
942+
// The last agent created should have the override value
943+
const agentArgs = agentSpy.lastCall.args[0];
944+
expect(agentArgs).to.have.property('ca', 'override-ca');
945+
});
946+
});
947+
948+
it('should not create per-request httpsAgent when no TLS options are present', () => {
949+
const vault = index({
950+
endpoint: 'http://localhost:8200',
951+
token: '123',
952+
});
953+
return vault.read('secret/hello').then(() => {
954+
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
955+
expect(axiosCallArg).to.not.have.property('httpsAgent');
956+
});
957+
});
958+
959+
it('should reuse default httpsAgent when config TLS options are unchanged', () => {
960+
const vault = index({
961+
endpoint: 'http://localhost:8200',
962+
token: '123',
963+
requestOptions: {
964+
ca: 'my-ca',
965+
},
966+
});
967+
const agentCountAfterInit = agentSpy.callCount;
968+
return vault.read('secret/hello').then(() => {
969+
// No new agent should be created for request with same config options
970+
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
971+
expect(axiosCallArg).to.not.have.property('httpsAgent');
972+
expect(agentSpy.callCount).to.equal(agentCountAfterInit);
973+
});
974+
});
975+
976+
it('should handle strictSSL: false in requestOptions', () => {
977+
index({
978+
endpoint: 'http://localhost:8200',
979+
token: '123',
980+
requestOptions: {
981+
strictSSL: false,
982+
},
983+
});
984+
agentSpy.should.have.been.called();
985+
const agentArgs = agentSpy.lastCall.args[0];
986+
expect(agentArgs).to.have.property('rejectUnauthorized', false);
987+
});
988+
989+
it('should forward timeout from requestOptions to axios', () => {
990+
const vault = index({
991+
endpoint: 'http://localhost:8200',
992+
token: '123',
993+
requestOptions: {
994+
timeout: 5000,
995+
},
996+
});
997+
return vault.read('secret/hello').then(() => {
998+
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
999+
expect(axiosCallArg).to.have.property('timeout', 5000);
1000+
});
1001+
});
1002+
1003+
it('should forward httpsAgent from requestOptions to axios', () => {
1004+
const customAgent = new https.Agent({ keepAlive: true });
1005+
const vault = index({
1006+
endpoint: 'http://localhost:8200',
1007+
token: '123',
1008+
requestOptions: {
1009+
httpsAgent: customAgent,
1010+
},
1011+
});
1012+
return vault.read('secret/hello').then(() => {
1013+
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
1014+
expect(axiosCallArg).to.have.property('httpsAgent', customAgent);
1015+
});
1016+
});
1017+
1018+
it('should forward httpAgent from requestOptions to axios', () => {
1019+
const http = require('http');
1020+
const customAgent = new http.Agent();
1021+
const vault = index({
1022+
endpoint: 'http://localhost:8200',
1023+
token: '123',
1024+
requestOptions: {
1025+
httpAgent: customAgent,
1026+
},
1027+
});
1028+
return vault.read('secret/hello').then(() => {
1029+
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
1030+
expect(axiosCallArg).to.have.property('httpAgent', customAgent);
1031+
});
1032+
});
1033+
});
8591034
});

0 commit comments

Comments
 (0)