UNPKG

lucyclient

Version:
486 lines (398 loc) 18.3 kB
const osUtil = require('./OSUtilities'); const con = require('./TCPPacket'); const net = require('net'); const dgram = require('dgram'); const tcpOut = con.TCPOut; const TCPInPacket = con.TCPIn; //TODO winston should be a peer dependency ? //Logging const winston = require('winston'); winston.loggers.add('lucyLogger'); const logger = winston.loggers.get('lucyLogger'); const loggingFormat = winston.format.printf(info => { return `${info.timestamp} ${info.level}: ${info.message}` }); logger.configure({ level: 'info', format: winston.format.combine( winston.format.timestamp(), loggingFormat ), transports: [new winston.transports.Console()] }); //Settings const MULTICAST_PORT = 65001; //Multicast port udp network discovery request will be send to const MULTICAST_ADDRESS = '228.250.206.232'; //Multicast ip udp network discovery request will be send to const tcpRetryCountBeforeReFetch = 15; /*After 15 failed handshake attempts try to retrieve the *Server routing info via udp*/ //Only counts for currently retrieved logger //logger.level = 'silly'; //Todo as will all other libraries do we need a watchdog timer for disconnects? /** * * * @example * Retrieve server credentials automatically via UDP * let lucy = new SmartHouse("MyAwesomeDevice",true/false); * @example * Supply information yourself * let lucy = new SmartHouse("MyAwesomeDevice",true/false,'192.168.000.00',65000); * * @param deviceName Unique device name used to identify the device at the server. (Has to match at the server) * @param udp Do we wish to register an udp socket at the server? * @param hostAddress Host address of the server. If undefined it will be retrieved via udp network discovery * @param hostPort Host port of the server. If host address is defined this has to be defined as well. * @constructor */ function SmartHouse(deviceName,udp,hostAddress,hostPort){ //Connection state for asynch connection handling this.DEVICE_NAME = deviceName; //Name of the device used to match to the class on the server this.currentConnectionState = this.connectionState["DISCONNECTED"]; // this.currentUDPConnectionState = this.connectionState["DISCONNECTED"]; //NodeJS will split up tcp packets. The callback handler only contains //partial information therefore we need to remember old data and reconstruct valid packets this.carryOverData = undefined; this.client = undefined; //TCP client this.UDP = udp; //UDP Client this.udpCallback = undefined; //UDP event handler called when udp data arrives. Set by user this.hostAddress = hostAddress; //Host IP of the server used to initiate handshake this.hostPort = hostPort; //Host Port of the server used to initiate handshake this.eventHandlerMap = {}; //Event handlers mapped to action events submitted by the server this.udpNetworkDiscovery = hostAddress === undefined; //Was the server address was ever feetched by upd? this.udpNetworkDiscoverySocket = undefined; this.tcpConnectionFailCount = -1; /** * Start connecting to server process. * - 1. If enabled and host unknown retrieve host and port via udp network discovery * - 2. Initiate handshake via tcp */ this.connectToServer = function(){ //Don't try to connect if we are not disconnected! if(this.currentConnectionState !== this.connectionState["DISCONNECTED"]){ logger.debug("Connection state not disconnected. Return"); return; } //If host is unknown feetch it. if(this.udpNetworkDiscovery && this.hostAddress === undefined){ const _this = this; //We already initiated a socket. if(this.currentUDPConnectionState === this.connectionState["DISCONNECTED"]){ logger.info("UDP Network Discovery. Host unknown"); //TODO 2 socket declarations this.udpNetworkDiscoverySocket = dgram.createSocket('udp4'); this.udpNetworkDiscoverySocket.on('message',this.handleUDPNetworkDiscovery.bind(this)); //Once connected send the request packet this.udpNetworkDiscoverySocket.on('listening',function() { logger.info("Send request. - Retry every 10 seconds until server responds"); _this.sendUDPDeviceRecoveryRequest(); }); this.currentUDPConnectionState = this.connectionState["CONNECTED"]; }else { //Resend udp request this.sendUDPDeviceRecoveryRequest(); } //If after 2 seconds we did not receive a reasponds retry setTimeout(function(){ if(this.hostAddress === undefined) _this.connectToServer(); } ,10000); }else{ this.establishConnectionToServer(); } }; /** * Request the server to send us it's host and server address. */ this.sendUDPDeviceRecoveryRequest = function(){ logger.debug("Send UDP request to server"); const requestPacket = new Buffer([0x01]); this.udpNetworkDiscoverySocket.send(requestPacket,0,1,MULTICAST_PORT,MULTICAST_ADDRESS, function(err){ if(err !== null) { logger.error("Error sending udp request package " + err); } }); }; //TODO we need to retry on error. Set timeout and use on timeout callback. After x failed attempts //Try to re fetch server url and port if reuquired. this.establishConnectionToServer = function(){ const _this = this; this.client = new net.Socket(); this.client.connect(this.hostPort,this.hostAddress,this.performHandshake.bind(this)); this.client.on('error', function(ex){ if(ex.syscall === 'connect' && ex.code === 'ECONNREFUSED'){ logger.debug("Host server not reachable"); //Reconnection attempt will be handled by client.on.close(); }else{ logger.error(ex); } }); this.client.on('data',this.handleTCPData.bind(this)); this.client.on('close', function() { logger.info('Socket closed. Try reconnecting in 10 seconds'); //Server dropped the connection. Attempt to reconnect _this.currentConnectionState = _this.connectionState["DISCONNECTED"]; _this.tcpConnectionFailCount++; //If we failed to connect more than x times and network discovery is enabled refetch if(_this.tcpConnectionFailCount > tcpRetryCountBeforeReFetch && _this.udpNetworkDiscovery){ setTimeout(function(){ _this.hostAddress = undefined; _this.connectToServer(); _this.tcpConnectionFailCount = -1; },10000); }else{ setTimeout(function(){ //Retry _this.establishConnectionToServer(); },10000); } }); this.client.on('timeout', function() { //Close the socket and reopen it. logger.warn("timeout"); }); }; this.performHandshake = function(){ logger.debug('Init handshake'); // Welcome flag name length as short user agent length as short = 5 bytes const buf1 = Buffer.alloc(5); logger.silly("Device Name: " + this.DEVICE_NAME); const userAgent = osUtil.userAgent; const deviceNameLength = this.DEVICE_NAME.length; const userAgentLength = userAgent.length; buf1.writeInt16BE(deviceNameLength,1); buf1.writeInt16BE(userAgentLength,3); logger.silly(buf1.toString('hex')); this.client.write(buf1); let udpBuffer; let buf2 = Buffer.allocUnsafe(deviceNameLength); const bufUserAgent = Buffer.allocUnsafe(userAgentLength); buf2.write(this.DEVICE_NAME); //UDP_FLAG; if(this.UDP){ udpBuffer = Buffer.from([0x00]); }else{ udpBuffer = Buffer.from([0x01]); } bufUserAgent.write(userAgent); buf2 = Buffer.concat([buf2,udpBuffer,bufUserAgent]); this.client.write(buf2); //Done }; this.handleTCPData = function(data) { logger.silly("TCP data received: " + data.toString('hex')); if(this.currentConnectionState !== this.connectionState["CONNECTED"]){ this.handleHandshakeResponse(data); }else{ //Handle normal packet //1. Check packet validity //If we have fragmented data from last time append out buffer if(this.carryOverData !== undefined){ logger.silly("Carry Over: " + this.carryOverData.toString('hex')); logger.silly("Buffer dump: " + data.toString('hex')); data = Buffer.concat([this.carryOverData,data]); this.carryOverData = undefined; logger.silly("Reconsructed packet: " + data.toString('hex')); } let offset = 0; let flagOffset; for(flagOffset = 0; flagOffset < 3;){ if(offset >= data.length){ break; } if(data[offset] === beginRequestFlag[flagOffset]){ logger.silly("Request flag match: "); logger.silly(data[offset] + " " + beginRequestFlag[flagOffset]); flagOffset++; }else{ logger.silly(data[offset] + " " + beginRequestFlag[flagOffset]); flagOffset = 0; logger.silly("Skip bytes"); } offset++; } //TODO reorder if(flagOffset === 3){ logger.silly("Begin request flag sucessfully parsed"); }else{ logger.silly("Begin request flag failed " + flagOffset); return; } //Disregard invalid data //Copy everything starting from valid begin request flag to the buffer which starts at offset-3 //Interesting data = von offset - end if(offset-3 > 0){ logger.silly("Slice packet offset: " + offset-3 + "\nBefore:"); logger.silly(data.toString('hex')); data = data.slice(offset-3); logger.silly("After: " ); logger.silly(data.toString('hex')); } if(data.length < this.MIN_VALID_PACKET_LENGTH){ //data got teared apart. We need to merge it with the next package logger.silly("fragmented package push to temp. " + data.length); this.carryOverData = data; return; }else{ var payloadLength = data.readInt32BE(12); logger.debug("Data received - Payload length: " + payloadLength); if(data.length < this.MIN_VALID_PACKET_LENGTH + payloadLength) { logger.debug("fragmented package push to temp. payload length not reached"); this.carryOverData = data; return; } } var packet = new TCPInPacket(data, 3); //Residual data if(data.length > packet.payloadLength + 16){ this.carryOverData = data.slice(packet.payloadLength + 16,data.length); } const actionId = packet.actionId; if(packet.requestFlag){ //A request packet hand over to event handlers if (actionId === 0) { logger.debug("Hearbeat packet received"); this.handleHeartbeat(packet); } else { logger.debug("Not a heartbeat packet. ActionId: " + actionId); //Distribute packet to event handlers if (this.eventHandlerMap[actionId] !== undefined) { this.eventHandlerMap[actionId](packet); } } }else{ //A response to a request we have send earlier. Invoke callback function //TODO } } }; this.handleUDPData = function(msg,rInfo){ //This was bound earlier if(this.udpCallback !== undefined){ this.udpCallback(msg); } }; /** * Will be called upon requesting the server to advertise it's ip address and port * @param routingInfo * @param message */ this.handleUDPNetworkDiscovery = function(message,routingInfo){ logger.info("UDP Network Discovery Server Answer Received"); logger.debug("Raw UDP Message: " + message); const parsedPort = message.readInt32BE(0); message = message.slice(4); const parsedHost = message.toString(); logger.info("Host: " + parsedHost + " Parsed Port: " + parsedPort); this.udpNetworkDiscoverySocket.close(this.connectionState); this.currentUDPConnectionState = this.connectionState["DISCONNECTED"]; this.currentConnectionState = this.connectionState["DISCONNECTED"]; //We parsed host and port. continue connecting this.hostAddress = parsedHost; this.hostPort = parsedPort; this.establishConnectionToServer(); }; this.handleHandshakeResponse = function(data){ //TODO what is this context? //Handle handshake error codes var returnCode = data.readInt32BE(0); if(this.currentConnectionState === this.connectionState["DISCONNECTED"]){ if(returnCode === ACCEPTED){ this.currentConnectionState = this.connectionState["NAME_SUBMISSION"]; }else if(returnCode === BAD_REQUEST){ logger.error("Malformed request!"); }else { //Unexpected error code logger.error("Unexpected error code"); } }else if(this.currentConnectionState === this.connectionState["NAME_SUBMISSION"]){ if(returnCode === PROTOCOL_SWITCHED){ //Protocol switched logger.info("Handshake successful"); this.currentConnectionState = this.connectionState["CONNECTED"]; if(this.UDP){ //TODO error this.udpClient = dgram.createSocket('udp4'); this.udpClient.on('message',this.handleUDPData.bind(this)); this.udpClient.bind(); } this.tcpConnectionFailCount = -1; }else if(returnCode === DEVICE_NOT_FOUND){ logger.error("No device on server registerd with suupplied name!"); }else if(returnCode === BAD_REQUEST){ logger.error("Malformed request!"); }else{ //Unexpected error code logger.error("Unexpected error code"); } } }; this.handleHeartbeat = function(packet){ //setTimeout() schedule asynch const responsePacket = packet.createResponse(); responsePacket.sendDataToServer(this.client); logger.debug("Heartbeat response send"); }; } const beginRequestFlag = [ 0x00, 0x01, 0x02 ]; //return codes const PROTOCOL_SWITCHED = 101; const ACCEPTED = 202; const BAD_REQUEST = 400; const DEVICE_NOT_FOUND = 404; //Begin flag = 3 //Request flag = 1 //Action id = 4 //UniqueRequestId = 4; //PayloadLength = 4; SmartHouse.prototype = { registerEventCallback : function(id,callback){ if(this.eventHandlerMap.length < id || this.eventHandlerMap[id] !== undefined) { logger.warn("Override already set event callback with id: " + id); } this.eventHandlerMap[id] = callback; }, registerUDPCallback : function(callback){ if(this.udpCallback !== undefined){ logger.warn("Override already present udp callback"); } this.udpCallback = callback; }, sendUDP: function(data){ this.udpClient.send(data,this.hostPort,this.hostAddress); }, /** * Create a tcp request packet which will be send to the server * @param actionId action id the server can use to identify the packet intention * @param callback callback function which will be called once the server responds to this packet * undefined if no responds is expected * @param timeoutCallback function will be called in case of the package not receiving a timeout within the specified * time interval * @param timeout timeout in ms nodejs will wait until discarding the callback. This avoid resource leaks * in case of never handled packages and indicates server failure. * defaults to 20 seconds * @returns {TCPOutPacket} */ createRequest : function(actionId,callback, timeoutCallback,timeout = 20000){ //Just forward this function call so we can expose it to the user by simply creating a smart house instance return tcpOut.createRequest(actionId,callback, timeoutCallback,timeout); }, /** * Shuts down the smart house client and closes sockets. * This function does not reset event callbacks. */ shutdown : function(){ if(this.client !== undefined){ if(this.currentConnectionState !== this.connectionState["DISCONNECTED"]){ this.client.end(); this.currentConnectionState !== this.connectionState["DISCONNECTED"]; //TODO do we also want to reset event callbacks? //Also prevent reconnecting. } } }, connectionState : Object.freeze({"NAME_SUBMISSION":0,"CONNECTED":1,"DISCONNECTED":2}), MIN_VALID_PACKET_LENGTH : 16 }; module.exports = SmartHouse;