/* Copyright (C) 2014 Ishraq Ibne Ashraf Copyright (C) 2014 Matthias Bolte Copyright (C) 2014 Olaf Lüke Redistribution and use in source and binary forms of this file, with or without modification, are permitted. See the Creative Commons Zero (CC0 1.0) License for more details. */ var Device = require('./Device'); IPConnection.FUNCTION_ENUMERATE = 254; IPConnection.FUNCTION_DISCONNECT_PROBE = 128; IPConnection.CALLBACK_ENUMERATE = 253; IPConnection.CALLBACK_CONNECTED = 0; IPConnection.CALLBACK_DISCONNECTED = 1; IPConnection.BROADCAST_UID = 0; // Enumeration type parameter to the enumerate callback IPConnection.ENUMERATION_TYPE_AVAILABLE = 0; IPConnection.ENUMERATION_TYPE_CONNECTED = 1; IPConnection.ENUMERATION_TYPE_DISCONNECTED = 2; // Connect reason parameter to the connected callback IPConnection.CONNECT_REASON_REQUEST = 0; IPConnection.CONNECT_REASON_AUTO_RECONNECT = 1; // Disconnect reason parameter to the disconnected callback IPConnection.DISCONNECT_REASON_REQUEST = 0; IPConnection.DISCONNECT_REASON_ERROR = 1; IPConnection.DISCONNECT_REASON_SHUTDOWN = 2; // Returned by getConnectionState() IPConnection.CONNECTION_STATE_DISCONNECTED = 0; IPConnection.CONNECTION_STATE_CONNECTED = 1; IPConnection.CONNECTION_STATE_PENDING = 2; //auto-reconnect in process IPConnection.DISCONNECT_PROBE_INTERVAL = 5000; IPConnection.RETRY_CONNECTION_INTERVAL = 2000; // Error codes IPConnection.ERROR_ALREADY_CONNECTED = 11; IPConnection.ERROR_NOT_CONNECTED = 12; IPConnection.ERROR_CONNECT_FAILED = 13; IPConnection.ERROR_INVALID_FUNCTION_ID = 21; IPConnection.ERROR_TIMEOUT = 31; IPConnection.ERROR_INVALID_PARAMETER = 41; IPConnection.ERROR_FUNCTION_NOT_SUPPORTED = 42; IPConnection.ERROR_UNKNOWN_ERROR = 43; IPConnection.TASK_KIND_CONNECT = 0; IPConnection.TASK_KIND_DISCONNECT = 1; IPConnection.TASK_KIND_AUTO_RECONNECT = 2; IPConnection.TASK_KIND_AUTHENTICATE = 3; // Socket implementation for Node.js and Websocket. // The API resembles the Node.js API. function TFSocket(PORT, HOST, ipcon) { this.port = PORT; this.host = HOST; this.socket = null; if (process.browser) { var webSocketURL = "ws://" + this.host + ":" + this.port + "/"; if (typeof MozWebSocket != "undefined") { this.socket = new MozWebSocket(webSocketURL, "tfp"); } else { this.socket = new WebSocket(webSocketURL, "tfp"); } this.socket.binaryType = 'arraybuffer'; } else { var net = require('net'); this.socket = new net.Socket(); } this.on = function (str, func) { if (process.browser) { switch (str) { case "connect": this.socket.onopen = func; break; case "data": // Websockets in browsers return a MessageEvent. We just // expose the data from the event as a Buffer as in Node.js. this.socket.onmessage = function (messageEvent) { var data = new Buffer(new Uint8Array(messageEvent.data)); func(data); }; break; case "error": // There is no easy way to get errno for error in browser websockets. // We assume error['errno'] === 'ECONNRESET' this.socket.onerror = function () { var error = {"errno": "ECONNRESET"}; func(error); }; break; case "close": this.socket.onclose = func; break; } } else { this.socket.on(str, func); } }; this.connect = function () { if (process.browser) { // In the browser we already connected by creating a WebSocket object } else { this.socket.connect(this.port, this.host, null); } }; this.setNoDelay = function (value) { if (process.browser) { // Currently no API available in browsers // But Nagle algorithm seems te be turned off in most browsers by default anyway } else { this.socket.setNoDelay(value); } }; this.write = function (data) { if (process.browser) { // Some browers can't send a nodejs Buffer through a websocket, // we copy it into an ArrayBuffer var arrayBuffer = new Uint8Array(data).buffer; this.socket.send(arrayBuffer); ipcon.resetDisconnectProbe(); } else { this.socket.write(data, ipcon.resetDisconnectProbe()); } }; this.end = function () { if (process.browser) { this.socket.close(); } else { this.socket.end(); } }; this.destroy = function () { if (process.browser) { // There is no end/destroy in browser socket, so we close in end // and do nothing in destroy } else { this.socket.destroy(); } }; } BrickDaemon.FUNCTION_GET_AUTHENTICATION_NONCE = 1; BrickDaemon.FUNCTION_AUTHENTICATE = 2; function BrickDaemon(uid, ipcon) { Device.call(this, this, uid, ipcon); BrickDaemon.prototype = Object.create(Device); this.responseExpected = {}; this.callbackFormats = {}; this.APIVersion = [2, 0, 0]; this.responseExpected[BrickDaemon.FUNCTION_GET_AUTHENTICATION_NONCE] = Device.RESPONSE_EXPECTED_ALWAYS_TRUE; this.responseExpected[BrickDaemon.FUNCTION_AUTHENTICATE] = Device.RESPONSE_EXPECTED_TRUE; this.getAuthenticationNonce = function(returnCallback, errorCallback) { this.ipcon.sendRequest(this, BrickDaemon.FUNCTION_GET_AUTHENTICATION_NONCE, [], '', 'B4', returnCallback, errorCallback); }; this.authenticate = function(clientNonce, digest, returnCallback, errorCallback) { this.ipcon.sendRequest(this, BrickDaemon.FUNCTION_AUTHENTICATE, [clientNonce, digest], 'B4 B20', '', returnCallback, errorCallback); }; } // the IPConnection class and constructor function IPConnection() { // Creates an IP Connection object that can be used to enumerate the available // devices. It is also required for the constructor of Bricks and Bricklets. this.host = undefined; this.port = undefined; this.timeout = 2500; this.autoReconnect = true; this.nextSequenceNumber = 0; this.nextAuthenticationNonce = 0; this.devices = {}; this.registeredCallbacks = {}; this.socket = undefined; this.disconnectProbeIID = undefined; this.taskQueue = []; this.isConnected = false; this.connectErrorCallback = undefined; this.mergeBuffer = new Buffer(0); this.brickd = new BrickDaemon('2', this); this.disconnectProbe = function () { if (this.socket !== undefined) { this.socket.write(this.createPacketHeader(undefined, 8, IPConnection.FUNCTION_DISCONNECT_PROBE), this.resetDisconnectProbe()); } }; this.pushTask = function (handler, kind) { this.taskQueue.push({"handler": handler, "kind": kind}); if (this.taskQueue.length === 1) { this.executeTask(); } }; this.executeTask = function () { var task = this.taskQueue[0]; if (task !== undefined) { task.handler(); } }; this.popTask = function () { this.taskQueue.splice(0, 1); this.executeTask(); }; this.removeNextTask = function () { this.taskQueue.splice(1, 1); }; this.getCurrentTaskKind = function () { var task = this.taskQueue[0]; if (task !== undefined) { return task.kind; } return undefined; }; this.getNextTaskKind = function () { var task = this.taskQueue[1]; if (task !== undefined) { return task.kind; } return undefined; }; this.disconnect = function (errorCallback) { this.pushTask(this.disconnectInternal.bind(this, errorCallback), IPConnection.TASK_KIND_DISCONNECT); }; this.disconnectInternal = function (errorCallback) { var autoReconnectAborted = false; if (this.getNextTaskKind() === IPConnection.TASK_KIND_AUTO_RECONNECT) { // Remove auto-reconnect task, to break recursion this.removeNextTask(); autoReconnectAborted = true; } if (!this.isConnected) { if (!autoReconnectAborted && errorCallback !== undefined) { // Not using `this.` for the error callback function because // we want to call what user provided not the saved one errorCallback(IPConnection.ERROR_NOT_CONNECTED); } this.popTask(); return; } this.socket.end(); this.socket.destroy(); // no popTask() here, will be done in handleConnectionClose() return; }; this.connect = function (host, port, errorCallback) { this.pushTask(this.connectInternal.bind(this, host, port, errorCallback), IPConnection.TASK_KIND_CONNECT); }; this.connectInternal = function (host, port, errorCallback) { if (this.isConnected) { if (errorCallback !== undefined) { // Not using `this.` for the error callback function because // we want to call what user provided not the saved one errorCallback(IPConnection.ERROR_ALREADY_CONNECTED); } this.popTask(); return; } // Saving the user provided error callback function for future use this.connectErrorCallback = errorCallback; clearInterval(this.disconnectProbeIID); this.host = host; this.port = port; this.socket = new TFSocket(this.port, this.host, this); this.socket.setNoDelay(true); this.socket.on('connect', this.handleConnect.bind(this)); this.socket.on('data', this.handleIncomingData.bind(this)); this.socket.on('error', this.handleConnectionError.bind(this)); this.socket.on('close', this.handleConnectionClose.bind(this)); this.socket.connect(); }; this.handleConnect = function () { var connectReason = IPConnection.CONNECT_REASON_REQUEST; if (this.getCurrentTaskKind() === IPConnection.TASK_KIND_AUTO_RECONNECT) { connectReason = IPConnection.CONNECT_REASON_AUTO_RECONNECT; } clearInterval(this.disconnectProbeIID); this.isConnected = true; // Check and call functions if registered for callback connected if (this.registeredCallbacks[IPConnection.CALLBACK_CONNECTED] !== undefined) { this.registeredCallbacks[IPConnection.CALLBACK_CONNECTED](connectReason); } this.disconnectProbeIID = setInterval(this.disconnectProbe.bind(this), IPConnection.DISCONNECT_PROBE_INTERVAL); this.popTask(); }; this.handleIncomingData = function (data) { this.resetDisconnectProbe(); if (data.length === 0) { return; } this.mergeBuffer = bufferConcat([this.mergeBuffer, data]); if (this.mergeBuffer.length < 8) { return; } if (this.mergeBuffer.length < this.mergeBuffer.readUInt8(4)) { return; } while (this.mergeBuffer.length >= 8) { var newPacket = new Buffer(this.mergeBuffer.readUInt8(4)); this.mergeBuffer.copy(newPacket, 0, 0, this.mergeBuffer.readUInt8(4)); this.handlePacket(newPacket); this.mergeBuffer = this.mergeBuffer.slice(this.mergeBuffer.readUInt8(4)); } }; this.handleConnectionError = function (error) { if (error.errno === 'ECONNRESET') { // Check and call functions if registered for callback disconnected if (this.registeredCallbacks[IPConnection.CALLBACK_DISCONNECTED] !== undefined) { this.registeredCallbacks[IPConnection.CALLBACK_DISCONNECTED](IPConnection.DISCONNECT_REASON_SHUTDOWN); } } }; this.handleAutoReconnectError = function (error) { if (!this.isConnected && this.autoReconnect && error !== IPConnection.ERROR_ALREADY_CONNECTED) { this.pushTask(this.connectInternal.bind(this, this.host, this.port, this.handleAutoReconnectError), IPConnection.TASK_KIND_AUTO_RECONNECT); } }; this.handleConnectionClose = function () { if (this.getCurrentTaskKind() === IPConnection.TASK_KIND_DISCONNECT) { // This disconnect was requested var uid; for (uid in this.devices) { for (var i=0;i>> 4) & 0x0F; }; this.getRFromPacket = function (packetR) { return (packetR.readUInt8(6) >>> 3) & 0x01; }; this.getEFromPacket = function (packetE) { // Getting Error bits(E, 2bits) return (packetE.readUInt8(7) >>> 6) & 0x03; }; this.getPayloadFromPacket = function (packetPayload) { var payloadReturn = new Buffer(packetPayload.length - 8); packetPayload.copy(payloadReturn, 0, 8, packetPayload.length); return new Buffer(payloadReturn); }; function pack(data, format) { var formatArray = format.split(' '); if (formatArray.length <= 0) { return new Buffer(0); } var packedBuffer = new Buffer(0); for (var i=0; i 1) { var singleFormatArray = formatArray[i].split(''); for(var j=0; j 1) { var singleFormatArray = formatArray[i].split(''); if (singleFormatArray[0] === 's') { constructedString = ''; skip = false; for(var j=0; j 0) { this.handleResponse(packet); } }; this.getConnectionState = function () { if (this.isConnected) { return IPConnection.CONNECTION_STATE_CONNECTED; } if (this.getCurrentTaskKind() === IPConnection.TASK_KIND_AUTO_RECONNECT) { return IPConnection.CONNECTION_STATE_PENDING; } return IPConnection.CONNECTION_STATE_DISCONNECTED; }; this.setAutoReconnect = function (autoReconnect) { this.autoReconnect = autoReconnect; }; this.getAutoReconnect = function () { return this.autoReconnect; }; this.setTimeout = function (timeout) { this.timeout = timeout; }; this.getTimeout = function () { return this.timeout; }; this.enumerate = function (errorCallback) { if (this.getConnectionState() !== IPConnection.CONNECTION_STATE_CONNECTED) { if (errorCallback !== undefined) { errorCallback(IPConnection.ERROR_NOT_CONNECTED); } return; } this.socket.write(this.createPacketHeader(undefined, 8, IPConnection.FUNCTION_ENUMERATE), this.resetDisconnectProbe()); }; this.getRandomUInt32 = function (returnCallback) { if (process.browser) { if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { var r = new Uint32Array(1); window.crypto.getRandomValues(r); returnCallback(r[0]); } else if (typeof window !== 'undefined' && window.msCrypto && window.msCrypto.getRandomValues) { var r = new Uint32Array(1); window.msCrypto.getRandomValues(r); returnCallback(r[0]); } else { // fallback to non-crypto random numbers returnCallback(Math.ceil(Math.random() * 4294967295)); } } else { var crypto = require('crypto'); crypto.randomBytes(4, function(error, buffer) { if (error) { crypto.pseudoRandomBytes(4, function(error, buffer) { if (error) { returnCallback(Math.ceil(Math.random() * 4294967295)); } else { var data = new Buffer(buffer); returnCallback(data.readUInt32LE(0)); } }); } else { var data = new Buffer(buffer); returnCallback(data.readUInt32LE(0)); } }); } }; this.authenticateInternal = function (secret, returnCallback, errorCallback) { this.brickd.getAuthenticationNonce(function (serverNonce) { var serverNonceBytes = pack([serverNonce], 'B4'); var clientNonceNumber = this.nextAuthenticationNonce++; var clientNonceBytes = pack([clientNonceNumber], 'I'); var clientNonce = unpack(clientNonceBytes, 'B4')[0]; var combinedNonceBytes = pack([serverNonce, clientNonce], 'B4 B4'); var crypto = require('crypto'); var hmac = crypto.createHmac('sha1', secret); hmac.update(combinedNonceBytes); var digestBytes = hmac.digest(); var digest = unpack(digestBytes, 'B20')[0]; this.brickd.authenticate(clientNonce, digest, function () { if (returnCallback !== undefined) { returnCallback(); } this.popTask(); }.bind(this), function (error) { if (errorCallback !== undefined) { errorCallback(error); } this.popTask(); }.bind(this)); }.bind(this), function (error) { if (errorCallback !== undefined) { errorCallback(error); } this.popTask(); }.bind(this)); }; this.authenticate = function (secret, returnCallback, errorCallback) { // need to do authenticate() as a task because two authenticate() calls // are not allowed to overlap, otherwise the correct order of operations // in the handshake process cannot be guaranteed this.pushTask(function () { if (this.nextAuthenticationNonce === 0) { this.getRandomUInt32(function (r) { this.nextAuthenticationNonce = r; this.authenticateInternal(secret, returnCallback, errorCallback); }.bind(this)); } else { this.authenticateInternal(secret, returnCallback, errorCallback); } }.bind(this), IPConnection.TASK_KIND_AUTHENTICATE); }; this.on = function (FID, CBFunction) { this.registeredCallbacks[FID] = CBFunction; }; this.getNextSequenceNumber = function () { if (this.nextSequenceNumber >= 15) { this.nextSequenceNumber = 0; } return ++this.nextSequenceNumber; }; this.createPacketHeader = function (headerDevice, headerLength, headerFunctionID, headerErrorCB) { var UID = IPConnection.BROADCAST_UID; var len = headerLength; var FID = headerFunctionID; var seq = this.getNextSequenceNumber(); var responseBits = 0; var EFutureUse = 0; var returnOnError = false; if (headerDevice !== undefined) { var responseExpected = headerDevice.getResponseExpected(headerFunctionID, function (errorCode) { returnOnError = true; if (headerErrorCB !== undefined) { headerErrorCB(errorCode); } } ); if (returnOnError) { returnOnError = false; return; } UID = headerDevice.uid; if (responseExpected) { responseBits = 1; } } var seqResponseOOBits = seq << 4; if (responseBits) { seqResponseOOBits |= (responseBits << 3); } var returnHeader = new Buffer(8); returnHeader.writeUInt32LE(UID, 0); returnHeader.writeUInt8(len, 4); returnHeader.writeUInt8(FID, 5); returnHeader.writeUInt8(seqResponseOOBits, 6); returnHeader.writeUInt8(EFutureUse , 7); return returnHeader; }; function bufferConcat(arrayOfBuffers) { var newBufferSize = 0; var targetStart = 0; for (var i = 0; i