WebSocketClient does not respect injecting CAs
I have a custom CA that I need to use in order to access a websocket through wss. I would expect that something like this would connect to the websocket:
require('ssl-root-cas/latest').inject(). addFile(__dirname + '/my-awesome-ca.pem');
let WebsocketClient = require('websocket').client;
let client = new WebsocketClient({ });
client.connect(`wss://${url}`);
However, when I run this code, I get the following error:
{ Error: unable to verify the first certificate
at Error (native)
at TLSSocket.<anonymous> (_tls_wrap.js:1060:38)
at emitNone (events.js:86:13)
at TLSSocket.emit (events.js:185:7)
at TLSSocket._finishInit (_tls_wrap.js:584:8)
at TLSWrap.ssl.onhandshakedone (_tls_wrap.js:416:38) code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' }
This implies that the certificate I added is not being used.
I can verify that the certificate is being added properly since I can connect to the websocket using the ws module, like this:
require('ssl-root-cas/latest').inject(). addFile(__dirname + '/my-awesome-ca.pem');
var WebSocket = require('ws');
var ws = new WebSocket(`${url}`);
ws.on('open', function open() {
console.log('OPENED');
});
And this works as expected.
I tried other variants to get this working, like this:
let allCas = require('ssl-root-cas/latest').inject(). addFile(__dirname + '/my-awesome-ca.pem');
let WebsocketClient = require('websocket').client;
let client = new WebsocketClient({ tlsOptions: {ca: allCas } });
client.connect(`wss://${url}`);
But the same error.
When I run this option (turning off security and not something I want to do), things work as expected:
let allCas = require('ssl-root-cas/latest').inject(). addFile(__dirname + '/my-awesome-ca.pem');
let WebsocketClient = require('websocket').client;
let client = new WebsocketClient({ tlsOptions: {rejectUnauthorized: false } });
client.connect(`wss://${url}`);
My guess is that for some reason the WebSocketClient class is not applying the global CAs when constructing the initial http request.
I can provide more information if you like.
It looks like the problem comes from the {agent: false} option for creating the http request.
If I change the call to connect to this:
client.connect(`wss://${url}`, undefined, undefined, undefined, {agent: undefined});
I am able to connect correctly. The call above uses the default, global agent instead of constructing a new one.
I am not sure of the proper solution to this, but there are some options I can come up with:
- better documentation (ie- no code changes)
- change
{agent: false}to{agent: undefined}in the WebSocket client constructor - augment the WebSocket client constructor config object to toggle the kind of agent you want to use.
I think this is expected.
"agent: false" uses a list of well known CAs. Your injected CAs are ignored. "agent: undefined" uses a global HTTPS agent which ignores the list of well known CAs. Then your injected CAs are used.
Unfortunately this is poorly documented and I inferred this behavior from HTTPS docs at https://nodejs.org/api/https.html
I think this depends on your initial set of expectations. :) It may be that this behavior adheres to the original intention of the author of this module, but nowhere in the docs of this module do I see that a new agent is created for each request (did I miss it?).
But, the fact that I need to resort to such an ugly call to connect implies that this feature has not been anticipated yet.
So, at a minimum, I'd say the documentation needs to be updated, but my personal preference would be to augment the WebSocketClient config to support this as an option directly.
This is something I could do.