homebridge-plugin-wrapper
Version:
Wrapper for Homebridge and NodeJS-HAP with reduced dependencies that allows to intercept plugin values and also send to them
759 lines • 38.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HAPConnection = exports.HAPConnectionEvent = exports.HAPConnectionState = exports.EventedHTTPServer = exports.EventedHTTPServerEvent = exports.HAPEncryption = void 0;
var tslib_1 = require("tslib");
var domain_formatter_1 = require("@homebridge/ciao/lib/util/domain-formatter");
var assert_1 = (0, tslib_1.__importDefault)(require("assert"));
var debug_1 = (0, tslib_1.__importDefault)(require("debug"));
var events_1 = require("events");
var http_1 = (0, tslib_1.__importDefault)(require("http"));
var net_1 = (0, tslib_1.__importDefault)(require("net"));
var os_1 = (0, tslib_1.__importDefault)(require("os"));
var hapCrypto = (0, tslib_1.__importStar)(require("./hapCrypto"));
var net_utils_1 = require("./net-utils");
var uuid = (0, tslib_1.__importStar)(require("./uuid"));
var debug = (0, debug_1.default)("HAP-NodeJS:EventedHTTPServer");
var debugCon = (0, debug_1.default)("HAP-NodeJS:EventedHTTPServer:Connection");
/**
* Simple struct to hold vars needed to support HAP encryption.
*/
var HAPEncryption = /** @class */ (function () {
function HAPEncryption(clientPublicKey, secretKey, publicKey, sharedSecret, hkdfPairEncryptionKey) {
this.accessoryToControllerCount = 0;
this.controllerToAccessoryCount = 0;
this.clientPublicKey = clientPublicKey;
this.secretKey = secretKey;
this.publicKey = publicKey;
this.sharedSecret = sharedSecret;
this.hkdfPairEncryptionKey = hkdfPairEncryptionKey;
this.accessoryToControllerKey = Buffer.alloc(0);
this.controllerToAccessoryKey = Buffer.alloc(0);
}
return HAPEncryption;
}());
exports.HAPEncryption = HAPEncryption;
var EventedHTTPServerEvent;
(function (EventedHTTPServerEvent) {
EventedHTTPServerEvent["LISTENING"] = "listening";
EventedHTTPServerEvent["CONNECTION_OPENED"] = "connection-opened";
EventedHTTPServerEvent["REQUEST"] = "request";
EventedHTTPServerEvent["CONNECTION_CLOSED"] = "connection-closed";
})(EventedHTTPServerEvent = exports.EventedHTTPServerEvent || (exports.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.
*/
var EventedHTTPServer = /** @class */ (function (_super) {
(0, tslib_1.__extends)(EventedHTTPServer, _super);
function EventedHTTPServer() {
var _this = _super.call(this) || this;
/**
* Set of all currently connected HAP connections.
*/
_this.connections = new Set();
/**
* Session dictionary indexed by username/identifier. The username uniquely identifies every person added to the home.
* So there can be multiple sessions open for a single username (multiple devices connected to the same Apple ID).
*/
_this.connectionsByUsername = new Map();
_this.tcpServer = net_1.default.createServer();
var interval = setInterval(function () {
var e_1, _a;
var connectionString = "";
try {
for (var _b = (0, tslib_1.__values)(_this.connections), _c = _b.next(); !_c.done; _c = _b.next()) {
var connection = _c.value;
if (connectionString) {
connectionString += ", ";
}
connectionString += connection.remoteAddress + ":" + connection.remotePort;
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
debug("Currently " + _this.connections.size + " hap connections open: " + connectionString);
}, 60000);
interval.unref();
return _this;
}
EventedHTTPServer.prototype.scheduleNextConnectionIdleTimeout = function () {
var e_2, _a;
this.connectionIdleTimeout = undefined;
if (!this.tcpServer.listening) {
return;
}
debug("Running idle timeout timer...");
var currentTime = new Date().getTime();
var nextTimeout = -1;
try {
for (var _b = (0, tslib_1.__values)(this.connections), _c = _b.next(); !_c.done; _c = _b.next()) {
var connection = _c.value;
var timeDelta = currentTime - connection.lastSocketOperation;
if (timeDelta >= EventedHTTPServer.MAX_CONNECTION_IDLE_TIME) {
debug("[%s] Closing connection as it was inactive for " + timeDelta + "ms");
connection.close();
}
else {
nextTimeout = Math.max(nextTimeout, EventedHTTPServer.MAX_CONNECTION_IDLE_TIME - timeDelta);
}
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_2) throw e_2.error; }
}
if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT) {
this.connectionIdleTimeout = setTimeout(this.scheduleNextConnectionIdleTimeout.bind(this), nextTimeout);
}
};
EventedHTTPServer.prototype.listen = function (targetPort, hostname) {
var _this = this;
this.tcpServer.listen(targetPort, hostname, function () {
var address = _this.tcpServer.address(); // address() is only a string when listening to unix domain sockets
debug("Server listening on %s:%s", address.family === "IPv6" ? "[".concat(address.address, "]") : address.address, address.port);
_this.emit("listening" /* LISTENING */, address.port, address.address);
});
this.tcpServer.on("connection", this.onConnection.bind(this));
};
EventedHTTPServer.prototype.stop = function () {
var e_3, _a;
this.tcpServer.close();
try {
for (var _b = (0, tslib_1.__values)(this.connections), _c = _b.next(); !_c.done; _c = _b.next()) {
var connection = _c.value;
connection.close();
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_3) throw e_3.error; }
}
};
EventedHTTPServer.prototype.destroy = function () {
this.stop();
this.removeAllListeners();
};
/**
* Send an event notification for given characteristic and changed value to all connected clients.
* If {@param 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 ButtonEvent} and the {@link ProgrammableSwitchEvent} characteristics.
*/
EventedHTTPServer.prototype.broadcastEvent = function (aid, iid, value, originator, immediateDelivery) {
var e_4, _a;
try {
for (var _b = (0, tslib_1.__values)(this.connections), _c = _b.next(); !_c.done; _c = _b.next()) {
var connection = _c.value;
if (connection === originator) {
debug("[%s] Muting event '%s' notification for this connection since it originated here.", connection.remoteAddress, aid + "." + iid);
continue;
}
connection.sendEvent(aid, iid, value, immediateDelivery);
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_4) throw e_4.error; }
}
};
EventedHTTPServer.prototype.onConnection = function (socket) {
var _this = this;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
var connection = new HAPConnection(this, socket);
connection.on("request" /* REQUEST */, function (request, response) {
_this.emit("request" /* REQUEST */, connection, request, response);
});
connection.on("authenticated" /* AUTHENTICATED */, this.handleConnectionAuthenticated.bind(this, connection));
connection.on("closed" /* 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" /* CONNECTION_OPENED */, connection);
if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT && !this.connectionIdleTimeout) {
this.scheduleNextConnectionIdleTimeout();
}
};
EventedHTTPServer.prototype.handleConnectionAuthenticated = function (connection, username) {
var connections = this.connectionsByUsername.get(username);
if (!connections) {
this.connectionsByUsername.set(username, [connection]);
}
else if (!connections.includes(connection)) { // ensure this doesn't get added more than one time
connections.push(connection);
}
};
EventedHTTPServer.prototype.handleConnectionClose = function (connection) {
this.emit("connection-closed" /* CONNECTION_CLOSED */, connection);
this.connections.delete(connection);
if (connection.username) { // aka connection was authenticated
var connections = this.connectionsByUsername.get(connection.username);
if (connections) {
var index = connections.indexOf(connection);
if (index !== -1) {
connections.splice(index, 1);
}
if (connections.length === 0) {
this.connectionsByUsername.delete(connection.username);
}
}
}
};
EventedHTTPServer.destroyExistingConnectionsAfterUnpair = function (initiator, username) {
var e_5, _a;
var connections = initiator.server.connectionsByUsername.get(username);
if (connections) {
try {
for (var connections_1 = (0, tslib_1.__values)(connections), connections_1_1 = connections_1.next(); !connections_1_1.done; connections_1_1 = connections_1.next()) {
var connection = connections_1_1.value;
connection.closeConnectionAsOfUnpair(initiator);
}
}
catch (e_5_1) { e_5 = { error: e_5_1 }; }
finally {
try {
if (connections_1_1 && !connections_1_1.done && (_a = connections_1.return)) _a.call(connections_1);
}
finally { if (e_5) throw e_5.error; }
}
}
};
EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT = 16; // if we have more (or equal) # connections we start the timeout
EventedHTTPServer.MAX_CONNECTION_IDLE_TIME = 60 * 60 * 1000; // 1h
return EventedHTTPServer;
}(events_1.EventEmitter));
exports.EventedHTTPServer = EventedHTTPServer;
/**
* @private
*/
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 || (exports.HAPConnectionState = {}));
var HAPConnectionEvent;
(function (HAPConnectionEvent) {
HAPConnectionEvent["REQUEST"] = "request";
HAPConnectionEvent["AUTHENTICATED"] = "authenticated";
HAPConnectionEvent["CLOSED"] = "closed";
})(HAPConnectionEvent = exports.HAPConnectionEvent || (exports.HAPConnectionEvent = {}));
/**
* Manages a single iOS-initiated HTTP connection during its lifetime.
* @private
*/
var HAPConnection = /** @class */ (function (_super) {
(0, tslib_1.__extends)(HAPConnection, _super);
function HAPConnection(server, clientSocket) {
var _this = _super.call(this) || this;
_this.state = 0 /* CONNECTING */;
_this.lastSocketOperation = new Date().getTime();
_this.pendingClientSocketData = Buffer.alloc(0); // data received from client before HTTP proxy is fully setup
_this.handlingRequest = false; // true while we are composing an HTTP response (so events can wait)
_this.registeredEvents = new Set();
_this.queuedEvents = [];
// queue of unencrypted event data waiting to be sent until after an in-progress HTTP response is being written
_this.pendingEventData = [];
_this.server = server;
_this.sessionID = uuid.generate(clientSocket.remoteAddress + ":" + clientSocket.remotePort);
_this.localAddress = clientSocket.localAddress;
_this.remoteAddress = clientSocket.remoteAddress; // cache because it becomes undefined in 'onClientSocketClose'
_this.remotePort = clientSocket.remotePort;
_this.networkInterface = HAPConnection.getLocalNetworkInterface(clientSocket);
// clientSocket is the socket connected to the actual iOS device
_this.tcpSocket = clientSocket;
_this.tcpSocket.on("data", _this.onTCPSocketData.bind(_this));
_this.tcpSocket.on("close", _this.onTCPSocketClose.bind(_this));
// we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely.
_this.tcpSocket.on("error", _this.onTCPSocketError.bind(_this));
_this.tcpSocket.setNoDelay(true); // disable Nagle algorithm
// "HAP accessory servers must not use keepalive messages, which periodically wake up iOS devices".
// Thus, we don't configure any tcp keepalive
// create our internal HTTP server for this connection that we will proxy data to and from
_this.internalHttpServer = http_1.default.createServer();
_this.internalHttpServer.timeout = 0; // clients expect to hold connections open as long as they want
_this.internalHttpServer.keepAliveTimeout = 0; // workaround for https://github.com/nodejs/node/issues/13391
_this.internalHttpServer.on("listening", _this.onHttpServerListening.bind(_this));
_this.internalHttpServer.on("request", _this.handleHttpServerRequest.bind(_this));
_this.internalHttpServer.on("error", _this.onHttpServerError.bind(_this));
// close event is added later on the "connect" event as possible listen retries would throw unnecessary close events
_this.internalHttpServer.listen(0, _this.internalHttpServerAddress = (0, net_utils_1.getOSLoopbackAddressIfAvailable)());
return _this;
}
/**
* This method is called once the connection has gone through pair-verify.
* As any HomeKit controller will initiate a pair-verify after the pair-setup procedure, this method gets
* not called on the initial pair-setup.
*
* Once this method has been called, the connection is authenticated and encryption is turned on.
*/
HAPConnection.prototype.connectionAuthenticated = function (username) {
this.state = 2 /* AUTHENTICATED */;
this.username = username;
this.emit("authenticated" /* AUTHENTICATED */, username);
};
HAPConnection.prototype.isAuthenticated = function () {
return this.state === 2 /* AUTHENTICATED */;
};
HAPConnection.prototype.close = function () {
if (this.state >= 4 /* CLOSING */) {
return; // already closed/closing
}
this.state = 4 /* CLOSING */;
this.tcpSocket.destroy();
};
HAPConnection.prototype.closeConnectionAsOfUnpair = function (initiator) {
if (this === initiator) {
// the initiator of the unpair request is this connection, meaning it unpaired itself.
// we still need to send the response packet to the unpair request.
this.state = 3 /* TO_BE_TEARED_DOWN */;
}
else {
// as HomeKit requires it, destroy any active session which got unpaired
this.close();
}
};
HAPConnection.prototype.sendEvent = function (aid, iid, value, immediateDelivery) {
(0, assert_1.default)(aid != null, "HAPConnection.sendEvent: aid must be defined!");
(0, assert_1.default)(iid != null, "HAPConnection.sendEvent: iid must be defined!");
var eventName = aid + "." + iid;
if (!this.registeredEvents.has(eventName)) {
return;
}
var event = {
aid: aid,
iid: iid,
value: value,
};
if (immediateDelivery) {
// some characteristics are required to deliver notifications immediately
// we will flush all other events too, on that occasion.
this.queuedEvents.push(event);
if (this.eventsTimer) {
clearTimeout(this.eventsTimer);
}
this.handleEventsTimeout();
return;
}
// we search the list of queued events in reverse order.
// if the last element with the same aid and iid has the same value we don't want to send the event notification twice.
// BUT, we do not want to override previous event notifications which have a different value. Automations must be executed!
for (var i = this.queuedEvents.length - 1; i >= 0; i--) {
var queuedEvent = this.queuedEvents[i];
if (queuedEvent.aid === aid && queuedEvent.iid === iid) {
if (queuedEvent.value === value) {
return; // the same event was already queued. do not add it again!
}
break; // we break in any case
}
}
this.queuedEvents.push(event);
// if we are handling a request or there is already a timer running we just add it in the queue.
// remember: we flush the event queue after we send out the response.
if (!this.handlingRequest && !this.eventsTimer) {
this.eventsTimer = setTimeout(this.handleEventsTimeout.bind(this), 250);
this.eventsTimer.unref();
}
};
HAPConnection.prototype.handleEventsTimeout = function () {
this.eventsTimer = undefined;
if (this.state > 2 /* AUTHENTICATED */ || this.handlingRequest) {
// connection is closed or about to be closed. no need to send any further events
// OR we are currently sending a response
return;
}
this.writeQueuedEventNotifications();
};
HAPConnection.prototype.writePendingEventNotifications = function () {
var e_6, _a;
try {
for (var _b = (0, tslib_1.__values)(this.pendingEventData), _c = _b.next(); !_c.done; _c = _b.next()) {
var buffer = _c.value;
this.tcpSocket.write(this.encrypt(buffer));
}
}
catch (e_6_1) { e_6 = { error: e_6_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_6) throw e_6.error; }
}
this.pendingEventData.splice(0, this.pendingEventData.length);
};
HAPConnection.prototype.writeQueuedEventNotifications = function () {
var e_7, _a;
if (this.queuedEvents.length === 0 || this.eventsTimer) {
return; // don't send empty event notifications or if there is a timeout running
}
var eventData = {
characteristics: [],
};
try {
for (var _b = (0, tslib_1.__values)(this.queuedEvents), _c = _b.next(); !_c.done; _c = _b.next()) {
var queuedEvent = _c.value;
if (!this.registeredEvents.has(queuedEvent.aid + "." + queuedEvent.iid)) {
continue; // client unregistered that event in the meantime
}
eventData.characteristics.push(queuedEvent);
}
}
catch (e_7_1) { e_7 = { error: e_7_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_7) throw e_7.error; }
}
this.queuedEvents.splice(0, this.queuedEvents.length);
this.writeEventNotification(eventData);
};
/**
* This will create an EVENT/1.0 notification header with the provided event notification.
* If currently an HTTP request is in progress the assembled packet will be
* added to the pending events list.
*
* @param notification - The event which should be sent out
*/
HAPConnection.prototype.writeEventNotification = function (notification) {
debugCon("[%s] Sending HAP event notifications %o", this.remoteAddress, notification.characteristics);
var dataBuffer = Buffer.from(JSON.stringify(notification), "utf8");
var header = Buffer.from("EVENT/1.0 200 OK\r\n" +
"Content-Type: application/hap+json\r\n" +
"Content-Length: " + dataBuffer.length + "\r\n" +
"\r\n", "utf8");
var buffer = Buffer.concat([header, dataBuffer]);
if (this.handlingRequest) {
// it is important that we not encrypt the pending event data. This would increment the nonce used in encryption
this.pendingEventData.push(buffer);
}
else {
this.tcpSocket.write(this.encrypt(buffer), this.handleTCPSocketWriteFulfilled.bind(this));
}
};
HAPConnection.prototype.enableEventNotifications = function (aid, iid) {
this.registeredEvents.add(aid + "." + iid);
};
HAPConnection.prototype.disableEventNotifications = function (aid, iid) {
this.registeredEvents.delete(aid + "." + iid);
};
HAPConnection.prototype.hasEventNotifications = function (aid, iid) {
return this.registeredEvents.has(aid + "." + iid);
};
HAPConnection.prototype.getRegisteredEvents = function () {
return this.registeredEvents;
};
HAPConnection.prototype.clearRegisteredEvents = function () {
this.registeredEvents.clear();
};
HAPConnection.prototype.encrypt = function (data) {
// if accessoryToControllerKey is not empty, then encryption is enabled for this connection. However, we'll
// need to be careful to ensure that we don't encrypt the last few bytes of the response from handlePairVerifyStepTwo.
// Since all communication calls are asynchronous, we could easily receive this 'encrypt' event for those bytes.
// So we want to make sure that we aren't encrypting data until we have *received* some encrypted data from the client first.
if (this.encryption && this.encryption.accessoryToControllerKey.length > 0 && this.encryption.controllerToAccessoryCount > 0) {
return hapCrypto.layerEncrypt(data, this.encryption);
}
return data; // otherwise, we don't encrypt and return plaintext
};
HAPConnection.prototype.decrypt = function (data) {
if (this.encryption && this.encryption.controllerToAccessoryKey.length > 0) {
// below call may throw an error if decryption failed
return hapCrypto.layerDecrypt(data, this.encryption);
}
return data; // otherwise, we don't decrypt and return plaintext
};
HAPConnection.prototype.onHttpServerListening = function () {
var _this = this;
var addressInfo = this.internalHttpServer.address(); // address() is only a string when listening to unix domain sockets
var addressString = addressInfo.family === "IPv6" ? "[".concat(addressInfo.address, "]") : addressInfo.address;
this.internalHttpServerPort = addressInfo.port;
debugCon("[%s] Internal HTTP server listening on %s:%s", this.remoteAddress, addressString, addressInfo.port);
this.internalHttpServer.on("close", this.onHttpServerClose.bind(this));
// now we can establish a connection to this running HTTP server for proxying data
this.httpSocket = net_1.default.createConnection(this.internalHttpServerPort, this.internalHttpServerAddress); // previously we used addressInfo.address
this.httpSocket.setNoDelay(true); // disable Nagle algorithm
this.httpSocket.on("data", this.handleHttpServerResponse.bind(this));
// we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely.
this.httpSocket.on("error", this.onHttpSocketError.bind(this));
this.httpSocket.on("close", this.onHttpSocketClose.bind(this));
this.httpSocket.on("connect", function () {
// we are now fully set up:
// - clientSocket is connected to the iOS device
// - serverSocket is connected to the httpServer
// - ready to proxy data!
_this.state = 1 /* FULLY_SET_UP */;
debugCon("[%s] Internal HTTP socket connected. HAPConnection now fully set up!", _this.remoteAddress);
// start by flushing any pending buffered data received from the client while we were setting up
if (_this.pendingClientSocketData && _this.pendingClientSocketData.length > 0) {
_this.httpSocket.write(_this.pendingClientSocketData);
}
_this.pendingClientSocketData = undefined;
});
};
/**
* This event handler is called when we receive data from a HomeKit controller on our tcp socket.
* We store the data if the internal http server is not read yet, or forward it to the http server.
*/
HAPConnection.prototype.onTCPSocketData = function (data) {
if (this.state > 2 /* 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 /* FULLY_SET_UP */) { // we're not setup yet, so add this data to our intermediate buffer
this.pendingClientSocketData = Buffer.concat([this.pendingClientSocketData, data]);
}
else {
this.httpSocket.write(data); // proxy it along to the HTTP server
}
};
/**
* This event handler is called when the internal http server receives a request.
* Meaning we received data from the HomeKit controller in {@link onTCPSocketData}, which then send the
* data unencrypted to the internal http server. And now it landed here, fully parsed as a http request.
*/
HAPConnection.prototype.handleHttpServerRequest = function (request, response) {
if (this.state > 2 /* AUTHENTICATED */) {
// don't accept data of a connection which is about to be closed or already closed
return;
}
request.socket.setNoDelay(true);
response.connection.setNoDelay(true); // deprecated since 13.0.0
debugCon("[%s] HTTP request: %s", this.remoteAddress, request.url);
this.emit("request" /* REQUEST */, request, response);
};
/**
* This event handler is called by the socket which is connected to our internal http server.
* It is called with the response returned from the http server.
* In this method we have to encrypt and forward the message back to the HomeKit controller.
*/
HAPConnection.prototype.handleHttpServerResponse = function (data) {
var _this = this;
data = this.encrypt(data);
this.tcpSocket.write(data, this.handleTCPSocketWriteFulfilled.bind(this));
debugCon("[%s] HTTP Response is finished", this.remoteAddress);
this.handlingRequest = false;
if (this.state === 3 /* TO_BE_TEARED_DOWN */) {
setTimeout(function () { return _this.close(); }, 10);
}
else if (this.state < 3 /* TO_BE_TEARED_DOWN */) {
this.writePendingEventNotifications();
this.writeQueuedEventNotifications();
}
};
HAPConnection.prototype.handleTCPSocketWriteFulfilled = function () {
this.lastSocketOperation = new Date().getTime();
};
HAPConnection.prototype.onTCPSocketError = function (err) {
debugCon("[%s] Client connection error: %s", this.remoteAddress, err.message);
// onTCPSocketClose will be called next
};
HAPConnection.prototype.onTCPSocketClose = function () {
this.state = 5 /* CLOSED */;
debugCon("[%s] Client connection closed", this.remoteAddress);
if (this.httpSocket) {
this.httpSocket.destroy();
}
this.internalHttpServer.close();
this.emit("closed" /* CLOSED */); // sending final closed event
this.removeAllListeners(); // cleanup listeners, we are officially dead now
};
HAPConnection.prototype.onHttpServerError = function (err) {
debugCon("[%s] HTTP server error: %s", this.remoteAddress, err.message);
if (err.code === "EADDRINUSE") {
this.internalHttpServerPort = undefined;
this.internalHttpServer.close();
this.internalHttpServer.listen(0, this.internalHttpServerAddress = (0, net_utils_1.getOSLoopbackAddressIfAvailable)());
}
};
HAPConnection.prototype.onHttpServerClose = function () {
debugCon("[%s] HTTP server was closed", this.remoteAddress);
// make sure the iOS side is closed as well
this.close();
};
HAPConnection.prototype.onHttpSocketError = function (err) {
debugCon("[%s] HTTP connection error: ", this.remoteAddress, err.message);
// onHttpSocketClose will be called next
};
HAPConnection.prototype.onHttpSocketClose = function () {
debugCon("[%s] HTTP connection was closed", this.remoteAddress);
// we only support a single long-lived connection to our internal HTTP server. Since it's closed,
// we'll need to shut it down entirely.
this.internalHttpServer.close();
};
HAPConnection.prototype.getLocalAddress = function (ipVersion) {
var e_8, _a, e_9, _b;
var infos = os_1.default.networkInterfaces()[this.networkInterface];
if (ipVersion === "ipv4") {
try {
for (var infos_1 = (0, tslib_1.__values)(infos), infos_1_1 = infos_1.next(); !infos_1_1.done; infos_1_1 = infos_1.next()) {
var info = infos_1_1.value;
// @ts-expect-error Nodejs 18+ uses the number 4 the string "IPv4"
if (info.family === "IPv4" || info.family === 4) {
return info.address;
}
}
}
catch (e_8_1) { e_8 = { error: e_8_1 }; }
finally {
try {
if (infos_1_1 && !infos_1_1.done && (_a = infos_1.return)) _a.call(infos_1);
}
finally { if (e_8) throw e_8.error; }
}
throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface);
}
else {
var localUniqueAddress = undefined;
try {
for (var infos_2 = (0, tslib_1.__values)(infos), infos_2_1 = infos_2.next(); !infos_2_1.done; infos_2_1 = infos_2.next()) {
var info = infos_2_1.value;
// @ts-expect-error Nodejs 18+ uses the number 6 instead of the string "IPv6"
if (info.family === "IPv6" || info.family === 6) {
if (!info.scopeid) {
return info.address;
}
else if (!localUniqueAddress) {
localUniqueAddress = info.address;
}
}
}
}
catch (e_9_1) { e_9 = { error: e_9_1 }; }
finally {
try {
if (infos_2_1 && !infos_2_1.done && (_b = infos_2.return)) _b.call(infos_2);
}
finally { if (e_9) throw e_9.error; }
}
if (!localUniqueAddress) {
throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface);
}
return localUniqueAddress;
}
};
HAPConnection.getLocalNetworkInterface = function (socket) {
var e_10, _a, e_11, _b, e_12, _c, e_13, _d;
var localAddress = socket.localAddress;
if (localAddress.startsWith("::ffff:")) { // IPv4-Mapped IPv6 Address https://tools.ietf.org/html/rfc4291#section-2.5.5.2
localAddress = localAddress.substring(7);
}
else {
var index = localAddress.indexOf("%");
if (index !== -1) { // link-local ipv6
localAddress = localAddress.substring(0, index);
}
}
var interfaces = os_1.default.networkInterfaces();
try {
for (var _e = (0, tslib_1.__values)(Object.entries(interfaces)), _f = _e.next(); !_f.done; _f = _e.next()) {
var _g = (0, tslib_1.__read)(_f.value, 2), name = _g[0], infos = _g[1];
try {
for (var infos_3 = (e_11 = void 0, (0, tslib_1.__values)(infos)), infos_3_1 = infos_3.next(); !infos_3_1.done; infos_3_1 = infos_3.next()) {
var info = infos_3_1.value;
if (info.address === localAddress) {
return name;
}
}
}
catch (e_11_1) { e_11 = { error: e_11_1 }; }
finally {
try {
if (infos_3_1 && !infos_3_1.done && (_b = infos_3.return)) _b.call(infos_3);
}
finally { if (e_11) throw e_11.error; }
}
}
}
catch (e_10_1) { e_10 = { error: e_10_1 }; }
finally {
try {
if (_f && !_f.done && (_a = _e.return)) _a.call(_e);
}
finally { if (e_10) throw e_10.error; }
}
// we couldn't map the address from above, we try now to match subnets (see https://github.com/homebridge/HAP-NodeJS/issues/847)
var family = net_1.default.isIPv4(localAddress) ? "IPv4" : "IPv6";
try {
for (var _h = (0, tslib_1.__values)(Object.entries(interfaces)), _j = _h.next(); !_j.done; _j = _h.next()) {
var _k = (0, tslib_1.__read)(_j.value, 2), name = _k[0], infos = _k[1];
try {
for (var infos_4 = (e_13 = void 0, (0, tslib_1.__values)(infos)), infos_4_1 = infos_4.next(); !infos_4_1.done; infos_4_1 = infos_4.next()) {
var info = infos_4_1.value;
if (info.family !== family) {
continue;
}
// check if the localAddress is in the same subnet
if ((0, domain_formatter_1.getNetAddress)(localAddress, info.netmask) === (0, domain_formatter_1.getNetAddress)(info.address, info.netmask)) {
return name;
}
}
}
catch (e_13_1) { e_13 = { error: e_13_1 }; }
finally {
try {
if (infos_4_1 && !infos_4_1.done && (_d = infos_4.return)) _d.call(infos_4);
}
finally { if (e_13) throw e_13.error; }
}
}
}
catch (e_12_1) { e_12 = { error: e_12_1 }; }
finally {
try {
if (_j && !_j.done && (_c = _h.return)) _c.call(_h);
}
finally { if (e_12) throw e_12.error; }
}
console.log("WARNING couldn't map socket coming from remote address ".concat(socket.remoteAddress, ":").concat(socket.remotePort, " at local address ").concat(socket.localAddress, " to a interface!"));
return Object.keys(interfaces)[1]; // just use the first interface after the loopback interface as fallback
};
return HAPConnection;
}(events_1.EventEmitter));
exports.HAPConnection = HAPConnection;
//# sourceMappingURL=eventedhttp.js.map