diff --git a/package-lock.json b/package-lock.json index 1eaa54ff..dc3f416d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -157,7 +157,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -443,7 +442,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz", "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -492,7 +490,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -519,7 +516,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -547,7 +543,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", @@ -748,7 +743,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -825,7 +819,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -847,7 +840,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1175,7 +1167,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1562,6 +1553,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1583,6 +1575,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2539,8 +2532,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@lezer/cpp": { "version": "1.1.3", @@ -2580,7 +2572,6 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.3.0" } @@ -2612,7 +2603,6 @@ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", @@ -2635,7 +2625,6 @@ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } @@ -5031,7 +5020,6 @@ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -5189,7 +5177,6 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -5312,7 +5299,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5355,7 +5341,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5366,7 +5351,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5534,7 +5518,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -5942,8 +5925,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/7zip-bin": { "version": "5.2.0", @@ -5978,7 +5960,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6401,7 +6382,6 @@ "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -6536,7 +6516,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7540,7 +7519,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7617,7 +7595,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -8076,7 +8055,6 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -8174,7 +8152,8 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true }, "node_modules/dot-prop": { "version": "5.3.0", @@ -8526,6 +8505,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8546,6 +8526,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8561,6 +8542,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -8571,6 +8553,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -8833,7 +8816,6 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10432,7 +10414,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -11973,6 +11954,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -14000,6 +13982,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -14017,6 +14000,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -14468,7 +14452,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14478,7 +14461,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14505,7 +14487,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14653,7 +14634,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -14862,8 +14842,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -16182,6 +16161,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -16222,6 +16202,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -16236,6 +16217,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -16340,7 +16322,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16546,7 +16527,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16959,7 +16939,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -17051,7 +17030,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 2a65c76a..d4307793 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -18,6 +18,7 @@ import { collectUptimeMetrics } from "./widgets/uptime-collector.js"; import { collectProcessesMetrics } from "./widgets/processes-collector.js"; import { collectSystemMetrics } from "./widgets/system-collector.js"; import { collectLoginStats } from "./widgets/login-stats-collector.js"; +import { collectPortsMetrics } from "./widgets/ports-collector.js"; import { createSocks5Connection } from "../utils/socks5-helper.js"; async function resolveJumpHost( @@ -1661,6 +1662,29 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ }); } + let ports: { + source: "ss" | "netstat" | "none"; + ports: Array<{ + protocol: "tcp" | "udp"; + localAddress: string; + localPort: number; + state?: string; + pid?: number; + process?: string; + }>; + } = { + source: "none", + ports: [], + }; + try { + ports = await collectPortsMetrics(client); + } catch (e) { + statsLogger.debug("Failed to collect ports metrics", { + operation: "ports_metrics_failed", + error: e instanceof Error ? e.message : String(e), + }); + } + const result = { cpu, memory, @@ -1670,6 +1694,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ processes, system, login_stats, + ports, }; metricsCache.set(host.id, result); diff --git a/src/backend/ssh/widgets/ports-collector.ts b/src/backend/ssh/widgets/ports-collector.ts new file mode 100644 index 00000000..57e8161e --- /dev/null +++ b/src/backend/ssh/widgets/ports-collector.ts @@ -0,0 +1,155 @@ +import type { Client } from "ssh2"; +import { execCommand } from "./common-utils.js"; +import type { PortsMetrics, ListeningPort } from "../../../types/stats-widgets.js"; + +function parseSsOutput(output: string): ListeningPort[] { + const ports: ListeningPort[] = []; + const lines = output.split("\n").slice(1); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + const parts = trimmed.split(/\s+/); + if (parts.length < 5) continue; + + const protocol = parts[0]?.toLowerCase(); + if (protocol !== "tcp" && protocol !== "udp") continue; + + const state = parts[1]; + const localAddr = parts[4]; + + if (!localAddr) continue; + + const lastColon = localAddr.lastIndexOf(":"); + if (lastColon === -1) continue; + + const address = localAddr.substring(0, lastColon); + const portStr = localAddr.substring(lastColon + 1); + const port = parseInt(portStr, 10); + + if (isNaN(port)) continue; + + const portEntry: ListeningPort = { + protocol: protocol as "tcp" | "udp", + localAddress: address.replace(/^\[|\]$/g, ""), + localPort: port, + state: protocol === "tcp" ? state : undefined, + }; + + const processInfo = parts[6]; + if (processInfo && processInfo.startsWith("users:")) { + const pidMatch = processInfo.match(/pid=(\d+)/); + const nameMatch = processInfo.match(/\("([^"]+)"/); + if (pidMatch) portEntry.pid = parseInt(pidMatch[1], 10); + if (nameMatch) portEntry.process = nameMatch[1]; + } + + ports.push(portEntry); + } + + return ports; +} + +function parseNetstatOutput(output: string): ListeningPort[] { + const ports: ListeningPort[] = []; + const lines = output.split("\n"); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + const parts = trimmed.split(/\s+/); + if (parts.length < 4) continue; + + const proto = parts[0]?.toLowerCase(); + if (!proto) continue; + + let protocol: "tcp" | "udp"; + if (proto.startsWith("tcp")) { + protocol = "tcp"; + } else if (proto.startsWith("udp")) { + protocol = "udp"; + } else { + continue; + } + + const localAddr = parts[3]; + if (!localAddr) continue; + + const lastColon = localAddr.lastIndexOf(":"); + if (lastColon === -1) continue; + + const address = localAddr.substring(0, lastColon); + const portStr = localAddr.substring(lastColon + 1); + const port = parseInt(portStr, 10); + + if (isNaN(port)) continue; + + const portEntry: ListeningPort = { + protocol, + localAddress: address, + localPort: port, + }; + + if (protocol === "tcp" && parts.length >= 6) { + portEntry.state = parts[5]; + } + + const pidProgram = parts[parts.length - 1]; + if (pidProgram && pidProgram.includes("/")) { + const [pidStr, process] = pidProgram.split("/"); + const pid = parseInt(pidStr, 10); + if (!isNaN(pid)) portEntry.pid = pid; + if (process) portEntry.process = process; + } + + ports.push(portEntry); + } + + return ports; +} + +export async function collectPortsMetrics( + client: Client, +): Promise { + try { + const ssResult = await execCommand( + client, + "ss -tulnp 2>/dev/null", + 15000, + ); + + if (ssResult.stdout && ssResult.stdout.includes("Local")) { + const ports = parseSsOutput(ssResult.stdout); + return { + source: "ss", + ports: ports.sort((a, b) => a.localPort - b.localPort), + }; + } + + const netstatResult = await execCommand( + client, + "netstat -tulnp 2>/dev/null", + 15000, + ); + + if (netstatResult.stdout && netstatResult.stdout.includes("Local")) { + const ports = parseNetstatOutput(netstatResult.stdout); + return { + source: "netstat", + ports: ports.sort((a, b) => a.localPort - b.localPort), + }; + } + + return { + source: "none", + ports: [], + }; + } catch { + return { + source: "none", + ports: [], + }; + } +} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index d82d7164..03a90cba 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1714,7 +1714,16 @@ "executingQuickAction": "Executing {{name}}...", "quickActionSuccess": "{{name}} completed successfully", "quickActionFailed": "{{name}} failed", - "quickActionError": "Failed to execute {{name}}" + "quickActionError": "Failed to execute {{name}}", + "ports": { + "title": "Listening Ports", + "protocol": "Protocol", + "port": "Port", + "address": "Address", + "state": "State", + "process": "Process", + "noData": "No listening ports data" + } }, "auth": { "tagline": "SSH SERVER MANAGER", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 8df76327..fb424aec 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -416,5 +416,16 @@ "hostManager": "主机管理器", "cannotSplitTab": "无法分割此标签页", "tabNavigation": "标签导航" + }, + "serverStats": { + "ports": { + "title": "监听端口", + "protocol": "协议", + "port": "端口", + "address": "地址", + "state": "状态", + "process": "进程", + "noData": "无监听端口数据" + } } } \ No newline at end of file diff --git a/src/types/stats-widgets.ts b/src/types/stats-widgets.ts index f7040ae4..3ef78053 100644 --- a/src/types/stats-widgets.ts +++ b/src/types/stats-widgets.ts @@ -6,7 +6,22 @@ export type WidgetType = | "uptime" | "processes" | "system" - | "login_stats"; + | "login_stats" + | "ports"; + +export interface ListeningPort { + protocol: "tcp" | "udp"; + localAddress: string; + localPort: number; + state?: string; + pid?: number; + process?: string; +} + +export interface PortsMetrics { + source: "ss" | "netstat" | "none"; + ports: ListeningPort[]; +} export interface StatsConfig { enabledWidgets: WidgetType[]; diff --git a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx index 53b357c4..65a83760 100644 --- a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx @@ -462,6 +462,7 @@ export function HostManagerEditor({ "processes", "system", "login_stats", + "ports", ]), ) .default([ @@ -3576,6 +3577,7 @@ export function HostManagerEditor({ "processes", "system", "login_stats", + "ports", ] as const ).map((widget) => (
))} diff --git a/src/ui/desktop/apps/server-stats/ServerStats.tsx b/src/ui/desktop/apps/server-stats/ServerStats.tsx index fe6f37e4..e21419ec 100644 --- a/src/ui/desktop/apps/server-stats/ServerStats.tsx +++ b/src/ui/desktop/apps/server-stats/ServerStats.tsx @@ -26,6 +26,7 @@ import { ProcessesWidget, SystemWidget, LoginStatsWidget, + PortsWidget, } from "./widgets"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; @@ -159,6 +160,11 @@ export function ServerStats({ ); + case "ports": + return ( + + ); + default: return null; } diff --git a/src/ui/desktop/apps/server-stats/widgets/PortsWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/PortsWidget.tsx new file mode 100644 index 00000000..15fd1db9 --- /dev/null +++ b/src/ui/desktop/apps/server-stats/widgets/PortsWidget.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import { Network } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import type { ServerMetrics } from "@/ui/main-axios.ts"; +import type { PortsMetrics, ListeningPort } from "@/types/stats-widgets"; + +interface PortsWidgetProps { + metrics: ServerMetrics | null; + metricsHistory: ServerMetrics[]; +} + +function PortRow({ port }: { port: ListeningPort }) { + const formatAddress = (addr: string) => { + if (addr === "0.0.0.0" || addr === "*" || addr === "::") { + return "*"; + } + return addr; + }; + + return ( +
+
+ {port.protocol.toUpperCase()} +
+
+ {port.localPort} +
+
+ {formatAddress(port.localAddress)} +
+
+ {port.state || "-"} +
+
+ {port.process || (port.pid ? `PID:${port.pid}` : "-")} +
+
+ ); +} + +export function PortsWidget({ metrics }: PortsWidgetProps) { + const { t } = useTranslation(); + + const portsData = ( + metrics as ServerMetrics & { ports?: PortsMetrics } + )?.ports; + + const tcpPorts = portsData?.ports.filter(p => p.protocol === "tcp") || []; + const udpPorts = portsData?.ports.filter(p => p.protocol === "udp") || []; + + return ( +
+
+ +

+ {t("serverStats.ports.title")} +

+ {portsData && portsData.source !== "none" && ( + + {portsData.source} + + )} +
+ +
+ + TCP: {tcpPorts.length} + + + UDP: {udpPorts.length} + +
+ + {portsData && portsData.ports.length > 0 ? ( +
+
+
{t("serverStats.ports.protocol")}
+
{t("serverStats.ports.port")}
+
{t("serverStats.ports.address")}
+
{t("serverStats.ports.state")}
+
{t("serverStats.ports.process")}
+
+
+ {portsData.ports.map((port, idx) => ( + + ))} +
+
+ ) : ( +
+

+ {t("serverStats.ports.noData")} +

+
+ )} +
+ ); +} diff --git a/src/ui/desktop/apps/server-stats/widgets/index.ts b/src/ui/desktop/apps/server-stats/widgets/index.ts index b72f8a11..a85166f7 100644 --- a/src/ui/desktop/apps/server-stats/widgets/index.ts +++ b/src/ui/desktop/apps/server-stats/widgets/index.ts @@ -6,3 +6,4 @@ export { UptimeWidget } from "./UptimeWidget"; export { ProcessesWidget } from "./ProcessesWidget"; export { SystemWidget } from "./SystemWidget"; export { LoginStatsWidget } from "./LoginStatsWidget"; +export { PortsWidget } from "./PortsWidget";