From 1004c78db7d6763f21c98fa3db2f12e688ca33ff Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 28 May 2024 17:06:34 +0200 Subject: [PATCH] feat: add `unixSocket` option and `+unix` suffix support to protocol (#1874) * fix: consider path when protocol is ws/wss * fix: add test * feat: add `unixSocket` option and `+unix` suffix support to protocol * fix: readme * fix: minor refactor * fix: validate protocol only when parsing url * fix: use object.assign * fix: edge cases * style: add comments --- README.md | 6 +++-- src/lib/client.ts | 11 +++++++-- src/lib/connect/index.ts | 48 +++++++++++++++++++++++++++------------- test/mqtt.ts | 21 ++++++++++++++++++ 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d4d31f96d..e82ca1b55 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,6 @@ Hello mqtt MQTT.js can be used in React Native applications. To use it, see the [React Native example](https://github.com/MaximoLiberata/react-native-mqtt.js-example) - If you want to run your own MQTT broker, you can use [Mosquitto](http://mosquitto.org) or [Aedes-cli](https://github.com/moscajs/aedes-cli), and launch it. @@ -354,7 +353,9 @@ Connects to the broker specified by the given url and options and returns a [Client](#client). The URL can be on the following protocols: 'mqtt', 'mqtts', 'tcp', -'tls', 'ws', 'wss', 'wxs', 'alis'. The URL can also be an object as returned by +'tls', 'ws', 'wss', 'wxs', 'alis'. If you are trying to connect to a unix socket just append the `+unix` suffix to the protocol (ex: `mqtt+unix`). This will set the `unixSocket` property automatically. + +The URL can also be an object as returned by [`URL.parse()`](http://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost), in that case the two objects are merged, i.e. you can pass a single object with both the URL and the connect options. @@ -466,6 +467,7 @@ The arguments are: - `log`: custom log function. Default uses [debug](https://www.npmjs.com/package/debug) package. - `manualConnect`: prevents the constructor to call `connect`. In this case after the `mqtt.connect` is called you should call `client.connect` manually. - `timerVariant`: defaults to `auto`, which tries to determine which timer is most appropriate for you environment, if you're having detection issues, you can set it to `worker` or `native` + - `unixSocket`: if you want to connect to a unix socket, set this to true In case mqtts (mqtt over tls) is required, the `options` object is passed through to [`tls.connect()`](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback). If using a **self-signed certificate**, set `rejectUnauthorized: false`. However, be cautious as this exposes you to potential man in the middle attacks and isn't recommended for production. diff --git a/src/lib/client.ts b/src/lib/client.ts index 192b1c1ed..8ea645f61 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -64,7 +64,7 @@ const defaultConnectOptions: IClientOptions = { timerVariant: 'auto', } -export type MqttProtocol = +export type BaseMqttProtocol = | 'wss' | 'ws' | 'mqtt' @@ -76,6 +76,11 @@ export type MqttProtocol = | 'ali' | 'alis' +// create a type that allows all MqttProtocol + `+unix` string +export type MqttProtocolWithUnix = `${BaseMqttProtocol}+unix` + +export type MqttProtocol = BaseMqttProtocol | MqttProtocolWithUnix + export type StorePutCallback = () => void export interface ISecureClientOptions { @@ -142,7 +147,9 @@ export interface IClientOptions extends ISecureClientOptions { host?: string /** @deprecated use `host instead */ hostname?: string - /** Websocket `path` added as suffix */ + /** Set to true if the connection is to a unix socket */ + unixSocket?: boolean + /** Websocket `path` added as suffix or Unix socket path when `unixSocket` option is true */ path?: string /** The `MqttProtocol` to use */ protocol?: MqttProtocol diff --git a/src/lib/connect/index.ts b/src/lib/connect/index.ts index 9a8deee5f..41dc94e22 100644 --- a/src/lib/connect/index.ts +++ b/src/lib/connect/index.ts @@ -71,31 +71,46 @@ function connect( opts = opts || {} + // try to parse the broker url if (brokerUrl && typeof brokerUrl === 'string') { // eslint-disable-next-line - const parsed = url.parse(brokerUrl, true) - if (parsed.port != null) { + const parsedUrl = url.parse(brokerUrl, true) + const parsedOptions: Partial = {} + + if (parsedUrl.port != null) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - parsed.port = Number(parsed.port) + parsedOptions.port = Number(parsedUrl.port) } - opts = { - ...{ - port: parsed.port, - host: parsed.hostname, - protocol: parsed.protocol, - query: parsed.query, - auth: parsed.auth, - }, - ...opts, - } as IClientOptions + parsedOptions.host = parsedUrl.hostname + parsedOptions.query = parsedUrl.query as Record + parsedOptions.auth = parsedUrl.auth + parsedOptions.protocol = parsedUrl.protocol as MqttProtocol + parsedOptions.path = parsedUrl.path + + parsedOptions.protocol = parsedOptions.protocol?.replace( + /:$/, + '', + ) as MqttProtocol - if (opts.protocol === null) { + opts = { ...parsedOptions, ...opts } + + // when parsing an url expect the protocol to be set + if (!opts.protocol) { throw new Error('Missing protocol') } + } + + opts.unixSocket = opts.unixSocket || opts.protocol?.includes('+unix') - opts.protocol = opts.protocol.replace(/:$/, '') as MqttProtocol + if (opts.unixSocket) { + opts.protocol = opts.protocol.replace('+unix', '') as MqttProtocol + } else if (!opts.protocol?.startsWith('ws')) { + // consider path only with ws protocol or unix socket + // url.parse could return path (for example when url ends with a `/`) + // that could break the connection. See https://github.com/mqttjs/MQTT.js/pull/1874 + delete opts.path } // merge in the auth options if supplied @@ -136,6 +151,9 @@ function connect( if (!protocols[opts.protocol]) { const isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 + // returns the first available protocol based on available protocols (that depends on environment) + // if no protocol is specified this will return mqtt on node and ws on browser + // if secure it will return mqtts on node and wss on browser opts.protocol = [ 'mqtt', 'mqtts', diff --git a/test/mqtt.ts b/test/mqtt.ts index 392856316..c76f90873 100644 --- a/test/mqtt.ts +++ b/test/mqtt.ts @@ -31,6 +31,26 @@ describe('mqtt', () => { c.should.be.instanceOf(mqtt.MqttClient) c.options.should.have.property('username', 'user') c.options.should.have.property('password', 'pass') + c.options.should.not.have.property('path') + c.end((err) => done(err)) + }) + + it('should return an MqttClient with path set when protocol is ws/wss', function _test(t, done) { + const c = mqtt.connect('ws://localhost:1883/mqtt') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('path', '/mqtt') + c.options.should.have.property('unixSocket', false) + c.end((err) => done(err)) + }) + + it('should work with unix sockets', function _test(t, done) { + const c = mqtt.connect('mqtt+unix:///tmp/mqtt.sock') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('path', '/tmp/mqtt.sock') + c.options.should.have.property('unixSocket', true) + c.end((err) => done(err)) }) @@ -47,6 +67,7 @@ describe('mqtt', () => { const c = mqtt.connect('mqtt://user@localhost:1883') c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.not.have.property('path') c.options.should.have.property('username', 'user') c.end((err) => done(err)) })