diff --git a/README.md b/README.md index bb74ed5..6ee01b0 100644 --- a/README.md +++ b/README.md @@ -512,6 +512,7 @@ Payload: | --- | --- | --- | --- | | `GET` | `/api/hosts` | User | List host library. | | `POST` | `/api/hosts` | User | Create host. | +| `POST` | `/api/hosts/:id/rdp-session` | User | Create a short-lived browser RDP launch session for a visible Windows host with an assigned visible username/password credential. Returns a same-origin `/rdp/:token` URL. | | `GET` | `/api/hosts/import/vcenter/status` | User | Return legacy effective vCenter status plus visible saved VMware connection records. | | `GET` | `/api/hosts/import/vcenter/connections` | User | List visible saved VMware connections. | | `POST` | `/api/hosts/import/vcenter/connections` | User | Create a VMware connection record using a Credential Vault `credentialId`. Use `targetType: "vcenter"` for vCenter inventory or `targetType: "host"` for standalone ESXi Web Services inventory. | @@ -562,6 +563,15 @@ and `sourceRef` to the vCenter VM id. `osFamily` is normalized to `windows`, infer it from inventory. vCenter imports infer Windows/Linux from VMware Tools guest identity when available; unknown guests are imported as `other`. +Windows hosts with an assigned visible username/password Credential Vault entry +show an RDP icon in the Hosts table. Clicking it calls +`POST /api/hosts/:id/rdp-session`, opens a same-origin `/rdp/:token` +tab, and renders a browser RDP canvas through `mstsc.js`. The browser receives +only a short-lived opaque launch token; POSHManager resolves the host address +and decrypts the assigned credential server-side. The RDP gateway uses the +default RDP port `3389`; PowerShell remoting `port` metadata remains separate +from RDP launch behavior. + Host Groups let operators aggregate manual, imported, and VMware-sourced hosts without duplicating RunPlans. Manual groups store an explicit list of visible host IDs that an operator can add/remove in the Hosts screen. Dynamic groups @@ -1379,6 +1389,7 @@ on the target platform before broad execution. - SQLite access uses prepared statements through `node:sqlite`. - Credential secrets are encrypted with AES-256-GCM and are not returned by API reads. +- Browser RDP sessions use `mstsc.js` with short-lived POSHManager launch tokens; host passwords stay in the API process and are never embedded in the Vue app or launch URL. The upstream `mstsc.js` dependency is legacy and currently brings transitive npm audit findings, so expose `/rdp` only through authenticated POSHManager/reverse-proxy paths and restrict network access to trusted operators. - Request logs redact authorization and cookie headers. - Protected routes use JWT auth. - Admin-only APIs are guarded by `requireAdmin`. diff --git a/client/src/App.vue b/client/src/App.vue index bf0b632..93a0648 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -336,7 +336,30 @@ - + + + + + + ${escapePreviewHtml(isError ? 'RDP launch failed' : `Opening RDP - ${hostName}`)} + + + +
+ ${isError ? 'POSHManager RDP error' : 'POSHManager RDP'} +

${escapePreviewHtml(hostName)}

+

${escapePreviewHtml(message || (isError ? 'The RDP launch failed.' : 'Preparing the browser RDP session...'))}

