homebridge-config-ui-x
Version:
A web based management, configuration and control platform for Homebridge.
206 lines • 9.66 kB
JavaScript
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
;