UNPKG

homebridge-config-ui-x

Version:

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

206 lines • 9.66 kB
"use strict"; 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PluginsSettingsUiService = void 0; const node_child_process_1 = require("node:child_process"); const node_path_1 = require("node:path"); const node_process_1 = __importDefault(require("node:process")); const axios_1 = require("@nestjs/axios"); const common_1 = require("@nestjs/common"); const fs_extra_1 = require("fs-extra"); const node_cache_1 = __importDefault(require("node-cache")); const rxjs_1 = require("rxjs"); const config_service_1 = require("../../../core/config/config.service"); const logger_service_1 = require("../../../core/logger/logger.service"); const plugins_service_1 = require("../../plugins/plugins.service"); let PluginsSettingsUiService = class PluginsSettingsUiService { constructor(loggerService, pluginsService, configService, httpService) { this.loggerService = loggerService; this.pluginsService = pluginsService; this.configService = configService; this.httpService = httpService; this.pluginUiMetadataCache = new node_cache_1.default({ stdTTL: 86400 }); this.pluginUiLastVersionCache = new node_cache_1.default({ stdTTL: 86400 }); } 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 = (0, node_path_1.normalize)(assetPath).replace(/^(\.\.(\/|\\|$))+/, ''); const filePath = (0, node_path_1.join)(pluginUi.publicPath, safeSuffix); if (!filePath.startsWith((0, node_path_1.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 = (0, node_path_1.resolve)(node_process_1.default.env.UIX_BASE_PATH, 'public', (0, node_path_1.basename)(filePath)); if (await (0, fs_extra_1.pathExists)(filePath)) { return reply.sendFile((0, node_path_1.basename)(filePath), (0, node_path_1.dirname)(filePath)); } else if (fallbackPath.match(/^.*\.(jpe?g|gif|png|svg|ttf|woff2|css)$/i) && await (0, fs_extra_1.pathExists)(fallbackPath)) { return reply.sendFile((0, node_path_1.basename)(fallbackPath), (0, node_path_1.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 common_1.NotFoundException(); } } async serveAssetsFromDevServer(reply, pluginUi, assetPath) { try { const response = await (0, rxjs_1.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 (0, rxjs_1.firstValueFrom)(this.httpService.get(pluginUi.devServer, { responseType: 'text' }))).data; } else { return await (0, fs_extra_1.readFile)((0, node_path_1.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 (0, fs_extra_1.pathExists)((0, node_path_1.resolve)(pluginUi.serverPath))) { client.emit('ready', { server: false }); return; } const childEnv = Object.assign({}, node_process_1.default.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 = (0, node_child_process_1.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 { node_process_1.default.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); } }); } }; exports.PluginsSettingsUiService = PluginsSettingsUiService; exports.PluginsSettingsUiService = PluginsSettingsUiService = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [logger_service_1.Logger, plugins_service_1.PluginsService, config_service_1.ConfigService, axios_1.HttpService]) ], PluginsSettingsUiService); //# sourceMappingURL=plugins-settings-ui.service.js.map