UNPKG

homebridge-plugin-wrapper

Version:

Wrapper for Homebridge and NodeJS-HAP with reduced dependencies that allows to intercept plugin values and also send to them

389 lines 19.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AvahiAdvertiser = exports.BonjourHAPAdvertiser = exports.CiaoAdvertiser = exports.AdvertiserEvent = exports.PairingFeatureFlag = exports.StatusFlag = void 0; var tslib_1 = require("tslib"); // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// <reference path="../../@types/bonjour-hap.d.ts" /> var ciao_1 = (0, tslib_1.__importDefault)(require("@homebridge/ciao")); var assert_1 = (0, tslib_1.__importDefault)(require("assert")); var bonjour_hap_1 = (0, tslib_1.__importDefault)(require("bonjour-hap")); var crypto_1 = (0, tslib_1.__importDefault)(require("crypto")); var debug_1 = (0, tslib_1.__importDefault)(require("debug")); var dbus_native_1 = (0, tslib_1.__importDefault)(require("@homebridge/dbus-native")); var events_1 = require("events"); var promise_utils_1 = require("./util/promise-utils"); var debug = (0, debug_1.default)("HAP-NodeJS:Advertiser"); /** * This enum lists all bitmasks for all known status flags. * When the bit for the given bitmask is set, it represents the state described by the name. */ var StatusFlag; (function (StatusFlag) { StatusFlag[StatusFlag["NOT_PAIRED"] = 1] = "NOT_PAIRED"; StatusFlag[StatusFlag["NOT_JOINED_WIFI"] = 2] = "NOT_JOINED_WIFI"; StatusFlag[StatusFlag["PROBLEM_DETECTED"] = 4] = "PROBLEM_DETECTED"; })(StatusFlag = exports.StatusFlag || (exports.StatusFlag = {})); /** * This enum lists all bitmasks for all known pairing feature flags. * When the bit for the given bitmask is set, it represents the state described by the name. */ var PairingFeatureFlag; (function (PairingFeatureFlag) { PairingFeatureFlag[PairingFeatureFlag["SUPPORTS_HARDWARE_AUTHENTICATION"] = 1] = "SUPPORTS_HARDWARE_AUTHENTICATION"; PairingFeatureFlag[PairingFeatureFlag["SUPPORTS_SOFTWARE_AUTHENTICATION"] = 2] = "SUPPORTS_SOFTWARE_AUTHENTICATION"; })(PairingFeatureFlag = exports.PairingFeatureFlag || (exports.PairingFeatureFlag = {})); var AdvertiserEvent; (function (AdvertiserEvent) { AdvertiserEvent["UPDATED_NAME"] = "updated-name"; })(AdvertiserEvent = exports.AdvertiserEvent || (exports.AdvertiserEvent = {})); /** * Advertiser uses mdns to broadcast the presence of an Accessory to the local network. * * Note that as of iOS 9, an accessory can only pair with a single client. Instead of pairing your * accessories with multiple iOS devices in your home, Apple intends for you to use Home Sharing. * To support this requirement, we provide the ability to be "discoverable" or not (via a "service flag" on the * mdns payload). */ var CiaoAdvertiser = /** @class */ (function (_super) { (0, tslib_1.__extends)(CiaoAdvertiser, _super); function CiaoAdvertiser(accessoryInfo, responderOptions, serviceOptions) { var _this = _super.call(this) || this; _this.accessoryInfo = accessoryInfo; _this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); _this.responder = ciao_1.default.getResponder((0, tslib_1.__assign)({}, responderOptions)); _this.advertisedService = _this.responder.createService((0, tslib_1.__assign)({ name: _this.accessoryInfo.displayName, type: "hap" /* HAP */, txt: CiaoAdvertiser.createTxt(accessoryInfo, _this.setupHash) }, serviceOptions)); _this.advertisedService.on("name-change" /* NAME_CHANGED */, _this.emit.bind(_this, "updated-name" /* UPDATED_NAME */)); debug("Preparing Advertiser for '".concat(_this.accessoryInfo.displayName, "' using ciao backend!")); return _this; } CiaoAdvertiser.prototype.initPort = function (port) { this.advertisedService.updatePort(port); }; CiaoAdvertiser.prototype.startAdvertising = function () { debug("Starting to advertise '".concat(this.accessoryInfo.displayName, "' using ciao backend!")); return this.advertisedService.advertise(); }; CiaoAdvertiser.prototype.updateAdvertisement = function (silent) { var txt = CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash); debug("Updating txt record (txt: %o, silent: %d)", txt, silent); this.advertisedService.updateTxt(txt, silent); }; CiaoAdvertiser.prototype.destroy = function () { return (0, tslib_1.__awaiter)(this, void 0, void 0, function () { return (0, tslib_1.__generator)(this, function (_a) { switch (_a.label) { case 0: // advertisedService.destroy(); is called implicitly via the shutdown call return [4 /*yield*/, this.responder.shutdown()]; case 1: // advertisedService.destroy(); is called implicitly via the shutdown call _a.sent(); this.removeAllListeners(); return [2 /*return*/]; } }); }); }; CiaoAdvertiser.createTxt = function (accessoryInfo, setupHash) { var statusFlags = []; if (!accessoryInfo.paired()) { statusFlags.push(1 /* NOT_PAIRED */); } return { "c#": accessoryInfo.getConfigVersion(), ff: CiaoAdvertiser.ff(), id: accessoryInfo.username, md: accessoryInfo.model, pv: CiaoAdvertiser.protocolVersion, "s#": 1, sf: CiaoAdvertiser.sf.apply(CiaoAdvertiser, (0, tslib_1.__spreadArray)([], (0, tslib_1.__read)(statusFlags), false)), ci: accessoryInfo.category, sh: setupHash, }; }; CiaoAdvertiser.computeSetupHash = function (accessoryInfo) { var hash = crypto_1.default.createHash("sha512"); hash.update(accessoryInfo.setupID + accessoryInfo.username.toUpperCase()); return hash.digest().slice(0, 4).toString("base64"); }; CiaoAdvertiser.ff = function () { var flags = []; for (var _i = 0; _i < arguments.length; _i++) { flags[_i] = arguments[_i]; } var value = 0; flags.forEach(function (flag) { return value |= flag; }); return value; }; CiaoAdvertiser.sf = function () { var flags = []; for (var _i = 0; _i < arguments.length; _i++) { flags[_i] = arguments[_i]; } var value = 0; flags.forEach(function (flag) { return value |= flag; }); return value; }; CiaoAdvertiser.protocolVersion = "1.1"; CiaoAdvertiser.protocolVersionService = "1.1.0"; return CiaoAdvertiser; }(events_1.EventEmitter)); exports.CiaoAdvertiser = CiaoAdvertiser; /** * Advertiser base on the legacy "bonjour-hap" library. */ var BonjourHAPAdvertiser = /** @class */ (function (_super) { (0, tslib_1.__extends)(BonjourHAPAdvertiser, _super); function BonjourHAPAdvertiser(accessoryInfo, responderOptions, serviceOptions) { var _this = _super.call(this) || this; _this.destroyed = false; _this.accessoryInfo = accessoryInfo; _this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); _this.serviceOptions = serviceOptions; _this.bonjour = (0, bonjour_hap_1.default)(responderOptions); debug("Preparing Advertiser for '".concat(_this.accessoryInfo.displayName, "' using bonjour-hap backend!")); return _this; } BonjourHAPAdvertiser.prototype.initPort = function (port) { this.port = port; }; BonjourHAPAdvertiser.prototype.startAdvertising = function () { (0, assert_1.default)(!this.destroyed, "Can't advertise on a destroyed bonjour instance!"); if (this.port == null) { throw new Error("Tried starting bonjour-hap advertisement without initializing port!"); } debug("Starting to advertise '".concat(this.accessoryInfo.displayName, "' using bonjour-hap backend!")); if (this.advertisement) { this.destroy(); } var hostname = this.accessoryInfo.username.replace(/:/ig, "_") + ".local"; this.advertisement = this.bonjour.publish((0, tslib_1.__assign)({ name: this.accessoryInfo.displayName, type: "hap", port: this.port, txt: CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), host: hostname, addUnsafeServiceEnumerationRecord: true }, this.serviceOptions)); return (0, promise_utils_1.PromiseTimeout)(1); }; BonjourHAPAdvertiser.prototype.updateAdvertisement = function (silent) { var txt = CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash); debug("Updating txt record (txt: %o, silent: %d)", txt, silent); if (this.advertisement) { this.advertisement.updateTxt(txt, silent); } }; BonjourHAPAdvertiser.prototype.destroy = function () { var _this = this; if (this.advertisement) { this.advertisement.stop(function () { _this.advertisement.destroy(); _this.advertisement = undefined; _this.bonjour.destroy(); }); } else { this.bonjour.destroy(); } }; return BonjourHAPAdvertiser; }(events_1.EventEmitter)); exports.BonjourHAPAdvertiser = BonjourHAPAdvertiser; /** * Advertiser based on the Avahi D-Bus library. * For (very crappy) docs on the interface, see the XML files at: https://github.com/lathiat/avahi/tree/master/avahi-daemon. */ var AvahiAdvertiser = /** @class */ (function (_super) { (0, tslib_1.__extends)(AvahiAdvertiser, _super); function AvahiAdvertiser(accessoryInfo) { var _this = _super.call(this) || this; _this.accessoryInfo = accessoryInfo; _this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); _this.bus = dbus_native_1.default.systemBus(); debug("Preparing Advertiser for '".concat(_this.accessoryInfo.displayName, "' using Avahi backend!")); return _this; } AvahiAdvertiser.prototype.createTxt = function () { return Object .entries(CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash)) .map(function (el) { return Buffer.from(el[0] + "=" + el[1]); }); }; AvahiAdvertiser.prototype.initPort = function (port) { this.port = port; }; AvahiAdvertiser.prototype.startAdvertising = function () { return (0, tslib_1.__awaiter)(this, void 0, void 0, function () { var _a; return (0, tslib_1.__generator)(this, function (_b) { switch (_b.label) { case 0: if (this.port == null) { throw new Error("Tried starting Avahi advertisement without initializing port!"); } if (!this.bus) { throw new Error("Tried to start Avahi advertisement on a destroyed advertiser!"); } debug("Starting to advertise '".concat(this.accessoryInfo.displayName, "' using Avahi backend!")); _a = this; return [4 /*yield*/, AvahiAdvertiser.avahiInvoke(this.bus, "/", "Server", "EntryGroupNew")]; case 1: _a.path = (_b.sent()); return [4 /*yield*/, AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "AddService", { body: [ -1, -1, 0, this.accessoryInfo.displayName, "_hap._tcp", "", "", this.port, this.createTxt(), // txt ], signature: "iiussssqaay", })]; case 2: _b.sent(); return [4 /*yield*/, AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "Commit")]; case 3: _b.sent(); return [2 /*return*/]; } }); }); }; AvahiAdvertiser.prototype.updateAdvertisement = function (silent) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function () { var error_1; return (0, tslib_1.__generator)(this, function (_a) { switch (_a.label) { case 0: if (!this.bus) { throw new Error("Tried to update Avahi advertisement on a destroyed advertiser!"); } if (!this.path) { debug("Tried to update advertisement without a valid `path`!"); return [2 /*return*/]; } debug("Updating txt record (txt: %o, silent: %d)", CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), silent); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "UpdateServiceTxt", { body: [-1, -1, 0, this.accessoryInfo.displayName, "_hap._tcp", "", this.createTxt()], signature: "iiusssaay", })]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: error_1 = _a.sent(); console.error("Failed to update avahi advertisement: " + error_1); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; AvahiAdvertiser.prototype.destroy = function () { return (0, tslib_1.__awaiter)(this, void 0, void 0, function () { var error_2; return (0, tslib_1.__generator)(this, function (_a) { switch (_a.label) { case 0: if (!this.bus) { throw new Error("Tried to destroy Avahi advertisement on a destroyed advertiser!"); } if (!this.path) return [3 /*break*/, 5]; _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "Free")]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: error_2 = _a.sent(); // Typically, this fails if e.g. avahi service was stopped in the meantime. debug("Destroying Avahi advertisement failed: " + error_2); return [3 /*break*/, 4]; case 4: this.path = undefined; _a.label = 5; case 5: this.bus.connection.stream.destroy(); this.bus = undefined; return [2 /*return*/]; } }); }); }; AvahiAdvertiser.isAvailable = function () { return (0, tslib_1.__awaiter)(this, void 0, void 0, function () { var bus, error_3, version, error_4; return (0, tslib_1.__generator)(this, function (_a) { switch (_a.label) { case 0: bus = dbus_native_1.default.systemBus(); _a.label = 1; case 1: _a.trys.push([1, , 9, 10]); _a.label = 2; case 2: _a.trys.push([2, 4, , 5]); return [4 /*yield*/, this.messageBusConnectionResult(bus)]; case 3: _a.sent(); return [3 /*break*/, 5]; case 4: error_3 = _a.sent(); debug("Avahi/DBus classified unavailable due to missing dbus interface!"); return [2 /*return*/, false]; case 5: _a.trys.push([5, 7, , 8]); return [4 /*yield*/, this.avahiInvoke(bus, "/", "Server", "GetVersionString")]; case 6: version = _a.sent(); debug("Detected Avahi over DBus interface running version '%s'.", version); return [3 /*break*/, 8]; case 7: error_4 = _a.sent(); debug("Avahi/DBus classified unavailable due to missing avahi interface!"); return [2 /*return*/, false]; case 8: return [2 /*return*/, true]; case 9: bus.connection.stream.destroy(); return [7 /*endfinally*/]; case 10: return [2 /*return*/]; } }); }); }; AvahiAdvertiser.messageBusConnectionResult = function (bus) { return new Promise(function (resolve, reject) { var errorHandler = function (error) { // eslint-disable-next-line @typescript-eslint/no-use-before-define bus.connection.removeListener("connect", connectHandler); reject(error); }; var connectHandler = function () { bus.connection.removeListener("error", errorHandler); resolve(); }; bus.connection.once("connect", connectHandler); bus.connection.once("error", errorHandler); }); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any AvahiAdvertiser.avahiInvoke = function (bus, path, dbusInterface, member, others) { return new Promise(function (resolve, reject) { var command = (0, tslib_1.__assign)({ destination: "org.freedesktop.Avahi", path: path, interface: "org.freedesktop.Avahi." + dbusInterface, member: member }, (others || {})); // eslint-disable-next-line @typescript-eslint/no-explicit-any bus.invoke(command, function (err, result) { if (err) { reject(new Error("avahiInvoke error: ".concat(JSON.stringify(err)))); } else { resolve(result); } }); }); }; return AvahiAdvertiser; }(events_1.EventEmitter)); exports.AvahiAdvertiser = AvahiAdvertiser; //# sourceMappingURL=Advertiser.js.map