UNPKG

homebridge-config-ui-x

Version:

A web based management, configuration and control platform for Homebridge.

213 lines • 9.33 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import { fork } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { basename, dirname, join, normalize, resolve } from 'node:path'; import process from 'node:process'; import { HttpService } from '@nestjs/axios'; import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { pathExists } from 'fs-extra/esm'; import NodeCache from 'node-cache'; import { firstValueFrom } from 'rxjs'; import { ConfigService } from '../../../core/config/config.service.js'; import { Logger } from '../../../core/logger/logger.service.js'; import { RE_PATH_TRAVERSAL, RE_STATIC_ASSET_EXT } from '../../../core/regex.constants.js'; import { PluginsService } from '../../plugins/plugins.service.js'; let PluginsSettingsUiService = class PluginsSettingsUiService { loggerService; pluginsService; configService; httpService; pluginUiMetadataCache = new NodeCache({ stdTTL: 86400 }); pluginUiLastVersionCache = new NodeCache({ stdTTL: 86400 }); constructor(loggerService, pluginsService, configService, httpService) { this.loggerService = loggerService; this.pluginsService = pluginsService; this.configService = configService; this.httpService = httpService; } async serveCustomUiAsset(reply, pluginName, assetPath, origin, version) { try { if (!assetPath) { assetPath = 'index.html'; } if (assetPath === 'index.html' && version) { if (version !== this.pluginUiLastVersionCache.get(pluginName)) { this.pluginUiMetadataCache.del(pluginName); } } const pluginUi = this.pluginUiMetadataCache.get(pluginName) || (await this.getPluginUiMetadata(pluginName)); const safeSuffix = normalize(assetPath).replace(RE_PATH_TRAVERSAL, ''); const filePath = join(pluginUi.publicPath, safeSuffix); if (!filePath.startsWith(resolve(pluginUi.publicPath))) { return reply.code(404).send('Not Found'); } reply.header('Content-Security-Policy', ''); if (assetPath === 'index.html') { return reply .type('text/html') .send(await this.buildIndexHtml(pluginUi, origin)); } if (pluginUi.devServer) { return await this.serveAssetsFromDevServer(reply, pluginUi, assetPath); } const fallbackPath = resolve(process.env.UIX_BASE_PATH, 'public', basename(filePath)); if (await pathExists(filePath)) { return reply.sendFile(basename(filePath), dirname(filePath)); } else if (RE_STATIC_ASSET_EXT.test(fallbackPath) && await pathExists(fallbackPath)) { return reply.sendFile(basename(fallbackPath), dirname(fallbackPath)); } else { this.loggerService.warn(`[${pluginName}] asset not found: ${assetPath}.`); return reply.code(404).send('Not Found'); } } catch (e) { this.loggerService.error(`[${pluginName}] UI threw an error - ${e.message}.`); return e.message === 'Not Found' ? reply.code(404).send(e.message) : reply.code(500).send(e.message); } } async getPluginUiMetadata(pluginName) { try { const pluginUi = await this.pluginsService.getPluginUiMetadata(pluginName); this.pluginUiMetadataCache.set(pluginName, pluginUi); this.pluginUiLastVersionCache.set(pluginName, pluginUi.plugin.installedVersion); return pluginUi; } catch (e) { this.loggerService.warn(`[${pluginName}] custom UI threw an error - ${e.message}.`); throw new NotFoundException(); } } async serveAssetsFromDevServer(reply, pluginUi, assetPath) { try { const response = await firstValueFrom(this.httpService.get(`${pluginUi.devServer}/${assetPath}`, { responseType: 'text' })); for (const [key, value] of Object.entries(response.headers)) { reply.header(key, value); } reply.send(response.data); } catch { reply.code(404).send('Not Found'); } } async getIndexHtmlBody(pluginUi) { if (pluginUi.devServer) { return (await firstValueFrom(this.httpService.get(pluginUi.devServer, { responseType: 'text' }))).data; } else { return await readFile(join(pluginUi.publicPath, 'index.html'), 'utf8'); } } async buildIndexHtml(pluginUi, origin) { const body = await this.getIndexHtmlBody(pluginUi); return ` <!doctype html> <html> <head> <meta charset="utf-8"> <title>${pluginUi.plugin.name}</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <script> window._homebridge = { plugin: ${JSON.stringify(pluginUi.plugin)}, serverEnv: ${JSON.stringify(this.configService.uiSettings(true))}, }; </script> <script src="${origin || 'http://localhost:4200'}/assets/plugin-ui-utils/ui.js?v=${this.configService.package.version}"></script> <script> window.addEventListener('load', () => { window.parent.postMessage({action: 'loaded'}, '*'); }, false) </script> </head> <body style="display:none;"> ${body} </body> </html> `; } async startCustomUiHandler(pluginName, client) { const pluginUi = this.pluginUiMetadataCache.get(pluginName) || (await this.getPluginUiMetadata(pluginName)); if (!await pathExists(resolve(pluginUi.serverPath))) { client.emit('ready', { server: false }); return; } const childEnv = { ...process.env }; childEnv.HOMEBRIDGE_STORAGE_PATH = this.configService.storagePath; childEnv.HOMEBRIDGE_CONFIG_PATH = this.configService.configPath; childEnv.HOMEBRIDGE_UI_VERSION = this.configService.package.version; const child = fork(pluginUi.serverPath, [], { silent: true, env: childEnv, }); child.stdout.on('data', (data) => { this.loggerService.log(`[${pluginName}] ${data.toString().trim()}`); }); child.stderr.on('data', (data) => { this.loggerService.error(`[${pluginName}] ${data.toString().trim()}`); }); child.on('exit', () => { this.loggerService.debug(`[${pluginName}] custom UI closed (child process ended).`); }); child.addListener('message', (response) => { if (typeof response === 'object' && response.action) { response.action = response.action === 'error' ? 'server_error' : response.action; client.emit(response.action, response.payload); } }); const cleanup = () => { this.loggerService.debug(`[${pluginName}] custom UI closing (terminating child process)...`); const childPid = child.pid; if (child.connected) { child.disconnect(); } setTimeout(() => { try { process.kill(childPid, 'SIGTERM'); } catch (e) { } }, 5000); client.removeAllListeners('end'); client.removeAllListeners('disconnect'); client.removeAllListeners('request'); }; client.on('disconnect', () => { cleanup(); }); client.on('end', () => { cleanup(); }); client.on('request', (request) => { if (child.connected) { child.send(request); } }); } }; PluginsSettingsUiService = __decorate([ Injectable(), __param(0, Inject(Logger)), __param(1, Inject(PluginsService)), __param(2, Inject(ConfigService)), __param(3, Inject(HttpService)), __metadata("design:paramtypes", [Logger, PluginsService, ConfigService, HttpService]) ], PluginsSettingsUiService); export { PluginsSettingsUiService }; //# sourceMappingURL=plugins-settings-ui.service.js.map