homebridge-config-ui-x 5.10.1-alpha.0 → 5.10.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -15
- package/README.md +3 -3
- package/dist/core/auth/auth.controller.d.ts +4 -0
- package/dist/core/config/config.interfaces.d.ts +14 -0
- package/dist/core/config/config.service.d.ts +6 -0
- package/dist/core/config/config.service.js +8 -0
- package/dist/core/config/config.service.js.map +1 -1
- package/dist/core/config/config.startup.js +48 -17
- package/dist/core/config/config.startup.js.map +1 -1
- package/dist/core/feature-flags/feature-flags.registry.js +5 -0
- package/dist/core/feature-flags/feature-flags.registry.js.map +1 -1
- package/dist/core/homebridge-ipc/homebridge-ipc.service.js +1 -0
- package/dist/core/homebridge-ipc/homebridge-ipc.service.js.map +1 -1
- package/dist/core/spa/spa-html.service.d.ts +5 -0
- package/dist/core/spa/spa-html.service.js +32 -0
- package/dist/core/spa/spa-html.service.js.map +1 -0
- package/dist/core/spa/spa.filter.d.ts +3 -0
- package/dist/core/spa/spa.filter.js +22 -2
- package/dist/core/spa/spa.filter.js.map +1 -1
- package/dist/core/ssl/ssl-cert-generator.service.d.ts +15 -0
- package/dist/core/ssl/ssl-cert-generator.service.js +125 -0
- package/dist/core/ssl/ssl-cert-generator.service.js.map +1 -0
- package/dist/globalDefaults.js +3 -0
- package/dist/globalDefaults.js.map +1 -1
- package/dist/main.js +18 -4
- package/dist/main.js.map +1 -1
- package/dist/modules/accessories/accessories.controller.d.ts +1 -1
- package/dist/modules/accessories/accessories.interfaces.d.ts +94 -0
- package/dist/modules/accessories/accessories.interfaces.js +2 -0
- package/dist/modules/accessories/accessories.interfaces.js.map +1 -0
- package/dist/modules/accessories/accessories.module.js +2 -0
- package/dist/modules/accessories/accessories.module.js.map +1 -1
- package/dist/modules/accessories/accessories.service.d.ts +21 -3
- package/dist/modules/accessories/accessories.service.js +280 -17
- package/dist/modules/accessories/accessories.service.js.map +1 -1
- package/dist/modules/child-bridges/child-bridges.interfaces.d.ts +9 -0
- package/dist/modules/config-editor/config-editor.controller.d.ts +4 -0
- package/dist/modules/config-editor/config-editor.controller.js +64 -0
- package/dist/modules/config-editor/config-editor.controller.js.map +1 -1
- package/dist/modules/config-editor/config-editor.service.d.ts +6 -1
- package/dist/modules/config-editor/config-editor.service.js +45 -1
- package/dist/modules/config-editor/config-editor.service.js.map +1 -1
- package/dist/modules/custom-plugins/plugins-settings-ui/plugins-settings-ui.service.js +1 -1
- package/dist/modules/custom-plugins/plugins-settings-ui/plugins-settings-ui.service.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +0 -1
- package/dist/modules/plugins/plugins.service.js +18 -17
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/server/server.controller.d.ts +50 -0
- package/dist/modules/server/server.controller.js +201 -2
- package/dist/modules/server/server.controller.js.map +1 -1
- package/dist/modules/server/server.service.d.ts +48 -0
- package/dist/modules/server/server.service.js +502 -14
- package/dist/modules/server/server.service.js.map +1 -1
- package/dist/modules/status/status.gateway.d.ts +2 -0
- package/dist/modules/status/status.interfaces.d.ts +11 -0
- package/dist/modules/status/status.service.d.ts +4 -1
- package/dist/modules/status/status.service.js +21 -2
- package/dist/modules/status/status.service.js.map +1 -1
- package/docs/matter-todo.md +15 -0
- package/docs/ssl-upload-pr.md +103 -0
- package/package.json +8 -3
- package/public/3rdpartylicenses.txt +554 -606
- package/public/assets/matter.svg +8 -0
- package/public/assets/monaco/min/vs/assets/editor.worker-Be8ye1pW.js +26 -0
- package/public/assets/monaco/min/vs/assets/json.worker-DKiEKt88.js +58 -0
- package/public/assets/monaco/min/vs/basic-languages/monaco.contribution.js +1 -0
- package/public/assets/monaco/min/vs/editor/editor.main.css +1 -8
- package/public/assets/monaco/min/vs/editor/editor.main.js +4 -797
- package/public/assets/monaco/min/vs/editor.api-CalNCsUg.js +903 -0
- package/public/assets/monaco/min/vs/jsonMode-DULH5oaX.js +7 -0
- package/public/assets/monaco/min/vs/language/json/monaco.contribution.js +1 -0
- package/public/assets/monaco/min/vs/loader.js +1364 -7
- package/public/assets/monaco/min/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/public/assets/monaco/min/vs/monaco.contribution-D2OdxNBt.js +1 -0
- package/public/assets/monaco/min/vs/monaco.contribution-DO3azKX8.js +1 -0
- package/public/assets/monaco/min/vs/monaco.contribution-EcChJV6a.js +1 -0
- package/public/assets/monaco/min/vs/monaco.contribution-qLAYrEOP.js +1 -0
- package/public/assets/monaco/min/vs/nls.messages-loader.js +1 -0
- package/public/assets/monaco/min/vs/workers-DcJshg-q.js +1 -0
- package/public/assets/plugin-ui-utils/ui.js +3 -0
- package/public/assets/plugin-ui-utils/ui.js.map +1 -1
- package/public/chunk-2D7CDA3H.js +1 -0
- package/public/chunk-2GBZLCJW.js +1 -0
- package/public/chunk-33YDZRAK.js +1 -0
- package/public/chunk-3DG6CT5D.js +2 -0
- package/public/chunk-3F75CKTQ.js +1 -0
- package/public/chunk-3OSRGC5M.js +1 -0
- package/public/chunk-3UTT42KH.js +1 -0
- package/public/chunk-442FBVB5.js +1 -0
- package/public/chunk-4PPFVY5K.js +1 -0
- package/public/chunk-4RWLV5CJ.js +1 -0
- package/public/chunk-4UDSSFBN.js +1 -0
- package/public/chunk-4XZK5MQY.js +1 -0
- package/public/chunk-5S4MIY7E.js +1 -0
- package/public/chunk-5X5S4L7Y.js +1 -0
- package/public/chunk-6ELMQIBK.js +1 -0
- package/public/chunk-6I27GJJF.js +1 -0
- package/public/{chunk-ACVPGZ3H.js → chunk-7CDYTG4I.js} +1 -1
- package/public/chunk-7DWXCRSP.js +1 -0
- package/public/chunk-7RJ5H5OP.js +1 -0
- package/public/chunk-ABQLMP2L.js +1 -0
- package/public/chunk-B4AJQJMI.js +1 -0
- package/public/chunk-BEI5QPKL.js +5 -0
- package/public/chunk-BEWAKOMK.js +1 -0
- package/public/{chunk-FHGIL4GK.js → chunk-BMOLDRJ3.js} +1 -1
- package/public/chunk-BOW5E4YK.js +50 -0
- package/public/chunk-BSST5MPT.js +1 -0
- package/public/chunk-BSXPNFI5.js +1 -0
- package/public/chunk-C4IIDRJP.js +1 -0
- package/public/chunk-CIWR3BDK.js +5 -0
- package/public/chunk-CMS4LVBG.js +1 -0
- package/public/chunk-CO3QJQRO.js +1 -0
- package/public/chunk-CS2WBXWW.js +1 -0
- package/public/chunk-CTWHZU36.js +1 -0
- package/public/chunk-DAMERMP3.js +1 -0
- package/public/chunk-DM74WNXP.js +1 -0
- package/public/chunk-DMKHEJF5.js +3 -0
- package/public/chunk-DTFJZNTX.js +1 -0
- package/public/chunk-DWU5C3KH.js +1 -0
- package/public/chunk-EBFB2GZT.js +1 -0
- package/public/chunk-EEA6UA46.js +2 -0
- package/public/chunk-EJACNX3Z.js +1 -0
- package/public/{chunk-U73PAFR5.js → chunk-FE43ULOM.js} +1 -1
- package/public/chunk-GPMU3WMO.js +68 -0
- package/public/chunk-GWHA2DEF.js +1 -0
- package/public/chunk-GXD2SS6L.js +1 -0
- package/public/chunk-H6QDC36H.js +1 -0
- package/public/{chunk-UPB765LT.js → chunk-H7F7H5MW.js} +1 -1
- package/public/chunk-HAJT335F.js +1 -0
- package/public/chunk-HK3N2YPM.js +1 -0
- package/public/chunk-HLNMCR4K.js +1 -0
- package/public/chunk-IC6BLNW5.js +1 -0
- package/public/chunk-IMRVMPXI.js +1 -0
- package/public/chunk-IPRQGPVV.js +1 -0
- package/public/chunk-IVZ5ER6L.js +1 -0
- package/public/chunk-JRQC3M7W.js +4 -0
- package/public/chunk-JT7TYR5C.js +1 -0
- package/public/chunk-KDXCBENV.js +2 -0
- package/public/chunk-KGOOP2T6.js +1 -0
- package/public/chunk-KMSHJTEM.js +1 -0
- package/public/chunk-L77RLTDM.js +1 -0
- package/public/chunk-LCGZABYG.js +1 -0
- package/public/chunk-LG53BDFP.js +1 -0
- package/public/chunk-LJMU7UD3.js +1 -0
- package/public/chunk-LKNJHPL6.js +1 -0
- package/public/chunk-LKODOMMX.js +1 -0
- package/public/chunk-LUUAC57N.js +1 -0
- package/public/chunk-MIWSF7W2.js +1 -0
- package/public/chunk-MQTRL6KN.js +1 -0
- package/public/{chunk-NLVJP2FS.js → chunk-MS6AXMT2.js} +52 -2
- package/public/chunk-NP23WSXR.js +1 -0
- package/public/chunk-O3SZVIF6.js +1 -0
- package/public/chunk-ODXGF3LF.js +1 -0
- package/public/chunk-OPDTKONB.js +1 -0
- package/public/chunk-ORK6IK47.js +4 -0
- package/public/chunk-OZI5E5HI.js +1 -0
- package/public/chunk-P4KZ2HSY.js +1 -0
- package/public/chunk-PI4V6TBF.js +1 -0
- package/public/chunk-PMN4OFOP.js +1 -0
- package/public/chunk-Q4WN5E2X.js +1 -0
- package/public/chunk-QA6IRNX4.js +1 -0
- package/public/chunk-QGV5S3KG.js +1 -0
- package/public/chunk-QKEI7JJT.js +1 -0
- package/public/chunk-QNQ4HXBD.js +1 -0
- package/public/chunk-QNQIBHMP.js +1 -0
- package/public/chunk-R4E7RV22.js +1 -0
- package/public/chunk-R5Q7W4AU.js +1 -0
- package/public/chunk-RQG4TZOZ.js +1 -0
- package/public/chunk-SN3EVQDP.js +1 -0
- package/public/chunk-T3526HQB.js +1 -0
- package/public/{chunk-J3GNTL4X.js → chunk-T4IEJNVJ.js} +1 -1
- package/public/chunk-TTPURYKZ.js +1 -0
- package/public/{chunk-ZAQCBQTS.js → chunk-U4YCHJUJ.js} +2 -2
- package/public/chunk-UAFCVKMR.js +1 -0
- package/public/chunk-UBCYUHPO.js +1 -0
- package/public/chunk-UJEB6C35.js +1 -0
- package/public/chunk-UQP4IIIX.js +1 -0
- package/public/chunk-URTTNRYM.js +1 -0
- package/public/chunk-USFURAEW.js +1 -0
- package/public/chunk-UVRDZGSU.js +1 -0
- package/public/chunk-VBTEO65F.js +1 -0
- package/public/chunk-WCRO6YQB.js +16 -0
- package/public/chunk-XL5BAQKW.js +8 -0
- package/public/chunk-XUAJJMQ4.js +1 -0
- package/public/chunk-Y67I4K2H.js +1 -0
- package/public/chunk-YHOHHKYL.js +1 -0
- package/public/chunk-ZABK6QJY.js +1 -0
- package/public/chunk-ZCGZM7LU.js +1 -0
- package/public/chunk-ZE7IZECI.js +19 -0
- package/public/chunk-ZGIR65XW.js +1 -0
- package/public/chunk-ZXID2WUA.js +1 -0
- package/public/index.html +2 -2
- package/public/main-VGG7NRKO.js +1 -0
- package/public/media/matter-P563JGDL.svg +8 -0
- package/public/styles-CT2LPGES.css +1 -0
- package/scripts/extract-plugin-alias.js +53 -2
- package/public/assets/monaco/min/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/public/assets/monaco/min/vs/base/worker/workerMain.js +0 -31
- package/public/assets/monaco/min/vs/basic-languages/shell/shell.js +0 -10
- package/public/assets/monaco/min/vs/language/json/jsonMode.js +0 -19
- package/public/assets/monaco/min/vs/language/json/jsonWorker.js +0 -42
- package/public/chunk-22RLE62I.js +0 -1
- package/public/chunk-2A6XY76J.js +0 -1
- package/public/chunk-2AF3WD4J.js +0 -2
- package/public/chunk-3DBRQ7MK.js +0 -1
- package/public/chunk-4IR2BGMY.js +0 -2
- package/public/chunk-57IPNC7F.js +0 -1
- package/public/chunk-6QNJJYEZ.js +0 -4
- package/public/chunk-72KEQFMG.js +0 -1
- package/public/chunk-A5FPX3XJ.js +0 -1
- package/public/chunk-A6U6HYY3.js +0 -1
- package/public/chunk-APMBCXUH.js +0 -1
- package/public/chunk-AQFUOCN5.js +0 -1
- package/public/chunk-B4HZ3C7R.js +0 -1
- package/public/chunk-BJQY6ZPJ.js +0 -1
- package/public/chunk-BQRR3H5O.js +0 -1
- package/public/chunk-C534VT6D.js +0 -1
- package/public/chunk-C64SJK5O.js +0 -16
- package/public/chunk-CAYKYCA4.js +0 -1
- package/public/chunk-DEAIAR5T.js +0 -1
- package/public/chunk-E2BBQYGT.js +0 -8
- package/public/chunk-E2J5A7BL.js +0 -1
- package/public/chunk-EANCGCZ4.js +0 -1
- package/public/chunk-ELLOA7NV.js +0 -1
- package/public/chunk-F535KZNK.js +0 -1
- package/public/chunk-FBCPF7OW.js +0 -1
- package/public/chunk-FC5NA4YQ.js +0 -51
- package/public/chunk-FXPYZAMJ.js +0 -1
- package/public/chunk-FYY57EXZ.js +0 -5
- package/public/chunk-G2R4JMBA.js +0 -3
- package/public/chunk-H4NAW7LB.js +0 -1
- package/public/chunk-HGZUNSQU.js +0 -1
- package/public/chunk-HPNXSGCR.js +0 -1
- package/public/chunk-HW3LDSBY.js +0 -1
- package/public/chunk-IADCQFCV.js +0 -5
- package/public/chunk-ISM5U2ZP.js +0 -1
- package/public/chunk-IYQU2T5J.js +0 -19
- package/public/chunk-JICTKNBT.js +0 -1
- package/public/chunk-JVB5DKDR.js +0 -1
- package/public/chunk-KU7PGJPQ.js +0 -1
- package/public/chunk-L5OY5K3Z.js +0 -1
- package/public/chunk-NZHDV2UO.js +0 -1
- package/public/chunk-NZUGX67J.js +0 -1
- package/public/chunk-PHY7W3BQ.js +0 -1
- package/public/chunk-QRNZH52Q.js +0 -1
- package/public/chunk-RDXOP2RA.js +0 -1
- package/public/chunk-RQF25DTQ.js +0 -1
- package/public/chunk-RR2BOWSZ.js +0 -40
- package/public/chunk-SQSP77TI.js +0 -1
- package/public/chunk-TOWEYTJH.js +0 -1
- package/public/chunk-UHXVBJ2O.js +0 -1
- package/public/chunk-VLLREE2S.js +0 -1
- package/public/chunk-W5BUXVY6.js +0 -5
- package/public/chunk-WCOZQRWI.js +0 -1
- package/public/chunk-WDMQUU57.js +0 -1
- package/public/chunk-WOVMIXAU.js +0 -4
- package/public/chunk-XTLENV3O.js +0 -1
- package/public/chunk-YINTXBI6.js +0 -50
- package/public/chunk-YS7B334L.js +0 -1
- package/public/chunk-YYUGFDEY.js +0 -1
- package/public/chunk-ZB45BBXI.js +0 -1
- package/public/chunk-ZSZ4ITUC.js +0 -1
- package/public/main-QWNFO5VR.js +0 -1
- package/public/styles-7EFV5QBG.css +0 -1
- /package/public/{polyfills-5KWHJ7II.js → polyfills-NISNRVWY.js} +0 -0
|
@@ -12,11 +12,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
12
12
|
};
|
|
13
13
|
import { Buffer } from 'node:buffer';
|
|
14
14
|
import { exec, spawn } from 'node:child_process';
|
|
15
|
+
import { createPrivateKey, createPublicKey, X509Certificate } from 'node:crypto';
|
|
15
16
|
import { createWriteStream } from 'node:fs';
|
|
16
17
|
import { readdir, unlink } from 'node:fs/promises';
|
|
17
18
|
import { extname, join, resolve } from 'node:path';
|
|
18
19
|
import process from 'node:process';
|
|
19
20
|
import { pipeline } from 'node:stream';
|
|
21
|
+
import { createSecureContext } from 'node:tls';
|
|
20
22
|
import { promisify } from 'node:util';
|
|
21
23
|
import { Categories } from '@homebridge/hap-client/dist/hap-types.js';
|
|
22
24
|
import { BadRequestException, Inject, Injectable, InternalServerErrorException, NotFoundException, ServiceUnavailableException, } from '@nestjs/common';
|
|
@@ -27,6 +29,7 @@ import { check as tcpCheck } from 'tcp-port-used';
|
|
|
27
29
|
import { ConfigService } from '../../core/config/config.service.js';
|
|
28
30
|
import { HomebridgeIpcService } from '../../core/homebridge-ipc/homebridge-ipc.service.js';
|
|
29
31
|
import { Logger } from '../../core/logger/logger.service.js';
|
|
32
|
+
import { SslCertGeneratorService } from '../../core/ssl/ssl-cert-generator.service.js';
|
|
30
33
|
import { AccessoriesService } from '../accessories/accessories.service.js';
|
|
31
34
|
import { ConfigEditorService } from '../config-editor/config-editor.service.js';
|
|
32
35
|
const pump = promisify(pipeline);
|
|
@@ -50,25 +53,37 @@ let ServerService = class ServerService {
|
|
|
50
53
|
this.accessoryId = this.configService.homebridgeConfig.bridge.username.split(':').join('');
|
|
51
54
|
this.accessoryInfoPath = join(this.configService.storagePath, 'persist', `AccessoryInfo.${this.accessoryId}.json`);
|
|
52
55
|
}
|
|
53
|
-
async deleteSingleDeviceAccessories(id, cachedAccessoriesDir) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await
|
|
58
|
-
|
|
56
|
+
async deleteSingleDeviceAccessories(id, cachedAccessoriesDir, protocol = 'both') {
|
|
57
|
+
if (protocol === 'hap' || protocol === 'both') {
|
|
58
|
+
const cachedAccessories = join(cachedAccessoriesDir, `cachedAccessories.${id}`);
|
|
59
|
+
const cachedAccessoriesBackup = join(cachedAccessoriesDir, `.cachedAccessories.${id}.bak`);
|
|
60
|
+
if (await pathExists(cachedAccessories)) {
|
|
61
|
+
await unlink(cachedAccessories);
|
|
62
|
+
this.logger.warn(`Bridge ${id} HAP accessory removal: removed ${cachedAccessories}.`);
|
|
63
|
+
}
|
|
64
|
+
if (await pathExists(cachedAccessoriesBackup)) {
|
|
65
|
+
await unlink(cachedAccessoriesBackup);
|
|
66
|
+
this.logger.warn(`Bridge ${id} HAP accessory removal: removed ${cachedAccessoriesBackup}.`);
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
this.
|
|
69
|
+
if (protocol === 'matter' || protocol === 'both') {
|
|
70
|
+
const deviceId = id.split(':').join('').toUpperCase();
|
|
71
|
+
const matterPath = join(this.configService.storagePath, 'matter', deviceId);
|
|
72
|
+
if (await pathExists(matterPath)) {
|
|
73
|
+
await remove(matterPath);
|
|
74
|
+
this.logger.warn(`Bridge ${id} Matter accessory removal: removed Matter bridge storage at ${matterPath}.`);
|
|
75
|
+
}
|
|
63
76
|
}
|
|
64
77
|
}
|
|
65
78
|
async deleteSingleDevicePairing(id, resetPairingInfo) {
|
|
66
79
|
const persistPath = join(this.configService.storagePath, 'persist');
|
|
67
80
|
const accessoryInfo = join(persistPath, `AccessoryInfo.${id}.json`);
|
|
68
81
|
const identifierCache = join(persistPath, `IdentifierCache.${id}.json`);
|
|
82
|
+
const deviceId = id.includes(':') ? id.split(':').join('').toUpperCase() : id.toUpperCase();
|
|
83
|
+
const matterPath = join(this.configService.storagePath, 'matter', deviceId);
|
|
69
84
|
try {
|
|
70
85
|
const configFile = await this.configEditorService.getConfigFile();
|
|
71
|
-
const username = id.match(/.{1,2}/g)
|
|
86
|
+
const username = id.includes(':') ? id.toUpperCase() : id.match(/.{1,2}/g)?.join(':').toUpperCase() || id.toUpperCase();
|
|
72
87
|
const uiConfig = configFile.platforms.find(x => x.platform === 'config');
|
|
73
88
|
let blacklistChanged = false;
|
|
74
89
|
let bridgesChanged = false;
|
|
@@ -134,6 +149,10 @@ let ServerService = class ServerService {
|
|
|
134
149
|
await unlink(identifierCache);
|
|
135
150
|
this.logger.warn(`Bridge ${id} reset: removed ${identifierCache}.`);
|
|
136
151
|
}
|
|
152
|
+
if (await pathExists(matterPath)) {
|
|
153
|
+
await remove(matterPath);
|
|
154
|
+
this.logger.warn(`Bridge ${id} reset: removed Matter bridge storage at ${matterPath}.`);
|
|
155
|
+
}
|
|
137
156
|
await this.deleteDeviceAccessories(id);
|
|
138
157
|
}
|
|
139
158
|
async restartServer() {
|
|
@@ -177,6 +196,12 @@ let ServerService = class ServerService {
|
|
|
177
196
|
await this.configEditorService.updateConfigFile(configFile);
|
|
178
197
|
await remove(resolve(this.configService.storagePath, 'accessories'));
|
|
179
198
|
await remove(resolve(this.configService.storagePath, 'persist'));
|
|
199
|
+
const deviceId = oldUsername.split(':').join('').toUpperCase();
|
|
200
|
+
const matterPath = join(this.configService.storagePath, 'matter', deviceId);
|
|
201
|
+
if (await pathExists(matterPath)) {
|
|
202
|
+
await remove(matterPath);
|
|
203
|
+
this.logger.warn(`Bridge ${oldUsername} reset: removed Matter bridge storage at ${matterPath}.`);
|
|
204
|
+
}
|
|
180
205
|
this.logger.log('Homebridge bridge reset: accessories and persist directories were removed.');
|
|
181
206
|
}
|
|
182
207
|
async getDevicePairings() {
|
|
@@ -184,9 +209,68 @@ let ServerService = class ServerService {
|
|
|
184
209
|
const devices = (await readdir(persistPath))
|
|
185
210
|
.filter(x => x.match(/AccessoryInfo\.([A-Fa-f0-9]+)\.json$/));
|
|
186
211
|
const configFile = await this.configEditorService.getConfigFile();
|
|
187
|
-
|
|
212
|
+
const hapDevices = await Promise.all(devices.map(async (x) => {
|
|
188
213
|
return await this.getDevicePairingById(x.split('.')[1], configFile);
|
|
189
214
|
}));
|
|
215
|
+
const matterExternalDevices = await this.getMatterExternalAccessories(hapDevices);
|
|
216
|
+
return [...hapDevices, ...matterExternalDevices].sort((a, b) => a.name.localeCompare(b.name));
|
|
217
|
+
}
|
|
218
|
+
async getMatterExternalAccessories(hapDevices) {
|
|
219
|
+
const matterPath = join(this.configService.storagePath, 'matter');
|
|
220
|
+
if (!await pathExists(matterPath)) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
const matterDirs = (await readdir(matterPath))
|
|
224
|
+
.filter(x => x.match(/^[A-F0-9]{12}$/));
|
|
225
|
+
const matterExternalDevices = [];
|
|
226
|
+
for (const deviceId of matterDirs) {
|
|
227
|
+
try {
|
|
228
|
+
const hasHapAccessoryInfo = hapDevices.some(d => d._id === deviceId);
|
|
229
|
+
if (hasHapAccessoryInfo) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const mainBridgeId = this.configService.homebridgeConfig.bridge.username.split(':').join('').toUpperCase();
|
|
233
|
+
if (deviceId.toUpperCase() === mainBridgeId) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const accessoriesPath = join(matterPath, deviceId, 'accessories.json');
|
|
237
|
+
if (!await pathExists(accessoriesPath)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const accessories = await readJson(accessoriesPath);
|
|
241
|
+
if (!Array.isArray(accessories) || accessories.length === 0) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const accessory = accessories[0];
|
|
245
|
+
const commissioningPath = join(matterPath, deviceId, 'commissioning.json');
|
|
246
|
+
let commissioned = false;
|
|
247
|
+
if (await pathExists(commissioningPath)) {
|
|
248
|
+
const commissioningInfo = await readJson(commissioningPath);
|
|
249
|
+
commissioned = commissioningInfo.commissioned || false;
|
|
250
|
+
}
|
|
251
|
+
const device = {
|
|
252
|
+
_id: deviceId,
|
|
253
|
+
_username: deviceId.match(/.{1,2}/g)?.join(':').toUpperCase() || deviceId,
|
|
254
|
+
_main: false,
|
|
255
|
+
_category: 'other',
|
|
256
|
+
_matter: true,
|
|
257
|
+
_matterOnly: true,
|
|
258
|
+
_isPaired: commissioned,
|
|
259
|
+
_plugin: accessory.plugin,
|
|
260
|
+
name: accessory.displayName || 'Matter External Accessory',
|
|
261
|
+
displayName: accessory.displayName || 'Matter External Accessory',
|
|
262
|
+
manufacturer: accessory.manufacturer || 'Unknown',
|
|
263
|
+
model: accessory.model || 'Unknown',
|
|
264
|
+
serialNumber: accessory.serialNumber || deviceId,
|
|
265
|
+
category: 1,
|
|
266
|
+
};
|
|
267
|
+
matterExternalDevices.push(device);
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
this.logger.error(`Failed to read Matter external accessory ${deviceId}: ${e.message}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return matterExternalDevices;
|
|
190
274
|
}
|
|
191
275
|
async getDevicePairingById(deviceId, configFile = null) {
|
|
192
276
|
const persistPath = join(this.configService.storagePath, 'persist');
|
|
@@ -219,6 +303,10 @@ let ServerService = class ServerService {
|
|
|
219
303
|
device._isPaired = device.pairedClients && Object.keys(device.pairedClients).length > 0;
|
|
220
304
|
device._setupCode = this.generateSetupCode(device);
|
|
221
305
|
device._couldBeStale = !device._main && device._category === 'bridge' && !pluginBlock;
|
|
306
|
+
device._matter = !!(pluginBlock?._bridge?.matter);
|
|
307
|
+
if (device._matter && pluginBlock && 'accessory' in pluginBlock) {
|
|
308
|
+
this.logger.warn(`Device ${deviceId} has Matter configuration on an accessory-based plugin. Matter is only supported for platform-based plugins.`);
|
|
309
|
+
}
|
|
222
310
|
delete device.signSk;
|
|
223
311
|
delete device.signPk;
|
|
224
312
|
delete device.configHash;
|
|
@@ -232,6 +320,44 @@ let ServerService = class ServerService {
|
|
|
232
320
|
await this.deleteSingleDevicePairing(id, resetPairingInfo);
|
|
233
321
|
return { ok: true };
|
|
234
322
|
}
|
|
323
|
+
async deleteDeviceMatterConfig(id) {
|
|
324
|
+
try {
|
|
325
|
+
const configFile = await this.configEditorService.getConfigFile();
|
|
326
|
+
const username = id.includes(':') ? id.toUpperCase() : id.match(/.{1,2}/g)?.join(':').toUpperCase() || id.toUpperCase();
|
|
327
|
+
const pluginBlocks = [
|
|
328
|
+
...(configFile.accessories || []),
|
|
329
|
+
...(configFile.platforms || []),
|
|
330
|
+
]
|
|
331
|
+
.filter((block) => block._bridge?.username?.toUpperCase() === username.toUpperCase());
|
|
332
|
+
const pluginBlock = pluginBlocks.find((block) => block._bridge?.matter);
|
|
333
|
+
if (!pluginBlock) {
|
|
334
|
+
this.logger.error(`Failed to find Matter configuration for child bridge ${id}.`);
|
|
335
|
+
throw new NotFoundException(`Matter configuration not found for bridge ${id}`);
|
|
336
|
+
}
|
|
337
|
+
if ('accessory' in pluginBlock) {
|
|
338
|
+
this.logger.warn(`Removing Matter configuration from accessory-based plugin block for bridge ${id}. Matter is only supported for platform-based plugins.`);
|
|
339
|
+
}
|
|
340
|
+
delete pluginBlock._bridge.matter;
|
|
341
|
+
this.logger.warn(`Bridge ${id} Matter configuration removed from config.json.`);
|
|
342
|
+
await this.configEditorService.updateConfigFile(configFile);
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
if (e instanceof NotFoundException) {
|
|
346
|
+
throw e;
|
|
347
|
+
}
|
|
348
|
+
this.logger.error(`Failed to remove Matter configuration for child bridge ${id} as ${e.message}.`);
|
|
349
|
+
throw new InternalServerErrorException(`Failed to remove Matter configuration: ${e.message}`);
|
|
350
|
+
}
|
|
351
|
+
this.logger.warn(`Shutting down Homebridge before removing Matter storage for bridge ${id}...`);
|
|
352
|
+
await this.homebridgeIpcService.restartAndWaitForClose();
|
|
353
|
+
const deviceId = id.includes(':') ? id.split(':').join('').toUpperCase() : id.toUpperCase();
|
|
354
|
+
const matterPath = join(this.configService.storagePath, 'matter', deviceId);
|
|
355
|
+
if (await pathExists(matterPath)) {
|
|
356
|
+
await remove(matterPath);
|
|
357
|
+
this.logger.warn(`Bridge ${id} Matter storage removed at ${matterPath}.`);
|
|
358
|
+
}
|
|
359
|
+
return { ok: true };
|
|
360
|
+
}
|
|
235
361
|
async deleteDevicesPairing(bridges) {
|
|
236
362
|
this.logger.warn(`Shutting down Homebridge before resetting paired bridges ${bridges.map(x => x.id).join(', ')}...`);
|
|
237
363
|
await this.homebridgeIpcService.restartAndWaitForClose();
|
|
@@ -255,9 +381,9 @@ let ServerService = class ServerService {
|
|
|
255
381
|
this.logger.warn(`Shutting down Homebridge before removing accessories for paired bridges ${bridges.map(x => x.id).join(', ')}...`);
|
|
256
382
|
await this.homebridgeIpcService.restartAndWaitForClose();
|
|
257
383
|
const cachedAccessoriesDir = join(this.configService.storagePath, 'accessories');
|
|
258
|
-
for (const { id } of bridges) {
|
|
384
|
+
for (const { id, protocol } of bridges) {
|
|
259
385
|
try {
|
|
260
|
-
await this.deleteSingleDeviceAccessories(id, cachedAccessoriesDir);
|
|
386
|
+
await this.deleteSingleDeviceAccessories(id, cachedAccessoriesDir, protocol || 'both');
|
|
261
387
|
}
|
|
262
388
|
catch (e) {
|
|
263
389
|
this.logger.error(`Failed to remove accessories for bridge ${id} as ${e.message}.`);
|
|
@@ -338,13 +464,19 @@ let ServerService = class ServerService {
|
|
|
338
464
|
await this.homebridgeIpcService.restartAndWaitForClose();
|
|
339
465
|
this.logger.warn('Shutting down Homebridge before removing cached accessories');
|
|
340
466
|
try {
|
|
341
|
-
this.logger.log('Clearing all cached accessories...');
|
|
467
|
+
this.logger.log('Clearing all HAP cached accessories...');
|
|
342
468
|
for (const thisCachedAccessoriesPath of cachedAccessoryPaths) {
|
|
343
469
|
if (await pathExists(thisCachedAccessoriesPath)) {
|
|
344
470
|
await unlink(thisCachedAccessoriesPath);
|
|
345
471
|
this.logger.warn(`Removed ${thisCachedAccessoriesPath}.`);
|
|
346
472
|
}
|
|
347
473
|
}
|
|
474
|
+
const matterDir = join(this.configService.storagePath, 'matter');
|
|
475
|
+
if (await pathExists(matterDir)) {
|
|
476
|
+
this.logger.log('Clearing all Matter cached accessories...');
|
|
477
|
+
await remove(matterDir);
|
|
478
|
+
this.logger.warn(`Removed Matter storage directory at ${matterDir}.`);
|
|
479
|
+
}
|
|
348
480
|
}
|
|
349
481
|
catch (e) {
|
|
350
482
|
this.logger.error(`Failed to clear all cached accessories at ${cachedAccessoriesPath} as ${e.message}.`);
|
|
@@ -353,6 +485,96 @@ let ServerService = class ServerService {
|
|
|
353
485
|
}
|
|
354
486
|
return { ok: true };
|
|
355
487
|
}
|
|
488
|
+
async getMatterAccessories() {
|
|
489
|
+
const matterDir = join(this.configService.storagePath, 'matter');
|
|
490
|
+
if (!await pathExists(matterDir)) {
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
const matterBridges = (await readdir(matterDir))
|
|
494
|
+
.filter(x => x.match(/^[A-F0-9]+$/));
|
|
495
|
+
const matterAccessories = [];
|
|
496
|
+
await Promise.all(matterBridges.map(async (deviceId) => {
|
|
497
|
+
try {
|
|
498
|
+
const accessoriesPath = join(matterDir, deviceId, 'accessories.json');
|
|
499
|
+
if (await pathExists(accessoriesPath)) {
|
|
500
|
+
const accessories = await readJson(accessoriesPath);
|
|
501
|
+
if (Array.isArray(accessories)) {
|
|
502
|
+
for (const accessory of accessories) {
|
|
503
|
+
accessory.$deviceId = deviceId;
|
|
504
|
+
accessory.$protocol = 'matter';
|
|
505
|
+
matterAccessories.push(accessory);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
this.logger.error(`Failed to read Matter accessories for bridge ${deviceId}: ${e.message}`);
|
|
512
|
+
}
|
|
513
|
+
}));
|
|
514
|
+
return matterAccessories;
|
|
515
|
+
}
|
|
516
|
+
async deleteMatterAccessory(deviceId, uuid) {
|
|
517
|
+
const matterAccessoriesPath = join(this.configService.storagePath, 'matter', deviceId, 'accessories.json');
|
|
518
|
+
if (!await pathExists(matterAccessoriesPath)) {
|
|
519
|
+
this.logger.error(`Matter accessories file not found for bridge ${deviceId}`);
|
|
520
|
+
throw new NotFoundException();
|
|
521
|
+
}
|
|
522
|
+
this.logger.warn(`Shutting down Homebridge before removing Matter accessory ${uuid} from bridge ${deviceId}...`);
|
|
523
|
+
await this.homebridgeIpcService.restartAndWaitForClose();
|
|
524
|
+
const matterAccessories = await readJson(matterAccessoriesPath);
|
|
525
|
+
const accessoryIndex = matterAccessories.findIndex(x => x.uuid === uuid);
|
|
526
|
+
if (accessoryIndex > -1) {
|
|
527
|
+
matterAccessories.splice(accessoryIndex, 1);
|
|
528
|
+
await writeJson(matterAccessoriesPath, matterAccessories, { spaces: 2 });
|
|
529
|
+
this.logger.warn(`Removed Matter accessory with UUID ${uuid} from bridge ${deviceId}.`);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
this.logger.error(`Cannot find Matter accessory with UUID ${uuid} in bridge ${deviceId}.`);
|
|
533
|
+
throw new NotFoundException();
|
|
534
|
+
}
|
|
535
|
+
return { ok: true };
|
|
536
|
+
}
|
|
537
|
+
async deleteMatterAccessories(accessories) {
|
|
538
|
+
this.logger.warn(`Shutting down Homebridge before removing Matter accessories ${accessories.map(x => x.uuid).join(', ')}.`);
|
|
539
|
+
await this.homebridgeIpcService.restartAndWaitForClose();
|
|
540
|
+
const accessoriesByBridge = new Map();
|
|
541
|
+
for (const { deviceId, uuid } of accessories) {
|
|
542
|
+
if (!accessoriesByBridge.has(deviceId)) {
|
|
543
|
+
accessoriesByBridge.set(deviceId, []);
|
|
544
|
+
}
|
|
545
|
+
accessoriesByBridge.get(deviceId).push({ uuid });
|
|
546
|
+
}
|
|
547
|
+
for (const [deviceId, bridgeAccessories] of accessoriesByBridge.entries()) {
|
|
548
|
+
const matterAccessoriesPath = join(this.configService.storagePath, 'matter', deviceId, 'accessories.json');
|
|
549
|
+
try {
|
|
550
|
+
if (!await pathExists(matterAccessoriesPath)) {
|
|
551
|
+
this.logger.error(`Matter accessories file not found for bridge ${deviceId}`);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const matterAccessories = await readJson(matterAccessoriesPath);
|
|
555
|
+
for (const { uuid } of bridgeAccessories) {
|
|
556
|
+
try {
|
|
557
|
+
const accessoryIndex = matterAccessories.findIndex(x => x.uuid === uuid);
|
|
558
|
+
if (accessoryIndex > -1) {
|
|
559
|
+
matterAccessories.splice(accessoryIndex, 1);
|
|
560
|
+
this.logger.warn(`Removed Matter accessory with UUID ${uuid} from bridge ${deviceId}.`);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
this.logger.error(`Cannot find Matter accessory with UUID ${uuid} in bridge ${deviceId}.`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (e) {
|
|
567
|
+
this.logger.error(`Failed to remove Matter accessory with UUID ${uuid} from bridge ${deviceId} as ${e.message}.`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
await writeJson(matterAccessoriesPath, matterAccessories, { spaces: 2 });
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
this.logger.error(`Failed to process Matter accessories for bridge ${deviceId} as ${e.message}.`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return { ok: true };
|
|
577
|
+
}
|
|
356
578
|
async getSetupCode() {
|
|
357
579
|
if (this.setupCode) {
|
|
358
580
|
return this.setupCode;
|
|
@@ -457,6 +679,30 @@ let ServerService = class ServerService {
|
|
|
457
679
|
}
|
|
458
680
|
return { port };
|
|
459
681
|
}
|
|
682
|
+
async lookupUnusedMatterPort() {
|
|
683
|
+
const min = 5530;
|
|
684
|
+
const max = 5541;
|
|
685
|
+
const config = await this.configEditorService.getConfigFile();
|
|
686
|
+
const usedMatterPorts = new Set();
|
|
687
|
+
if (config.bridge?.matter?.port) {
|
|
688
|
+
usedMatterPorts.add(config.bridge.matter.port);
|
|
689
|
+
}
|
|
690
|
+
for (const block of [...(config.accessories || []), ...(config.platforms || [])]) {
|
|
691
|
+
if (block._bridge?.matter?.port) {
|
|
692
|
+
if ('accessory' in block) {
|
|
693
|
+
this.logger.warn(`Found Matter configuration on accessory-based plugin block, skipping port ${block._bridge.matter.port}`);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
usedMatterPorts.add(block._bridge.matter.port);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
for (let port = min; port <= max; port += 1) {
|
|
700
|
+
if (!usedMatterPorts.has(port) && !await tcpCheck(port)) {
|
|
701
|
+
return { port };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
throw new InternalServerErrorException('No available ports in the Matter port range (5530-5541)');
|
|
705
|
+
}
|
|
460
706
|
async getHomebridgePort() {
|
|
461
707
|
const config = await this.configEditorService.getConfigFile();
|
|
462
708
|
return { port: config.bridge.port };
|
|
@@ -589,6 +835,248 @@ let ServerService = class ServerService {
|
|
|
589
835
|
});
|
|
590
836
|
});
|
|
591
837
|
}
|
|
838
|
+
async uploadSslKeyCert(req) {
|
|
839
|
+
const parts = req.parts ? req.parts() : null;
|
|
840
|
+
const files = [];
|
|
841
|
+
if (parts) {
|
|
842
|
+
for await (const part of parts) {
|
|
843
|
+
if (part.file) {
|
|
844
|
+
files.push(part);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
const single = await req.file();
|
|
850
|
+
if (single?.file) {
|
|
851
|
+
files.push(single);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (!files.length) {
|
|
855
|
+
throw new BadRequestException('No files uploaded. Please upload both the private key and certificate files.');
|
|
856
|
+
}
|
|
857
|
+
const readStreamToBuffer = async (stream) => {
|
|
858
|
+
const chunks = [];
|
|
859
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
860
|
+
stream.on('data', (d) => chunks.push(Buffer.isBuffer(d) ? d : Buffer.from(d)));
|
|
861
|
+
stream.on('end', () => resolvePromise());
|
|
862
|
+
stream.on('error', rejectPromise);
|
|
863
|
+
});
|
|
864
|
+
return Buffer.concat(chunks);
|
|
865
|
+
};
|
|
866
|
+
let keyPem = null;
|
|
867
|
+
let certPem = null;
|
|
868
|
+
for (const f of files) {
|
|
869
|
+
if (f.file?.truncated) {
|
|
870
|
+
throw new InternalServerErrorException(`Upload exceeds maximum size ${globalThis.backup.maxBackupSizeText}.`);
|
|
871
|
+
}
|
|
872
|
+
const buf = await readStreamToBuffer(f.file);
|
|
873
|
+
const text = buf.toString('utf8');
|
|
874
|
+
if (/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/.test(text)) {
|
|
875
|
+
keyPem = buf;
|
|
876
|
+
}
|
|
877
|
+
else if (/-----BEGIN CERTIFICATE-----/.test(text)) {
|
|
878
|
+
certPem = buf;
|
|
879
|
+
}
|
|
880
|
+
else if (f.fieldname === 'key') {
|
|
881
|
+
keyPem = buf;
|
|
882
|
+
}
|
|
883
|
+
else if (f.fieldname === 'cert') {
|
|
884
|
+
certPem = buf;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (!keyPem || !certPem) {
|
|
888
|
+
throw new BadRequestException('Both a PEM private key and certificate must be provided.');
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const x509 = new X509Certificate(certPem);
|
|
892
|
+
const certPub = x509.publicKey.export({ type: 'spki', format: 'der' });
|
|
893
|
+
const priv = createPrivateKey({ key: keyPem });
|
|
894
|
+
const pubFromPriv = createPublicKey(priv).export({ type: 'spki', format: 'der' });
|
|
895
|
+
if (!certPub.equals(pubFromPriv)) {
|
|
896
|
+
throw new BadRequestException('The private key does not match the certificate public key.');
|
|
897
|
+
}
|
|
898
|
+
createSecureContext({ key: keyPem, cert: certPem });
|
|
899
|
+
}
|
|
900
|
+
catch (e) {
|
|
901
|
+
if (e instanceof BadRequestException) {
|
|
902
|
+
throw e;
|
|
903
|
+
}
|
|
904
|
+
throw new BadRequestException(`Invalid key/certificate: ${e?.message || e}`);
|
|
905
|
+
}
|
|
906
|
+
const sslDir = join(this.configService.storagePath, 'ssl-certs');
|
|
907
|
+
const keyPath = join(sslDir, 'ui-ssl.key');
|
|
908
|
+
const certPath = join(sslDir, 'ui-ssl.crt');
|
|
909
|
+
const { ensureDir, writeFile } = await import('fs-extra');
|
|
910
|
+
await ensureDir(sslDir);
|
|
911
|
+
await writeFile(keyPath, keyPem);
|
|
912
|
+
await writeFile(certPath, certPem);
|
|
913
|
+
const configFile = await this.configEditorService.getConfigFile();
|
|
914
|
+
const uiConfigBlock = configFile.platforms.find((x) => x.platform === 'config');
|
|
915
|
+
if (!uiConfigBlock) {
|
|
916
|
+
throw new InternalServerErrorException('Config platform block not found.');
|
|
917
|
+
}
|
|
918
|
+
if (!uiConfigBlock.ssl) {
|
|
919
|
+
uiConfigBlock.ssl = {};
|
|
920
|
+
}
|
|
921
|
+
uiConfigBlock.ssl.key = keyPath;
|
|
922
|
+
uiConfigBlock.ssl.cert = certPath;
|
|
923
|
+
delete uiConfigBlock.ssl.pfx;
|
|
924
|
+
delete uiConfigBlock.ssl.passphrase;
|
|
925
|
+
uiConfigBlock.ssl.selfSigned = false;
|
|
926
|
+
await this.configEditorService.updateConfigFile(configFile);
|
|
927
|
+
return {
|
|
928
|
+
ok: true,
|
|
929
|
+
type: 'keycert',
|
|
930
|
+
keyPath,
|
|
931
|
+
certPath,
|
|
932
|
+
details: 'Certificate and key validated and saved.',
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
async uploadSslPfx(req) {
|
|
936
|
+
let passphrase;
|
|
937
|
+
let filePart;
|
|
938
|
+
if (req.parts) {
|
|
939
|
+
for await (const part of req.parts()) {
|
|
940
|
+
if (part.type === 'file' || part.file) {
|
|
941
|
+
filePart = part;
|
|
942
|
+
}
|
|
943
|
+
else if (part.type === 'field' || part.value) {
|
|
944
|
+
if (part.fieldname === 'passphrase') {
|
|
945
|
+
passphrase = part.value;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
filePart = await req.file();
|
|
952
|
+
passphrase = req.body?.passphrase;
|
|
953
|
+
}
|
|
954
|
+
if (!filePart) {
|
|
955
|
+
throw new BadRequestException('No PFX file uploaded.');
|
|
956
|
+
}
|
|
957
|
+
if (filePart.file?.truncated) {
|
|
958
|
+
throw new InternalServerErrorException(`Upload exceeds maximum size ${globalThis.backup.maxBackupSizeText}.`);
|
|
959
|
+
}
|
|
960
|
+
const readStreamToBuffer = async (stream) => {
|
|
961
|
+
const chunks = [];
|
|
962
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
963
|
+
stream.on('data', (d) => chunks.push(Buffer.isBuffer(d) ? d : Buffer.from(d)));
|
|
964
|
+
stream.on('end', () => resolvePromise());
|
|
965
|
+
stream.on('error', rejectPromise);
|
|
966
|
+
});
|
|
967
|
+
return Buffer.concat(chunks);
|
|
968
|
+
};
|
|
969
|
+
const pfxBuffer = await readStreamToBuffer(filePart.file);
|
|
970
|
+
try {
|
|
971
|
+
createSecureContext({ pfx: pfxBuffer, passphrase });
|
|
972
|
+
}
|
|
973
|
+
catch (e) {
|
|
974
|
+
throw new BadRequestException(`Invalid PFX or passphrase: ${e?.message || e}`);
|
|
975
|
+
}
|
|
976
|
+
const sslDir = join(this.configService.storagePath, 'ssl-certs');
|
|
977
|
+
const pfxPath = join(sslDir, 'ui-ssl.pfx');
|
|
978
|
+
const { ensureDir, writeFile } = await import('fs-extra');
|
|
979
|
+
await ensureDir(sslDir);
|
|
980
|
+
await writeFile(pfxPath, pfxBuffer);
|
|
981
|
+
const configFile = await this.configEditorService.getConfigFile();
|
|
982
|
+
const uiConfigBlock = configFile.platforms.find((x) => x.platform === 'config');
|
|
983
|
+
if (!uiConfigBlock) {
|
|
984
|
+
throw new InternalServerErrorException('Config platform block not found.');
|
|
985
|
+
}
|
|
986
|
+
if (!uiConfigBlock.ssl) {
|
|
987
|
+
uiConfigBlock.ssl = {};
|
|
988
|
+
}
|
|
989
|
+
uiConfigBlock.ssl.pfx = pfxPath;
|
|
990
|
+
uiConfigBlock.ssl.passphrase = passphrase || '';
|
|
991
|
+
delete uiConfigBlock.ssl.key;
|
|
992
|
+
delete uiConfigBlock.ssl.cert;
|
|
993
|
+
uiConfigBlock.ssl.selfSigned = false;
|
|
994
|
+
await this.configEditorService.updateConfigFile(configFile);
|
|
995
|
+
return {
|
|
996
|
+
ok: true,
|
|
997
|
+
type: 'pfx',
|
|
998
|
+
pfxPath,
|
|
999
|
+
details: 'PFX validated and saved.',
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
async validateCurrentSslConfig() {
|
|
1003
|
+
const configFile = await this.configEditorService.getConfigFile();
|
|
1004
|
+
const uiConfigBlock = configFile.platforms.find((x) => x.platform === 'config');
|
|
1005
|
+
const ssl = uiConfigBlock?.ssl || {};
|
|
1006
|
+
if (!ssl || (!ssl.selfSigned && !ssl.key && !ssl.cert && !ssl.pfx)) {
|
|
1007
|
+
return { ok: true, valid: true, type: 'off', details: 'HTTPS is disabled.' };
|
|
1008
|
+
}
|
|
1009
|
+
if (ssl.selfSigned) {
|
|
1010
|
+
return { ok: true, valid: true, type: 'selfsigned', details: 'Self-signed mode enabled.' };
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
if (ssl.key && ssl.cert) {
|
|
1014
|
+
const { readFile } = await import('fs-extra');
|
|
1015
|
+
const keyPem = await readFile(ssl.key);
|
|
1016
|
+
const certPem = await readFile(ssl.cert);
|
|
1017
|
+
const x509 = new X509Certificate(certPem);
|
|
1018
|
+
const certPub = x509.publicKey.export({ type: 'spki', format: 'der' });
|
|
1019
|
+
const priv = createPrivateKey({ key: keyPem });
|
|
1020
|
+
const pubFromPriv = createPublicKey(priv).export({ type: 'spki', format: 'der' });
|
|
1021
|
+
if (!certPub.equals(pubFromPriv)) {
|
|
1022
|
+
return { ok: true, valid: false, type: 'keycert', details: 'Private key does not match certificate.' };
|
|
1023
|
+
}
|
|
1024
|
+
createSecureContext({ key: keyPem, cert: certPem });
|
|
1025
|
+
return { ok: true, valid: true, type: 'keycert', details: 'Key and certificate are valid and match.' };
|
|
1026
|
+
}
|
|
1027
|
+
if (ssl.pfx) {
|
|
1028
|
+
const { readFile } = await import('fs-extra');
|
|
1029
|
+
const pfx = await readFile(ssl.pfx);
|
|
1030
|
+
createSecureContext({ pfx, passphrase: ssl.passphrase });
|
|
1031
|
+
return { ok: true, valid: true, type: 'pfx', details: 'PFX file and passphrase are valid.' };
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
catch (e) {
|
|
1035
|
+
return { ok: true, valid: false, type: ssl.pfx ? 'pfx' : 'keycert', details: e?.message || String(e) };
|
|
1036
|
+
}
|
|
1037
|
+
return { ok: true, valid: false, type: 'off', details: 'No SSL configuration found.' };
|
|
1038
|
+
}
|
|
1039
|
+
async generateSelfSignedCertificate(options = {}) {
|
|
1040
|
+
const hostnames = Array.isArray(options.hostnames) && options.hostnames.length
|
|
1041
|
+
? options.hostnames.map(h => String(h).trim()).filter(Boolean)
|
|
1042
|
+
: ['localhost', '127.0.0.1'];
|
|
1043
|
+
const mode = options.mode || 'keycert';
|
|
1044
|
+
const generator = new SslCertGeneratorService();
|
|
1045
|
+
await generator.generateCertificate(hostnames);
|
|
1046
|
+
const sslDir = join(this.configService.storagePath, 'ssl-certs');
|
|
1047
|
+
const keyPath = join(sslDir, 'private-key.pem');
|
|
1048
|
+
const certPath = join(sslDir, 'certificate.pem');
|
|
1049
|
+
const configFile = await this.configEditorService.getConfigFile();
|
|
1050
|
+
const uiConfigBlock = configFile.platforms.find((x) => x.platform === 'config');
|
|
1051
|
+
if (!uiConfigBlock.ssl) {
|
|
1052
|
+
uiConfigBlock.ssl = {};
|
|
1053
|
+
}
|
|
1054
|
+
if (mode === 'keycert') {
|
|
1055
|
+
uiConfigBlock.ssl.key = keyPath;
|
|
1056
|
+
uiConfigBlock.ssl.cert = certPath;
|
|
1057
|
+
delete uiConfigBlock.ssl.pfx;
|
|
1058
|
+
delete uiConfigBlock.ssl.passphrase;
|
|
1059
|
+
uiConfigBlock.ssl.selfSigned = false;
|
|
1060
|
+
uiConfigBlock.ssl.selfSignedHostnames = hostnames;
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
delete uiConfigBlock.ssl.key;
|
|
1064
|
+
delete uiConfigBlock.ssl.cert;
|
|
1065
|
+
delete uiConfigBlock.ssl.pfx;
|
|
1066
|
+
delete uiConfigBlock.ssl.passphrase;
|
|
1067
|
+
uiConfigBlock.ssl.selfSigned = true;
|
|
1068
|
+
uiConfigBlock.ssl.selfSignedHostnames = hostnames;
|
|
1069
|
+
}
|
|
1070
|
+
await this.configEditorService.updateConfigFile(configFile);
|
|
1071
|
+
return {
|
|
1072
|
+
ok: true,
|
|
1073
|
+
type: 'generated',
|
|
1074
|
+
mode,
|
|
1075
|
+
keyPath: mode === 'keycert' ? keyPath : undefined,
|
|
1076
|
+
certPath: mode === 'keycert' ? certPath : undefined,
|
|
1077
|
+
details: `Self-signed certificate generated for ${hostnames.join(', ')}`,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
592
1080
|
};
|
|
593
1081
|
ServerService = __decorate([
|
|
594
1082
|
Injectable(),
|