├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bin └── mqttwsBridge.js ├── doc └── mqtt-ws.md ├── example └── config.json ├── index.js ├── lib └── mqtt-ws.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .*.swp 4 | .lock-* 5 | build -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .*.swp 4 | .lock-* 5 | build 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jack Lund 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mqtt-ws 2 | [MQTT](http://mqtt.org/)/[WebSocket](http://en.wikipedia.org/wiki/WebSocket) bridge. Runs as a WebSocket server and connects to an MQTT server. WebSocket clients subscribe to an MQTT topic by specifying the topic as part of the WebSocket URL (see below). The client would then begin receiving data on the topic through the connection, and could even publish by sending data on the WS connection. 3 | 4 | You can also subscribe to wildcard topics the same way (URL encoded, of course), but you won't be able to publish on any wildcard topics. 5 | 6 | This module uses Einar Stangvik's [ws](https://github.com/einaros/ws) module for the WebSocket side, and Adam Rudd's [mqtt.js](https://github.com/adamvr/MQTT.js) module for the MQTT side. 7 | 8 | ## Rationale 9 | The reason I wrote this was to allow browser-based clients to have access to information being published over MQTT. There are other solutions to this problem as well; for instance, there is currently a [Paho Javascript client](http://git.eclipse.org/c/paho/org.eclipse.paho.mqtt.javascript.git) which tunnels the MQTT protocol through a WebSocket. While tunneling will certainly work (once the MQTT servers all support it), it makes the browser code more complex, and it also means that if you decide to change to a different pub/sub protocol, you then have to change all of your browser code to match. Using a bridge, you could simply change the backend pub/sub mechanism (and, of course, the bridge), and all you would have to change in your browser code would be, possibly, the URL that they connect to. This adds a great deal of flexibility in your architecture. 10 | 11 | ## Installation 12 | $ npm install mqtt-ws 13 | 14 | ## mqttwsBridge 15 | ### Usage 16 | Usage: mqttwsBridge 17 | 18 | Options: 19 | -p, --port MQTT port to connect to [default: 1883] 20 | -h, --host Hostname of MQTT server [default: "localhost"] 21 | -l, --listen WebSocket port to listen on [default: 80] 22 | -c, --configFile Configuration file 23 | --help Show this help 24 | 25 | ### Configuration File 26 | If you specify a configuration file on the command line, it is expecting a JSON-formatted file that looks something like this: 27 | 28 | { 29 | "mqtt": { 30 | "host": "mqttHost", 31 | "port": 1883 32 | }, 33 | "websocket": { 34 | "port": 8080 35 | } 36 | } 37 | 38 | The `mqtt` section specifies the MQTT connection parameters - `host` and `port`, and the `websocket` section is configuration information passed to the `ws` module. 39 | 40 | ### Logging 41 | Logging is done via [log4js](https://github.com/nomiddlename/log4js-node). Configuration for it may be added in the configuration file under key "log4js"; see the example in the `example` folder. By default, everything logs to the console. 42 | 43 | ### Connecting 44 | To connect to the bridge and subscribe to an MQTT topic, connect using the URL `ws://`_host[:port]_`/`_topic_[`?`_options]_. For instance, if I have the bridge running on host `myHost`, and port 8080, and want to subscribe to topic `room1/temperature`, I would use the URL `ws://myHost:8080/room1/temperature`. I could then publish on that topic by simply sending data over the WebSocket connection. 45 | 46 | You can also subscribe to [wildcard topics](http://www.eclipse.org/paho/files/mqttdoc/Cclient/wildcard.html). For instance, if I wanted to listen to all topics I could connect to the URL `ws://myHost:8080/%23` (_Note:_ the `#` has to be URL encoded). 47 | 48 | ### Messages 49 | 50 | #### Receiving Messages 51 | The websocket will receive just the message payload, unless it is subscribed to a wildcard topic, in which case it will receive the message in the form '_topic_: _payload_'. 52 | 53 | #### Publishing Messages 54 | Messages sent over the websocket connection will be published on the MQTT topic the websocket is subscribed to, unless it is a wildcard topic, in which case the message will be handled however the MQTT server handles such messages (in most cases, it will be dropped). 55 | 56 | ### Options 57 | In addition to a topic, you can specify MQTT options as query parameters in the URL. There are two sets of options you can specify - connection options and publishing options. Both are passed to [MQTT.js](https://github.com/adamvr/MQTT.js/) whenever a connection is created or a message is published. 58 | 59 | #### Connection Options 60 | The connection options are: 61 | 62 | * protocolId - String protocol ID 63 | * protocolVersion - Integer between 0 and 255 64 | * clean - Set to nonzero for a clean session, zero (the default) otherwise 65 | * keepalive - Set to keepalive value, between 0 and 65535 66 | * clientId - Set to the client ID, otherwise on is generated 67 | * username - Set to the username for authentication 68 | * password - Set to the password for authentication 69 | 70 | #### Publishing Options 71 | The publishing options are: 72 | 73 | * qos - Quality of service value, either 0 (the default), 1 or 2 74 | * retain - Set to true to retain the published value on the server, false (the default) not to 75 | 76 | #### Example 77 | For example, in the scenario above, to set up a connection subscribed to topic `foobar` with clientId `myClient`, username `foo`, password `myPassword`, and which always publishes with a QOS value of 1, use the URL: 78 | 79 | ws://myHost:8080/foobar?clientId=myClient&username=foo&password=myPassword&qos=1 80 | 81 | ## API 82 | There is an [API](doc/mqtt-ws.md) as well, which will allow you to create your own bridge server. 83 | -------------------------------------------------------------------------------- /bin/mqttwsBridge.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /*! 4 | * mqtt-ws: a node.js MQTT/WebSocket bridge 5 | * Copyright(c) 2013 M2M Connections, Inc 6 | * MIT Licensed 7 | */ 8 | 9 | var optimist = require('optimist'), 10 | myArgs = optimist 11 | .usage('MQTT/WebSocket Bridge\nUsage: $0') 12 | .alias({ 13 | 'p': 'port', 14 | 'h': 'host', 15 | 's': 'sslCert', 16 | 'k': 'sslKey', 17 | 'l': 'listen', 18 | 'c': 'configFile', 19 | }) 20 | .describe({ 21 | 's': 'Path to ssl certificate file', 22 | 'k': 'Path to ssl key file', 23 | 'p': 'MQTT port to connect to', 24 | 'h': 'Hostname of MQTT server', 25 | 'l': 'WebSocket port to listen on', 26 | 'c': 'Configuration file', 27 | 'help': 'Show this help' 28 | }) 29 | .argv, 30 | url = require('url'), 31 | util = require('util'), 32 | errno = require('errno'), 33 | log4js = require('log4js'), 34 | logger = log4js.getLogger(), 35 | mqttws = require('../lib/mqtt-ws'); 36 | 37 | if (myArgs.help) { 38 | optimist.showHelp(); 39 | process.exit(0); 40 | } 41 | 42 | // If we are given a config file, parse that, 43 | // otherwise just parse command line 44 | if (myArgs.c || myArgs.configFile) { 45 | var configFile = myArgs.c || myArgs.configFile; 46 | logger.info("Reading configuration from %s", configFile); 47 | require('fs').readFile(configFile, 'utf8', function(err, data) { 48 | if (err) { 49 | logger.info("Error reading config file %s: %s", configFile, err); 50 | process.exit(-1); 51 | } 52 | config = JSON.parse(data); 53 | run(parseCommandLine(myArgs, config)); 54 | }); 55 | } else { 56 | run(parseCommandLine(myArgs, {mqtt: {}, websocket: {}})); 57 | } 58 | 59 | // Parse the command line 60 | function parseCommandLine(args, config) { 61 | if (args.p || args.port) { 62 | config.mqtt.port = args.p || args.port; 63 | } 64 | if (args.h || args.host) { 65 | config.mqtt.host = args.h || args.host; 66 | } 67 | if (args.l || args.listen) { 68 | config.websocket.port = args.l || args.listen; 69 | } 70 | if (args.s || args.sslCert) { 71 | config.websocket.ssl_cert = args.s || args.sslCert; 72 | } 73 | if (args.k || args.sslKey) { 74 | config.websocket.ssl_key = args.k || args.sslKey; 75 | } 76 | 77 | return config; 78 | } 79 | 80 | function getErrnoDescription(err) { 81 | if (!err.errno) return undefined; 82 | var e; 83 | if (typeof err.errno == 'number') { 84 | e = errno.errno[err.errno]; 85 | if (e) { 86 | return e.description; 87 | } else { 88 | return undefined; 89 | } 90 | } else if (typeof err.errno == 'string') { 91 | for (e in errno.errno) { 92 | if (errno.errno[e].code == err.code) { 93 | return errno.errno[e].description; 94 | } 95 | } 96 | return undefined; 97 | } 98 | } 99 | 100 | function logError(err, message) { 101 | if (err.syscall) { 102 | var description = getErrnoDescription(err) || err.code; 103 | logger.error("%s on %s: %s", message, err.syscall, description); 104 | } else { 105 | logger.error("%s: %s", message, err); 106 | } 107 | } 108 | 109 | // Start the bridge 110 | function run(config) { 111 | if (config.log4js) { 112 | log4js.configure(config.log4js); 113 | } 114 | 115 | // Create our bridge 116 | bridge = mqttws.createBridge(config); 117 | logger.info("Listening for incoming WebSocket connections on port %d", 118 | bridge.port); 119 | 120 | // Set up error handling 121 | bridge.on('error', function(err) { 122 | logError(err, "WebSocket Error"); 123 | }); 124 | 125 | // Handle incoming WS connection 126 | bridge.on('connection', function(ws) { 127 | // URL-decode the URL, and use the URI part as the subscription topic 128 | logger.info("WebSocket connection from %s received", ws.connectString); 129 | 130 | var self = this; 131 | 132 | ws.on('error', function(err) { 133 | logError(err, util.format("WebSocket error in client %s", ws.connectString)); 134 | }); 135 | 136 | // Parse the URL 137 | var parsed = url.parse(ws.upgradeReq.url, true); 138 | // Connect to the MQTT server using the URL query as options 139 | var mqtt = bridge.connectMqtt(parsed.query); 140 | mqtt.topic = decodeURIComponent(parsed.pathname.substring(1)); 141 | 142 | ws.on('close', function() { 143 | logger.info("WebSocket client %s closed", ws.connectString); 144 | mqtt.end(); 145 | }); 146 | 147 | ws.on('message', function(message) { 148 | message = new Buffer(message); 149 | var char = ""; 150 | var topic = ""; 151 | var offset = 0; 152 | while(char != "|" && offset < message.length){ 153 | topic += char; 154 | char = String.fromCharCode(message.readUInt16LE(offset)); 155 | offset += 2; 156 | } 157 | logger.info("WebSocket client %s publishing to %s", ws.connectString, topic); 158 | mqtt.publish(topic, message.slice(offset), mqtt.options); 159 | }); 160 | 161 | mqtt.on('error', function(err) { 162 | logError(err, "MQTT error"); 163 | }); 164 | 165 | mqtt.on('connect', function() { 166 | logger.info("Connected to MQTT server at %s:%d", mqtt.host, mqtt.port); 167 | logger.info("WebSocket client %s subscribing to '%s'", ws.connectString, mqtt.topic); 168 | mqtt.subscribe(mqtt.topic); 169 | }); 170 | 171 | mqtt.on('close', function() { 172 | logger.info("MQTT connection for client %s closed", 173 | ws.connectString); 174 | ws.terminate(); 175 | }); 176 | 177 | mqtt.on('message', function(topic, message, packet) { 178 | ws.send(Buffer.concat([new Buffer(topic + "|", "utf16le"), new Buffer(message)]), {binary: true, mask: false}); 179 | }); 180 | }); 181 | } 182 | 183 | -------------------------------------------------------------------------------- /doc/mqtt-ws.md: -------------------------------------------------------------------------------- 1 | # mqtt-ws 2 | 3 | ## Class: mqttws.Bridge 4 | 5 | The main bridge server. It is an `EventEmitter`. 6 | 7 | ### new mqttws.Bridge([options]) 8 | 9 | * `options` Object, contains: 10 | * `mqtt` Object, contains: 11 | * `host` String - hostname of MQTT server 12 | * `port` Number - port MQTT server is listening on (default 1833) 13 | * `websocket` Object - contains options for [WebSocket server](https://github.com/einaros/ws/blob/master/doc/ws.md#new-wsserveroptions-callback). The main one is: 14 | * `port` Number - the port to listen on for incoming WS connections 15 | * `log4js` Object - contains options for [log4js](https://github.com/nomiddlename/log4js-node#configuration) 16 | 17 | Creates the bridge and begins listening on the port specified in `options->websocket`. 18 | 19 | ### Bridge.close() 20 | 21 | Closes the bridge and terminates all clients 22 | 23 | ### Bridge.connectMqtt(options) 24 | 25 | * `options` Object - contains the host, port and options to be passed to [mqtt](https://github.com/adamvr/MQTT.js/wiki/mqtt#mqttcreateclientport-host-options) 26 | 27 | Connects to the MQTT server, and returns the MQTT client. 28 | 29 | ### Event: 'error' 30 | 31 | `function (error) { }` 32 | 33 | WebSocket server errors, forwarded from the `ws` package 34 | 35 | ### Event: 'connection' 36 | 37 | `function (ws) { }` 38 | 39 | Incoming WebSocket connection. `ws` is the WebSocket client. 40 | 41 | ### Event: 'close' 42 | 43 | `function () { }` 44 | 45 | Closing the WebSocket server 46 | 47 | ## Class: ws.WebSocket 48 | 49 | WebSocket client class (see [here](https://github.com/einaros/ws/blob/master/doc/ws.md#class-wswebsocket)) 50 | 51 | ### Event 'error' 52 | 53 | `function (err) { }` 54 | 55 | WebSocket client errors 56 | 57 | ### Event 'close' 58 | 59 | `function () { }` 60 | 61 | WebSocket client closing 62 | 63 | ### Event 'message' 64 | 65 | `function (message) { }` 66 | 67 | Incoming message over the WebSocket 68 | 69 | ## Class mqtt.MqttClient 70 | 71 | MQTT Client class (see [here](https://github.com/adamvr/MQTT.js/wiki/client#mqttclient)) 72 | 73 | ### Event 'error' 74 | 75 | `function (err) { }` 76 | 77 | MQTT error 78 | 79 | ### Event 'connect' 80 | 81 | `function () { }` 82 | 83 | MQTT client connected to server 84 | 85 | ### Event 'message' 86 | 87 | `function (topic, message, packet) { }` 88 | 89 | MQTT incoming message 90 | 91 | * `topic` - message topic 92 | * `message` - message payload 93 | * `packet` - MQTT packet, containing: 94 | * `cmd` - MQTT command, e.g., 'publish' 95 | * `retain` - MQTT retain flag 96 | * `qos` - MQTT QOS for the message 97 | * `dup` - Dup flag 98 | * `length` - Message length 99 | * `topic` - Message topic 100 | * `payload` - Message payload 101 | 102 | ### Event 'close' 103 | 104 | `function () { }` 105 | 106 | MQTT client closed -------------------------------------------------------------------------------- /example/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mqtt": { 3 | "host": "mqttHost", 4 | "port": 1883 5 | }, 6 | "websocket": { 7 | "port": 8080 8 | }, 9 | "log4js": { 10 | "appenders": [ 11 | { 12 | "type": "console" 13 | }, 14 | { 15 | "type": "file", 16 | "filename": "mqttwsBridge.log", 17 | "maxLogSize": 1024, 18 | "backups": 3 19 | } 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mqtt-ws: a node.js MQTT/WebSocket bridge 3 | * Copyright(c) 2013 M2M Connections, Inc 4 | * MIT Licensed 5 | */ 6 | 7 | module.exports = require('./lib/mqttws'); -------------------------------------------------------------------------------- /lib/mqtt-ws.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mqtt-ws: a node.js MQTT/WebSocket bridge 3 | * Copyright(c) 2013 M2M Connections, Inc 4 | * MIT Licensed 5 | */ 6 | 7 | var mqtt = require('mqtt'), 8 | WebSocketServer = require('ws').Server, 9 | events = require('events'), 10 | util = require('util'), 11 | https = require('https'), 12 | fs = require('fs'), 13 | underscore = require('underscore'); 14 | 15 | var defaultOptions = { 16 | mqtt: { 17 | host: "localhost", 18 | port: 1883, 19 | }, 20 | websocket: { 21 | port: 80 22 | } 23 | }; 24 | 25 | var Bridge = module.exports = function Bridge(options) { 26 | options = options || {mqtt: {}, websocket: {}}; 27 | this.options = {}; 28 | this.options.mqtt = underscore.extend(defaultOptions.mqtt, options.mqtt); 29 | this.options.websocket = underscore.extend(defaultOptions.websocket, options.websocket); 30 | this.port = this.options.websocket.port; 31 | 32 | var self = this; 33 | 34 | // Create Websocket Server 35 | var app = https.createServer({ 36 | key: fs.readFileSync(this.options.websocket.ssl_key), 37 | cert: fs.readFileSync(this.options.websocket.ssl_cert) 38 | }, function(req, res){ 39 | res.writeHead(418); 40 | res.end("

418 - I'm a teapot

"); 41 | }).listen(this.port); 42 | this.wss = new WebSocketServer({server:app}); 43 | this.wss.on('error', function(err) { 44 | self.emit('error', err); 45 | }); 46 | 47 | // Incoming WS connection 48 | this.wss.on('connection', function(ws) { 49 | // Set connection string we can use as client identifier 50 | ws.connectString = util.format("%s:%d", 51 | ws.upgradeReq.connection.remoteAddress, ws.upgradeReq.connection.remotePort); 52 | 53 | // Signal we've got a connection 54 | self.emit('connection', ws); 55 | }); 56 | events.EventEmitter.call(this); 57 | }; 58 | util.inherits(Bridge, events.EventEmitter); 59 | 60 | Bridge.prototype.connectMqtt = function(options) { 61 | // Create our client 62 | options.encoding = "binary"; 63 | var mqttClient = mqtt.createClient(this.options.mqtt.port, this.options.mqtt.host, options); 64 | mqttClient.options = options; 65 | 66 | // Note: Have to do this because the MQTT client blocks connection 67 | // errors, which we want to capture 68 | mqttClient.stream.on('error', mqttClient.emit.bind(mqttClient, 'error')); 69 | 70 | // Disable reconnection 71 | mqttClient._reconnect = function() {}; 72 | 73 | // Set the host and port 74 | mqttClient.host = this.options.mqtt.host; 75 | mqttClient.port = this.options.mqtt.port; 76 | 77 | return mqttClient; 78 | } 79 | 80 | Bridge.prototype.close = function() { 81 | this.wss.close(); 82 | this.emit('close'); 83 | } 84 | 85 | module.exports.createBridge = function(options) { 86 | return new Bridge(options); 87 | }; 88 | 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-ws", 3 | "version": "0.2.0", 4 | "description": "a Node.js MQTT/WebSocket bridge", 5 | "author": "Jack Lund ", 6 | "license": "MIT", 7 | "readmeFilename": "README.md", 8 | "preferGlobal": true, 9 | "main": "lib/mqtt-ws.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/M2MConnections/mqtt-ws" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/M2MConnections/mqtt-ws/issues" 16 | }, 17 | "dependencies": { 18 | "errno": "*", 19 | "log4js": "*", 20 | "mqtt": "*", 21 | "optimist": "*", 22 | "options": ">=0.0.5", 23 | "underscore": "*", 24 | "ws": "*" 25 | }, 26 | "engines": { 27 | "node": ">=0.4.0" 28 | }, 29 | "keywords": [ 30 | "Node.js", 31 | "Websockets", 32 | "MQTT" 33 | ], 34 | "bin": { 35 | "mqttwsBridge": "bin/mqttwsBridge.js" 36 | }, 37 | "directories": { 38 | "doc": "doc", 39 | "example": "example" 40 | }, 41 | "devDependencies": {}, 42 | "gitHead": "a30ac1a692b869b50c6ea56679618e61eaeec8b4", 43 | "scripts": { 44 | "test": "echo \"Error: no test specified\" && exit 1" 45 | } 46 | } 47 | --------------------------------------------------------------------------------