hap-nodejs
Version:
HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.
706 lines • 35.5 kB
JavaScript
"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