diff --git a/package-lock.json b/package-lock.json index c9a540dec..d8665b552 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@homebridge/ciao": "^1.1.5", - "@homebridge/dbus-native": "^0.4.2", + "@homebridge/dbus-native": "^0.5.0", "bonjour-hap": "~3.6.3", "debug": "^4.3.4", "fast-srp-hap": "2.0.4", @@ -676,9 +676,9 @@ } }, "node_modules/@homebridge/dbus-native": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.4.2.tgz", - "integrity": "sha512-rg6DUg6xOttzn73HA1+3G2o1ezRj0+DzPMEJqasrpq7FcAxMcTyOZ96GfcDN4pLUz62hMuywIeVZ4F6cc/g6Ig==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.0.tgz", + "integrity": "sha512-ei0jyHE/uNDl/6D6heRwsqnESrrXuSlfp+xlwGfg3mo1OqhKvyb/Kp73uxQyOJ3f1T1ocLSyA5uzoR1AbfaXIQ==", "dependencies": { "@homebridge/long": "^5.2.1", "@homebridge/put": "~0.0.8", @@ -6792,9 +6792,9 @@ } }, "@homebridge/dbus-native": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.4.2.tgz", - "integrity": "sha512-rg6DUg6xOttzn73HA1+3G2o1ezRj0+DzPMEJqasrpq7FcAxMcTyOZ96GfcDN4pLUz62hMuywIeVZ4F6cc/g6Ig==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.0.tgz", + "integrity": "sha512-ei0jyHE/uNDl/6D6heRwsqnESrrXuSlfp+xlwGfg3mo1OqhKvyb/Kp73uxQyOJ3f1T1ocLSyA5uzoR1AbfaXIQ==", "requires": { "@homebridge/long": "^5.2.1", "@homebridge/put": "~0.0.8", diff --git a/package.json b/package.json index 5cb8724af..6d698404f 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ ], "dependencies": { "@homebridge/ciao": "^1.1.5", - "@homebridge/dbus-native": "^0.4.2", + "@homebridge/dbus-native": "^0.5.0", "bonjour-hap": "~3.6.3", "debug": "^4.3.4", "fast-srp-hap": "2.0.4", diff --git a/src/lib/Advertiser.ts b/src/lib/Advertiser.ts index 6b3661c7d..3febb9ade 100644 --- a/src/lib/Advertiser.ts +++ b/src/lib/Advertiser.ts @@ -2,7 +2,7 @@ /// import ciao, { CiaoService, MDNSServerOptions, Responder, ServiceEvent, ServiceTxt, ServiceType } from "@homebridge/ciao"; import { InterfaceName, IPAddress } from "@homebridge/ciao/lib/NetworkManager"; -import dbus, { DBusInterface, MessageBus } from "@homebridge/dbus-native"; +import dbus, { DBusInterface, InvokeError, MessageBus } from "@homebridge/dbus-native"; import assert from "assert"; import bonjour, { BonjourHAP, BonjourHAPService, MulticastOptions } from "bonjour-hap"; import crypto from "crypto"; @@ -278,6 +278,25 @@ function messageBusConnectionResult(bus: MessageBus): Promise { }); } +export class DBusInvokeError extends Error { + readonly errorName: string; + + constructor(errorObject: InvokeError) { + super(); + + Object.setPrototypeOf(this, DBusInvokeError.prototype); + + this.name = "DBusInvokeError"; + + this.errorName = errorObject.name; + + if (Array.isArray(errorObject.message) && errorObject.message.length === 1) { + this.message = errorObject.message[0]; + } else { + this.message = errorObject.message.toString(); + } + } +} // eslint-disable-next-line @typescript-eslint/no-explicit-any function dbusInvoke( bus: MessageBus, destination: string, path: string, dbusInterface: string, member: string, others?: any): Promise { @@ -290,10 +309,9 @@ function dbusInvoke( bus: MessageBus, destination: string, path: string, dbusInt ...(others || {}), }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - bus.invoke(command, (err: any, result: any) => { + bus.invoke(command, (err, result) => { if (err) { - reject(new Error(`dbusInvoke error: ${JSON.stringify(err)}`)); + reject(new DBusInvokeError(err)); } else { resolve(result); } @@ -504,6 +522,13 @@ export class AvahiAdvertiser extends EventEmitter implements Advertiser { type ResolvedServiceTxt = Array>; +const RESOLVED_PERMISSIONS_ERRORS = [ + "org.freedesktop.DBus.Error.AccessDenied", + "org.freedesktop.DBus.Error.AuthFailed", + "org.freedesktop.DBus.Error.InteractiveAuthorizationRequired", +]; + + /** * Advertiser based on the systemd-resolved D-Bus library. * For docs on the interface, see: https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html @@ -547,18 +572,27 @@ export class ResolvedAdvertiser extends EventEmitter implements Advertiser { debug(`Starting to advertise '${this.accessoryInfo.displayName}' using systemd-resolved backend!`); - this.path = await ResolvedAdvertiser.resolvedInvoke(this.bus, "RegisterService", { - body: [ - this.accessoryInfo.displayName, // name - this.accessoryInfo.displayName, // name_template - "_hap._tcp", // type - this.port, // service_port - 0, // service_priority - 0, // service_weight - [this.createTxt()], // txt_datas - ], - signature: "sssqqqaa{say}", - }); + try { + this.path = await ResolvedAdvertiser.managerInvoke(this.bus, "RegisterService", { + body: [ + this.accessoryInfo.displayName, // name + this.accessoryInfo.displayName, // name_template + "_hap._tcp", // type + this.port, // service_port + 0, // service_priority + 0, // service_weight + [this.createTxt()], // txt_datas + ], + signature: "sssqqqaa{say}", + }); + } catch (error) { + if (error instanceof DBusInvokeError) { + if (RESOLVED_PERMISSIONS_ERRORS.includes(error.errorName)) { + error.message = `Permissions issue. See https://homebridge.io/w/mDNS-Options for more info. ${error.message}`; + } + } + throw error; + } } public async updateAdvertisement(silent?: boolean): Promise { @@ -580,7 +614,7 @@ export class ResolvedAdvertiser extends EventEmitter implements Advertiser { if (this.path) { try { - await ResolvedAdvertiser.resolvedInvoke(this.bus, "UnregisterService", { + await ResolvedAdvertiser.managerInvoke(this.bus, "UnregisterService", { body: [this.path], signature: "o", }); @@ -616,7 +650,7 @@ export class ResolvedAdvertiser extends EventEmitter implements Advertiser { try { // Ensure that systemd-resolved is accessible. - await this.resolvedInvoke(bus, "ResolveHostname", { + await this.managerInvoke(bus, "ResolveHostname", { body: [0, "127.0.0.1", 0, 0], signature: "isit", }); @@ -626,6 +660,30 @@ export class ResolvedAdvertiser extends EventEmitter implements Advertiser { return false; } + try { + const mdnsStatus = await this.resolvedInvoke( + bus, + "org.freedesktop.DBus.Properties", + "Get", + { + body: ["org.freedesktop.resolve1.Manager", "MulticastDNS"], + signature: "ss", + }, + ); + + if (mdnsStatus[0][0].type !== "s") { + throw new Error("Invalid type for MulticastDNS"); + } + + if (mdnsStatus[1][0] !== "yes" ) { + debug("systemd-resolved/DBus classified unavailable because MulticastDNS is not enabled!"); + return false; + } + } catch (error) { + debug("systemd-resolved/DBus classified unavailable due to failure checking system status: " + error); + return false; + } + return true; } finally { bus.connection.stream.destroy(); @@ -633,14 +691,19 @@ export class ResolvedAdvertiser extends EventEmitter implements Advertiser { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private static resolvedInvoke(bus: MessageBus, member: string, others?: any): Promise { + private static resolvedInvoke(bus: MessageBus, dbusInterface: string, member: string, others?: any): Promise { return dbusInvoke( bus, "org.freedesktop.resolve1", "/org/freedesktop/resolve1", - "org.freedesktop.resolve1.Manager", + dbusInterface, member, others, ); } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private static managerInvoke(bus: MessageBus, member: string, others?: any): Promise { + return this.resolvedInvoke(bus, "org.freedesktop.resolve1.Manager", member, others); + } } diff --git a/src/types/dbus-native.d.ts b/src/types/dbus-native.d.ts index bca3b1579..cb02c35d3 100644 --- a/src/types/dbus-native.d.ts +++ b/src/types/dbus-native.d.ts @@ -4,11 +4,17 @@ declare module "@homebridge/dbus-native" { function systemBus(): MessageBus; + export class InvokeError { + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message: any; + } + export class MessageBus { connection: BusConnection; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any - public invoke(message: any, callback: any): void; + public invoke(message: any, callback: (error: InvokeError | undefined, value: any) => void): void; public getService(name: string): DBusService; }