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..1b51260f7 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,87 @@ 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 { + private dir = "/homebridge/"; + private port?: number; + + private readonly accessoryInfo: AccessoryInfo; + private readonly setupHash: string; + + constructor(accessoryInfo: AccessoryInfo) { + super(); + this.accessoryInfo = accessoryInfo; + this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); + + 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 == 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") + ` + + +`); + } + + public initPort(port: number): void { + this.port = port; + this.publish(); + } + + public startAdvertising(): Promise { + return this.publish(); + } + + public updateAdvertisement(silent?: boolean): void { + debug(`Updating avahi-file advertisement (silent: ${silent})`); + this.publish(); + } + + public async destroy(): Promise { + this.removeAllListeners(); + try { + await fs.promises.unlink(this.filePath); + } catch (err) { + if (err.code === "ENOENT") { + return; + } + throw err; + } + } +} + /** * Advertiser base on the legacy "bonjour-hap" library. *