From 4ad59b2a46184815ce40db41be7a72a648a55670 Mon Sep 17 00:00:00 2001 From: Sandy Milne Date: Sun, 15 Oct 2017 13:43:38 +1100 Subject: [PATCH] Inital commit of Orvibo socket server code --- .gitignore | 2 + Example.js | 64 +++++++++++++ Orvibo.js | 226 ++++++++++++++++++++++++++++++++++++++++++++++ OrviboPacket.js | 71 +++++++++++++++ OrviboSettings.js | 13 +++ PacketBuilder.js | 182 +++++++++++++++++++++++++++++++++++++ README.md | 7 +- Utils.js | 27 ++++++ package-lock.json | 13 +++ package.json | 29 ++++++ 10 files changed, 628 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100644 Example.js create mode 100644 Orvibo.js create mode 100644 OrviboPacket.js create mode 100644 OrviboSettings.js create mode 100644 PacketBuilder.js create mode 100644 Utils.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e087fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +node_modules diff --git a/Example.js b/Example.js new file mode 100644 index 0000000..b617de9 --- /dev/null +++ b/Example.js @@ -0,0 +1,64 @@ +const Orvibo = require('./Orvibo'); +const http = require('http'); +const url = require('url'); + +const httpPort = 3000; + +let orvbio = new Orvibo(); + +// When a socket first connects and initiates the handshake it will emit the connected event with the uid of the socket; +orvbio.on('plugConnected', ({uid, name}) => { + console.log(`Connected ${uid} name = ${name}`); +}); + +// If the socket state is updated this event will fire +orvbio.on('plugStateUpdated', ({uid, state , name}) => { + console.log(`Plug ${name} ${uid} updated state ${state}`); +}); + +// The plug sends a hearbeat to let the server know it's still alive +orvbio.on('gotHeartbeat', ({uid, state , name}) => { + console.log(`Plug ${name} ${uid} sent heartbeat`); +}); + +// Called when the plug disconnects +orvbio.on('plugDisconnected', ({uid, name }) => { + console.log(`Plug ${uid} - ${name} disconnected`); +}); + +// Called when the plug disconnects with an error ie it's been unplugged +orvbio.on('plugDisconnectedWithError', ({uid, name }) => { + console.log(`Plug ${uid} - ${name} disconnected with error`); +}); + + + +// Start the Orvibo socket server +orvbio.startServer(); + +// Create a basic example HTTP server +// If there are no parameters it will return the uid, state, modelId and name of the socket +// You can then use the uid to toggle the state of the switch like +// http://localhost:3000?uid=5dcf7ff76e7a + +const requestHandler = (request, response) => { + response.writeHead(200, {'Content-Type': 'application/json'}); + let q = url.parse(request.url, true).query; + if (q.uid != null) { + orvbio.toggleSocket(q.uid); + } + + // Get all currently connected sockets, their names and states + let sockets = orvbio.getConnectedSocket(); + + response.end(JSON.stringify(sockets)); +}; + +const httpServer = http.createServer(requestHandler) + +httpServer.listen(httpPort, (err) => { + if (err) { + return console.log('something bad happened', err) + } + console.log(`http server is listening on ${httpPort}`) +}); \ No newline at end of file diff --git a/Orvibo.js b/Orvibo.js new file mode 100644 index 0000000..c8d1287 --- /dev/null +++ b/Orvibo.js @@ -0,0 +1,226 @@ +const net = require('net'); +const util = require('util'); +const Packet = require('./OrviboPacket'); +const PacketBuilder = require('./PacketBuilder'); +const Utils = require('./Utils'); +const Settings = require('./OrviboSettings'); +const EventEmitter = require('events').EventEmitter; + +const ORVIBO_KEY = Settings.ORVIBO_KEY; +const LOG_PACKET = Settings.LOG_PACKET; + +if (ORVIBO_KEY === '') { + console.log('Please add Orvibo PK key to OrviboSettings.js file. See Readme'); + process.exit(1); +} + +const Orvibo = function() {}; + +Object.assign(Orvibo.prototype, EventEmitter.prototype); + +let HEARTBEAT = 32; +let HELLO = 0; +let HANDSHAKE = 6; +let STATE_UPDATE = 42; +let STATE_UPDATE_CONFIRM = 15; +let UNKNOWN_CMD = 'UNKNOWN_CMD'; + +let port = 10001; +let bindHost = '0.0.0.0'; + +console.log(`Starting server Orvibo socket server on port ${port}`); + +let plugConnections = []; +let packetData = {}; + +// Put your smart socket data here and it will be matched to the uid +let plugInfo = Settings.plugInfo; + +let getNameForUid = (uid) => { + let item = plugInfo.find(item => item.uid === uid); + return item != null ? item.name : 'unknown'; +}; + +let getData = (id) => { + return packetData[id]; +}; + +let setData = (id, data) => { + packetData[id] = data; +}; + +let respondAndSetData = (data, socket, packetFunction) => { + setData(socket.id, data); + socket.write(packetFunction(data)); +}; + +let helloHandler = (plugPacket, socket) => { + let pkData = { + serial: plugPacket.getSerial(), + encryptionKey: Utils.generateRandomTextValue(16), + id: Utils.generateRandomHexValue(32), + modelId: plugPacket.getModelId(), + }; + respondAndSetData(pkData, socket, PacketBuilder.helloPacket); +}; + +let handshakeHandler = function(plugPacket, socket, socketData) { + let uid = plugPacket.getUid(); + let pkData = Object.assign({}, socketData, { + serial: plugPacket.getSerial(), + uid, + name: getNameForUid(uid) + }); + respondAndSetData(pkData, socket, PacketBuilder.handshakePacket); + this.emit('plugConnected', {uid:pkData.uid, name: pkData.name}); +}; + +let heartbeatHandler = function(plugPacket, socket, socketData) { + let pkData = Object.assign({}, socketData, { + serial: plugPacket.getSerial(), + uid: plugPacket.getUid() + }); + respondAndSetData(pkData, socket, PacketBuilder.heartbeatPacket); + this.emit('gotHeartbeat', {uid:pkData.uid, name: pkData.name}); +}; + +let stateUpdateHandler = function(plugPacket, socket, socketData) { + let pkData = Object.assign({}, socketData, { + serial: plugPacket.getSerial(), + uid: plugPacket.getUid(), + state: plugPacket.getValue1() + }); + respondAndSetData(pkData, socket, PacketBuilder.comfirmStatePacket); + this.emit('plugStateUpdated', {uid:pkData.uid, state: pkData.state, name: pkData.name}); +}; + +let stateConfirmHandler = function() { + // Do nothing at this stage +}; + +let unknownCmdHandler = function(plugPacket, socket, socketData) { + let pkData = Object.assign({}, socketData, { + serial: plugPacket.getSerial(), + uid: plugPacket.getUid(), + }); + respondAndSetData(pkData, socket, PacketBuilder.defaultPacket); +}; + +Orvibo.prototype.handlers = function() { + return { + [HELLO]: helloHandler.bind(this), + [HANDSHAKE]: handshakeHandler.bind(this), + [HEARTBEAT]: heartbeatHandler.bind(this), + [STATE_UPDATE]: stateUpdateHandler.bind(this), + [STATE_UPDATE_CONFIRM] : stateConfirmHandler, + [UNKNOWN_CMD]: unknownCmdHandler.bind(this) + }; +}; + +Orvibo.prototype.startServer = function() { + + let self = this; + + let handlers = this.handlers(); + + this.server = net.createServer(function(socket) { + + socket.id = Utils.generateRandomTextValue(16); + socket.setKeepAlive(true, 10000); + plugConnections.push(socket); + + socket.on('data', function (data) { + + let socketData = getData(socket.id); + let plugPacket = new Packet(data); + + if (!plugPacket.validCRC()) { + console.log('Got invalid CRC'); + return; + } + + if (plugPacket.packetTypeText() === 'pk') { + plugPacket.processPacket(ORVIBO_KEY); + } else { + plugPacket.processPacket(socketData.encryptionKey); + } + + LOG_PACKET && plugPacket.logPacket("Socket -> "); + + let handler = handlers[plugPacket.getCommand()]; + if (handler != null) { + handler(plugPacket, socket, socketData); + } else { + handlers[UNKNOWN_CMD](plugPacket, socket, socketData); + } + }); + + socket.on('end', function () { + let pkData = getData(socket.id); + self.emit('plugDisconnected', {uid: pkData.uid, name: pkData.name}); + delete packetData[socket.id]; + plugConnections.splice(plugConnections.indexOf(socket), 1); + }); + + socket.on('error', (err) => { + console.log(err); + console.log('error with socket ' + socket.id); + self.emit('plugDisconnectedWithError', getData(socket.id)); + delete packetData[socket.id]; + plugConnections.splice(plugConnections.indexOf(socket), 1); + + }); + + }); + + this.server.listen(port, bindHost); +}; + +Orvibo.prototype.toggleSocket = function(uid) { + + let socketId = null; + for (const key of Object.keys(packetData)) { + if (packetData[key].uid === uid) { + socketId = key; + break + } + } + if (socketId === null) { + console.log('Could not find socket ' + uid); + return; + } + let socket = plugConnections.find(s => s.id === socketId); + if (socket != null) { + let socketData = getData(socketId); + let data = Object.assign({}, socketData,{ + state: socketData.state === 1 ? 0 : 1, + serial: Utils.generateRandomNumber(8), + clientSessionId: socketData.clientSessionId ? socketData.clientSessionId : Utils.generateRandomHexValue(32), + deviceId: socketData.deviceId ? socketData.deviceId : Utils.generateRandomHexValue(32), + }); + + setData(socket.id, data); + + let packet = PacketBuilder.updatePacket(data); + socket.write(packet); + + } else { + console.log('Can not find socket '); + } +}; + +Orvibo.prototype.getConnectedSocket = function() { + let sockets = []; + for (const key of Object.keys(packetData)) { + let socketData = getData(key); + sockets.push({ + name: socketData.name, + state: socketData.state, + uid: socketData.uid, + modelId: socketData.modelId + }); + } + return sockets; +}; + +module.exports = Orvibo; \ No newline at end of file diff --git a/OrviboPacket.js b/OrviboPacket.js new file mode 100644 index 0000000..b5ac544 --- /dev/null +++ b/OrviboPacket.js @@ -0,0 +1,71 @@ +const crc32 = require('buffer-crc32'); +const crypto = require('crypto'); + +let pkt = function(packetBuffer) { + this.magic = packetBuffer.slice(0,2); + this.packetLength = packetBuffer.slice(2,4); + this.packetType = packetBuffer.slice(4,6); + this.crc32 = packetBuffer.slice(6,10); + this.packetId = packetBuffer.slice(10,42); + this.payload = packetBuffer.slice(42, packetBuffer.length); +}; + +pkt.prototype.logPacket = function(type) { + let output = { + PacketId : this.packetIdText(), + Payload : this.payloadJSON + }; + console.log(type, JSON.stringify(this.payloadJSON)); +}; + +pkt.prototype.getCommand = function() { + return this.payloadJSON.cmd; +}; + +pkt.prototype.getSerial = function() { + return this.payloadJSON.serial; +}; + +pkt.prototype.getUid = function() { + return this.payloadJSON.uid; +}; + +pkt.prototype.getValue1 = function() { + return this.payloadJSON.value1; +}; + +pkt.prototype.getModelId = function() { + return this.payloadJSON.modelId; +}; + +pkt.prototype.processPacket = function(key) { + this.payloadJSON = this.decodeJSON(key); +}; + +pkt.prototype.getKeyValue = function() { + return this.payloadJSON.key; +}; + +pkt.prototype.validCRC = function() { + return crc32(this.payload).toString('hex') === this.crc32.toString('hex'); +}; + +pkt.prototype.packetTypeText = function() { + return this.packetType.toString('ascii'); +}; + +pkt.prototype.packetIdText = function() { + return this.packetId.toString('ascii'); +}; + +pkt.prototype.decodeJSON = function(key) { + let decipher = crypto.createDecipheriv('aes-128-ecb', key, ''); + decipher.setAutoPadding(true); + let decrypted = decipher.update(this.payload.toString('hex'), 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + // Sometimes there are bad chars on the end of the JSON so check here + decrypted = decrypted.substring(0, decrypted.indexOf('}') + 1); + return JSON.parse(decrypted); +}; + +module.exports = pkt; \ No newline at end of file diff --git a/OrviboSettings.js b/OrviboSettings.js new file mode 100644 index 0000000..e0cbdd3 --- /dev/null +++ b/OrviboSettings.js @@ -0,0 +1,13 @@ +// Settings/Config store for orvibo socket server + +module.exports = { + LOG_PACKET: false, + ORVIBO_KEY: '', // put your PK key here as plain text + plugInfo : [ + // Add uid and a name so you can easily identify the connected sockets + { + uid :'53dd7fe74de7', + name: "Lamp in Kitchen" + }, + ], +} \ No newline at end of file diff --git a/PacketBuilder.js b/PacketBuilder.js new file mode 100644 index 0000000..de2d26d --- /dev/null +++ b/PacketBuilder.js @@ -0,0 +1,182 @@ +const crc32 = require('buffer-crc32'); +const crypto = require('crypto'); +const Settings = require('./orviboSettings'); + +const ORVIBO_KEY = Settings.ORVIBO_KEY; + +let Packet = { + encryptionKey : ORVIBO_KEY, + magic: new Buffer("6864", 'hex'), + build: function () { + let packetId = new Buffer(this.id, 'ascii'); + let payload = encodePayload(JSON.stringify(this.json), this.encryptionKey); + let crc = crc32(payload); + let length = getLength([this.magic, this.packetType, packetId, crc, payload], 2); // Extra 2 for the length field itself + return Buffer.concat([this.magic, length, this.packetType, crc, packetId, payload]); + } +}; + +let PKPacket = Object.assign({}, Packet, { + packetType: new Buffer('pk', 'ascii'), +}); + +let DKPacket = Object.assign({}, Packet, { + packetType: new Buffer('dk', 'ascii'), +}); + +let helloPacket = function({ serial, encryptionKey, id }) { + let json = { + cmd: 0, + status: 0, + serial: serial, + key: encryptionKey + }; + + let pkt = Object.assign(Object.create(PKPacket), { + json: json, + id: id + }); + + return pkt.build(); +}; + +let handshakePacket = function({ serial, encryptionKey, id }) { + + let json = { + cmd: 6, + status:0, + serial: serial + }; + + let pkt = Object.assign(Object.create(DKPacket), { + json: json, + id: id, + encryptionKey, encryptionKey + }); + + return pkt.build(); +}; + +let heartbeatPacket = function({serial, uid, encryptionKey, id}) { + let json = { + cmd: 32, + status:0, + serial: serial, + uid: uid, + utc: new Date().getTime() + }; + + let pkt = Object.assign(Object.create(DKPacket), { + json: json, + id: id, + encryptionKey, encryptionKey + }); + + return pkt.build(); +}; + +let comfirmStatePacket = function({serial, uid, state, encryptionKey, id}) { + + var json = { + uid: uid, + cmd: 42, + statusType: 0, + value3: 0, + alarmType: 1, + serial: serial, + value4: 0, + deviceId: 0, + value1: state, + value2: 0, + updateTimeSec: new Date().getTime(), + status: 0 + }; + + let pkt = Object.assign(Object.create(DKPacket), { + json: json, + id: id, + encryptionKey, encryptionKey + }); + + return pkt.build(); +}; + +let defaultPacket = function({serial, uid, cmd, id, encryptionKey}) { + + let json = { + uid: uid, + cmd: cmd, + serial: serial, + status: 0 + }; + + let pkt = Object.assign(Object.create(DKPacket), { + json: json, + id: id, + encryptionKey, encryptionKey + }); + + return pkt.build(); +}; + + +let updatePacket = function({ uid, state, serial, id, clientSessionId , deviceId, encryptionKey}) { + let json = { + uid: uid, + delayTime: 0, + cmd: 15, + order: state === 0 ? "on" : "off", + userName: "iloveorvibo@orvibo.com", + ver: "3.0.0", + value3: 0, + serial: serial, + value4: 0, + deviceId: deviceId, + value1: state, + value2: 0, + clientSessionId: clientSessionId + }; + + let pkt = Object.assign(Object.create(DKPacket), { + json: json, + id: id, + encryptionKey, encryptionKey + }); + + return pkt.build(); +}; + + +let encodePayload = function(json, key) { + let cipher = crypto.createCipheriv('aes-128-ecb', key, ''); + cipher.setAutoPadding(true); + let crypted = cipher.update(json,'utf8','hex'); + crypted += cipher.final('hex'); + return new Buffer(crypted, 'hex'); +}; + +let getLength = function(items, extra) { + let length = extra || 0; + for (let i = 0; i < items.length; i++) { + length += items[i].length; + } + return getHexLengthPadded(length); +}; + +let getHexLengthPadded = function(lengthDecimal) { + let lengthHex = lengthDecimal.toString(16); + let paddingLength = 4 - lengthHex.length; + let padding = ''; + for (let i = 0; i < paddingLength; i++) { + padding +=0; + } + return new Buffer(padding + lengthHex, 'hex'); +}; + + +module.exports.helloPacket = helloPacket; +module.exports.handshakePacket = handshakePacket; +module.exports.heartbeatPacket = heartbeatPacket; +module.exports.comfirmStatePacket = comfirmStatePacket; +module.exports.defaultPacket = defaultPacket; +module.exports.updatePacket = updatePacket; \ No newline at end of file diff --git a/README.md b/README.md index d5cbd46..cb27a5c 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,7 @@ You will need to add the Orvibo PK key to decrypt and encrypt the initial packet Once you have this key you will need to add it the ``OrviboSettings.js`` file. -Because these new sockets don't use UDP packets to communicate like the older versions you will also need to redirect all traffic from the host name -``` -homemate.orvibo.com -``` +Because these new sockets don't use UDP packets to communicate like the older versions you will also need to redirect all traffic from the host name ``homemate.orvibo.com`` on TCP port 10001 the computer running the server. I used an Ubuntu machine running dnsmasq and set this server as my DNS server in my router but depending on your network you might have to do it differntly. @@ -37,7 +34,6 @@ to the official Orvibo server. Clone the repo and then run ``` npm install - ``` to install the dependencies (buffer-crc32) @@ -50,7 +46,6 @@ To start the example http server run ``` npm start - ``` This will start the Orvibo socket server create a basic example HTTP server used for interacting with sockets. Calling this http server with no parameters will return the uid, state, modelId and name of the socket. diff --git a/Utils.js b/Utils.js new file mode 100644 index 0000000..df9f2cd --- /dev/null +++ b/Utils.js @@ -0,0 +1,27 @@ +module.exports.generateRandomTextValue = function(length) { + let chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let result = ''; + for (let i = length; i > 0; --i) { + result += chars[Math.floor(Math.random() * chars.length)]; + } + return result; +}; + +module.exports.generateRandomHexValue = function(length) { + let chars = '0123456789abcdef'; + let result = ''; + for (let i = length; i > 0; --i) { + result += chars[Math.floor(Math.random() * chars.length)]; + } + return result; +}; + +module.exports.generateRandomNumber = function(length) { + let numbers = '0123456789'; + let result = ''; + for (let i = length; i > 0; --i) { + result += numbers[Math.floor(Math.random() * numbers.length)]; + } + return parseInt(result); +}; + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2830cde --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "orvibo-b25-server", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2bcecd0 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "orvibo-b25-server", + "version": "1.0.0", + "description": "A server to control the Orvibo B25 range of smart sockets", + "main": "Orvibo.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node Example.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sandysound/orvibo-b25-server.git" + }, + "keywords": [ + "Orvibo", + "B25", + "Smart", + "Socket" + ], + "author": "Sandy Milne", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/sandysound/orvibo-b25-server/issues" + }, + "homepage": "https://github.com/sandysound/orvibo-b25-server#readme", + "dependencies": { + "buffer-crc32": "^0.2.13" + } +}