+
${isError ? 'Launch failed' : 'Creating secure launch token...'}
+
+ +`); + popup.document.close(); +} + +async function launchRdpHost(host) { + if (!canLaunchRdp(host)) return; + const popup = window.open('about:blank', '_blank'); + writeRdpLaunchPopup(popup, host.name, 'loading'); + try { + const result = await api.post(`/api/hosts/${host.id}/rdp-session`, {}); + const launchUrl = new URL(result.url, window.location.origin).toString(); + if (popup) { + popup.location.replace(launchUrl); + } else { + window.open(launchUrl, '_blank', 'noopener,noreferrer'); + } + notify(`Opening RDP session to ${host.name}`); + } catch (err) { + writeRdpLaunchPopup(popup, host.name, 'error', err.message); + notify(err.message, 'error'); + } +} + async function saveHostGroup(payload) { const body = { ...payload }; const groupId = body.id; diff --git a/client/src/style.css b/client/src/style.css index 7e45b7a..e584ac9 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -506,6 +506,29 @@ textarea:focus { padding: 0 12px; } +.icon-button { + width: 38px; + min-width: 38px; + height: 38px; + padding: 0; + gap: 0; +} + +.icon-button i { + margin: 0; + font-size: 17px; + line-height: 1; +} + +.host-row-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + width: 100%; + min-width: 0; +} + .sidebar { position: fixed; top: 12px; diff --git a/client/src/views/JobOutputView.vue b/client/src/views/JobOutputView.vue index 319f732..772fb04 100644 --- a/client/src/views/JobOutputView.vue +++ b/client/src/views/JobOutputView.vue @@ -94,11 +94,23 @@ @@ -405,12 +417,12 @@ function formatBytes(bytes = 0) { .job-output-list :deep(th:nth-child(1)), .job-output-list :deep(td:nth-child(1)) { - width: 30%; + width: 32%; } .job-output-list :deep(th:nth-child(2)), .job-output-list :deep(td:nth-child(2)) { - width: 22%; + width: 23%; } .job-output-list :deep(th:nth-child(3)), @@ -425,12 +437,12 @@ function formatBytes(bytes = 0) { .job-output-list :deep(th:nth-child(5)), .job-output-list :deep(td:nth-child(5)) { - width: 14%; + width: 15%; } .job-output-list :deep(th:last-child), .job-output-list :deep(td:last-child) { - width: 13%; + width: 9%; } .job-output-title { @@ -533,16 +545,24 @@ function formatBytes(bytes = 0) { } .job-output-row-actions { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + display: flex; + justify-content: flex-end; gap: 8px; min-width: 0; } -.job-output-row-actions .ghost-button { +.job-output-row-actions .icon-only { + width: 38px; + height: 38px; justify-content: center; - min-width: 0; - padding-inline: 10px; + min-width: 38px; + padding: 0; +} + +.job-output-row-actions .icon-only i { + margin: 0; + font-size: 17px; + line-height: 1; } @media (max-width: 760px) { diff --git a/package-lock.json b/package-lock.json index 09342bd..e383609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.3", "monaco-editor": "^0.53.0", + "mstsc.js": "^0.2.4", "multer": "^2.2.0", "nanoid": "^5.1.6", "path": "^0.12.7", @@ -954,6 +955,11 @@ "node": ">= 0.6" } }, + "node_modules/after": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.1.tgz", + "integrity": "sha512-SuI3vWhCFeSmkmmJ3efyuOkrhGyp/AuHthh3F5DinGYh2kR9t/0xUlm3/Vn2qMScfgg+cKho5fW7TUEYUhYeiA==" + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1000,12 +1006,29 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha512-6ZjfQaBSy6CuIH0+B0NrxMfDE5VIOCP/5gOqSpEIsaAZx9/giszzrXg6PZ7G51U/n88UmlAgYLNQ9wAnII7PJA==" + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1016,6 +1039,14 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-arraybuffer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz", + "integrity": "sha512-ewBKKVVPIl78B26mYQHYlaxR7NydMiD/GxwLNIwTAfLIE4xhN2Gxcy30//azq5UrejXjzGpWjcBu3NUJxzMMzg==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1036,6 +1067,14 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz", + "integrity": "sha512-DSjtfjhAsHl9J4OJj7e4+toV2zqxJrGwVd3CLlsCp8QmicvOn7irG0Mb8brOc/nur3SdO8lIbNlY1s1ZDJdUKQ==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -1045,6 +1084,26 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/benchmark": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-1.0.0.tgz", + "integrity": "sha512-qSlOi0If8sI+icu3l/W5rd4R0etJz9orLPWpDdt1lPgEFzEHYYnkfMuotj+Lx5SyMkmfawlPoW9RmoEm19ziHA==", + "engines": [ + "node", + "rhino" + ] + }, + "node_modules/better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha512-bYeph2DFlpK1XmGs6fvlLRUN29QISM3GBuUwSFsMY2XRx4AvC0WNCS57j4c/xGrK2RS24C1w3YoBOsw9fT46tQ==", + "dependencies": { + "callsite": "1.0.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1058,6 +1117,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==", + "license": "MIT", + "optional": true + }, + "node_modules/blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha512-YRc9zvVz4wNaxcXmiSgb9LAg7YYwqQ2xd0Sj6osfA7k/PKmIGVlnOYs3wOFdkRC9/JpQu8sGt/zHgJV7xzerfg==" + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -1151,6 +1222,18 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bufferutil": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.2.1.tgz", + "integrity": "sha512-rtE2s2JHFmfaldMwWFSUaPTxfxq6Um3xw9PEUK5bAfW83UTXp3WQpE7slnD2bd9GUgb0BA7JC/7ZxeBrIq+8Dw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "1.2.x", + "nan": "^2.0.5" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1200,6 +1283,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1317,6 +1408,21 @@ "node": ">=12.20" } }, + "node_modules/component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==" + }, + "node_modules/component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha512-YhIbp3PJiznERfjlIkK0ue4obZxt2S60+0W8z24ZymOHT8sHloOqWOqZRU2eN5OlY8U08VFsP02letcu26FilA==" + }, + "node_modules/component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==" + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -1446,6 +1552,16 @@ "node": ">= 0.8" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1524,6 +1640,83 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.5.4.tgz", + "integrity": "sha512-hmvHvhL8RGkwwpIJAFo7XxSKn4r05bL/a0liG3hevorgWfkblbt4ofgZttlWA6zz1mYlLoC6KR6unHdYvjjTOg==", + "dependencies": { + "base64id": "0.1.0", + "debug": "1.0.3", + "engine.io-parser": "1.2.2", + "ws": "0.8.0" + } + }, + "node_modules/engine.io-client": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.5.4.tgz", + "integrity": "sha512-6ad4/4A/hOqJT381LC8Ozk3yLxE3YqITBnHTMh2/zAYqXGgtWk4CdxbpvO5acZlma1SJseFVx4CEi8ED/92qmg==", + "dependencies": { + "component-emitter": "1.1.2", + "component-inherit": "0.0.3", + "debug": "1.0.4", + "engine.io-parser": "1.2.2", + "has-cors": "1.0.3", + "indexof": "0.0.1", + "parsejson": "0.0.1", + "parseqs": "0.0.2", + "parseuri": "0.0.4", + "ws": "0.8.0", + "xmlhttprequest": "https://github.com/rase-/node-XMLHttpRequest/archive/a6b6f2.tar.gz" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", + "integrity": "sha512-plA8d2GHafT7kXzMDs5r7NSfYP7IKHdO8rZPVAqI33Eum7Vq/Ef/ETXm6NncF/RMif4fzI0RetSArZ6PMIxP0g==", + "dependencies": { + "ms": "0.6.2" + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", + "integrity": "sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A==" + }, + "node_modules/engine.io-client/node_modules/parseuri": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.4.tgz", + "integrity": "sha512-9pW0ZCCDtEIzW7beHfLg2N13pgctOVRq1Z+1PKdpsF4wD9GhYUMAAxBhmPRPVVy1fg2z+eH/8uw8dgA0j9DNzw==", + "license": "MIT", + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.2.2.tgz", + "integrity": "sha512-eZ0Vx2rCg8z5J4rVVtAH4TWF/GiYG5UEtnRMmdwUseD2OShabn783jFziwHYr+ofBodUdF+v/XjGzY/kcpLsmg==", + "dependencies": { + "after": "0.8.1", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.2", + "blob": "0.0.4", + "has-binary": "0.1.6", + "utf8": "2.1.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.3.tgz", + "integrity": "sha512-MltK7Ykj/udtD728gD/RrONStwVnDpBNIP1h+CBcnwnJdHqHxfWHI1E8XLootUl7NOPAYTCCXlb8/Qmy7WyB1w==", + "dependencies": { + "ms": "0.6.2" + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", + "integrity": "sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A==" + }, "node_modules/enhanced-resolve": { "version": "5.21.6", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", @@ -1836,6 +2029,12 @@ "node": ">= 6" } }, + "node_modules/global": { + "version": "2.0.1", + "resolved": "https://github.com/component/global/archive/v2.0.1.tar.gz", + "integrity": "sha512-O91OcV/NbdmQJPHaRu2ekSP7bqFRLWgqSwaJvqHPZHUwmHBagQYTOra29+LnzzG3lZkXH1ANzHzfCxtAPM9HMA==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1854,6 +2053,33 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/has-binary": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz", + "integrity": "sha512-aBByfHrIiIt6PQ+jFXsLIFVNpHVyXDcCZ77VZ4kvxv6TvTwipSTDNvKnPN5xOi/cQTcxhLa4lBV2b49pZGQgXw==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/has-binary-data": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/has-binary-data/-/has-binary-data-0.1.3.tgz", + "integrity": "sha512-v9nautvyKZBpnUCALBHSXocsGwIErU+CipSKAAuibJbNl2jueR/qqtERnUGaKesG/ba0H47dqYKyRyGddKeBIg==", + "license": "ISC", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/has-cors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.0.3.tgz", + "integrity": "sha512-Mxk1ba23PNtB3zPigreijApS3uuH9bhgZkqQtLQj7ydWHsGeb9uOtk4gsK6mZj4rYG6VNS/CT9G1XkYfgItpKg==", + "license": "MIT", + "dependencies": { + "global": "https://github.com/component/global/archive/v2.0.1.tar.gz" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1933,6 +2159,11 @@ "dev": true, "license": "ISC" }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2012,6 +2243,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -2027,6 +2264,12 @@ "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", "license": "MIT" }, + "node_modules/json3": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.2.6.tgz", + "integrity": "sha512-KA+GHhYTLTo7Ri4DyjwUgW8kn98AYtVZtBC94qL5yD0ZSYct8/eF8qBmTNyk+gPE578bKeIL4WBq+MUyd1I26g==", + "deprecated": "Please use the native JSON object instead of JSON 3" + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -2356,6 +2599,12 @@ "node": ">= 0.4" } }, + "node_modules/lodash": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.3.0.tgz", + "integrity": "sha512-gpux6tVfBHsUdUIciz5HoV0ChAxUTvi0ChpQMIjAsKtg6FTYFtd1B1G0JlqHvAio3teaMVGPDPk2seVq1INwOQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2454,6 +2703,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -2510,6 +2780,296 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mstsc.js": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/mstsc.js/-/mstsc.js-0.2.4.tgz", + "integrity": "sha512-Ca4sVVA07AiRuScF2pHrMTw9lAij/0p9jaktennQvrRlumFj6eXp5rRBxw89rASlOSVv1Eb4l0BXB9kB8BRDxw==", + "engines": [ + "node = 0.10.x" + ], + "license": "AGPL v3.0", + "dependencies": { + "express": "4.x.x", + "node-rdpjs": ">=0.2.0", + "socket.io": "1.3.x" + } + }, + "node_modules/mstsc.js/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mstsc.js/node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/mstsc.js/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mstsc.js/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/mstsc.js/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/mstsc.js/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mstsc.js/node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mstsc.js/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mstsc.js/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mstsc.js/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mstsc.js/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mstsc.js/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mstsc.js/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mstsc.js/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mstsc.js/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mstsc.js/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/mstsc.js/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mstsc.js/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/mstsc.js/node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/mstsc.js/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/multer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", @@ -2572,6 +3132,13 @@ "node": ">= 0.6" } }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "5.1.15", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.15.tgz", @@ -2599,6 +3166,19 @@ "node": ">= 0.6" } }, + "node_modules/node-rdpjs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/node-rdpjs/-/node-rdpjs-0.3.0.tgz", + "integrity": "sha512-k0p31bhAkv4YB4Vt0WWona36z3FCLVat37dkxQhnAFG5Xmd46h5E+gqDPAacHT5k8RFLB+Aoy0tlTYY9PWkI4w==", + "engines": [ + "node = 0.10.x" + ], + "license": "AGPL v3.0", + "dependencies": { + "lodash": "3.3.0", + "starttls": "^1.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", @@ -2670,6 +3250,11 @@ "node": ">=0.10.0" } }, + "node_modules/object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha512-S0sN3agnVh2SZNEIGc0N1X4Z5K0JeFbGBrnuZpsxuUh5XLF0BnvWkMjRXo/zGKLd/eghvNIKcx1pQkmUjXIyrA==" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2682,6 +3267,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.1.tgz", + "integrity": "sha512-DsJ69TA3wPICBmxYj6rij6uGKvKb9s2mtebzhuN/eI1GabJ3xC7fZ7PWjW0GS06hSclD0GxKGGAHQo5P7R2ZTg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2712,12 +3306,47 @@ "fn.name": "1.x.x" } }, + "node_modules/options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha512-bOj3L1ypm++N+n7CEbbe473A414AB7z+amKYshRb//iuL3MpdDCLhPnw6aVTdKB9g5ZRVHIEp8eUln6L2NUStg==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/parsejson": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.1.tgz", + "integrity": "sha512-W9CRvTfYQY/kbRc5Q6YTWarb/QDxdEGbd6RCP8CLUQDJV89RVHoS2A0dZYNtAcq31fulGNN4ZhAhiQQazwlKJg==", + "license": "MIT", + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseqs": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.2.tgz", + "integrity": "sha512-vyyyfQGUFZnDhgrrdn+hh1JuOfvbXU5oRr6dijfkSIbaFuxGgTSCA/RNVcsADmo0k2NX6wERVTMKkXokjuObJA==", + "license": "MIT", + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseuri": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.2.tgz", + "integrity": "sha512-m0H+R0u5LXOx8sbxufnvgKrRLpkVpvtMf0AyWXYSqLwo2MWrVEgCIbgpaSVa398xl6wTLe0A7CGhiC4hBdEzHQ==", + "license": "MIT", + "dependencies": { + "better-assert": "~1.0.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3237,6 +3866,125 @@ "node": ">=10" } }, + "node_modules/socket.io": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.3.7.tgz", + "integrity": "sha512-gvr6oqkU6qOTlUpGoEBo7b5rU1oNtuir+3nLAY+JgXmYJu/CtbPx3N8/Clcz4rSMtA822JuFMCHRU5WLu/t+bg==", + "dependencies": { + "debug": "2.1.0", + "engine.io": "1.5.4", + "has-binary-data": "0.1.3", + "socket.io-adapter": "0.3.1", + "socket.io-client": "1.3.7", + "socket.io-parser": "2.2.4" + } + }, + "node_modules/socket.io-adapter": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.3.1.tgz", + "integrity": "sha512-DmOAXQyYdztliqS38RnnvG/YPHnPhGRg1C8/IT+Fkrg5JaC7Xtox+ItIbJ12B/5vmBJvaQFF1Px9U0ePjn+R3g==", + "dependencies": { + "debug": "1.0.2", + "object-keys": "1.0.1", + "socket.io-parser": "2.2.2" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.2.tgz", + "integrity": "sha512-T9bufXIzQvCa4VrTIpLvvwdLhH+wuBtvIJJA3xgzVcaVETGmTIWMfEXQEd1K4p8BaRmQJPn6MPut38H7YQ+iIA==", + "dependencies": { + "ms": "0.6.2" + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", + "integrity": "sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A==" + }, + "node_modules/socket.io-adapter/node_modules/socket.io-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.2.tgz", + "integrity": "sha512-fM+hIkoQPh0oNLoL7yx39fvtqMncRTBekjENFPT8dEYHXqvRYC8yWwT+m9PW8wmYieUcqzw7yJnHNXzPAC9i9w==", + "license": "MIT", + "dependencies": { + "benchmark": "1.0.0", + "component-emitter": "1.1.2", + "debug": "0.7.4", + "isarray": "0.0.1", + "json3": "3.2.6" + } + }, + "node_modules/socket.io-adapter/node_modules/socket.io-parser/node_modules/debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==", + "engines": { + "node": "*" + } + }, + "node_modules/socket.io-client": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.3.7.tgz", + "integrity": "sha512-yA4irP1priy7yewFB31N44xjFcGxRI4Yjyk572rdMdKAMtEPS+ntgzV0H0VSiky5DE1vh6djUNZHiOaXRdxi6w==", + "license": "MIT", + "dependencies": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.1.2", + "debug": "0.7.4", + "engine.io-client": "1.5.4", + "has-binary": "0.1.6", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.2", + "socket.io-parser": "2.2.4", + "to-array": "0.1.3" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==", + "engines": { + "node": "*" + } + }, + "node_modules/socket.io-parser": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.4.tgz", + "integrity": "sha512-j0TD2g5DtrmQwcBy+C0RejylNk43rdeCBRRm8EBExrE/f5RlztV+d0k4PRR/tXrcaLcgZeXRpsZvcPVKHarj8g==", + "license": "MIT", + "dependencies": { + "benchmark": "1.0.0", + "component-emitter": "1.1.2", + "debug": "0.7.4", + "isarray": "0.0.1", + "json3": "3.2.6" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==", + "engines": { + "node": "*" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.0.tgz", + "integrity": "sha512-mXKNuRulIxh5zRPbJ0znN6gOJljoA1I/pQaZS9QYCwM4LdeInk5sEioHFeLayLJg8YL+FNrwPZbbltDR/HIdGA==", + "license": "MIT", + "dependencies": { + "ms": "0.6.2" + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", + "integrity": "sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A==" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3255,6 +4003,11 @@ "node": "*" } }, + "node_modules/starttls": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/starttls/-/starttls-1.0.1.tgz", + "integrity": "sha512-mfOPDJsq1GSvCyl6qQK5Slj486+6c2E/DJ3vo0TXs4kB+6xFBBuua2ngiudFyjpRYr9BIibsW9wn/5gl4EPaoA==" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3404,6 +4157,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/to-array": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.3.tgz", + "integrity": "sha512-JQk/QMS4oHyU2VufVeyjN25dcnZnr1PV1pa1oKSj7l5tVO9WrU62og3fYzB3mrgJZZgBxdrrA/v6iZzMDuyFYw==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3498,6 +4256,12 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha512-QMpnpVtYaWEeY+MwKDN/UdKlE/LsFZXM5lO1u7GaZzNgmIbGixHEmVMIKT+vqYOALu3m5GYQy9kz4Xu4IVn7Ow==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3549,6 +4313,31 @@ "node": ">= 0.8" } }, + "node_modules/utf-8-validate": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-1.2.2.tgz", + "integrity": "sha512-CcV1z1L/e1wFAZwl8T6o1MmxIsg/ClZ4nmUolyIhb3ZJKbD/ZQTZXstCf6BiRcvaThSJVI8SqWLodWq/hnWDxQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "~1.2.1", + "nan": "~2.4.0" + } + }, + "node_modules/utf-8-validate/node_modules/nan": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "integrity": "sha512-Ym8Mn5u8D8Fwo7fHWhD7xEyKe/y/J8Epkxp6iJfZhtgnRva+GN+dQddiWGE2cksWCV92K/HzdHlJWo7aZJDlFw==", + "license": "MIT", + "optional": true + }, + "node_modules/utf8": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.0.tgz", + "integrity": "sha512-meBQCGPqrY9eH0gLoJ45HzjLWGKLaROTGkWPbWhyGO/b/Q7lB3T3NClcKe6aQCgDXF3hwx5WzYBXbA+c8j17Yw==", + "license": "MIT" + }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -3570,6 +4359,15 @@ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "license": "ISC" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3776,6 +4574,28 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-0.8.0.tgz", + "integrity": "sha512-JRxYZsisQGuKfRb+QohqX+CZhH3CE++Zz/BZpouhGbf/MLvL9qQ1i7Zp/uLGMxs67z6IX/3/aymoAVuoJzRieQ==", + "license": "MIT", + "dependencies": { + "options": ">=0.0.5", + "ultron": "1.0.x" + }, + "optionalDependencies": { + "bufferutil": "1.2.x", + "utf-8-validate": "1.2.x" + } + }, + "node_modules/xmlhttprequest": { + "version": "1.5.0", + "resolved": "https://github.com/rase-/node-XMLHttpRequest/archive/a6b6f2.tar.gz", + "integrity": "sha512-GO6pmHif8rvZ9YddEoem4hQo0OvcTZJnPGyKxBNsFwgEwNYxbpfewye2ulTDAanWXTcfl2+XKE6/DK7SAoKqMw==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 5342cda..04e5d10 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.3", "monaco-editor": "^0.53.0", + "mstsc.js": "^0.2.4", "multer": "^2.2.0", "nanoid": "^5.1.6", "path": "^0.12.7", diff --git a/server/controllers/hostController.js b/server/controllers/hostController.js index 9ec3343..8b5a048 100644 --- a/server/controllers/hostController.js +++ b/server/controllers/hostController.js @@ -10,6 +10,7 @@ import { updateVCenterConnection } from '../models/vcenterConnectionModel.js'; import { buildHostPayloadFromVm, previewVCenterHosts, testVCenterConnection, vcenterStatus } from '../services/vcenterService.js'; +import { createRdpLaunchSession } from '../services/rdpGatewayService.js'; export function index(req, res) { res.json(listHosts(req.user.id)); @@ -32,6 +33,14 @@ export function destroy(req, res) { res.status(204).end(); } +export function createRdpSession(req, res) { + try { + res.status(201).json(createRdpLaunchSession(req.params.id, req.user.id)); + } catch (error) { + res.status(error.statusCode || 400).json({ error: error.message }); + } +} + export function listGroups(req, res) { res.json(listHostGroups(req.user.id)); } diff --git a/server/data/helpCatalog.js b/server/data/helpCatalog.js index 1908535..7534b29 100644 --- a/server/data/helpCatalog.js +++ b/server/data/helpCatalog.js @@ -64,6 +64,7 @@ const operationsArticles = [ 'Create credentials in Credential Vault first, then attach them to hosts. Secrets are encrypted using AES-256-GCM.', 'Hosts support WinRM, SSH, local, and API transport metadata. PowerShell remoting and firewall prerequisites must be configured outside POSHManager.', 'Each host tracks an OS type of Windows, Linux, or Other. vCenter imports infer this from VMware Tools when possible; manual hosts should set it during add/edit.', + 'Windows hosts with an assigned username/password Credential Vault entry show an RDP icon. The icon opens a short-lived /rdp/:token URL in a new tab and renders a mstsc.js browser RDP canvas while the API keeps the real password server-side.', 'When Linux targets are selected, POSHManager checks scripts for common Windows-only PowerShell patterns such as registry providers, C:\\ paths, WMI/Win32 classes, Windows executables, and risky alias usage.' ]), section('Host Groups and VMware sources', [ @@ -134,6 +135,7 @@ const apiArticles = [ article('api-hosts-credentials-runplans', 'API', 'Hosts, Credentials, RunPlans, Jobs, And Logs API', 'Manage execution targets and inspect results through the API.', [ apiExample('Post', '/api/credentials', 'Create an encrypted username/password credential.', '@{ name = "WinRM Admin"; kind = "username_password"; username = "CONTOSO\\svc-posh"; secret = "change-me"; visibility = "personal" }'), apiExample('Post', '/api/hosts', 'Create a managed host.', '@{ name = "APP01"; address = "app01.contoso.local"; fqdn = "app01.contoso.local"; osFamily = "windows"; transport = "winrm"; port = 5986; credentialId = $null; tags = @("prod","web"); notes = "" }'), + apiExample('Post', '/api/hosts/hst_123/rdp-session', 'Create a short-lived browser RDP launch URL for a visible Windows host with an assigned visible username/password credential.', '$session = Invoke-RestMethod -Method Post -Uri "$base/api/hosts/hst_123/rdp-session" -Headers $headers\nStart-Process "$base$($session.url)"'), apiExample('Post', '/api/scripts/scr_123/compatibility', 'Analyze a script against direct host and Host Group targets before executing against Linux hosts.', '@{ hostIds = @("hst_linux_01"); hostGroupIds = @("hg_mixed_targets") }'), apiExample('Get', '/api/runplans/rp_123/compatibility', 'Analyze a RunPlan script against its current target OS mix before execution.'), apiExample('Post', '/api/runplans', 'Create a RunPlan. hostIds should contain existing host ids.', '@{ name = "Patch Validation"; description = "Run validation script"; scriptId = "scr_123"; visibility = "shared"; parallel = $true; hostIds = @("hst_123") }'), diff --git a/server/index.js b/server/index.js index 4f4cc45..237674d 100644 --- a/server/index.js +++ b/server/index.js @@ -11,6 +11,7 @@ import { startCatalogWorker } from './services/catalogWorker.js'; import { startPromotionWorker } from './services/promotionWorker.js'; import { startHostGroupWorker } from './services/hostGroupWorker.js'; import { startRunPlanScheduleWorker } from './services/runPlanScheduleWorker.js'; +import { mountRdpGateway, startRdpGateway } from './services/rdpGatewayService.js'; // Fail fast in production rather than ship with well-known default secrets. // These defaults are fine for local development but are publicly known, so a @@ -55,14 +56,16 @@ app.use(cors({ app.use(express.json({ limit: config.jsonLimit })); app.use(requestLogger); app.use('/api', apiRoutes); +mountRdpGateway(app); app.use((error, req, res, next) => { logger.error('unhandled error', { error }); res.status(500).json({ error: 'Unexpected server error' }); }); -app.listen(config.port, () => { +const httpServer = app.listen(config.port, () => { logger.info(`POSHManager API listening on ${config.port}`); + startRdpGateway(httpServer); startCatalogWorker(); startPromotionWorker(); startHostGroupWorker(); diff --git a/server/routes/hostRoutes.js b/server/routes/hostRoutes.js index fa8bc16..b00dbe8 100644 --- a/server/routes/hostRoutes.js +++ b/server/routes/hostRoutes.js @@ -2,6 +2,7 @@ import { Router } from 'express'; import { create, createGroup, + createRdpSession, createVCenterConnectionRecord, deleteGroup, deleteVCenterConnectionRecord, @@ -39,5 +40,6 @@ hostRoutes.delete('/groups/:groupId', requireAuth, deleteGroup); hostRoutes.post('/groups/:groupId/sync', requireAuth, syncGroup); hostRoutes.get('/', requireAuth, index); hostRoutes.post('/', requireAuth, create); +hostRoutes.post('/:id/rdp-session', requireAuth, createRdpSession); hostRoutes.put('/:id', requireAuth, update); hostRoutes.delete('/:id', requireAuth, destroy); diff --git a/server/services/rdpGatewayService.js b/server/services/rdpGatewayService.js new file mode 100644 index 0000000..6b180dd --- /dev/null +++ b/server/services/rdpGatewayService.js @@ -0,0 +1,300 @@ +import { createRequire } from 'node:module'; +import { randomBytes } from 'node:crypto'; +import { path } from '../utils/pathUtils.js'; +import express from 'express'; +import { db, visibleClause } from '../db.js'; +import { decryptSecret } from './cryptoStore.js'; +import { logger } from './logger.js'; +import { normalizeOsFamily } from '../utils/osFamily.js'; + +const require = createRequire(import.meta.url); +const rdp = require('node-rdpjs'); +const socketIo = require('socket.io'); +const mstscPackagePath = require.resolve('mstsc.js/package.json'); +const mstscClientDir = path.join(path.dirname(mstscPackagePath), 'client'); +const SESSION_TTL_MS = 5 * 60 * 1000; +const sessions = new Map(); +let socketServer = null; + +function token() { + return randomBytes(24).toString('base64url'); +} + +function escapeHtml(value = '') { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function splitDomainUsername(username = '') { + const value = String(username || ''); + const match = value.match(/^([^\\]+)\\(.+)$/); + if (!match) return { domain: '', username: value }; + return { domain: match[1], username: match[2] }; +} + +function cleanupExpiredSessions() { + const cutoff = Date.now() - SESSION_TTL_MS; + for (const [sessionToken, session] of sessions.entries()) { + if (session.createdAt < cutoff) sessions.delete(sessionToken); + } +} + +function loadRdpTarget(hostId, userId) { + return db.prepare(` + SELECT h.*, c.name AS credential_name, c.kind AS credential_kind, c.username AS credential_username, + c.secret_cipher, c.secret_iv, c.secret_tag + FROM hosts h + JOIN credentials c ON c.id = h.credential_id + WHERE h.id = ? + AND ${visibleClause('h')} + AND ${visibleClause('c')} + `).get(hostId, userId, userId, userId, userId); +} + +export function createRdpLaunchSession(hostId, userId) { + cleanupExpiredSessions(); + const target = loadRdpTarget(hostId, userId); + if (!target) { + const error = new Error('Windows host with an assigned visible credential is required for RDP launch.'); + error.statusCode = 404; + throw error; + } + if (normalizeOsFamily(target.os_family, 'other') !== 'windows') { + const error = new Error('RDP launch is only available for Windows hosts.'); + error.statusCode = 400; + throw error; + } + if (target.credential_kind !== 'username_password') { + const error = new Error('RDP launch requires a username/password credential assigned to the host.'); + error.statusCode = 400; + throw error; + } + + const password = decryptSecret(target); + if (!password) { + const error = new Error('Assigned host credential does not have a decryptable password.'); + error.statusCode = 400; + throw error; + } + + const sessionToken = token(); + const parsedUsername = splitDomainUsername(target.credential_username); + const session = { + token: sessionToken, + userId, + hostId: target.id, + hostName: target.name, + address: target.fqdn || target.address, + port: 3389, + domain: parsedUsername.domain, + username: parsedUsername.username, + password, + credentialName: target.credential_name, + createdAt: Date.now() + }; + sessions.set(sessionToken, session); + logger.info('rdp session created', { + hostId: target.id, + hostName: target.name, + userId, + credentialId: target.credential_id + }); + return { + token: sessionToken, + url: `/rdp/${sessionToken}`, + expiresInSeconds: Math.floor(SESSION_TTL_MS / 1000), + host: { + id: target.id, + name: target.name, + address: session.address + } + }; +} + +function renderSessionPage(session) { + const safeToken = escapeHtml(session.token); + const safeHost = escapeHtml(session.hostName); + const safeAddress = escapeHtml(session.address); + return ` + + + + + RDP - ${safeHost} + + + + + + + + + + +
+
+ POSHManager RDP +

