UNPKG

hap-nodejs

Version:

HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.

778 lines 41.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HAPConnection = exports.HAPConnectionEvent = exports.HAPConnectionState = exports.EventedHTTPServer = exports.EventedHTTPServerEvent = exports.HAPEncryption = void 0; var tslib_1 = require("tslib"); var domain_formatter_1 = require("@homebridge/ciao/lib/util/domain-formatter"); var assert_1 = tslib_1.__importDefault(require("assert")); var debug_1 = tslib_1.__importDefault(require("debug")); var events_1 = require("events"); var http_1 = tslib_1.__importDefault(require("http")); var net_1 = tslib_1.__importDefault(require("net")); var os_1 = tslib_1.__importDefault(require("os")); var hapCrypto = tslib_1.__importStar(require("./hapCrypto")); var net_utils_1 = require("./net-utils"); var uuid = tslib_1.__importStar(require("./uuid")); var debug = (0, debug_1.default)("HAP-NodeJS:EventedHTTPServer"); var debugCon = (0, debug_1.default)("HAP-NodeJS:EventedHTTPServer:Connection"); var debugEvents = (0, debug_1.default)("HAP-NodeJS:EventEmitter"); /** * Simple struct to hold vars needed to support HAP encryption. * * @group Cryptography */ var HAPEncryption = /** @class */ (function () { function HAPEncryption(clientPublicKey, secretKey, publicKey, sharedSecret, hkdfPairEncryptionKey) { this.accessoryToControllerCount = 0; this.controllerToAccessoryCount = 0; this.clientPublicKey = clientPublicKey; this.secretKey = secretKey; this.publicKey = publicKey; this.sharedSecret = sharedSecret; this.hkdfPairEncryptionKey = hkdfPairEncryptionKey; this.accessoryToControllerKey = Buffer.alloc(0); this.controllerToAccessoryKey = Buffer.alloc(0); } return HAPEncryption; }()); exports.HAPEncryption = HAPEncryption; /** * @group HAP Accessory Server */ var EventedHTTPServerEvent; (function (EventedHTTPServerEvent) { EventedHTTPServerEvent["LISTENING"] = "listening"; EventedHTTPServerEvent["CONNECTION_OPENED"] = "connection-opened"; EventedHTTPServerEvent["REQUEST"] = "request"; EventedHTTPServerEvent["CONNECTION_CLOSED"] = "connection-closed"; })(EventedHTTPServerEvent || (exports.EventedHTTPServerEvent = EventedHTTPServerEvent = {})); /** * EventedHTTPServer provides an HTTP-like server that supports HAP "extensions" for security and events. * * Implementation * -------------- * In order to implement the "custom HTTP" server required by the HAP protocol (see HAPServer.js) without completely * reinventing the wheel, we create both a generic TCP socket server and a standard Node HTTP server. * The TCP socket server acts as a proxy, allowing users of this class to transform data (for encryption) as necessary * and passing through bytes directly to the HTTP server for processing. This way we get Node to do all * the "heavy lifting" of HTTP like parsing headers and formatting responses. * * Events are sent by simply waiting for current HTTP traffic to subside and then sending a custom response packet * directly down the wire via the socket. * * Each connection to the main TCP server gets its own internal HTTP server, so we can track ongoing requests/responses * for safe event insertion. * * @group HAP Accessory Server */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging var EventedHTTPServer = /** @class */ (function (_super) { tslib_1.__extends(EventedHTTPServer, _super); function EventedHTTPServer() { var _this = _super.call(this) || this; /** * Set of all currently connected HAP connections. */ _this.connections = new Set(); /** * Session dictionary indexed by username/identifier. The username uniquely identifies every person added to the home. * So there can be multiple sessions open for a single username (multiple devices connected to the same Apple ID). */ _this.connectionsByUsername = new Map(); _this.tcpServer = net_1.default.createServer(); return _this; } EventedHTTPServer.prototype.scheduleNextConnectionIdleTimeout = function () { var e_1, _a; this.connectionIdleTimeout = undefined; if (!this.tcpServer.listening) { return; } debug("Running idle timeout timer..."); var currentTime = new Date().getTime(); var nextTimeout = -1; try { for (var _b = tslib_1.__values(this.connections), _c = _b.next(); !_c.done; _c = _b.next()) { var connection = _c.value; var timeDelta = currentTime - connection.lastSocketOperation; if (timeDelta >= EventedHTTPServer.MAX_CONNECTION_IDLE_TIME) { debug("[%s] Closing connection as it was inactive for " + timeDelta + "ms"); connection.close(); } else { nextTimeout = Math.max(nextTimeout, EventedHTTPServer.MAX_CONNECTION_IDLE_TIME - timeDelta); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT) { this.connectionIdleTimeout = setTimeout(this.scheduleNextConnectionIdleTimeout.bind(this), nextTimeout); } }; EventedHTTPServer.prototype.address = function () { return this.tcpServer.address(); }; EventedHTTPServer.prototype.listen = function (targetPort, hostname) { var _this = this; this.tcpServer.listen(targetPort, hostname, function () { var address = _this.tcpServer.address(); // address() is only a string when listening to unix domain sockets debug("Server listening on %s:%s", address.family === "IPv6" ? "[".concat(address.address, "]") : address.address, address.port); _this.connectionLoggingInterval = setInterval(function () { var connectionInformation = tslib_1.__spreadArray([], tslib_1.__read(_this.connections), false).map(function (connection) { return "".concat(connection.remoteAddress, ":").concat(connection.remotePort); }) .join(", "); debug("Currently %d hap connections open: %s", _this.connections.size, connectionInformation); }, 60000); _this.connectionLoggingInterval.unref(); _this.emit("listening" /* EventedHTTPServerEvent.LISTENING */, address.port, address.address); }); this.tcpServer.on("connection", this.onConnection.bind(this)); }; EventedHTTPServer.prototype.stop = function () { var e_2, _a; if (this.connectionLoggingInterval != null) { clearInterval(this.connectionLoggingInterval); this.connectionLoggingInterval = undefined; } if (this.connectionIdleTimeout != null) { clearTimeout(this.connectionIdleTimeout); this.connectionIdleTimeout = undefined; } this.tcpServer.close(); try { for (var _b = tslib_1.__values(this.connections), _c = _b.next(); !_c.done; _c = _b.next()) { var connection = _c.value; connection.close(); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } }; EventedHTTPServer.prototype.destroy = function () { this.stop(); this.removeAllListeners(); }; /** * Send an event notification for given characteristic and changed value to all connected clients. * If `originator` is specified, the given {@link HAPConnection} will be excluded from the broadcast. * * @param aid - The accessory id of the updated characteristic. * @param iid - The instance id of the updated characteristic. * @param value - The newly set value of the characteristic. * @param originator - If specified, the connection will not get an event message. * @param immediateDelivery - The HAP spec requires some characteristics to be delivery immediately. * Namely, for the {@link Characteristic.ButtonEvent} and the {@link Characteristic.ProgrammableSwitchEvent} characteristics. */ EventedHTTPServer.prototype.broadcastEvent = function (aid, iid, value, originator, immediateDelivery) { var e_3, _a; try { for (var _b = tslib_1.__values(this.connections), _c = _b.next(); !_c.done; _c = _b.next()) { var connection = _c.value; if (connection === originator) { debug("[%s] Muting event '%s' notification for this connection since it originated here.", connection.remoteAddress, aid + "." + iid); continue; } connection.sendEvent(aid, iid, value, immediateDelivery); } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_3) throw e_3.error; } } }; EventedHTTPServer.prototype.onConnection = function (socket) { var _this = this; // eslint-disable-next-line @typescript-eslint/no-use-before-define var connection = new HAPConnection(this, socket); connection.on("request" /* HAPConnectionEvent.REQUEST */, function (request, response) { _this.emit("request" /* EventedHTTPServerEvent.REQUEST */, connection, request, response); }); connection.on("authenticated" /* HAPConnectionEvent.AUTHENTICATED */, this.handleConnectionAuthenticated.bind(this, connection)); connection.on("closed" /* HAPConnectionEvent.CLOSED */, this.handleConnectionClose.bind(this, connection)); this.connections.add(connection); debug("[%s] New connection from client on interface %s (%s)", connection.remoteAddress, connection.networkInterface, connection.localAddress); this.emit("connection-opened" /* EventedHTTPServerEvent.CONNECTION_OPENED */, connection); if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT && !this.connectionIdleTimeout) { this.scheduleNextConnectionIdleTimeout(); } }; EventedHTTPServer.prototype.handleConnectionAuthenticated = function (connection, username) { var connections = this.connectionsByUsername.get(username); if (!connections) { this.connectionsByUsername.set(username, [connection]); } else if (!connections.includes(connection)) { // ensure this doesn't get added more than one time connections.push(connection); } }; EventedHTTPServer.prototype.handleConnectionClose = function (connection) { this.emit("connection-closed" /* EventedHTTPServerEvent.CONNECTION_CLOSED */, connection); this.connections.delete(connection); if (connection.username) { // aka connection was authenticated var connections = this.connectionsByUsername.get(connection.username); if (connections) { var index = connections.indexOf(connection); if (index !== -1) { connections.splice(index, 1); } if (connections.length === 0) { this.connectionsByUsername.delete(connection.username); } } } }; /** * This method is to be called when a given {@link HAPConnection} performs a request that should result in the disconnection * of all other {@link HAPConnection} with the same {@link HAPUsername}. * * The initiator MUST be in the middle of a http request were the response was not served yet. * Otherwise, the initiator connection might reside in a state where it isn't disconnected and can't make any further requests. * * @param initiator - The connection that requested to disconnect all connections of the same username. * @param username - The username for which all connections shall be closed. */ EventedHTTPServer.destroyExistingConnectionsAfterUnpair = function (initiator, username) { var e_4, _a; var connections = initiator.server.connectionsByUsername.get(username); if (connections) { try { for (var connections_1 = tslib_1.__values(connections), connections_1_1 = connections_1.next(); !connections_1_1.done; connections_1_1 = connections_1.next()) { var connection = connections_1_1.value; connection.closeConnectionAsOfUnpair(initiator); } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (connections_1_1 && !connections_1_1.done && (_a = connections_1.return)) _a.call(connections_1); } finally { if (e_4) throw e_4.error; } } } }; EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT = 16; // if we have more (or equal) # connections we start the timeout EventedHTTPServer.MAX_CONNECTION_IDLE_TIME = 60 * 60 * 1000; // 1h return EventedHTTPServer; }(events_1.EventEmitter)); exports.EventedHTTPServer = EventedHTTPServer; /** * @private * @group HAP Accessory Server */ var HAPConnectionState; (function (HAPConnectionState) { HAPConnectionState[HAPConnectionState["CONNECTING"] = 0] = "CONNECTING"; HAPConnectionState[HAPConnectionState["FULLY_SET_UP"] = 1] = "FULLY_SET_UP"; HAPConnectionState[HAPConnectionState["AUTHENTICATED"] = 2] = "AUTHENTICATED"; // above signals represent an alive connection // below states are considered "closed or soon closed" HAPConnectionState[HAPConnectionState["TO_BE_TEARED_DOWN"] = 3] = "TO_BE_TEARED_DOWN"; HAPConnectionState[HAPConnectionState["CLOSING"] = 4] = "CLOSING"; HAPConnectionState[HAPConnectionState["CLOSED"] = 5] = "CLOSED"; })(HAPConnectionState || (exports.HAPConnectionState = HAPConnectionState = {})); /** * @group HAP Accessory Server */ var HAPConnectionEvent; (function (HAPConnectionEvent) { HAPConnectionEvent["REQUEST"] = "request"; HAPConnectionEvent["AUTHENTICATED"] = "authenticated"; HAPConnectionEvent["CLOSED"] = "closed"; })(HAPConnectionEvent || (exports.HAPConnectionEvent = HAPConnectionEvent = {})); /** * Manages a single iOS-initiated HTTP connection during its lifetime. * @group HAP Accessory Server */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging var HAPConnection = /** @class */ (function (_super) { tslib_1.__extends(HAPConnection, _super); function HAPConnection(server, clientSocket) { var _this = _super.call(this) || this; _this.state = 0 /* HAPConnectionState.CONNECTING */; _this.lastSocketOperation = new Date().getTime(); _this.pendingClientSocketData = Buffer.alloc(0); // data received from client before HTTP proxy is fully setup _this.handlingRequest = false; // true while we are composing an HTTP response (so events can wait) _this.registeredEvents = new Set(); _this.queuedEvents = []; /** * If true, the above {@link queuedEvents} contains events which are set to be delivered immediately! */ _this.eventsQueuedForImmediateDelivery = false; _this.server = server; _this.sessionID = uuid.generate(clientSocket.remoteAddress + ":" + clientSocket.remotePort); _this.localAddress = clientSocket.localAddress; _this.remoteAddress = clientSocket.remoteAddress; // cache because it becomes undefined in 'onClientSocketClose' _this.remotePort = clientSocket.remotePort; _this.networkInterface = HAPConnection.getLocalNetworkInterface(clientSocket); // clientSocket is the socket connected to the actual iOS device _this.tcpSocket = clientSocket; _this.tcpSocket.on("data", _this.onTCPSocketData.bind(_this)); _this.tcpSocket.on("close", _this.onTCPSocketClose.bind(_this)); // we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely. _this.tcpSocket.on("error", _this.onTCPSocketError.bind(_this)); _this.tcpSocket.setNoDelay(true); // disable Nagle algorithm // "HAP accessory servers must not use keepalive messages, which periodically wake up iOS devices". // Thus, we don't configure any tcp keepalive // create our internal HTTP server for this connection that we will proxy data to and from _this.internalHttpServer = http_1.default.createServer(); _this.internalHttpServer.timeout = 0; // clients expect to hold connections open as long as they want _this.internalHttpServer.keepAliveTimeout = 0; // workaround for https://github.com/nodejs/node/issues/13391 _this.internalHttpServer.on("listening", _this.onHttpServerListening.bind(_this)); _this.internalHttpServer.on("request", _this.handleHttpServerRequest.bind(_this)); _this.internalHttpServer.on("error", _this.onHttpServerError.bind(_this)); // close event is added later on the "connect" event as possible listen retries would throw unnecessary close events _this.internalHttpServer.listen(0, _this.internalHttpServerAddress = (0, net_utils_1.getOSLoopbackAddressIfAvailable)()); return _this; } HAPConnection.prototype.debugListenerRegistration = function (event, registration, beforeCount) { if (registration === void 0) { registration = true; } if (beforeCount === void 0) { beforeCount = -1; } var stackTrace = new Error().stack.split("\n")[3]; var eventCount = this.listeners(event).length; var tabs1 = event === "authenticated" /* HAPConnectionEvent.AUTHENTICATED */ ? "\t" : "\t\t"; var tabs2 = !registration ? "\t" : "\t\t"; // eslint-disable-next-line max-len debugEvents("[".concat(this.remoteAddress, "] ").concat(registration ? "Registered" : "Unregistered", " event '").concat(String(event).toUpperCase(), "' ").concat(tabs1, "(total: ").concat(eventCount).concat(!registration ? " Before: " + beforeCount : "", ") ").concat(tabs2).concat(stackTrace)); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any HAPConnection.prototype.on = function (event, listener) { var result = _super.prototype.on.call(this, event, listener); this.debugListenerRegistration(event); return result; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any HAPConnection.prototype.addListener = function (event, listener) { var result = _super.prototype.addListener.call(this, event, listener); this.debugListenerRegistration(event); return result; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any HAPConnection.prototype.removeListener = function (event, listener) { var beforeCount = this.listeners(event).length; var result = _super.prototype.removeListener.call(this, event, listener); this.debugListenerRegistration(event, false, beforeCount); return result; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any HAPConnection.prototype.off = function (event, listener) { var result = _super.prototype.off.call(this, event, listener); var beforeCount = this.listeners(event).length; this.debugListenerRegistration(event, false, beforeCount); return result; }; /** * This method is called once the connection has gone through pair-verify. * As any HomeKit controller will initiate a pair-verify after the pair-setup procedure, this method gets * not called on the initial pair-setup. * * Once this method has been called, the connection is authenticated and encryption is turned on. */ HAPConnection.prototype.connectionAuthenticated = function (username) { this.state = 2 /* HAPConnectionState.AUTHENTICATED */; this.username = username; this.emit("authenticated" /* HAPConnectionEvent.AUTHENTICATED */, username); }; HAPConnection.prototype.isAuthenticated = function () { return this.state === 2 /* HAPConnectionState.AUTHENTICATED */; }; HAPConnection.prototype.close = function () { if (this.state >= 4 /* HAPConnectionState.CLOSING */) { return; // already closed/closing } this.state = 4 /* HAPConnectionState.CLOSING */; this.tcpSocket.destroy(); }; HAPConnection.prototype.closeConnectionAsOfUnpair = function (initiator) { if (this === initiator) { // the initiator of the unpair request is this connection, meaning it unpaired itself. // we still need to send the response packet to the unpair request. this.state = 3 /* HAPConnectionState.TO_BE_TEARED_DOWN */; } else { // as HomeKit requires it, destroy any active session which got unpaired this.close(); } }; HAPConnection.prototype.sendEvent = function (aid, iid, value, immediateDelivery) { (0, assert_1.default)(aid != null, "HAPConnection.sendEvent: aid must be defined!"); (0, assert_1.default)(iid != null, "HAPConnection.sendEvent: iid must be defined!"); var eventName = aid + "." + iid; if (!this.registeredEvents.has(eventName)) { // non verified connections can't register events, so this case is covered! return; } var event = { aid: aid, iid: iid, value: value, }; if (immediateDelivery) { // some characteristics are required to deliver notifications immediately // we will flush all other events too, on that occasion. this.queuedEvents.push(event); this.eventsQueuedForImmediateDelivery = true; if (this.eventsTimer) { clearTimeout(this.eventsTimer); this.eventsTimer = undefined; } this.handleEventsTimeout(); return; } // we search the list of queued events in reverse order. // if the last element with the same aid and iid has the same value we don't want to send the event notification twice. // BUT, we do not want to override previous event notifications which have a different value. Automations must be executed! for (var i = this.queuedEvents.length - 1; i >= 0; i--) { var queuedEvent = this.queuedEvents[i]; if (queuedEvent.aid === aid && queuedEvent.iid === iid) { if (queuedEvent.value === value) { return; // the same event was already queued. do not add it again! } break; // we break in any case } } this.queuedEvents.push(event); // if there is already a timer running we just add it in the queue. if (!this.eventsTimer) { this.eventsTimer = setTimeout(this.handleEventsTimeout.bind(this), 250); this.eventsTimer.unref(); } }; HAPConnection.prototype.handleEventsTimeout = function () { this.eventsTimer = undefined; if (this.state > 2 /* HAPConnectionState.AUTHENTICATED */) { // connection is closed or about to be closed. no need to send any further events return; } this.writeQueuedEventNotifications(); }; HAPConnection.prototype.writeQueuedEventNotifications = function () { var e_5, _a; if (this.queuedEvents.length === 0 || this.handlingRequest) { return; // don't send empty event notifications or if we are currently handling a request } if (this.eventsTimer) { // this method might be called when we have enqueued data AND data that is queued for immediate delivery! clearTimeout(this.eventsTimer); this.eventsTimer = undefined; } var eventData = { characteristics: [], }; try { for (var _b = tslib_1.__values(this.queuedEvents), _c = _b.next(); !_c.done; _c = _b.next()) { var queuedEvent = _c.value; if (!this.registeredEvents.has(queuedEvent.aid + "." + queuedEvent.iid)) { continue; // client unregistered that event in the meantime } eventData.characteristics.push(queuedEvent); } } catch (e_5_1) { e_5 = { error: e_5_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_5) throw e_5.error; } } this.queuedEvents.splice(0, this.queuedEvents.length); this.eventsQueuedForImmediateDelivery = false; this.writeEventNotification(eventData); }; /** * This will create an EVENT/1.0 notification header with the provided event notification. * If currently an HTTP request is in progress the assembled packet will be * added to the pending events list. * * @param notification - The event which should be sent out */ HAPConnection.prototype.writeEventNotification = function (notification) { debugCon("[%s] Sending HAP event notifications %o", this.remoteAddress, notification.characteristics); (0, assert_1.default)(!this.handlingRequest, "Can't write event notifications while handling a request!"); // Apple backend processes events in reverse order, so we need to reverse the array // so that events are processed in chronological order. notification.characteristics.reverse(); var dataBuffer = Buffer.from(JSON.stringify(notification), "utf8"); var header = Buffer.from("EVENT/1.0 200 OK\r\n" + "Content-Type: application/hap+json\r\n" + "Content-Length: " + dataBuffer.length + "\r\n" + "\r\n", "utf8"); var buffer = Buffer.concat([header, dataBuffer]); this.tcpSocket.write(this.encrypt(buffer), this.handleTCPSocketWriteFulfilled.bind(this)); }; HAPConnection.prototype.enableEventNotifications = function (aid, iid) { this.registeredEvents.add(aid + "." + iid); }; HAPConnection.prototype.disableEventNotifications = function (aid, iid) { this.registeredEvents.delete(aid + "." + iid); }; HAPConnection.prototype.hasEventNotifications = function (aid, iid) { return this.registeredEvents.has(aid + "." + iid); }; HAPConnection.prototype.getRegisteredEvents = function () { return this.registeredEvents; }; HAPConnection.prototype.clearRegisteredEvents = function () { this.registeredEvents.clear(); }; HAPConnection.prototype.encrypt = function (data) { // if accessoryToControllerKey is not empty, then encryption is enabled for this connection. However, we'll // need to be careful to ensure that we don't encrypt the last few bytes of the response from handlePairVerifyStepTwo. // Since all communication calls are asynchronous, we could easily receive this 'encrypt' event for those bytes. // So we want to make sure that we aren't encrypting data until we have *received* some encrypted data from the client first. if (this.encryption && this.encryption.accessoryToControllerKey.length > 0 && this.encryption.controllerToAccessoryCount > 0) { return hapCrypto.layerEncrypt(data, this.encryption); } return data; // otherwise, we don't encrypt and return plaintext }; HAPConnection.prototype.decrypt = function (data) { if (this.encryption && this.encryption.controllerToAccessoryKey.length > 0) { // below call may throw an error if decryption failed return hapCrypto.layerDecrypt(data, this.encryption); } return data; // otherwise, we don't decrypt and return plaintext }; HAPConnection.prototype.onHttpServerListening = function () { var _this = this; var addressInfo = this.internalHttpServer.address(); // address() is only a string when listening to unix domain sockets var addressString = addressInfo.family === "IPv6" ? "[".concat(addressInfo.address, "]") : addressInfo.address; this.internalHttpServerPort = addressInfo.port; debugCon("[%s] Internal HTTP server listening on %s:%s", this.remoteAddress, addressString, addressInfo.port); this.internalHttpServer.on("close", this.onHttpServerClose.bind(this)); // now we can establish a connection to this running HTTP server for proxying data this.httpSocket = net_1.default.createConnection(this.internalHttpServerPort, this.internalHttpServerAddress); // previously we used addressInfo.address this.httpSocket.setNoDelay(true); // disable Nagle algorithm this.httpSocket.on("data", this.handleHttpServerResponse.bind(this)); // we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely. this.httpSocket.on("error", this.onHttpSocketError.bind(this)); this.httpSocket.on("close", this.onHttpSocketClose.bind(this)); this.httpSocket.on("connect", function () { // we are now fully set up: // - clientSocket is connected to the iOS device // - serverSocket is connected to the httpServer // - ready to proxy data! _this.state = 1 /* HAPConnectionState.FULLY_SET_UP */; debugCon("[%s] Internal HTTP socket connected. HAPConnection now fully set up!", _this.remoteAddress); // start by flushing any pending buffered data received from the client while we were setting up if (_this.pendingClientSocketData && _this.pendingClientSocketData.length > 0) { _this.httpSocket.write(_this.pendingClientSocketData); } _this.pendingClientSocketData = undefined; }); }; /** * This event handler is called when we receive data from a HomeKit controller on our tcp socket. * We store the data if the internal http server is not read yet, or forward it to the http server. */ HAPConnection.prototype.onTCPSocketData = function (data) { if (this.state > 2 /* HAPConnectionState.AUTHENTICATED */) { // don't accept data of a connection which is about to be closed or already closed return; } this.handlingRequest = true; // reverted to false once response was sent out this.lastSocketOperation = new Date().getTime(); try { data = this.decrypt(data); } catch (error) { // decryption and/or verification failed, disconnect the client debugCon("[%s] Error occurred trying to decrypt incoming packet: %s", this.remoteAddress, error.message); this.close(); return; } if (this.state < 1 /* HAPConnectionState.FULLY_SET_UP */) { // we're not setup yet, so add this data to our intermediate buffer this.pendingClientSocketData = Buffer.concat([this.pendingClientSocketData, data]); } else { this.httpSocket.write(data); // proxy it along to the HTTP server } }; /** * This event handler is called when the internal http server receives a request. * Meaning we received data from the HomeKit controller in {@link onTCPSocketData}, which then send the * data unencrypted to the internal http server. And now it landed here, fully parsed as a http request. */ HAPConnection.prototype.handleHttpServerRequest = function (request, response) { if (this.state > 2 /* HAPConnectionState.AUTHENTICATED */) { // don't accept data of a connection which is about to be closed or already closed return; } debugCon("[%s] HTTP request: %s", this.remoteAddress, request.url); request.socket.setNoDelay(true); this.emit("request" /* HAPConnectionEvent.REQUEST */, request, response); }; /** * This event handler is called by the socket which is connected to our internal http server. * It is called with the response returned from the http server. * In this method we have to encrypt and forward the message back to the HomeKit controller. */ HAPConnection.prototype.handleHttpServerResponse = function (data) { var _this = this; data = this.encrypt(data); this.tcpSocket.write(data, this.handleTCPSocketWriteFulfilled.bind(this)); debugCon("[%s] HTTP Response is finished", this.remoteAddress); this.handlingRequest = false; if (this.state === 3 /* HAPConnectionState.TO_BE_TEARED_DOWN */) { setTimeout(function () { return _this.close(); }, 10); } else if (this.state < 3 /* HAPConnectionState.TO_BE_TEARED_DOWN */) { if (!this.eventsTimer || this.eventsQueuedForImmediateDelivery) { // we deliver events if there is no eventsTimer (meaning it ran out in the meantime) // or when the queue contains events set to be delivered immediately this.writeQueuedEventNotifications(); } } }; HAPConnection.prototype.handleTCPSocketWriteFulfilled = function () { this.lastSocketOperation = new Date().getTime(); }; HAPConnection.prototype.onTCPSocketError = function (err) { debugCon("[%s] Client connection error: %s", this.remoteAddress, err.message); // onTCPSocketClose will be called next }; HAPConnection.prototype.onTCPSocketClose = function () { this.state = 5 /* HAPConnectionState.CLOSED */; debugCon("[%s] Client connection closed", this.remoteAddress); if (this.httpSocket) { this.httpSocket.destroy(); } this.internalHttpServer.close(); this.emit("closed" /* HAPConnectionEvent.CLOSED */); // sending final closed event this.removeAllListeners(); // cleanup listeners, we are officially dead now }; HAPConnection.prototype.onHttpServerError = function (err) { debugCon("[%s] HTTP server error: %s", this.remoteAddress, err.message); if (err.code === "EADDRINUSE") { this.internalHttpServerPort = undefined; this.internalHttpServer.close(); this.internalHttpServer.listen(0, this.internalHttpServerAddress = (0, net_utils_1.getOSLoopbackAddressIfAvailable)()); } }; HAPConnection.prototype.onHttpServerClose = function () { debugCon("[%s] HTTP server was closed", this.remoteAddress); // make sure the iOS side is closed as well this.close(); }; HAPConnection.prototype.onHttpSocketError = function (err) { debugCon("[%s] HTTP connection error: ", this.remoteAddress, err.message); // onHttpSocketClose will be called next }; HAPConnection.prototype.onHttpSocketClose = function () { debugCon("[%s] HTTP connection was closed", this.remoteAddress); // we only support a single long-lived connection to our internal HTTP server. Since it's closed, // we'll need to shut it down entirely. this.internalHttpServer.close(); }; HAPConnection.prototype.getLocalAddress = function (ipVersion) { var e_6, _a; var interfaceDetails = os_1.default.networkInterfaces()[this.networkInterface]; if (!interfaceDetails) { throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface); } // Find our first local IPv4 address. if (ipVersion === "ipv4") { var ipv4Info = interfaceDetails.find(function (info) { return info.family === "IPv4"; }); if (ipv4Info) { return ipv4Info.address; } throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface + "."); } var localUniqueAddress; try { for (var _b = tslib_1.__values(interfaceDetails.filter(function (entry) { return entry.family === "IPv6"; })), _c = _b.next(); !_c.done; _c = _b.next()) { var v6entry = _c.value; if (!v6entry.scopeid) { return v6entry.address; } localUniqueAddress !== null && localUniqueAddress !== void 0 ? localUniqueAddress : (localUniqueAddress = v6entry.address); } } catch (e_6_1) { e_6 = { error: e_6_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_6) throw e_6.error; } } if (localUniqueAddress) { return localUniqueAddress; } throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface); }; HAPConnection.getLocalNetworkInterface = function (socket) { var e_7, _a, e_8, _b; var localAddress = socket.localAddress; // Grab the list of network interfaces. var interfaces = os_1.default.networkInterfaces(); // Default to the first non-loopback interface we see. var defaultInterface = function () { var _a, _b; return (_b = (_a = Object.entries(interfaces).find(function (_a) { var _b = tslib_1.__read(_a, 2), addresses = _b[1]; return addresses === null || addresses === void 0 ? void 0 : addresses.some(function (address) { return !address.internal; }); })) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "unknown"; }; // No local address return our default. if (!localAddress) { return defaultInterface(); } // Handle IPv4-mapped IPv6 addresses. localAddress = localAddress.replace(/^::ffff:/i, ""); // Handle edge cases where we have an IPv4-mapped IPv6 address without the requisite prefix. if (/^::(?:\d{1,3}\.){3}\d{1,3}$/.test(localAddress)) { localAddress = localAddress.replace(/^::/, ""); } // Handle link-local IPv6 addresses. localAddress = localAddress.split("%")[0]; try { // Let's find an exact match using the IP. for (var _c = tslib_1.__values(Object.entries(interfaces)), _d = _c.next(); !_d.done; _d = _c.next()) { var _e = tslib_1.__read(_d.value, 2), name = _e[0], addresses = _e[1]; if (addresses === null || addresses === void 0 ? void 0 : addresses.some(function (_a) { var address = _a.address; return address === localAddress; })) { return name; } } } catch (e_7_1) { e_7 = { error: e_7_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_7) throw e_7.error; } } // We couldn't find an interface to match the address from above, so we attempt to match subnets (see https://github.com/homebridge/HAP-NodeJS/issues/847). var family = net_1.default.isIPv4(localAddress) ? "IPv4" : "IPv6"; try { // Let's find a match based on the subnet. for (var _f = tslib_1.__values(Object.entries(interfaces)), _g = _f.next(); !_g.done; _g = _f.next()) { var _h = tslib_1.__read(_g.value, 2), name = _h[0], addresses = _h[1]; if (addresses === null || addresses === void 0 ? void 0 : addresses.some(function (entry) { return entry.family === family && (0, domain_formatter_1.getNetAddress)(localAddress, entry.netmask) === (0, domain_formatter_1.getNetAddress)(entry.address, entry.netmask); })) { return name; } } } catch (e_8_1) { e_8 = { error: e_8_1 }; } finally { try { if (_g && !_g.done && (_b = _f.return)) _b.call(_f); } finally { if (e_8) throw e_8.error; } } console.log("WARNING: unable to determine which interface to use for socket coming from " + socket.remoteAddress + ":" + socket.remotePort + " to " + socket.localAddress + "."); return defaultInterface(); }; return HAPConnection; }(events_1.EventEmitter)); exports.HAPConnection = HAPConnection; //# sourceMappingURL=eventedhttp.js.map