Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,10 @@ const vault = require('node-vault')({
});
```

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

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

Expand Down
8 changes: 3 additions & 5 deletions example/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ process.env.DEBUG = 'node-vault'; // switch on debug mode

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

const options = {
requestOptions: {
followAllRedirects: true,
},
};
// Note: axios follows redirects by default (up to 5).
// Use maxRedirects in requestOptions to customise this behaviour.
const options = {};

vault.auths(options)
.then(console.log)
Expand Down
3 changes: 2 additions & 1 deletion example/pass_request_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
process.env.DEBUG = 'node-vault'; // switch on debug mode

// Pass request options at initialization time.
// These options are forwarded to postman-request for every request.
// TLS options (ca, cert, key, passphrase, agentOptions) are mapped to an
// https.Agent and forwarded to axios for every request.
const vault = require('./../src/index')({
requestOptions: {
agentOptions: {
Expand Down
14 changes: 13 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ declare namespace NodeVault {
[p: string]: any;
}

/** Backward-compatible TLS options from the former request/postman-request API. */
interface TlsOptions {
ca?: string | Buffer | Array<string | Buffer>;
cert?: string | Buffer | Array<string | Buffer>;
key?: string | Buffer | Array<string | Buffer>;
passphrase?: string;
pfx?: string | Buffer | Array<string | Buffer>;
strictSSL?: boolean;
agentOptions?: { [p: string]: any };
timeout?: number;
}

interface RequestOption extends Option {
path: string;
method: string;
Expand Down Expand Up @@ -149,7 +161,7 @@ declare namespace NodeVault {
noCustomHTTPVerbs?: boolean;
pathPrefix?: string;
token?: string;
requestOptions?: AxiosRequestConfig;
requestOptions?: AxiosRequestConfig & TlsOptions;
}
}

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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-vault",
"version": "0.11.0",
"version": "0.11.1",
"description": "Javascript client for HashiCorp's Vault",
"main": "./src/index.js",
"scripts": {
Expand Down
88 changes: 85 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,60 @@ module.exports = (config = {}) => {
if (config['request-promise'])
return config['request-promise'].defaults(rpDefaults);

const httpsAgent = rpDefaults.strictSSL === false
? new https.Agent({ rejectUnauthorized: false })
const baseAgentOptions = {};
if (rpDefaults.strictSSL === false) {
baseAgentOptions.rejectUnauthorized = false;
}

// Properties that map to https.Agent options for backward compatibility
// with the former postman-request / request library API.
const tlsOptionKeys = ['ca', 'cert', 'key', 'passphrase', 'pfx'];

// Build the default agent from base options + config.requestOptions TLS
// settings so the common case (no per-call overrides) reuses one agent.
const configReqOpts = config.requestOptions || {};
const defaultAgentOpts = { ...baseAgentOptions };
let hasDefaultTls = Object.keys(baseAgentOptions).length > 0;

tlsOptionKeys.forEach((prop) => {
if (configReqOpts[prop] !== undefined) {
defaultAgentOpts[prop] = configReqOpts[prop];
hasDefaultTls = true;
}
});
if (configReqOpts.agentOptions !== undefined) {
Object.assign(defaultAgentOpts, configReqOpts.agentOptions);
hasDefaultTls = true;
}
if (configReqOpts.strictSSL !== undefined) {
defaultAgentOpts.rejectUnauthorized = configReqOpts.strictSSL !== false;
hasDefaultTls = true;
}

const defaultHttpsAgent = hasDefaultTls
? new https.Agent(defaultAgentOpts)
: undefined;

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

// Snapshot config-level TLS references so we can detect per-call overrides.
const configTlsSnapshot = {};
tlsOptionKeys.forEach((prop) => {
if (configReqOpts[prop] !== undefined) configTlsSnapshot[prop] = configReqOpts[prop];
});
if (configReqOpts.agentOptions !== undefined) {
configTlsSnapshot.agentOptions = configReqOpts.agentOptions;
}
if (configReqOpts.strictSSL !== undefined) {
configTlsSnapshot.strictSSL = configReqOpts.strictSSL;
}

return function requestWrapper(options) {
const axiosOptions = {
method: options.method,
Expand All @@ -66,6 +108,46 @@ module.exports = (config = {}) => {
axiosOptions.data = options.json;
}

// Forward axios-native options when provided directly.
if (options.timeout !== undefined) {
axiosOptions.timeout = options.timeout;
}
if (options.httpAgent !== undefined) {
axiosOptions.httpAgent = options.httpAgent;
}

// Only create a per-request httpsAgent when per-call TLS options
// differ from the config defaults already baked into the instance.
let hasOverride = false;
const perRequestAgentOpts = {};

tlsOptionKeys.forEach((prop) => {
if (options[prop] !== undefined) {
perRequestAgentOpts[prop] = options[prop];
if (options[prop] !== configTlsSnapshot[prop]) hasOverride = true;
}
});

if (options.agentOptions !== undefined) {
Object.assign(perRequestAgentOpts, options.agentOptions);
if (options.agentOptions !== configTlsSnapshot.agentOptions) hasOverride = true;
}

if (options.strictSSL !== undefined) {
perRequestAgentOpts.rejectUnauthorized = options.strictSSL !== false;
if (options.strictSSL !== configTlsSnapshot.strictSSL) hasOverride = true;
}

if (hasOverride) {
axiosOptions.httpsAgent = new https.Agent({
...defaultAgentOpts,
...perRequestAgentOpts,
});
} else if (options.httpsAgent !== undefined) {
// Allow passing a pre-built httpsAgent directly (e.g. for proxies).
axiosOptions.httpsAgent = options.httpsAgent;
}

return instance(axiosOptions).then((response) => {
let requestPath;
try {
Expand Down
175 changes: 175 additions & 0 deletions test/unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -856,4 +856,179 @@ describe('node-vault', () => {
});
});
});

describe('axios TLS options forwarding', () => {
const https = require('https');
const axios = require('axios');
let axiosInstanceStub;
let axiosCreateStub;
let agentSpy;

beforeEach(() => {
// Stub axios.create to return a controllable instance stub
axiosInstanceStub = sinon.stub().resolves({
status: 200,
data: {},
});
axiosCreateStub = sinon.stub(axios, 'create').returns(axiosInstanceStub);
agentSpy = sinon.spy(https, 'Agent');
});

afterEach(() => {
sinon.restore();
});

it('should create default httpsAgent with ca option from config.requestOptions', () => {
index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
ca: 'my-custom-ca-cert',
},
});
agentSpy.should.have.been.called();
const agentArgs = agentSpy.lastCall.args[0];
expect(agentArgs).to.have.property('ca', 'my-custom-ca-cert');
expect(axiosCreateStub.lastCall.args[0]).to.have.property('httpsAgent');
});

it('should create default httpsAgent with cert and key from config.requestOptions', () => {
index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
cert: 'client-cert',
key: 'client-key',
passphrase: 'secret',
},
});
agentSpy.should.have.been.called();
const agentArgs = agentSpy.lastCall.args[0];
expect(agentArgs).to.have.property('cert', 'client-cert');
expect(agentArgs).to.have.property('key', 'client-key');
expect(agentArgs).to.have.property('passphrase', 'secret');
});

it('should create default httpsAgent from agentOptions in config.requestOptions', () => {
index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
agentOptions: {
securityOptions: 'SSL_OP_NO_SSLv3',
cert: 'agent-cert',
},
},
});
agentSpy.should.have.been.called();
const agentArgs = agentSpy.lastCall.args[0];
expect(agentArgs).to.have.property('securityOptions', 'SSL_OP_NO_SSLv3');
expect(agentArgs).to.have.property('cert', 'agent-cert');
});

it('should allow per-call TLS options to override config.requestOptions', () => {
const vault = index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
ca: 'default-ca',
},
});
return vault.read('secret/hello', { ca: 'override-ca' }).then(() => {
// Per-call override creates a new per-request agent
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
expect(axiosCallArg).to.have.property('httpsAgent');
expect(axiosCallArg.httpsAgent).to.be.an.instanceOf(https.Agent);
// The last agent created should have the override value
const agentArgs = agentSpy.lastCall.args[0];
expect(agentArgs).to.have.property('ca', 'override-ca');
});
});

it('should not create per-request httpsAgent when no TLS options are present', () => {
const vault = index({
endpoint: 'http://localhost:8200',
token: '123',
});
return vault.read('secret/hello').then(() => {
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
expect(axiosCallArg).to.not.have.property('httpsAgent');
});
});

it('should reuse default httpsAgent when config TLS options are unchanged', () => {
const vault = index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
ca: 'my-ca',
},
});
const agentCountAfterInit = agentSpy.callCount;
return vault.read('secret/hello').then(() => {
// No new agent should be created for request with same config options
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
expect(axiosCallArg).to.not.have.property('httpsAgent');
expect(agentSpy.callCount).to.equal(agentCountAfterInit);
});
});

it('should handle strictSSL: false in requestOptions', () => {
index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
strictSSL: false,
},
});
agentSpy.should.have.been.called();
const agentArgs = agentSpy.lastCall.args[0];
expect(agentArgs).to.have.property('rejectUnauthorized', false);
});

it('should forward timeout from requestOptions to axios', () => {
const vault = index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
timeout: 5000,
},
});
return vault.read('secret/hello').then(() => {
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
expect(axiosCallArg).to.have.property('timeout', 5000);
});
});

it('should forward httpsAgent from requestOptions to axios', () => {
const customAgent = new https.Agent({ keepAlive: true });
const vault = index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
httpsAgent: customAgent,
},
});
return vault.read('secret/hello').then(() => {
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
expect(axiosCallArg).to.have.property('httpsAgent', customAgent);
});
});

it('should forward httpAgent from requestOptions to axios', () => {
const http = require('http');
const customAgent = new http.Agent();
const vault = index({
endpoint: 'http://localhost:8200',
token: '123',
requestOptions: {
httpAgent: customAgent,
},
});
return vault.read('secret/hello').then(() => {
const axiosCallArg = axiosInstanceStub.firstCall.args[0];
expect(axiosCallArg).to.have.property('httpAgent', customAgent);
});
});
});
});
Loading