UNPKG

35.5 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.HAPConnection = exports.HAPConnectionEvent = exports.HAPConnectionState = exports.EventedHTTPServer = exports.EventedHTTPServerEvent = exports.HAPEncryption = void 0;
4const tslib_1 = require("tslib");
5const domain_formatter_1 = require("@homebridge/ciao/lib/util/domain-formatter");
6const assert_1 = tslib_1.__importDefault(require("assert"));
7const debug_1 = tslib_1.__importDefault(require("debug"));
8const events_1 = require("events");
9const http_1 = tslib_1.__importDefault(require("http"));
10const net_1 = tslib_1.__importDefault(require("net"));
11const os_1 = tslib_1.__importDefault(require("os"));
12const hapCrypto = tslib_1.__importStar(require("./hapCrypto"));
13const net_utils_1 = require("./net-utils");
14const uuid = tslib_1.__importStar(require("./uuid"));
15const debug = (0, debug_1.default)("HAP-NodeJS:EventedHTTPServer");
16const debugCon = (0, debug_1.default)("HAP-NodeJS:EventedHTTPServer:Connection");
17const debugEvents = (0, debug_1.default)("HAP-NodeJS:EventEmitter");
18/**
19 * Simple struct to hold vars needed to support HAP encryption.
20 *
21 * @group Cryptography
22 */
23class HAPEncryption {
24 clientPublicKey;
25 secretKey;
26 publicKey;
27 sharedSecret;
28 hkdfPairEncryptionKey;
29 accessoryToControllerCount = 0;
30 controllerToAccessoryCount = 0;
31 accessoryToControllerKey;
32 controllerToAccessoryKey;
33 incompleteFrame;
34 constructor(clientPublicKey, secretKey, publicKey, sharedSecret, hkdfPairEncryptionKey) {
35 this.clientPublicKey = clientPublicKey;
36 this.secretKey = secretKey;
37 this.publicKey = publicKey;
38 this.sharedSecret = sharedSecret;
39 this.hkdfPairEncryptionKey = hkdfPairEncryptionKey;
40 this.accessoryToControllerKey = Buffer.alloc(0);
41 this.controllerToAccessoryKey = Buffer.alloc(0);
42 }
43}
44exports.HAPEncryption = HAPEncryption;
45/**
46 * @group HAP Accessory Server
47 */
48var EventedHTTPServerEvent;
49(function (EventedHTTPServerEvent) {
50 EventedHTTPServerEvent["LISTENING"] = "listening";
51 EventedHTTPServerEvent["CONNECTION_OPENED"] = "connection-opened";
52 EventedHTTPServerEvent["REQUEST"] = "request";
53 EventedHTTPServerEvent["CONNECTION_CLOSED"] = "connection-closed";
54})(EventedHTTPServerEvent || (exports.EventedHTTPServerEvent = EventedHTTPServerEvent = {}));
55/**
56 * EventedHTTPServer provides an HTTP-like server that supports HAP "extensions" for security and events.
57 *
58 * Implementation
59 * --------------
60 * In order to implement the "custom HTTP" server required by the HAP protocol (see HAPServer.js) without completely
61 * reinventing the wheel, we create both a generic TCP socket server and a standard Node HTTP server.
62 * The TCP socket server acts as a proxy, allowing users of this class to transform data (for encryption) as necessary
63 * and passing through bytes directly to the HTTP server for processing. This way we get Node to do all
64 * the "heavy lifting" of HTTP like parsing headers and formatting responses.
65 *
66 * Events are sent by simply waiting for current HTTP traffic to subside and then sending a custom response packet
67 * directly down the wire via the socket.
68 *
69 * Each connection to the main TCP server gets its own internal HTTP server, so we can track ongoing requests/responses
70 * for safe event insertion.
71 *
72 * @group HAP Accessory Server
73 */
74// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
75class EventedHTTPServer extends events_1.EventEmitter {
76 static CONNECTION_TIMEOUT_LIMIT = 16; // if we have more (or equal) # connections we start the timeout
77 static MAX_CONNECTION_IDLE_TIME = 60 * 60 * 1000; // 1h
78 tcpServer;
79 /**
80 * Set of all currently connected HAP connections.
81 */
82 connections = new Set();
83 /**
84 * Session dictionary indexed by username/identifier. The username uniquely identifies every person added to the home.
85 * So there can be multiple sessions open for a single username (multiple devices connected to the same Apple ID).
86 */
87 connectionsByUsername = new Map();
88 connectionIdleTimeout;
89 connectionLoggingInterval;
90 constructor() {
91 super();
92 this.tcpServer = net_1.default.createServer();
93 }
94 scheduleNextConnectionIdleTimeout() {
95 this.connectionIdleTimeout = undefined;
96 if (!this.tcpServer.listening) {
97 return;
98 }
99 debug("Running idle timeout timer...");
100 const currentTime = new Date().getTime();
101 let nextTimeout = -1;
102 for (const connection of this.connections) {
103 const timeDelta = currentTime - connection.lastSocketOperation;
104 if (timeDelta >= EventedHTTPServer.MAX_CONNECTION_IDLE_TIME) {
105 debug("[%s] Closing connection as it was inactive for " + timeDelta + "ms");
106 connection.close();
107 }
108 else {
109 nextTimeout = Math.max(nextTimeout, EventedHTTPServer.MAX_CONNECTION_IDLE_TIME - timeDelta);
110 }
111 }
112 if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT) {
113 this.connectionIdleTimeout = setTimeout(this.scheduleNextConnectionIdleTimeout.bind(this), nextTimeout);
114 }
115 }
116 address() {
117 return this.tcpServer.address();
118 }
119 listen(targetPort, hostname) {
120 this.tcpServer.listen(targetPort, hostname, () => {
121 const address = this.tcpServer.address(); // address() is only a string when listening to unix domain sockets
122 debug("Server listening on %s:%s", address.family === "IPv6" ? `[${address.address}]` : address.address, address.port);
123 this.connectionLoggingInterval = setInterval(() => {
124 const connectionInformation = [...this.connections]
125 .map(connection => `${connection.remoteAddress}:${connection.remotePort}`)
126 .join(", ");
127 debug("Currently %d hap connections open: %s", this.connections.size, connectionInformation);
128 }, 60_000);
129 this.connectionLoggingInterval.unref();
130 this.emit("listening" /* EventedHTTPServerEvent.LISTENING */, address.port, address.address);
131 });
132 this.tcpServer.on("connection", this.onConnection.bind(this));
133 }
134 stop() {
135 if (this.connectionLoggingInterval != null) {
136 clearInterval(this.connectionLoggingInterval);
137 this.connectionLoggingInterval = undefined;
138 }
139 if (this.connectionIdleTimeout != null) {
140 clearTimeout(this.connectionIdleTimeout);
141 this.connectionIdleTimeout = undefined;
142 }
143 this.tcpServer.close();
144 for (const connection of this.connections) {
145 connection.close();
146 }
147 }
148 destroy() {
149 this.stop();
150 this.removeAllListeners();
151 }
152 /**
153 * Send an event notification for given characteristic and changed value to all connected clients.
154 * If `originator` is specified, the given {@link HAPConnection} will be excluded from the broadcast.
155 *
156 * @param aid - The accessory id of the updated characteristic.
157 * @param iid - The instance id of the updated characteristic.
158 * @param value - The newly set value of the characteristic.
159 * @param originator - If specified, the connection will not get an event message.
160 * @param immediateDelivery - The HAP spec requires some characteristics to be delivery immediately.
161 * Namely, for the {@link Characteristic.ButtonEvent} and the {@link Characteristic.ProgrammableSwitchEvent} characteristics.
162 */
163 broadcastEvent(aid, iid, value, originator, immediateDelivery) {
164 for (const connection of this.connections) {
165 if (connection === originator) {
166 debug("[%s] Muting event '%s' notification for this connection since it originated here.", connection.remoteAddress, aid + "." + iid);
167 continue;
168 }
169 connection.sendEvent(aid, iid, value, immediateDelivery);
170 }
171 }
172 onConnection(socket) {
173 // eslint-disable-next-line @typescript-eslint/no-use-before-define
174 const connection = new HAPConnection(this, socket);
175 connection.on("request" /* HAPConnectionEvent.REQUEST */, (request, response) => {
176 this.emit("request" /* EventedHTTPServerEvent.REQUEST */, connection, request, response);
177 });
178 connection.on("authenticated" /* HAPConnectionEvent.AUTHENTICATED */, this.handleConnectionAuthenticated.bind(this, connection));
179 connection.on("closed" /* HAPConnectionEvent.CLOSED */, this.handleConnectionClose.bind(this, connection));
180 this.connections.add(connection);
181 debug("[%s] New connection from client on interface %s (%s)", connection.remoteAddress, connection.networkInterface, connection.localAddress);
182 this.emit("connection-opened" /* EventedHTTPServerEvent.CONNECTION_OPENED */, connection);
183 if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT && !this.connectionIdleTimeout) {
184 this.scheduleNextConnectionIdleTimeout();
185 }
186 }
187 handleConnectionAuthenticated(connection, username) {
188 const connections = this.connectionsByUsername.get(username);
189 if (!connections) {
190 this.connectionsByUsername.set(username, [connection]);
191 }
192 else if (!connections.includes(connection)) { // ensure this doesn't get added more than one time
193 connections.push(connection);
194 }
195 }
196 handleConnectionClose(connection) {
197 this.emit("connection-closed" /* EventedHTTPServerEvent.CONNECTION_CLOSED */, connection);
198 this.connections.delete(connection);
199 if (connection.username) { // aka connection was authenticated
200 const connections = this.connectionsByUsername.get(connection.username);
201 if (connections) {
202 const index = connections.indexOf(connection);
203 if (index !== -1) {
204 connections.splice(index, 1);
205 }
206 if (connections.length === 0) {
207 this.connectionsByUsername.delete(connection.username);
208 }
209 }
210 }
211 }
212 /**
213 * This method is to be called when a given {@link HAPConnection} performs a request that should result in the disconnection
214 * of all other {@link HAPConnection} with the same {@link HAPUsername}.
215 *
216 * The initiator MUST be in the middle of a http request were the response was not served yet.
217 * Otherwise, the initiator connection might reside in a state where it isn't disconnected and can't make any further requests.
218 *
219 * @param initiator - The connection that requested to disconnect all connections of the same username.
220 * @param username - The username for which all connections shall be closed.
221 */
222 static destroyExistingConnectionsAfterUnpair(initiator, username) {
223 const connections = initiator.server.connectionsByUsername.get(username);
224 if (connections) {
225 for (const connection of connections) {
226 connection.closeConnectionAsOfUnpair(initiator);
227 }
228 }
229 }
230}
231exports.EventedHTTPServer = EventedHTTPServer;
232/**
233 * @private
234 * @group HAP Accessory Server
235 */
236var HAPConnectionState;
237(function (HAPConnectionState) {
238 HAPConnectionState[HAPConnectionState["CONNECTING"] = 0] = "CONNECTING";
239 HAPConnectionState[HAPConnectionState["FULLY_SET_UP"] = 1] = "FULLY_SET_UP";
240 HAPConnectionState[HAPConnectionState["AUTHENTICATED"] = 2] = "AUTHENTICATED";
241 // above signals represent an alive connection
242 // below states are considered "closed or soon closed"
243 HAPConnectionState[HAPConnectionState["TO_BE_TEARED_DOWN"] = 3] = "TO_BE_TEARED_DOWN";
244 HAPConnectionState[HAPConnectionState["CLOSING"] = 4] = "CLOSING";
245 HAPConnectionState[HAPConnectionState["CLOSED"] = 5] = "CLOSED";
246})(HAPConnectionState || (exports.HAPConnectionState = HAPConnectionState = {}));
247/**
248 * @group HAP Accessory Server
249 */
250var HAPConnectionEvent;
251(function (HAPConnectionEvent) {
252 HAPConnectionEvent["REQUEST"] = "request";
253 HAPConnectionEvent["AUTHENTICATED"] = "authenticated";
254 HAPConnectionEvent["CLOSED"] = "closed";
255})(HAPConnectionEvent || (exports.HAPConnectionEvent = HAPConnectionEvent = {}));
256/**
257 * Manages a single iOS-initiated HTTP connection during its lifetime.
258 * @group HAP Accessory Server
259 */
260// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
261class HAPConnection extends events_1.EventEmitter {
262 /**
263 * @private file-private API
264 */
265 server;
266 sessionID; // uuid unique to every HAP connection
267 state = 0 /* HAPConnectionState.CONNECTING */;
268 localAddress;
269 remoteAddress; // cache because it becomes undefined in 'onClientSocketClose'
270 remotePort;
271 networkInterface;
272 tcpSocket;
273 internalHttpServer;
274 httpSocket; // set when in state FULLY_SET_UP
275 internalHttpServerPort;
276 internalHttpServerAddress;
277 lastSocketOperation = new Date().getTime();
278 pendingClientSocketData = Buffer.alloc(0); // data received from client before HTTP proxy is fully setup
279 handlingRequest = false; // true while we are composing an HTTP response (so events can wait)
280 username; // username is unique to every user in the home, basically identifies an Apple ID
281 encryption; // created in handlePairVerifyStepOne
282 srpServer;
283 _pairSetupState; // TODO ensure those two states are always correctly reset?
284 _pairVerifyState;
285 registeredEvents = new Set();
286 eventsTimer;
287 queuedEvents = [];
288 /**
289 * If true, the above {@link queuedEvents} contains events which are set to be delivered immediately!
290 */
291 eventsQueuedForImmediateDelivery = false;
292 timedWritePid;
293 timedWriteTimeout;
294 constructor(server, clientSocket) {
295 super();
296 this.server = server;
297 this.sessionID = uuid.generate(clientSocket.remoteAddress + ":" + clientSocket.remotePort);
298 this.localAddress = clientSocket.localAddress;
299 this.remoteAddress = clientSocket.remoteAddress; // cache because it becomes undefined in 'onClientSocketClose'
300 this.remotePort = clientSocket.remotePort;
301 this.networkInterface = HAPConnection.getLocalNetworkInterface(clientSocket);
302 // clientSocket is the socket connected to the actual iOS device
303 this.tcpSocket = clientSocket;
304 this.tcpSocket.on("data", this.onTCPSocketData.bind(this));
305 this.tcpSocket.on("close", this.onTCPSocketClose.bind(this));
306 // we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely.
307 this.tcpSocket.on("error", this.onTCPSocketError.bind(this));
308 this.tcpSocket.setNoDelay(true); // disable Nagle algorithm
309 // "HAP accessory servers must not use keepalive messages, which periodically wake up iOS devices".
310 // Thus, we don't configure any tcp keepalive
311 // create our internal HTTP server for this connection that we will proxy data to and from
312 this.internalHttpServer = http_1.default.createServer();
313 this.internalHttpServer.timeout = 0; // clients expect to hold connections open as long as they want
314 this.internalHttpServer.keepAliveTimeout = 0; // workaround for https://github.com/nodejs/node/issues/13391
315 this.internalHttpServer.on("listening", this.onHttpServerListening.bind(this));
316 this.internalHttpServer.on("request", this.handleHttpServerRequest.bind(this));
317 this.internalHttpServer.on("error", this.onHttpServerError.bind(this));
318 // close event is added later on the "connect" event as possible listen retries would throw unnecessary close events
319 this.internalHttpServer.listen(0, this.internalHttpServerAddress = (0, net_utils_1.getOSLoopbackAddressIfAvailable)());
320 }
321 debugListenerRegistration(event, registration = true, beforeCount = -1) {
322 const stackTrace = new Error().stack.split("\n")[3];
323 const eventCount = this.listeners(event).length;
324 const tabs1 = event === "authenticated" /* HAPConnectionEvent.AUTHENTICATED */ ? "\t" : "\t\t";
325 const tabs2 = !registration ? "\t" : "\t\t";
326 // eslint-disable-next-line max-len
327 debugEvents(`[${this.remoteAddress}] ${registration ? "Registered" : "Unregistered"} event '${String(event).toUpperCase()}' ${tabs1}(total: ${eventCount}${!registration ? " Before: " + beforeCount : ""}) ${tabs2}${stackTrace}`);
328 }
329 // eslint-disable-next-line @typescript-eslint/no-explicit-any
330 on(event, listener) {
331 const result = super.on(event, listener);
332 this.debugListenerRegistration(event);
333 return result;
334 }
335 // eslint-disable-next-line @typescript-eslint/no-explicit-any
336 addListener(event, listener) {
337 const result = super.addListener(event, listener);
338 this.debugListenerRegistration(event);
339 return result;
340 }
341 // eslint-disable-next-line @typescript-eslint/no-explicit-any
342 removeListener(event, listener) {
343 const beforeCount = this.listeners(event).length;
344 const result = super.removeListener(event, listener);
345 this.debugListenerRegistration(event, false, beforeCount);
346 return result;
347 }
348 // eslint-disable-next-line @typescript-eslint/no-explicit-any
349 off(event, listener) {
350 const result = super.off(event, listener);
351 const beforeCount = this.listeners(event).length;
352 this.debugListenerRegistration(event, false, beforeCount);
353 return result;
354 }
355 /**
356 * This method is called once the connection has gone through pair-verify.
357 * As any HomeKit controller will initiate a pair-verify after the pair-setup procedure, this method gets
358 * not called on the initial pair-setup.
359 *
360 * Once this method has been called, the connection is authenticated and encryption is turned on.
361 */
362 connectionAuthenticated(username) {
363 this.state = 2 /* HAPConnectionState.AUTHENTICATED */;
364 this.username = username;
365 this.emit("authenticated" /* HAPConnectionEvent.AUTHENTICATED */, username);
366 }
367 isAuthenticated() {
368 return this.state === 2 /* HAPConnectionState.AUTHENTICATED */;
369 }
370 close() {
371 if (this.state >= 4 /* HAPConnectionState.CLOSING */) {
372 return; // already closed/closing
373 }
374 this.state = 4 /* HAPConnectionState.CLOSING */;
375 this.tcpSocket.destroy();
376 }
377 closeConnectionAsOfUnpair(initiator) {
378 if (this === initiator) {
379 // the initiator of the unpair request is this connection, meaning it unpaired itself.
380 // we still need to send the response packet to the unpair request.
381 this.state = 3 /* HAPConnectionState.TO_BE_TEARED_DOWN */;
382 }
383 else {
384 // as HomeKit requires it, destroy any active session which got unpaired
385 this.close();
386 }
387 }
388 sendEvent(aid, iid, value, immediateDelivery) {
389 (0, assert_1.default)(aid != null, "HAPConnection.sendEvent: aid must be defined!");
390 (0, assert_1.default)(iid != null, "HAPConnection.sendEvent: iid must be defined!");
391 const eventName = aid + "." + iid;
392 if (!this.registeredEvents.has(eventName)) {
393 // non verified connections can't register events, so this case is covered!
394 return;
395 }
396 const event = {
397 aid: aid,
398 iid: iid,
399 value: value,
400 };
401 if (immediateDelivery) {
402 // some characteristics are required to deliver notifications immediately
403 // we will flush all other events too, on that occasion.
404 this.queuedEvents.push(event);
405 this.eventsQueuedForImmediateDelivery = true;
406 if (this.eventsTimer) {
407 clearTimeout(this.eventsTimer);
408 this.eventsTimer = undefined;
409 }
410 this.handleEventsTimeout();
411 return;
412 }
413 // we search the list of queued events in reverse order.
414 // if the last element with the same aid and iid has the same value we don't want to send the event notification twice.
415 // BUT, we do not want to override previous event notifications which have a different value. Automations must be executed!
416 for (let i = this.queuedEvents.length - 1; i >= 0; i--) {
417 const queuedEvent = this.queuedEvents[i];
418 if (queuedEvent.aid === aid && queuedEvent.iid === iid) {
419 if (queuedEvent.value === value) {
420 return; // the same event was already queued. do not add it again!
421 }
422 break; // we break in any case
423 }
424 }
425 this.queuedEvents.push(event);
426 // if there is already a timer running we just add it in the queue.
427 if (!this.eventsTimer) {
428 this.eventsTimer = setTimeout(this.handleEventsTimeout.bind(this), 250);
429 this.eventsTimer.unref();
430 }
431 }
432 handleEventsTimeout() {
433 this.eventsTimer = undefined;
434 if (this.state > 2 /* HAPConnectionState.AUTHENTICATED */) {
435 // connection is closed or about to be closed. no need to send any further events
436 return;
437 }
438 this.writeQueuedEventNotifications();
439 }
440 writeQueuedEventNotifications() {
441 if (this.queuedEvents.length === 0 || this.handlingRequest) {
442 return; // don't send empty event notifications or if we are currently handling a request
443 }
444 if (this.eventsTimer) {
445 // this method might be called when we have enqueued data AND data that is queued for immediate delivery!
446 clearTimeout(this.eventsTimer);
447 this.eventsTimer = undefined;
448 }
449 const eventData = {
450 characteristics: [],
451 };
452 for (const queuedEvent of this.queuedEvents) {
453 if (!this.registeredEvents.has(queuedEvent.aid + "." + queuedEvent.iid)) {
454 continue; // client unregistered that event in the meantime
455 }
456 eventData.characteristics.push(queuedEvent);
457 }
458 this.queuedEvents.splice(0, this.queuedEvents.length);
459 this.eventsQueuedForImmediateDelivery = false;
460 this.writeEventNotification(eventData);
461 }
462 /**
463 * This will create an EVENT/1.0 notification header with the provided event notification.
464 * If currently an HTTP request is in progress the assembled packet will be
465 * added to the pending events list.
466 *
467 * @param notification - The event which should be sent out
468 */
469 writeEventNotification(notification) {
470 debugCon("[%s] Sending HAP event notifications %o", this.remoteAddress, notification.characteristics);
471 (0, assert_1.default)(!this.handlingRequest, "Can't write event notifications while handling a request!");
472 // Apple backend processes events in reverse order, so we need to reverse the array
473 // so that events are processed in chronological order.
474 notification.characteristics.reverse();
475 const dataBuffer = Buffer.from(JSON.stringify(notification), "utf8");
476 const header = Buffer.from("EVENT/1.0 200 OK\r\n" +
477 "Content-Type: application/hap+json\r\n" +
478 "Content-Length: " + dataBuffer.length + "\r\n" +
479 "\r\n", "utf8");
480 const buffer = Buffer.concat([header, dataBuffer]);
481 this.tcpSocket.write(this.encrypt(buffer), this.handleTCPSocketWriteFulfilled.bind(this));
482 }
483 enableEventNotifications(aid, iid) {
484 this.registeredEvents.add(aid + "." + iid);
485 }
486 disableEventNotifications(aid, iid) {
487 this.registeredEvents.delete(aid + "." + iid);
488 }
489 hasEventNotifications(aid, iid) {
490 return this.registeredEvents.has(aid + "." + iid);
491 }
492 getRegisteredEvents() {
493 return this.registeredEvents;
494 }
495 clearRegisteredEvents() {
496 this.registeredEvents.clear();
497 }
498 encrypt(data) {
499 // if accessoryToControllerKey is not empty, then encryption is enabled for this connection. However, we'll
500 // need to be careful to ensure that we don't encrypt the last few bytes of the response from handlePairVerifyStepTwo.
501 // Since all communication calls are asynchronous, we could easily receive this 'encrypt' event for those bytes.
502 // So we want to make sure that we aren't encrypting data until we have *received* some encrypted data from the client first.
503 if (this.encryption && this.encryption.accessoryToControllerKey.length > 0 && this.encryption.controllerToAccessoryCount > 0) {
504 return hapCrypto.layerEncrypt(data, this.encryption);
505 }
506 return data; // otherwise, we don't encrypt and return plaintext
507 }
508 decrypt(data) {
509 if (this.encryption && this.encryption.controllerToAccessoryKey.length > 0) {
510 // below call may throw an error if decryption failed
511 return hapCrypto.layerDecrypt(data, this.encryption);
512 }
513 return data; // otherwise, we don't decrypt and return plaintext
514 }
515 onHttpServerListening() {
516 const addressInfo = this.internalHttpServer.address(); // address() is only a string when listening to unix domain sockets
517 const addressString = addressInfo.family === "IPv6" ? `[${addressInfo.address}]` : addressInfo.address;
518 this.internalHttpServerPort = addressInfo.port;
519 debugCon("[%s] Internal HTTP server listening on %s:%s", this.remoteAddress, addressString, addressInfo.port);
520 this.internalHttpServer.on("close", this.onHttpServerClose.bind(this));
521 // now we can establish a connection to this running HTTP server for proxying data
522 this.httpSocket = net_1.default.createConnection(this.internalHttpServerPort, this.internalHttpServerAddress); // previously we used addressInfo.address
523 this.httpSocket.setNoDelay(true); // disable Nagle algorithm
524 this.httpSocket.on("data", this.handleHttpServerResponse.bind(this));
525 // we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely.
526 this.httpSocket.on("error", this.onHttpSocketError.bind(this));
527 this.httpSocket.on("close", this.onHttpSocketClose.bind(this));
528 this.httpSocket.on("connect", () => {
529 // we are now fully set up:
530 // - clientSocket is connected to the iOS device
531 // - serverSocket is connected to the httpServer
532 // - ready to proxy data!
533 this.state = 1 /* HAPConnectionState.FULLY_SET_UP */;
534 debugCon("[%s] Internal HTTP socket connected. HAPConnection now fully set up!", this.remoteAddress);
535 // start by flushing any pending buffered data received from the client while we were setting up
536 if (this.pendingClientSocketData && this.pendingClientSocketData.length > 0) {
537 this.httpSocket.write(this.pendingClientSocketData);
538 }
539 this.pendingClientSocketData = undefined;
540 });
541 }
542 /**
543 * This event handler is called when we receive data from a HomeKit controller on our tcp socket.
544 * We store the data if the internal http server is not read yet, or forward it to the http server.
545 */
546 onTCPSocketData(data) {
547 if (this.state > 2 /* HAPConnectionState.AUTHENTICATED */) {
548 // don't accept data of a connection which is about to be closed or already closed
549 return;
550 }
551 this.handlingRequest = true; // reverted to false once response was sent out
552 this.lastSocketOperation = new Date().getTime();
553 try {
554 data = this.decrypt(data);
555 }
556 catch (error) { // decryption and/or verification failed, disconnect the client
557 debugCon("[%s] Error occurred trying to decrypt incoming packet: %s", this.remoteAddress, error.message);
558 this.close();
559 return;
560 }
561 if (this.state < 1 /* HAPConnectionState.FULLY_SET_UP */) { // we're not setup yet, so add this data to our intermediate buffer
562 this.pendingClientSocketData = Buffer.concat([this.pendingClientSocketData, data]);
563 }
564 else {
565 this.httpSocket.write(data); // proxy it along to the HTTP server
566 }
567 }
568 /**
569 * This event handler is called when the internal http server receives a request.
570 * Meaning we received data from the HomeKit controller in {@link onTCPSocketData}, which then send the
571 * data unencrypted to the internal http server. And now it landed here, fully parsed as a http request.
572 */
573 handleHttpServerRequest(request, response) {
574 if (this.state > 2 /* HAPConnectionState.AUTHENTICATED */) {
575 // don't accept data of a connection which is about to be closed or already closed
576 return;
577 }
578 debugCon("[%s] HTTP request: %s", this.remoteAddress, request.url);
579 request.socket.setNoDelay(true);
580 this.emit("request" /* HAPConnectionEvent.REQUEST */, request, response);
581 }
582 /**
583 * This event handler is called by the socket which is connected to our internal http server.
584 * It is called with the response returned from the http server.
585 * In this method we have to encrypt and forward the message back to the HomeKit controller.
586 */
587 handleHttpServerResponse(data) {
588 data = this.encrypt(data);
589 this.tcpSocket.write(data, this.handleTCPSocketWriteFulfilled.bind(this));
590 debugCon("[%s] HTTP Response is finished", this.remoteAddress);
591 this.handlingRequest = false;
592 if (this.state === 3 /* HAPConnectionState.TO_BE_TEARED_DOWN */) {
593 setTimeout(() => this.close(), 10);
594 }
595 else if (this.state < 3 /* HAPConnectionState.TO_BE_TEARED_DOWN */) {
596 if (!this.eventsTimer || this.eventsQueuedForImmediateDelivery) {
597 // we deliver events if there is no eventsTimer (meaning it ran out in the meantime)
598 // or when the queue contains events set to be delivered immediately
599 this.writeQueuedEventNotifications();
600 }
601 }
602 }
603 handleTCPSocketWriteFulfilled() {
604 this.lastSocketOperation = new Date().getTime();
605 }
606 onTCPSocketError(err) {
607 debugCon("[%s] Client connection error: %s", this.remoteAddress, err.message);
608 // onTCPSocketClose will be called next
609 }
610 onTCPSocketClose() {
611 this.state = 5 /* HAPConnectionState.CLOSED */;
612 debugCon("[%s] Client connection closed", this.remoteAddress);
613 if (this.httpSocket) {
614 this.httpSocket.destroy();
615 }
616 this.internalHttpServer.close();
617 this.emit("closed" /* HAPConnectionEvent.CLOSED */); // sending final closed event
618 this.removeAllListeners(); // cleanup listeners, we are officially dead now
619 }
620 onHttpServerError(err) {
621 debugCon("[%s] HTTP server error: %s", this.remoteAddress, err.message);
622 if (err.code === "EADDRINUSE") {
623 this.internalHttpServerPort = undefined;
624 this.internalHttpServer.close();
625 this.internalHttpServer.listen(0, this.internalHttpServerAddress = (0, net_utils_1.getOSLoopbackAddressIfAvailable)());
626 }
627 }
628 onHttpServerClose() {
629 debugCon("[%s] HTTP server was closed", this.remoteAddress);
630 // make sure the iOS side is closed as well
631 this.close();
632 }
633 onHttpSocketError(err) {
634 debugCon("[%s] HTTP connection error: ", this.remoteAddress, err.message);
635 // onHttpSocketClose will be called next
636 }
637 onHttpSocketClose() {
638 debugCon("[%s] HTTP connection was closed", this.remoteAddress);
639 // we only support a single long-lived connection to our internal HTTP server. Since it's closed,
640 // we'll need to shut it down entirely.
641 this.internalHttpServer.close();
642 }
643 getLocalAddress(ipVersion) {
644 const interfaceDetails = os_1.default.networkInterfaces()[this.networkInterface];
645 if (!interfaceDetails) {
646 throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface);
647 }
648 // Find our first local IPv4 address.
649 if (ipVersion === "ipv4") {
650 const ipv4Info = interfaceDetails.find(info => info.family === "IPv4");
651 if (ipv4Info) {
652 return ipv4Info.address;
653 }
654 throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface + ".");
655 }
656 let localUniqueAddress;
657 for (const v6entry of interfaceDetails.filter(entry => entry.family === "IPv6")) {
658 if (!v6entry.scopeid) {
659 return v6entry.address;
660 }
661 localUniqueAddress ??= v6entry.address;
662 }
663 if (localUniqueAddress) {
664 return localUniqueAddress;
665 }
666 throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface);
667 }
668 static getLocalNetworkInterface(socket) {
669 let localAddress = socket.localAddress;
670 // Grab the list of network interfaces.
671 const interfaces = os_1.default.networkInterfaces();
672 // Default to the first non-loopback interface we see.
673 const defaultInterface = () => Object.entries(interfaces).find(([, addresses]) => addresses?.some(address => !address.internal))?.[0] ?? "unknown";
674 // No local address return our default.
675 if (!localAddress) {
676 return defaultInterface();
677 }
678 // Handle IPv4-mapped IPv6 addresses.
679 localAddress = localAddress.replace(/^::ffff:/i, "");
680 // Handle edge cases where we have an IPv4-mapped IPv6 address without the requisite prefix.
681 if (/^::(?:\d{1,3}\.){3}\d{1,3}$/.test(localAddress)) {
682 localAddress = localAddress.replace(/^::/, "");
683 }
684 // Handle link-local IPv6 addresses.
685 localAddress = localAddress.split("%")[0];
686 // Let's find an exact match using the IP.
687 for (const [name, addresses] of Object.entries(interfaces)) {
688 if (addresses?.some(({ address }) => address === localAddress)) {
689 return name;
690 }
691 }
692 // 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).
693 const family = net_1.default.isIPv4(localAddress) ? "IPv4" : "IPv6";
694 // Let's find a match based on the subnet.
695 for (const [name, addresses] of Object.entries(interfaces)) {
696 if (addresses?.some(entry => entry.family === family && (0, domain_formatter_1.getNetAddress)(localAddress, entry.netmask) === (0, domain_formatter_1.getNetAddress)(entry.address, entry.netmask))) {
697 return name;
698 }
699 }
700 console.log("WARNING: unable to determine which interface to use for socket coming from " + socket.remoteAddress + ":" + socket.remotePort + " to " +
701 socket.localAddress + ".");
702 return defaultInterface();
703 }
704}
705exports.HAPConnection = HAPConnection;
706//# sourceMappingURL=eventedhttp.js.map
\No newline at end of file