From 898503a55bd8149e3884b6e860c8f11383234c78 Mon Sep 17 00:00:00 2001 From: Jay Neubrand Date: Sun, 11 Feb 2024 09:47:52 +0100 Subject: [PATCH 1/2] add avahi file advertiser --- src/lib/Accessory.ts | 11 ++++-- src/lib/Advertiser.ts | 81 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/lib/Accessory.ts b/src/lib/Accessory.ts index 104a19e13..21e74e10e 100644 --- a/src/lib/Accessory.ts +++ b/src/lib/Accessory.ts @@ -28,7 +28,7 @@ import { VoidCallback, WithUUID, } from "../types"; -import { Advertiser, AdvertiserEvent, AvahiAdvertiser, BonjourHAPAdvertiser, CiaoAdvertiser, ResolvedAdvertiser } from "./Advertiser"; +import { Advertiser, AdvertiserEvent, AvahiAdvertiser, BonjourHAPAdvertiser, CiaoAdvertiser, ResolvedAdvertiser, AvahiFileAdvertiser } from "./Advertiser"; // noinspection JSDeprecatedSymbols import { LegacyCameraSource, LegacyCameraSourceAdapter, StreamController } from "./camera"; import { @@ -290,7 +290,7 @@ export interface PublishInfo { /** * @group Accessory */ -export const enum MDNSAdvertiser { +export enum MDNSAdvertiser { /** * Use the `@homebridge/ciao` module as advertiser. */ @@ -311,6 +311,10 @@ export const enum MDNSAdvertiser { * Consequentially, treat this feature as an experimental feature. */ RESOLVED = "resolved", + /** + * Use the `avahi-file` module as advertiser. + */ + AVAHI_FILE = "avahi-file", } /** @@ -1332,6 +1336,9 @@ export class Accessory extends EventEmitter { case MDNSAdvertiser.RESOLVED: this._advertiser = new ResolvedAdvertiser(this._accessoryInfo); break; + case MDNSAdvertiser.AVAHI_FILE: + this._advertiser = new AvahiFileAdvertiser(this._accessoryInfo); + break; default: throw new Error("Unsupported advertiser setting: '" + info.advertiser + "'"); } diff --git a/src/lib/Advertiser.ts b/src/lib/Advertiser.ts index 2ec2a23a4..d6656e898 100644 --- a/src/lib/Advertiser.ts +++ b/src/lib/Advertiser.ts @@ -10,6 +10,7 @@ import createDebug from "debug"; import { EventEmitter } from "events"; import { AccessoryInfo } from "./model/AccessoryInfo"; import { PromiseTimeout } from "./util/promise-utils"; +import fs from "fs"; const debug = createDebug("HAP-NodeJS:Advertiser"); @@ -200,6 +201,86 @@ export class CiaoAdvertiser extends EventEmitter implements Advertiser { } } +/** + * Advertiser proxies information through Avahi to broadcast the presence of an Accessory to the local network. + */ +export class AvahiFileAdvertiser extends EventEmitter implements Advertiser { + + static protocolVersion: string = "1.1"; + static protocolVersionService: string = "1.1.0"; + + private filePath: string = '/homebridge/'; + private port?: number; + + private readonly accessoryInfo: AccessoryInfo; + private readonly setupHash: string; + + constructor(accessoryInfo: AccessoryInfo) { + super(); + this.accessoryInfo = accessoryInfo; + this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); + this.filePath = '/homebridge/' + + this.publish(); + + console.log(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using avahi-file backend!`); + } + + private publish(): Promise { + if (!this.port) return Promise.resolve(); + return this.writeXMLTo(this.filePath + this.accessoryInfo.displayName.replace(/ /g, '-') + '.service'); + } + + private writeXMLTo(path: string): Promise { + return new Promise((accept, reject) => { + fs.writeFile(path, AvahiFileAdvertiser.serialize( + this.accessoryInfo.displayName, + "_hap._tcp", + this.port!, + CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), + ), err => { + if (err) + reject(err); + else + accept(); + }); + }); + } + + private static serialize(name: string, type: string, port: number, txt: ServiceTxt): string { + return ( +` + ${name} + + ${type} + ${port} +` + Object.entries(txt) + .map(([k, v]) => ` ${k}=${v}`) + .join('\n') + ` + + +`); + } + + public initPort(port: number): void { + this.port = port; + this.publish(); + } + + public startAdvertising(): Promise { + return this.publish(); + } + + public updateAdvertisement(silent?: boolean): void { + this.publish(); + } + + public async destroy(): Promise { + // TODO remove file + this.removeAllListeners(); + } +} + /** * Advertiser base on the legacy "bonjour-hap" library. * From 1aca2ac1b4d8a7baeb943bd7055172ba2e461df5 Mon Sep 17 00:00:00 2001 From: Jay Neubrand Date: Mon, 12 Feb 2024 15:10:51 +0100 Subject: [PATCH 2/2] avahi file advertiser tidying --- src/lib/Advertiser.ts | 61 ++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/lib/Advertiser.ts b/src/lib/Advertiser.ts index d6656e898..1b51260f7 100644 --- a/src/lib/Advertiser.ts +++ b/src/lib/Advertiser.ts @@ -203,13 +203,10 @@ export class CiaoAdvertiser extends EventEmitter implements Advertiser { /** * Advertiser proxies information through Avahi to broadcast the presence of an Accessory to the local network. + * Useful in situations where network isolation of homebridge is wanted (eg. running in a docker container without host networking) */ export class AvahiFileAdvertiser extends EventEmitter implements Advertiser { - - static protocolVersion: string = "1.1"; - static protocolVersionService: string = "1.1.0"; - - private filePath: string = '/homebridge/'; + private dir = "/homebridge/"; private port?: number; private readonly accessoryInfo: AccessoryInfo; @@ -219,44 +216,40 @@ export class AvahiFileAdvertiser extends EventEmitter implements Advertiser { super(); this.accessoryInfo = accessoryInfo; this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); - this.filePath = '/homebridge/' - - this.publish(); console.log(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using avahi-file backend!`); } + private get filePath(): string { + return this.dir + this.accessoryInfo.displayName.replace(/ /g, "-") + ".service"; + } + private publish(): Promise { - if (!this.port) return Promise.resolve(); - return this.writeXMLTo(this.filePath + this.accessoryInfo.displayName.replace(/ /g, '-') + '.service'); - } - - private writeXMLTo(path: string): Promise { - return new Promise((accept, reject) => { - fs.writeFile(path, AvahiFileAdvertiser.serialize( - this.accessoryInfo.displayName, - "_hap._tcp", - this.port!, - CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), - ), err => { - if (err) - reject(err); - else - accept(); - }); - }); + if (this.port == null) { + throw new Error("Tried starting avahi-file advertisement without initializing port!"); + } + return this.writeXMLTo(this.filePath); + } + + private async writeXMLTo(path: string): Promise { + await fs.promises.writeFile(path, AvahiFileAdvertiser.serialize( + this.accessoryInfo.displayName, + "_hap._tcp", + this.port!, + CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), + )); } private static serialize(name: string, type: string, port: number, txt: ServiceTxt): string { return ( -` + ` ${name} ${type} ${port} ` + Object.entries(txt) - .map(([k, v]) => ` ${k}=${v}`) - .join('\n') + ` + .map(([k, v]) => ` ${k}=${v}`) + .join("\n") + ` `); @@ -272,12 +265,20 @@ export class AvahiFileAdvertiser extends EventEmitter implements Advertiser { } public updateAdvertisement(silent?: boolean): void { + debug(`Updating avahi-file advertisement (silent: ${silent})`); this.publish(); } public async destroy(): Promise { - // TODO remove file this.removeAllListeners(); + try { + await fs.promises.unlink(this.filePath); + } catch (err) { + if (err.code === "ENOENT") { + return; + } + throw err; + } } }