${safeHost}

+

Opening a browser RDP session to ${safeAddress} using the assigned host credential. Close this tab to end the session.

+
Preparing secure session...
+
+
+ + + +`; +} + +export function mountRdpGateway(app) { + app.use('/rdp/assets', express.static(mstscClientDir, { + fallthrough: false, + immutable: true, + maxAge: '1h' + })); + + app.get('/rdp/sessions/:token', (req, res) => { + res.redirect(302, `/rdp/${encodeURIComponent(req.params.token)}`); + }); + + app.get('/rdp/:token', (req, res) => { + cleanupExpiredSessions(); + const session = sessions.get(req.params.token); + if (!session) return res.status(404).send('RDP session expired or not found.'); + res.type('html').send(renderSessionPage(session)); + }); +} + +export function startRdpGateway(httpServer) { + if (socketServer) return socketServer; + socketServer = socketIo(httpServer, { path: '/rdp/socket.io' }); + socketServer.on('connection', (client) => { + let rdpClient = null; + + client.on('infos', (infos = {}) => { + cleanupExpiredSessions(); + const sessionToken = infos.token || infos.sessionToken || infos.password; + const session = sessions.get(sessionToken); + if (!session) { + client.emit('rdp-error', { code: 'POSHM_RDP_SESSION', message: 'RDP session expired or invalid.' }); + client.disconnect(); + return; + } + + if (rdpClient) rdpClient.close(); + const screen = { + width: Math.max(640, Math.min(Number(infos.screen?.width || 1280), 3840)), + height: Math.max(480, Math.min(Number(infos.screen?.height || 800), 2160)) + }; + + logger.info('rdp connection starting', { + hostId: session.hostId, + hostName: session.hostName, + address: session.address, + userId: session.userId + }); + + rdpClient = rdp.createClient({ + domain: session.domain, + userName: session.username, + password: session.password, + enablePerf: true, + autoLogin: true, + screen, + locale: infos.locale, + logLevel: 'ERROR' + }).on('connect', () => { + client.emit('rdp-connect'); + logger.info('rdp connection established', { hostId: session.hostId, hostName: session.hostName, userId: session.userId }); + }).on('bitmap', (bitmap) => { + client.emit('rdp-bitmap', bitmap); + }).on('close', () => { + client.emit('rdp-close'); + sessions.delete(sessionToken); + logger.info('rdp connection closed', { hostId: session.hostId, hostName: session.hostName, userId: session.userId }); + }).on('error', (err) => { + client.emit('rdp-error', { code: err.code || 'RDP_ERROR', message: err.message || String(err) }); + logger.error('rdp connection failed', { hostId: session.hostId, hostName: session.hostName, userId: session.userId, error: err.message || String(err) }); + }).connect(session.address, session.port); + }); + + client.on('mouse', (x, y, button, isPressed) => { + if (rdpClient) rdpClient.sendPointerEvent(x, y, button, isPressed); + }); + client.on('wheel', (x, y, step, isNegative, isHorizontal) => { + if (rdpClient) rdpClient.sendWheelEvent(x, y, step, isNegative, isHorizontal); + }); + client.on('scancode', (code, isPressed) => { + if (rdpClient) rdpClient.sendKeyEventScancode(code, isPressed); + }); + client.on('unicode', (code, isPressed) => { + if (rdpClient) rdpClient.sendKeyEventUnicode(code, isPressed); + }); + client.on('disconnect', () => { + if (rdpClient) rdpClient.close(); + }); + }); + logger.info('POSHManager RDP gateway mounted at /rdp'); + return socketServer; +} diff --git a/server/test/hosts.test.mjs b/server/test/hosts.test.mjs index 4aa0ba1..8ccb001 100644 --- a/server/test/hosts.test.mjs +++ b/server/test/hosts.test.mjs @@ -3,8 +3,10 @@ import test from 'node:test'; import assert from 'node:assert/strict'; const { createHost, listHosts, updateHost, deleteHost, filterVisibleHostIds } = await load('../models/hostModel.js'); +const { createCredential } = await load('../models/credentialModel.js'); const { createHostGroup, listHostGroups, syncHostGroup } = await load('../models/hostGroupModel.js'); const { createRunPlan, loadRunPlan } = await load('../models/runPlanModel.js'); +const { createRdpLaunchSession } = await load('../services/rdpGatewayService.js'); const { db } = await load('../db.js'); const owner = adminUserId(); @@ -52,6 +54,46 @@ test('filterVisibleHostIds drops hosts the user cannot see', () => { assert.deepEqual(visibleToOther, [shared.id]); }); +test('RDP launch creates a short-lived browser session for Windows hosts with visible credentials', () => { + const credential = createCredential({ + name: 'RDP Admin', + kind: 'username_password', + username: 'CONTOSO\\rdpadmin', + secret: 'rdp-secret', + visibility: 'shared' + }, owner); + const host = createHost({ + name: 'RDP-WIN-01', + address: 'rdp-win-01.contoso.local', + fqdn: 'rdp-win-01.contoso.local', + osFamily: 'windows', + transport: 'winrm', + credentialId: credential.id, + tags: [], + visibility: 'shared' + }, owner); + const session = createRdpLaunchSession(host.id, other); + assert.match(session.url, /^\/rdp\/[^/]+$/); + assert.equal(session.host.name, 'RDP-WIN-01'); + assert.equal(session.host.address, 'rdp-win-01.contoso.local'); + assert.ok(!session.url.includes('rdp-secret')); +}); + +test('RDP launch rejects non-Windows hosts or hosts without credentials', () => { + const credential = createCredential({ + name: 'RDP Linux Credential', + kind: 'username_password', + username: 'linux-user', + secret: 'linux-secret', + visibility: 'shared' + }, owner); + const linux = createHost({ name: 'RDP-LINUX-01', address: 'rdp-linux.local', osFamily: 'linux', transport: 'ssh', credentialId: credential.id, tags: [], visibility: 'shared' }, owner); + assert.throws(() => createRdpLaunchSession(linux.id, owner), /Windows hosts/i); + + const noCredential = createHost({ name: 'RDP-NOCRED-01', address: 'rdp-nocred.local', osFamily: 'windows', transport: 'winrm', tags: [], visibility: 'shared' }, owner); + assert.throws(() => createRdpLaunchSession(noCredential.id, owner), /assigned visible credential/i); +}); + test('manual host groups keep explicit members', () => { const host = createHost({ name: 'ManualMember', address: 'manual-member.local', osFamily: 'windows', transport: 'winrm', tags: [], visibility: 'shared' }, owner); const group = createHostGroup({ name: 'Manual Group', mode: 'manual', hostIds: [host.id], visibility: 'shared' }, owner); diff --git a/vite.config.js b/vite.config.js index 1bd7062..02653a2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -20,6 +20,12 @@ export default defineConfig({ target: apiTarget, changeOrigin: true, secure: false + }, + '/rdp': { + target: apiTarget, + changeOrigin: true, + secure: false, + ws: true } } }, @@ -31,6 +37,12 @@ export default defineConfig({ target: apiTarget, changeOrigin: true, secure: false + }, + '/rdp': { + target: apiTarget, + changeOrigin: true, + secure: false, + ws: true } } },