UNPKG

hap-nodejs

Version:

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

706 lines 35.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HAPConnection = exports.HAPConnectionEvent = exports.HAPConnectionState = exports.EventedHTTPServer = exports.EventedHTTPServerEvent = exports.HAPEncryption = void 0; const tslib_1 = require("tslib"); const domain_formatter_1 = require("@homebridge/ciao/lib/util/domain-formatter"); const assert_1 = tslib_1.__importDefault(require("assert")); const debug_1 = tslib_1.__importDefault(require("debug")); const events_1 = require("events"); const http_1 = tslib_1.__importDefault(require("http")); const net_1 = tslib_1.__importDefault(require("net")); const os_1 = tslib_1.__importDefault(require("os")); const hapCrypto = tslib_1.__importStar(require("./hapCrypto")); const net_utils_1 = require("./net-utils"); const uuid = tslib_1.__importStar(require("./uuid")); const debug = (0, debug_1.default)("HAP-NodeJS:EventedHTTPServer"); const debugCon = (0, debug_1.default)("HAP-NodeJS:EventedHTTPServer:Connection"); const debugEvents = (0, debug_1.default)("HAP-NodeJS:EventEmitter"); /** * Simple struct to hold vars needed to support HAP encryption. * * @group Cryptography */ class HAPEncryption { clientPublicKey; secretKey; publicKey; sharedSecret; hkdfPairEncryptionKey; accessoryToControllerCount = 0; controllerToAccessoryCount = 0; accessoryToControllerKey; controllerToAccessoryKey; incompleteFrame; constructor(clientPublicKey, secretKey, publicKey, sharedSecret, hkdfPairEncryptionKey) { 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); } } 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 class EventedHTTPServer extends events_1.EventEmitter { static CONNECTION_TIMEOUT_LIMIT = 16; // if we have more (or equal) # connections we start the timeout static MAX_CONNECTION_IDLE_TIME = 60 * 60 * 1000; // 1h tcpServer; /** * Set of all currently connected HAP connections. */ 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). */ connectionsByUsername = new Map(); connectionIdleTimeout; connectionLoggingInterval; constructor() { super(); this.tcpServer = net_1.default.createServer(); } scheduleNextConnectionIdleTimeout() { this.connectionIdleTimeout = undefined; if (!this.tcpServer.listening) { return; } debug("Running idle timeout timer..."); const currentTime = new Date().getTime(); let nextTimeout = -1; for (const connection of this.connections) { const 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); } } if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT) { this.connectionIdleTimeout = setTimeout(this.scheduleNextConnectionIdleTimeout.bind(this), nextTimeout); } } address() { return this.tcpServer.address(); } listen(targetPort, hostname) { this.tcpServer.listen(targetPort, hostname, () => { const address = this.tcpServer.address(); // address() is only a string when listening to unix domain sockets debug("Server listening on %s:%s", address.family === "IPv6" ? `[${address.address}]` : address.address, address.port); this.connectionLoggingInterval = setInterval(() => { const connectionInformation = [...this.connections] .map(connection => `${connection.remoteAddress}:${connection.remotePort}`) .join(", "); debug("Currently %d hap connections open: %s", this.connections.size, connectionInformation); }, 60_000); this.connectionLoggingInterval.unref(); this.emit("listening" /* EventedHTTPServerEvent.LISTENING */, address.port, address.address); }); this.tcpServer.on("connection", this.onConnection.bind(this)); } stop() { if (this.connectionLoggingInterval != null) { clearInterval(this.connectionLoggingInterval); this.connectionLoggingInterval = undefined; } if (this.connectionIdleTimeout != null) { clearTimeout(this.connectionIdleTimeout); this.connectionIdleTimeout = undefined; } this.tcpServer.close(); for (const connection of this.connections) { connection.close(); } } destroy() { 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. */ broadcastEvent(aid, iid, value, originator, immediateDelivery) { for (const connection of this.connections) { 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); } } onConnection(socket) { // eslint-disable-next-line @typescript-eslint/no-use-before-define const connection = new HAPConnection(this, socket); connection.on("request" /* HAPConnectionEvent.REQUEST */, (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(); } } handleConnectionAuthenticated(connection, username) { const 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); } } handleConnectionClose(connection) { this.emit("connection-closed" /* EventedHTTPServerEvent.CONNECTION_CLOSED */, connection); this.connections.delete(connection); if (connection.username) { // aka connection was authenticated const connections = this.connectionsByUsername.get(connection.username); if (connections) { const 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. */ static destroyExistingConnectionsAfterUnpair(initiator, username) { const connections = initiator.server.connectionsByUsername.get(username); if (connections) { for (const connection of connections) { connection.closeConnectionAsOfUnpair(initiator); } } } } 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 class HAPConnection extends events_1.EventEmitter { /** * @private file-private API */ server; sessionID; // uuid unique to every HAP connection state = 0 /* HAPConnectionState.CONNECTING */; localAddress; remoteAddress; // cache because it becomes undefined in 'onClientSocketClose' remotePort; networkInterface; tcpSocket; internalHttpServer; httpSocket; // set when in state FULLY_SET_UP internalHttpServerPort; internalHttpServerAddress; lastSocketOperation = new Date().getTime(); pendingClientSocketData = Buffer.alloc(0); // data received from client before HTTP proxy is fully setup handlingRequest = false; // true while we are composing an HTTP response (so events can wait) username; // username is unique to every user in the home, basically identifies an Apple ID encryption; // created in handlePairVerifyStepOne srpServer; _pairSetupState; // TODO ensure those two states are always correctly reset? _pairVerifyState; registeredEvents = new Set(); eventsTimer; queuedEvents = []; /** * If true, the above {@link queuedEvents} contains events which are set to be delivered immediately! */ eventsQueuedForImmediateDelivery = false; timedWritePid; timedWriteTimeout; constructor(server, clientSocket) { super(); 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)()); } debugListenerRegistration(event, registration = true, beforeCount = -1) { const stackTrace = new Error().stack.split("\n")[3]; const eventCount = this.listeners(event).length; const tabs1 = event === "authenticated" /* HAPConnectionEvent.AUTHENTICATED */ ? "\t" : "\t\t"; const tabs2 = !registration ? "\t" : "\t\t"; // eslint-disable-next-line max-len debugEvents(`[${this.remoteAddress}] ${registration ? "Registered" : "Unregistered"} event '${String(event).toUpperCase()}' ${tabs1}(total: ${eventCount}${!registration ? " Before: " + beforeCount : ""}) ${tabs2}${stackTrace}`); } // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event, listener) { const result = super.on(event, listener); this.debugListenerRegistration(event); return result; } // eslint-disable-next-line @typescript-eslint/no-explicit-any addListener(event, listener) { const result = super.addListener(event, listener); this.debugListenerRegistration(event); return result; } // eslint-disable-next-line @typescript-eslint/no-explicit-any removeListener(event, listener) { const beforeCount = this.listeners(event).length; const result = super.removeListener(event, listener); this.debugListenerRegistration(event, false, beforeCount); return result; } // eslint-disable-next-line @typescript-eslint/no-explicit-any off(event, listener) { const result = super.off(event, listener); const 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. */ connectionAuthenticated(username) { this.state = 2 /* HAPConnectionState.AUTHENTICATED */; this.username = username; this.emit("authenticated" /* HAPConnectionEvent.AUTHENTICATED */, username); } isAuthenticated() { return this.state === 2 /* HAPConnectionState.AUTHENTICATED */; } close() { if (this.state >= 4 /* HAPConnectionState.CLOSING */) { return; // already closed/closing } this.state = 4 /* HAPConnectionState.CLOSING */; this.tcpSocket.destroy(); } closeConnectionAsOfUnpair(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(); } } sendEvent(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!"); const eventName = aid + "." + iid; if (!this.registeredEvents.has(eventName)) { // non verified connections can't register events, so this case is covered! return; } const 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 (let i = this.queuedEvents.length - 1; i >= 0; i--) { const 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(); } } handleEventsTimeout() { 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(); } writeQueuedEventNotifications() { 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; } const eventData = { characteristics: [], }; for (const queuedEvent of this.queuedEvents) { if (!this.registeredEvents.has(queuedEvent.aid + "." + queuedEvent.iid)) { continue; // client unregistered that event in the meantime } eventData.characteristics.push(queuedEvent); } 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 */ writeEventNotification(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(); const dataBuffer = Buffer.from(JSON.stringify(notification), "utf8"); const 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"); const buffer = Buffer.concat([header, dataBuffer]); this.tcpSocket.write(this.encrypt(buffer), this.handleTCPSocketWriteFulfilled.bind(this)); } enableEventNotifications(aid, iid) { this.registeredEvents.add(aid + "." + iid); } disableEventNotifications(aid, iid) { this.registeredEvents.delete(aid + "." + iid); } hasEventNotifications(aid, iid) { return this.registeredEvents.has(aid + "." + iid); } getRegisteredEvents() { return this.registeredEvents; } clearRegisteredEvents() { this.registeredEvents.clear(); } encrypt(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 } decrypt(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 } onHttpServerListening() { const addressInfo = this.internalHttpServer.address(); // address() is only a string when listening to unix domain sockets const addressString = addressInfo.family === "IPv6" ? `[${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", () => { // 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. */ onTCPSocketData(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. */ handleHttpServerRequest(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. */ handleHttpServerResponse(data) { 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(() => 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(); } } } handleTCPSocketWriteFulfilled() { this.lastSocketOperation = new Date().getTime(); } onTCPSocketError(err) { debugCon("[%s] Client connection error: %s", this.remoteAddress, err.message); // onTCPSocketClose will be called next } onTCPSocketClose() { 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 } onHttpServerError(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)()); } } onHttpServerClose() { debugCon("[%s] HTTP server was closed", this.remoteAddress); // make sure the iOS side is closed as well this.close(); } onHttpSocketError(err) { debugCon("[%s] HTTP connection error: ", this.remoteAddress, err.message); // onHttpSocketClose will be called next } onHttpSocketClose() { 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(); } getLocalAddress(ipVersion) { const 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") { const ipv4Info = interfaceDetails.find(info => info.family === "IPv4"); if (ipv4Info) { return ipv4Info.address; } throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface + "."); } let localUniqueAddress; for (const v6entry of interfaceDetails.filter(entry => entry.family === "IPv6")) { if (!v6entry.scopeid) { return v6entry.address; } localUniqueAddress ??= v6entry.address; } if (localUniqueAddress) { return localUniqueAddress; } throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface); } static getLocalNetworkInterface(socket) { let localAddress = socket.localAddress; // Grab the list of network interfaces. const interfaces = os_1.default.networkInterfaces(); // Default to the first non-loopback interface we see. const defaultInterface = () => Object.entries(interfaces).find(([, addresses]) => addresses?.some(address => !address.internal))?.[0] ?? "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]; // Let's find an exact match using the IP. for (const [name, addresses] of Object.entries(interfaces)) { if (addresses?.some(({ address }) => address === localAddress)) { return name; } } // 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). const family = net_1.default.isIPv4(localAddress) ? "IPv4" : "IPv6"; // Let's find a match based on the subnet. for (const [name, addresses] of Object.entries(interfaces)) { if (addresses?.some(entry => entry.family === family && (0, domain_formatter_1.getNetAddress)(localAddress, entry.netmask) === (0, domain_formatter_1.getNetAddress)(entry.address, entry.netmask))) { return name; } } console.log("WARNING: unable to determine which interface to use for socket coming from " + socket.remoteAddress + ":" + socket.remotePort + " to " + socket.localAddress + "."); return defaultInterface(); } } exports.HAPConnection = HAPConnection; //# sourceMappingURL=eventedhttp.js.map