pyre-cli 2.0.0 → 3.1.0
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/README.md +98 -13
- package/dist/index.js +1278 -824
- package/package.json +1 -1
- package/src/formatters/index.ts +1 -1
- package/src/formatters/render.ts +2 -1
- package/src/index.ts +63 -3
- package/src/live/input.ts +99 -1
- package/src/live/render.ts +70 -32
- package/src/live/state.ts +22 -15
- package/src/live/types.ts +1 -1
- package/src/monitors/collectors.ts +1 -1
- package/src/p2p/client.ts +99 -25
- package/src/p2p/protocol.ts +45 -4
- package/src/p2p/server.ts +237 -29
- package/src/p2p/types.ts +30 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import fs4 from "fs";
|
|
5
5
|
import readline2 from "readline";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import
|
|
7
|
+
import chalk9 from "chalk";
|
|
8
8
|
|
|
9
9
|
// src/monitors/collectors.ts
|
|
10
10
|
import chalk from "chalk";
|
|
@@ -511,7 +511,7 @@ async function collectPackets() {
|
|
|
511
511
|
let topProcesses = [];
|
|
512
512
|
let allProcesses = [];
|
|
513
513
|
try {
|
|
514
|
-
const lsof = (await run("lsof -i -n -P
|
|
514
|
+
const lsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
|
|
515
515
|
const lsofLines = lsof.split("\n");
|
|
516
516
|
topProcesses = lsofLines.map((line) => {
|
|
517
517
|
const parts = line.trim().split(/\s+/);
|
|
@@ -859,7 +859,8 @@ var TAB_DEFS = [
|
|
|
859
859
|
{ id: "packets", label: "Packets", key: "8" },
|
|
860
860
|
{ id: "tasks", label: "Tasks", key: "9" },
|
|
861
861
|
{ id: "disk", label: "Disk", key: "0" },
|
|
862
|
-
{ id: "process", label: "Process", key: "
|
|
862
|
+
{ id: "process", label: "Process", key: "P" },
|
|
863
|
+
{ id: "p2p", label: "P2P", key: "R" }
|
|
863
864
|
];
|
|
864
865
|
var TAB_BAR_INDENT = 2;
|
|
865
866
|
var TAB_SEPARATOR_WIDTH = 3;
|
|
@@ -1485,8 +1486,15 @@ var state = {
|
|
|
1485
1486
|
{ id: "packets", label: "Packets", key: "8" },
|
|
1486
1487
|
{ id: "tasks", label: "Tasks", key: "9" },
|
|
1487
1488
|
{ id: "disk", label: "Disk", key: "0" },
|
|
1488
|
-
{ id: "process", label: "Process", key: "P" }
|
|
1489
|
-
|
|
1489
|
+
{ id: "process", label: "Process", key: "P" },
|
|
1490
|
+
{ id: "p2p", label: "P2P", key: "R" }
|
|
1491
|
+
],
|
|
1492
|
+
p2pEvents: [],
|
|
1493
|
+
p2pBind: "",
|
|
1494
|
+
p2pServer: null,
|
|
1495
|
+
p2pServerRunning: false,
|
|
1496
|
+
p2pPort: 9876,
|
|
1497
|
+
p2pPassword: "pyre"
|
|
1490
1498
|
};
|
|
1491
1499
|
function setStatus(msg, ms = 3e3) {
|
|
1492
1500
|
state.statusMessage = msg;
|
|
@@ -1563,6 +1571,29 @@ function processRowBudget() {
|
|
|
1563
1571
|
const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
|
|
1564
1572
|
return Math.max(3, Math.min(40, state.termHeight - reserved));
|
|
1565
1573
|
}
|
|
1574
|
+
function p2pPanelLines() {
|
|
1575
|
+
const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
|
|
1576
|
+
const body = [];
|
|
1577
|
+
const bindAddr = state.p2pBind || `0.0.0.0:${state.p2pPort}`;
|
|
1578
|
+
body.push(`Status: ${state.p2pServerRunning ? chalk5.green("Running") : chalk5.red("Stopped")}`);
|
|
1579
|
+
body.push(`Bind: ${bindAddr}`);
|
|
1580
|
+
body.push(`Password: ${state.p2pPassword}`);
|
|
1581
|
+
body.push("");
|
|
1582
|
+
const typeColor = (t) => {
|
|
1583
|
+
if (t === "auth") return chalk5.green;
|
|
1584
|
+
if (t === "disconnect") return chalk5.yellow;
|
|
1585
|
+
if (t === "error" || t === "blocked" || t === "rate_limited") return chalk5.red;
|
|
1586
|
+
return chalk5.cyan;
|
|
1587
|
+
};
|
|
1588
|
+
body.push(chalk5.dim("Events (most recent):"));
|
|
1589
|
+
const recent = events.slice(-20).reverse();
|
|
1590
|
+
for (const evt of recent) {
|
|
1591
|
+
const time = new Date(evt.ts).toLocaleTimeString();
|
|
1592
|
+
body.push(`${chalk5.dim(time)} ${typeColor(evt.type)(evt.type.padEnd(14))} ${evt.detail}`);
|
|
1593
|
+
}
|
|
1594
|
+
const title = `P2P \xB7 ${state.p2pServerRunning ? "ON" : "OFF"} \xB7 ${events.length} events`;
|
|
1595
|
+
return { title, body };
|
|
1596
|
+
}
|
|
1566
1597
|
function renderCustomizerOverlay() {
|
|
1567
1598
|
const themesList = Object.keys(THEMES);
|
|
1568
1599
|
const lines = [
|
|
@@ -1590,19 +1621,25 @@ function renderCustomizerOverlay() {
|
|
|
1590
1621
|
function render() {
|
|
1591
1622
|
if (!state.lastData) return;
|
|
1592
1623
|
const lines = [];
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1624
|
+
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
1625
|
+
if (state.activePanel === "p2p") {
|
|
1626
|
+
const panelLines = p2pPanelLines();
|
|
1627
|
+
lines.push(...panel(panelLines.title, panelLines.body, state.termWidth, theme.process, theme.border));
|
|
1628
|
+
} else {
|
|
1629
|
+
lines.push(
|
|
1630
|
+
formatTable(state.lastData, {
|
|
1631
|
+
width: state.termWidth,
|
|
1632
|
+
sortBy: state.sortMode,
|
|
1633
|
+
filter: state.processFilter || void 0,
|
|
1634
|
+
processLimit: processRowBudget(),
|
|
1635
|
+
theme: state.currentTheme,
|
|
1636
|
+
visible: state.visiblePanels,
|
|
1637
|
+
treeView: state.treeView,
|
|
1638
|
+
activePanel: state.activePanel
|
|
1639
|
+
})
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
if (state.showGraphs && state.activePanel !== "p2p") {
|
|
1606
1643
|
lines.push("");
|
|
1607
1644
|
lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
|
|
1608
1645
|
}
|
|
@@ -1620,6 +1657,8 @@ function render() {
|
|
|
1620
1657
|
lines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
|
|
1621
1658
|
lines.push(chalk5.dim(` ${sigList}`));
|
|
1622
1659
|
lines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
|
|
1660
|
+
} else if (state.inputMode === "p2p") {
|
|
1661
|
+
lines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
|
|
1623
1662
|
} else if (state.statusMessage) {
|
|
1624
1663
|
lines.push(` ${state.statusMessage}`);
|
|
1625
1664
|
}
|
|
@@ -1630,6 +1669,7 @@ function footerLine() {
|
|
|
1630
1669
|
const keys = [
|
|
1631
1670
|
["q", "quit"],
|
|
1632
1671
|
["c", "customize UI"],
|
|
1672
|
+
["r", state.p2pServerRunning ? "stop p2p" : "start p2p"],
|
|
1633
1673
|
["p", state.paused ? "resume" : "pause"],
|
|
1634
1674
|
["g", state.showGraphs ? "hide graphs" : "show graphs"],
|
|
1635
1675
|
["b", `graph:${state.graphMode}`],
|
|
@@ -1651,6 +1691,7 @@ function footerLine() {
|
|
|
1651
1691
|
if (state.paused) badges.push(chalk5.yellow.bold("\u23F8 PAUSED"));
|
|
1652
1692
|
if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
|
|
1653
1693
|
if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
|
|
1694
|
+
if (state.p2pServerRunning) badges.push(chalk5.green.bold(`P2P ${state.p2pBind || "0.0.0.0"}:${state.p2pPort}`));
|
|
1654
1695
|
const badgeStr = badges.join(" ");
|
|
1655
1696
|
return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
|
|
1656
1697
|
}
|
|
@@ -1667,781 +1708,511 @@ function checkAlerts(data) {
|
|
|
1667
1708
|
}
|
|
1668
1709
|
}
|
|
1669
1710
|
|
|
1670
|
-
// src/
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1711
|
+
// src/p2p/server.ts
|
|
1712
|
+
import net from "net";
|
|
1713
|
+
import tls from "tls";
|
|
1714
|
+
import fs2 from "fs";
|
|
1715
|
+
import crypto2 from "crypto";
|
|
1716
|
+
import chalk6 from "chalk";
|
|
1717
|
+
|
|
1718
|
+
// src/p2p/protocol.ts
|
|
1719
|
+
import crypto from "crypto";
|
|
1720
|
+
var HEADER_SIZE = 4;
|
|
1721
|
+
var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
1722
|
+
var HMAC_ALGO = "sha256";
|
|
1723
|
+
function buildMessage(kind, payload, hmac) {
|
|
1724
|
+
const envelope = { kind, payload };
|
|
1725
|
+
if (hmac) envelope.hmac = hmac;
|
|
1726
|
+
const body = Buffer.from(JSON.stringify(envelope), "utf-8");
|
|
1727
|
+
const header2 = Buffer.alloc(HEADER_SIZE);
|
|
1728
|
+
header2.writeUInt32BE(body.length, 0);
|
|
1729
|
+
return Buffer.concat([header2, body]);
|
|
1730
|
+
}
|
|
1731
|
+
function generateNonce() {
|
|
1732
|
+
return crypto.randomBytes(32).toString("hex");
|
|
1733
|
+
}
|
|
1734
|
+
function hashPassword(password, nonce) {
|
|
1735
|
+
return crypto.createHash("sha256").update(`${password}:${nonce}`).digest("hex");
|
|
1736
|
+
}
|
|
1737
|
+
function computeHMAC(body, key) {
|
|
1738
|
+
if (!key) return void 0;
|
|
1739
|
+
return crypto.createHmac(HMAC_ALGO, key).update(body).digest("hex");
|
|
1740
|
+
}
|
|
1741
|
+
function verifyHMAC(body, expectedHmac, key) {
|
|
1742
|
+
if (!key) return expectedHmac === void 0;
|
|
1743
|
+
if (!expectedHmac) return false;
|
|
1744
|
+
try {
|
|
1745
|
+
const parsed = JSON.parse(body);
|
|
1746
|
+
const payloadWithoutHmac = { ...parsed, hmac: void 0 };
|
|
1747
|
+
const canonical = JSON.stringify(payloadWithoutHmac);
|
|
1748
|
+
const computed = crypto.createHmac(HMAC_ALGO, key).update(canonical).digest("hex");
|
|
1749
|
+
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expectedHmac));
|
|
1750
|
+
} catch {
|
|
1751
|
+
return false;
|
|
1677
1752
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1753
|
+
}
|
|
1754
|
+
function encodeDisconnect(reason) {
|
|
1755
|
+
return buildMessage("disconnect", { reason });
|
|
1756
|
+
}
|
|
1757
|
+
function decodeMessage(buf) {
|
|
1758
|
+
const str = buf.toString("utf-8");
|
|
1759
|
+
const msg = JSON.parse(str);
|
|
1760
|
+
return msg;
|
|
1761
|
+
}
|
|
1762
|
+
function signMessage(kind, payload, key) {
|
|
1763
|
+
const envelope = { kind, payload };
|
|
1764
|
+
const bodyJson = JSON.stringify(envelope);
|
|
1765
|
+
const hmac = computeHMAC(bodyJson, key);
|
|
1766
|
+
return buildMessage(kind, payload, hmac);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/p2p/server.ts
|
|
1770
|
+
var HEADER_SIZE2 = 4;
|
|
1771
|
+
var DEFAULT_RATE_LIMIT_MAX = 5;
|
|
1772
|
+
var DEFAULT_RATE_LIMIT_WINDOW = 6e4;
|
|
1773
|
+
var HMAC_KEY_LENGTH = 32;
|
|
1774
|
+
function readMessageFromPeer(peer) {
|
|
1775
|
+
while (peer.readBuffer.length > 0) {
|
|
1776
|
+
if (peer.readExpectingHeader) {
|
|
1777
|
+
if (peer.readBuffer.length >= HEADER_SIZE2) {
|
|
1778
|
+
peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
|
|
1779
|
+
peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE2);
|
|
1780
|
+
peer.readExpectingHeader = false;
|
|
1693
1781
|
} else {
|
|
1694
|
-
|
|
1695
|
-
state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
|
|
1782
|
+
break;
|
|
1696
1783
|
}
|
|
1697
1784
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
|
|
1708
|
-
} else if (key.name === "return" || str === " ") {
|
|
1709
|
-
killProcess(state.inputBuffer.trim(), state.inputBuffer.trim());
|
|
1710
|
-
state.inputMode = null;
|
|
1711
|
-
state.inputBuffer = "";
|
|
1712
|
-
render();
|
|
1713
|
-
return;
|
|
1785
|
+
if (!peer.readExpectingHeader) {
|
|
1786
|
+
if (peer.readBuffer.length >= peer.readMessageLength) {
|
|
1787
|
+
const body = peer.readBuffer.subarray(0, peer.readMessageLength);
|
|
1788
|
+
peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
|
|
1789
|
+
peer.readExpectingHeader = true;
|
|
1790
|
+
return body;
|
|
1791
|
+
} else {
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1714
1794
|
}
|
|
1715
|
-
render();
|
|
1716
|
-
return;
|
|
1717
1795
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
function isIPAllowed(ip, allowedIPs, deniedIPs) {
|
|
1799
|
+
for (const denied of deniedIPs) {
|
|
1800
|
+
if (ip === denied || ip.startsWith(denied)) {
|
|
1801
|
+
return false;
|
|
1724
1802
|
}
|
|
1725
|
-
state.inputMode = null;
|
|
1726
|
-
state.inputBuffer = "";
|
|
1727
|
-
render();
|
|
1728
|
-
return;
|
|
1729
1803
|
}
|
|
1730
|
-
if (
|
|
1731
|
-
|
|
1732
|
-
render();
|
|
1733
|
-
return;
|
|
1804
|
+
if (allowedIPs.length === 0) {
|
|
1805
|
+
return true;
|
|
1734
1806
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1807
|
+
for (const allowed of allowedIPs) {
|
|
1808
|
+
if (ip === allowed || ip.startsWith(allowed)) {
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1738
1811
|
}
|
|
1812
|
+
return false;
|
|
1739
1813
|
}
|
|
1740
|
-
function
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1814
|
+
function deriveHMACKey(password) {
|
|
1815
|
+
return crypto2.createHash("sha256").update(password).digest("hex").slice(0, HMAC_KEY_LENGTH);
|
|
1816
|
+
}
|
|
1817
|
+
var P2PServer = class {
|
|
1818
|
+
server = null;
|
|
1819
|
+
peers = /* @__PURE__ */ new Map();
|
|
1820
|
+
options;
|
|
1821
|
+
running = false;
|
|
1822
|
+
rateLimitMap = /* @__PURE__ */ new Map();
|
|
1823
|
+
auditStream = null;
|
|
1824
|
+
hmacKey;
|
|
1825
|
+
peerEvents = [];
|
|
1826
|
+
boundAddress = "";
|
|
1827
|
+
constructor(options) {
|
|
1828
|
+
this.options = options;
|
|
1829
|
+
this.hmacKey = deriveHMACKey(options.password);
|
|
1745
1830
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
setStatus(`Sent ${signal} to PID ${pid}`);
|
|
1749
|
-
} catch (err) {
|
|
1750
|
-
setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
|
|
1831
|
+
get peerEventHistory() {
|
|
1832
|
+
return this.peerEvents;
|
|
1751
1833
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1834
|
+
log(msg) {
|
|
1835
|
+
if (this.options.onLog) {
|
|
1836
|
+
this.options.onLog(msg.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
1837
|
+
} else {
|
|
1838
|
+
console.log(msg);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
emitPeerEvent(type, detail) {
|
|
1842
|
+
const evt = { type, detail, ts: Date.now() };
|
|
1843
|
+
this.peerEvents.push(evt);
|
|
1844
|
+
if (this.peerEvents.length > 200) this.peerEvents.shift();
|
|
1845
|
+
if (this.options.onPeerEvent) {
|
|
1846
|
+
this.options.onPeerEvent(evt);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
get info() {
|
|
1850
|
+
return {
|
|
1851
|
+
host: this.options.host,
|
|
1852
|
+
port: this.options.port,
|
|
1853
|
+
peersConnected: this.peers.size
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
async start() {
|
|
1857
|
+
if (this.options.auditLog) {
|
|
1858
|
+
try {
|
|
1859
|
+
fs2.mkdirSync(this.options.auditLog, { recursive: true });
|
|
1860
|
+
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
1861
|
+
this.auditStream = fs2.createWriteStream(logFile, { flags: "a" });
|
|
1862
|
+
} catch {
|
|
1863
|
+
this.log(chalk6.yellow("Warning: could not open audit log"));
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return new Promise((resolve, reject) => {
|
|
1867
|
+
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
1868
|
+
if (useTLS) {
|
|
1869
|
+
const tlsOptions = {
|
|
1870
|
+
key: fs2.readFileSync(this.options.tlsKey),
|
|
1871
|
+
cert: fs2.readFileSync(this.options.tlsCert)
|
|
1872
|
+
};
|
|
1873
|
+
if (this.options.tlsCA) {
|
|
1874
|
+
tlsOptions.ca = fs2.readFileSync(this.options.tlsCA);
|
|
1875
|
+
}
|
|
1876
|
+
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
1877
|
+
} else {
|
|
1878
|
+
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
1879
|
+
}
|
|
1880
|
+
this.server.on("error", (err) => reject(err));
|
|
1881
|
+
this.server.listen(this.options.port, this.options.host, () => {
|
|
1882
|
+
this.running = true;
|
|
1883
|
+
const addr = this.server?.address();
|
|
1884
|
+
const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
|
|
1885
|
+
this.boundAddress = typeof addr === "object" && addr ? addr.address : String(addr);
|
|
1886
|
+
const protocol = useTLS ? "TLS" : "TCP";
|
|
1887
|
+
this.log(chalk6.green(`P2P server listening on ${bound} (${protocol})`));
|
|
1888
|
+
this.log(chalk6.dim(` Password: ${this.options.password}`));
|
|
1889
|
+
this.log(chalk6.dim(` Interval: ${this.options.intervalMs}ms`));
|
|
1890
|
+
const allowedIPs = this.options.allowedIPs ?? [];
|
|
1891
|
+
const deniedIPs = this.options.deniedIPs ?? [];
|
|
1892
|
+
if (allowedIPs.length > 0) this.log(chalk6.dim(` Allowed IPs: ${allowedIPs.join(", ")}`));
|
|
1893
|
+
if (deniedIPs.length > 0) this.log(chalk6.dim(` Denied IPs: ${deniedIPs.join(", ")}`));
|
|
1894
|
+
this.emitPeerEvent("connect", `Server listening on ${bound}`);
|
|
1895
|
+
resolve();
|
|
1896
|
+
});
|
|
1780
1897
|
});
|
|
1781
|
-
writeLogRow(data);
|
|
1782
|
-
checkAlerts(data);
|
|
1783
|
-
render();
|
|
1784
|
-
} catch {
|
|
1785
1898
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
if (state.mouseEnabled) {
|
|
1803
|
-
process.stdout.write("\x1B[?1000h");
|
|
1899
|
+
stop() {
|
|
1900
|
+
this.running = false;
|
|
1901
|
+
for (const peer of this.peers.values()) {
|
|
1902
|
+
this.disconnectPeer(peer, "Server shutting down");
|
|
1903
|
+
}
|
|
1904
|
+
this.peers.clear();
|
|
1905
|
+
this.rateLimitMap.clear();
|
|
1906
|
+
this.peerEvents = [];
|
|
1907
|
+
if (this.server) {
|
|
1908
|
+
this.server.close();
|
|
1909
|
+
this.server = null;
|
|
1910
|
+
}
|
|
1911
|
+
if (this.auditStream) {
|
|
1912
|
+
this.auditStream.end();
|
|
1913
|
+
this.auditStream = null;
|
|
1914
|
+
}
|
|
1804
1915
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
if (!
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
1816
|
-
return;
|
|
1916
|
+
audit(ip, event, detail) {
|
|
1917
|
+
if (!this.auditStream) return;
|
|
1918
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1919
|
+
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
1920
|
+
`);
|
|
1921
|
+
}
|
|
1922
|
+
getPeerIP(socket) {
|
|
1923
|
+
const addr = socket.remoteAddress;
|
|
1924
|
+
if (!addr) return "unknown";
|
|
1925
|
+
if (addr.startsWith("::ffff:")) {
|
|
1926
|
+
return addr.slice(7);
|
|
1817
1927
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1928
|
+
return addr;
|
|
1929
|
+
}
|
|
1930
|
+
checkRateLimit(ip) {
|
|
1931
|
+
const maxAttempts = this.options.rateLimitMaxAttempts ?? DEFAULT_RATE_LIMIT_MAX;
|
|
1932
|
+
const windowMs = this.options.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW;
|
|
1933
|
+
const now = Date.now();
|
|
1934
|
+
const entry = this.rateLimitMap.get(ip);
|
|
1935
|
+
if (!entry) {
|
|
1936
|
+
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
1937
|
+
return true;
|
|
1821
1938
|
}
|
|
1822
|
-
if (
|
|
1823
|
-
|
|
1824
|
-
return;
|
|
1939
|
+
if (now - entry.firstAttempt > windowMs) {
|
|
1940
|
+
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
1941
|
+
return true;
|
|
1825
1942
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1943
|
+
entry.attempts++;
|
|
1944
|
+
if (entry.attempts > maxAttempts) {
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
return true;
|
|
1948
|
+
}
|
|
1949
|
+
cleanupRateLimit(ip) {
|
|
1950
|
+
this.rateLimitMap.delete(ip);
|
|
1951
|
+
}
|
|
1952
|
+
onConnection(socket) {
|
|
1953
|
+
const ip = this.getPeerIP(socket);
|
|
1954
|
+
const allowedIPs = this.options.allowedIPs ?? [];
|
|
1955
|
+
const deniedIPs = this.options.deniedIPs ?? [];
|
|
1956
|
+
if (!isIPAllowed(ip, allowedIPs, deniedIPs)) {
|
|
1957
|
+
this.audit(ip, "BLOCKED", "IP not allowed");
|
|
1958
|
+
this.log(chalk6.yellow(`Blocked IP: ${ip}`));
|
|
1959
|
+
this.emitPeerEvent("blocked", ip);
|
|
1960
|
+
socket.destroy();
|
|
1828
1961
|
return;
|
|
1829
1962
|
}
|
|
1830
|
-
if (
|
|
1831
|
-
|
|
1963
|
+
if (!this.checkRateLimit(ip)) {
|
|
1964
|
+
this.audit(ip, "RATE_LIMITED", "Too many auth attempts");
|
|
1965
|
+
this.log(chalk6.yellow(`Rate limit exceeded for IP: ${ip}`));
|
|
1966
|
+
this.emitPeerEvent("rate_limited", ip);
|
|
1967
|
+
socket.destroy();
|
|
1832
1968
|
return;
|
|
1833
1969
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1970
|
+
const peer = {
|
|
1971
|
+
socket,
|
|
1972
|
+
authenticated: false,
|
|
1973
|
+
intervalHandle: null,
|
|
1974
|
+
pingHandle: null,
|
|
1975
|
+
readBuffer: Buffer.alloc(0),
|
|
1976
|
+
readExpectingHeader: true,
|
|
1977
|
+
readMessageLength: 0,
|
|
1978
|
+
ip,
|
|
1979
|
+
pendingNonce: null
|
|
1980
|
+
};
|
|
1981
|
+
this.peers.set(socket, peer);
|
|
1982
|
+
const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
|
|
1983
|
+
this.log(chalk6.cyan(`Peer connected: ${remoteAddr}`));
|
|
1984
|
+
this.audit(ip, "CONNECT", remoteAddr);
|
|
1985
|
+
this.emitPeerEvent("connect", remoteAddr);
|
|
1986
|
+
socket.on("data", (data) => this.onData(peer, data));
|
|
1987
|
+
socket.on("close", () => this.onPeerClose(peer));
|
|
1988
|
+
socket.on("error", (err) => this.onPeerError(peer, err));
|
|
1989
|
+
socket.setTimeout(3e4);
|
|
1990
|
+
socket.on("timeout", () => {
|
|
1991
|
+
if (!peer.authenticated) {
|
|
1992
|
+
this.disconnectPeer(peer, "Authentication timeout");
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
const nonce = generateNonce();
|
|
1996
|
+
this.sendSigned(peer, signMessage("challenge", { nonce }, this.hmacKey));
|
|
1997
|
+
peer.pendingNonce = nonce;
|
|
1998
|
+
}
|
|
1999
|
+
onData(peer, data) {
|
|
2000
|
+
peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
|
|
2001
|
+
if (!peer.authenticated) {
|
|
2002
|
+
const body = readMessageFromPeer(peer);
|
|
2003
|
+
if (body) {
|
|
2004
|
+
this.handleAuthMessage(peer, body);
|
|
2005
|
+
}
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
handleAuthMessage(peer, body) {
|
|
2010
|
+
try {
|
|
2011
|
+
const raw = body.toString("utf-8");
|
|
2012
|
+
const msg = decodeMessage(body);
|
|
2013
|
+
if (msg.kind !== "auth") {
|
|
2014
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Expected auth message first" }, this.hmacKey));
|
|
2015
|
+
this.disconnectPeer(peer, "Expected auth message");
|
|
2016
|
+
this.emitPeerEvent("error", `Expected auth message from ${peer.ip}`);
|
|
1844
2017
|
return;
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2018
|
+
}
|
|
2019
|
+
const authPayload = msg.payload;
|
|
2020
|
+
if (!authPayload.passwordHash || !authPayload.nonce) {
|
|
2021
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid auth payload" }, this.hmacKey));
|
|
2022
|
+
this.disconnectPeer(peer, "Invalid auth payload");
|
|
2023
|
+
this.emitPeerEvent("error", `Invalid auth payload from ${peer.ip}`);
|
|
1849
2024
|
return;
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
case "6":
|
|
1857
|
-
case "7":
|
|
1858
|
-
case "8":
|
|
1859
|
-
case "9": {
|
|
1860
|
-
const tabMap = {
|
|
1861
|
-
"0": "disk",
|
|
1862
|
-
"1": "cpu",
|
|
1863
|
-
"2": "mem",
|
|
1864
|
-
"3": "gpu",
|
|
1865
|
-
"4": "power",
|
|
1866
|
-
"5": "battery",
|
|
1867
|
-
"6": "thermal",
|
|
1868
|
-
"7": "network",
|
|
1869
|
-
"8": "packets",
|
|
1870
|
-
"9": "tasks"
|
|
1871
|
-
};
|
|
1872
|
-
const panelId = tabMap[key.sequence];
|
|
1873
|
-
if (panelId) {
|
|
1874
|
-
state.activePanel = state.activePanel === panelId ? "grid" : panelId;
|
|
1875
|
-
setStatus(state.activePanel === "grid" ? "Grid view" : `${panelId} panel`);
|
|
1876
|
-
render();
|
|
1877
|
-
}
|
|
2025
|
+
}
|
|
2026
|
+
if (peer.pendingNonce && authPayload.nonce !== peer.pendingNonce) {
|
|
2027
|
+
this.audit(peer.ip, "AUTH_FAIL", "Nonce mismatch");
|
|
2028
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid nonce" }, this.hmacKey));
|
|
2029
|
+
this.disconnectPeer(peer, "Invalid nonce");
|
|
2030
|
+
this.emitPeerEvent("error", `Invalid nonce from ${peer.ip}`);
|
|
1878
2031
|
return;
|
|
1879
2032
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2033
|
+
const expectedHash = hashPassword(this.options.password, authPayload.nonce);
|
|
2034
|
+
if (authPayload.passwordHash !== expectedHash) {
|
|
2035
|
+
this.audit(peer.ip, "AUTH_FAIL", "Invalid password hash");
|
|
2036
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid password" }, this.hmacKey));
|
|
2037
|
+
this.disconnectPeer(peer, "Authentication failed");
|
|
2038
|
+
this.emitPeerEvent("error", `Invalid password from ${peer.ip}`);
|
|
1884
2039
|
return;
|
|
1885
2040
|
}
|
|
2041
|
+
peer.authenticated = true;
|
|
2042
|
+
this.cleanupRateLimit(peer.ip);
|
|
2043
|
+
peer.pendingNonce = null;
|
|
2044
|
+
peer.socket.write(signMessage("auth-ok", { ok: true }, this.hmacKey));
|
|
2045
|
+
this.log(chalk6.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
|
|
2046
|
+
this.audit(peer.ip, "AUTH_OK", "Challenge-response successful");
|
|
2047
|
+
this.emitPeerEvent("auth", peer.socket.remoteAddress ?? peer.ip);
|
|
2048
|
+
this.startDataStream(peer);
|
|
2049
|
+
this.startPing(peer);
|
|
2050
|
+
} catch {
|
|
2051
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid message format" }, this.hmacKey));
|
|
2052
|
+
this.disconnectPeer(peer, "Invalid message format");
|
|
1886
2053
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
case "left": {
|
|
1892
|
-
const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
|
|
1893
|
-
if (currentIdx > 0) {
|
|
1894
|
-
state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
|
|
1895
|
-
} else if (currentIdx === -1) {
|
|
1896
|
-
state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
|
|
1897
|
-
}
|
|
1898
|
-
setStatus(`Panel: ${state.activePanel}`);
|
|
1899
|
-
render();
|
|
1900
|
-
break;
|
|
1901
|
-
}
|
|
1902
|
-
case "right": {
|
|
1903
|
-
const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
|
|
1904
|
-
if (currentIdx < state.PANEL_TABS.length - 1) {
|
|
1905
|
-
state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
|
|
1906
|
-
} else if (currentIdx === -1) {
|
|
1907
|
-
state.activePanel = state.PANEL_TABS[0].id;
|
|
1908
|
-
}
|
|
1909
|
-
setStatus(`Panel: ${state.activePanel}`);
|
|
1910
|
-
render();
|
|
1911
|
-
break;
|
|
1912
|
-
}
|
|
1913
|
-
case "c":
|
|
1914
|
-
state.inputMode = "customizer";
|
|
1915
|
-
state.customizerIndex = 0;
|
|
1916
|
-
render();
|
|
1917
|
-
break;
|
|
1918
|
-
case "p":
|
|
1919
|
-
state.paused = !state.paused;
|
|
1920
|
-
setStatus(state.paused ? "Paused" : "Resumed");
|
|
1921
|
-
render();
|
|
1922
|
-
break;
|
|
1923
|
-
case "g":
|
|
1924
|
-
state.showGraphs = !state.showGraphs;
|
|
1925
|
-
render();
|
|
1926
|
-
break;
|
|
1927
|
-
case "b":
|
|
1928
|
-
state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
|
|
1929
|
-
setStatus(`Graph mode: ${state.graphMode}`);
|
|
1930
|
-
render();
|
|
1931
|
-
break;
|
|
1932
|
-
case "d":
|
|
1933
|
-
state.detailed = !state.detailed;
|
|
1934
|
-
setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
|
|
1935
|
-
break;
|
|
1936
|
-
case "s":
|
|
1937
|
-
const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
|
|
1938
|
-
const curIdx = sortCycle.indexOf(state.sortMode);
|
|
1939
|
-
state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
|
|
1940
|
-
setStatus(`Sorting by ${state.sortMode}`);
|
|
1941
|
-
render();
|
|
1942
|
-
break;
|
|
1943
|
-
case "k":
|
|
1944
|
-
state.inputMode = "kill";
|
|
1945
|
-
state.inputBuffer = "";
|
|
1946
|
-
render();
|
|
1947
|
-
break;
|
|
1948
|
-
case "S":
|
|
1949
|
-
state.inputMode = "signal";
|
|
1950
|
-
state.inputBuffer = "SIGTERM";
|
|
1951
|
-
render();
|
|
1952
|
-
break;
|
|
1953
|
-
case "t":
|
|
1954
|
-
state.treeView = !state.treeView;
|
|
1955
|
-
setStatus(state.treeView ? "Tree view enabled" : "Flat view enabled");
|
|
1956
|
-
render();
|
|
1957
|
-
break;
|
|
1958
|
-
case "e":
|
|
1959
|
-
exportSnapshot();
|
|
1960
|
-
render();
|
|
1961
|
-
break;
|
|
1962
|
-
case "l":
|
|
1963
|
-
toggleLogging();
|
|
1964
|
-
render();
|
|
1965
|
-
break;
|
|
1966
|
-
case "f":
|
|
1967
|
-
state.exportFormat = state.exportFormat === "json" ? "csv" : state.exportFormat === "csv" ? "tsv" : "json";
|
|
1968
|
-
setStatus(`Export format: ${state.exportFormat}`);
|
|
1969
|
-
render();
|
|
1970
|
-
break;
|
|
1971
|
-
case "tab":
|
|
1972
|
-
cycleTab(1);
|
|
1973
|
-
break;
|
|
1974
|
-
case "left":
|
|
1975
|
-
cycleTab(-1);
|
|
1976
|
-
break;
|
|
1977
|
-
case "right":
|
|
1978
|
-
cycleTab(1);
|
|
1979
|
-
break;
|
|
1980
|
-
default:
|
|
1981
|
-
if (str && str.length === 1) {
|
|
1982
|
-
const tabId = tabKeyToId(str);
|
|
1983
|
-
if (tabId) {
|
|
1984
|
-
state.activePanel = tabId;
|
|
1985
|
-
setStatus(`Panel: ${tabId.toUpperCase()}`);
|
|
1986
|
-
render();
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
2054
|
+
}
|
|
2055
|
+
sendSigned(peer, message) {
|
|
2056
|
+
if (!peer.socket.destroyed) {
|
|
2057
|
+
peer.socket.write(message);
|
|
1989
2058
|
}
|
|
1990
|
-
};
|
|
1991
|
-
process.stdin.on("keypress", state.keypressHandler);
|
|
1992
|
-
process.once("SIGINT", () => stopLive());
|
|
1993
|
-
}
|
|
1994
|
-
function tabKeyToId(str) {
|
|
1995
|
-
const map = { "1": "cpu", "2": "mem", "3": "gpu", "4": "power", "5": "battery", "6": "thermal", "7": "network", "8": "packets", "9": "tasks", "0": "disk", "p": "process" };
|
|
1996
|
-
return map[str] ?? null;
|
|
1997
|
-
}
|
|
1998
|
-
function cycleTab(direction) {
|
|
1999
|
-
const tabs = state.PANEL_TABS.map((t) => t.id);
|
|
2000
|
-
const current = state.activePanel === "grid" ? -1 : tabs.indexOf(state.activePanel);
|
|
2001
|
-
const next = (current + direction + tabs.length) % tabs.length;
|
|
2002
|
-
state.activePanel = tabs[next];
|
|
2003
|
-
setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
|
|
2004
|
-
render();
|
|
2005
|
-
}
|
|
2006
|
-
function handleNormalMouse(seq) {
|
|
2007
|
-
if (seq.length < 6) return;
|
|
2008
|
-
const cb = seq.charCodeAt(3) - 32;
|
|
2009
|
-
const cx = seq.charCodeAt(4) - 32;
|
|
2010
|
-
const cy = seq.charCodeAt(5) - 32;
|
|
2011
|
-
if (cb === 0 || cb === 1) {
|
|
2012
|
-
handleMouseClick(cy, cx);
|
|
2013
2059
|
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2060
|
+
startDataStream(peer) {
|
|
2061
|
+
const send = async () => {
|
|
2062
|
+
if (!peer.authenticated || peer.socket.destroyed) return;
|
|
2063
|
+
try {
|
|
2064
|
+
const data = await collectAll({ detailed: this.options.detailed });
|
|
2065
|
+
this.sendSigned(peer, signMessage("data", { stats: data }, this.hmacKey));
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
send();
|
|
2070
|
+
peer.intervalHandle = setInterval(send, this.options.intervalMs);
|
|
2023
2071
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
if (hit) {
|
|
2030
|
-
state.activePanel = state.activePanel === hit.id ? "grid" : hit.id;
|
|
2031
|
-
setStatus(state.activePanel === "grid" ? "Grid view" : `Panel: ${hit.id.toUpperCase()}`);
|
|
2032
|
-
render();
|
|
2072
|
+
startPing(peer) {
|
|
2073
|
+
peer.pingHandle = setInterval(() => {
|
|
2074
|
+
if (peer.socket.destroyed) return;
|
|
2075
|
+
this.sendSigned(peer, signMessage("ping", { ts: Date.now() }, this.hmacKey));
|
|
2076
|
+
}, 5e3);
|
|
2033
2077
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2078
|
+
onPeerClose(peer) {
|
|
2079
|
+
this.cleanupPeer(peer);
|
|
2080
|
+
this.log(chalk6.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
|
|
2081
|
+
this.emitPeerEvent("disconnect", peer.socket.remoteAddress ?? peer.ip);
|
|
2082
|
+
this.audit(peer.ip, "DISCONNECT", "Peer closed connection");
|
|
2039
2083
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2084
|
+
onPeerError(peer, err) {
|
|
2085
|
+
const code = err.code;
|
|
2086
|
+
if (code === "ECONNRESET") {
|
|
2087
|
+
this.log(chalk6.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
|
|
2088
|
+
this.audit(peer.ip, "ERROR", "Connection reset");
|
|
2089
|
+
this.emitPeerEvent("error", `connection reset: ${peer.socket.remoteAddress}`);
|
|
2090
|
+
} else {
|
|
2091
|
+
this.log(chalk6.red(`Peer error: ${err.message}`));
|
|
2092
|
+
this.audit(peer.ip, "ERROR", err.message);
|
|
2093
|
+
this.emitPeerEvent("error", err.message);
|
|
2094
|
+
}
|
|
2095
|
+
this.cleanupPeer(peer);
|
|
2043
2096
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
state.keypressHandler = null;
|
|
2051
|
-
}
|
|
2052
|
-
process.stdout.removeListener("resize", onResize);
|
|
2053
|
-
state.running = false;
|
|
2054
|
-
state.logging = false;
|
|
2055
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
2056
|
-
process.stdin.pause();
|
|
2057
|
-
if (state.mouseEnabled) {
|
|
2058
|
-
process.stdout.write("\x1B[?1000l");
|
|
2097
|
+
disconnectPeer(peer, reason) {
|
|
2098
|
+
try {
|
|
2099
|
+
this.sendSigned(peer, signMessage("disconnect", { reason }, this.hmacKey));
|
|
2100
|
+
} catch {
|
|
2101
|
+
}
|
|
2102
|
+
peer.socket.destroy();
|
|
2059
2103
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
// src/splash.ts
|
|
2067
|
-
import chalk6 from "chalk";
|
|
2068
|
-
var MAXV = 36;
|
|
2069
|
-
var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
|
|
2070
|
-
var palette = [
|
|
2071
|
-
"#0a0505",
|
|
2072
|
-
"#1a0806",
|
|
2073
|
-
"#2a0b06",
|
|
2074
|
-
"#3f1004",
|
|
2075
|
-
"#591404",
|
|
2076
|
-
"#791804",
|
|
2077
|
-
"#9e1c04",
|
|
2078
|
-
"#c92004",
|
|
2079
|
-
"#d63004",
|
|
2080
|
-
"#e34004",
|
|
2081
|
-
"#f05004",
|
|
2082
|
-
"#fd6004",
|
|
2083
|
-
"#ff7004",
|
|
2084
|
-
"#ff8004",
|
|
2085
|
-
"#ff9004",
|
|
2086
|
-
"#ffa004",
|
|
2087
|
-
"#ffb004",
|
|
2088
|
-
"#ffc004",
|
|
2089
|
-
"#ffd004",
|
|
2090
|
-
"#ffe004",
|
|
2091
|
-
"#fff004",
|
|
2092
|
-
"#fff820",
|
|
2093
|
-
"#ffff40",
|
|
2094
|
-
"#ffff60",
|
|
2095
|
-
"#ffff80",
|
|
2096
|
-
"#ffffa0",
|
|
2097
|
-
"#ffffc0",
|
|
2098
|
-
"#ffffe0",
|
|
2099
|
-
"#ffffff",
|
|
2100
|
-
"#ffffff",
|
|
2101
|
-
"#ffffff",
|
|
2102
|
-
"#ffffff",
|
|
2103
|
-
"#ffffff",
|
|
2104
|
-
"#ffffff",
|
|
2105
|
-
"#ffffff",
|
|
2106
|
-
"#ffffff"
|
|
2107
|
-
];
|
|
2108
|
-
function getRgb(hex) {
|
|
2109
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
2110
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
2111
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
2112
|
-
return { r, g, b };
|
|
2113
|
-
}
|
|
2114
|
-
var rgbPalette = palette.map(getRgb);
|
|
2115
|
-
function getChar(val) {
|
|
2116
|
-
const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
|
|
2117
|
-
return CHAR_RAMP[idx];
|
|
2118
|
-
}
|
|
2119
|
-
async function showSplash() {
|
|
2120
|
-
const cols = 60;
|
|
2121
|
-
const rows = 15;
|
|
2122
|
-
let fire = new Uint8Array(cols * rows);
|
|
2123
|
-
process.stdout.write("\x1B[2J");
|
|
2124
|
-
const termRows = process.stdout.rows || 24;
|
|
2125
|
-
const termCols = process.stdout.columns || 80;
|
|
2126
|
-
const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
|
|
2127
|
-
const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
|
|
2128
|
-
return new Promise((resolve) => {
|
|
2129
|
-
const interval = setInterval(() => {
|
|
2130
|
-
for (let x = 0; x < cols; x++) {
|
|
2131
|
-
fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
|
|
2132
|
-
}
|
|
2133
|
-
for (let y = rows - 1; y >= 1; y--) {
|
|
2134
|
-
for (let x = 0; x < cols; x++) {
|
|
2135
|
-
const src = y * cols + x;
|
|
2136
|
-
const val = fire[src];
|
|
2137
|
-
if (val === 0) {
|
|
2138
|
-
fire[(y - 1) * cols + x] = 0;
|
|
2139
|
-
continue;
|
|
2140
|
-
}
|
|
2141
|
-
const decay = Math.floor(Math.random() * 3);
|
|
2142
|
-
const drift = Math.floor(Math.random() * 3) - 1;
|
|
2143
|
-
let nx = x + drift;
|
|
2144
|
-
if (nx < 0) nx = 0;
|
|
2145
|
-
else if (nx >= cols) nx = cols - 1;
|
|
2146
|
-
fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
let output = "";
|
|
2150
|
-
for (let i = 0; i < startRow; i++) output += "\n";
|
|
2151
|
-
for (let y = 0; y < rows; y++) {
|
|
2152
|
-
output += " ".repeat(startCol);
|
|
2153
|
-
for (let x = 0; x < cols; x++) {
|
|
2154
|
-
const v = fire[y * cols + x];
|
|
2155
|
-
const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
|
|
2156
|
-
output += chalk6.rgb(color.r, color.g, color.b)(getChar(v));
|
|
2157
|
-
}
|
|
2158
|
-
output += "\n";
|
|
2159
|
-
}
|
|
2160
|
-
output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk6.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
|
|
2161
|
-
output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk6.dim("[Press Enter to continue]");
|
|
2162
|
-
process.stdout.write(`\x1B[H${output}`);
|
|
2163
|
-
}, 65);
|
|
2164
|
-
const onEnter = () => {
|
|
2165
|
-
process.stdin.removeListener("data", onEnter);
|
|
2166
|
-
clearInterval(interval);
|
|
2167
|
-
process.stdout.write("\x1B[2J\x1B[H");
|
|
2168
|
-
resolve();
|
|
2169
|
-
};
|
|
2170
|
-
process.stdin.setRawMode(true);
|
|
2171
|
-
process.stdin.resume();
|
|
2172
|
-
process.stdin.on("data", onEnter);
|
|
2173
|
-
setTimeout(onEnter, 5e3);
|
|
2174
|
-
});
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
// src/p2p/server.ts
|
|
2178
|
-
import net from "net";
|
|
2179
|
-
import chalk7 from "chalk";
|
|
2180
|
-
|
|
2181
|
-
// src/p2p/protocol.ts
|
|
2182
|
-
var HEADER_SIZE = 4;
|
|
2183
|
-
var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
2184
|
-
function buildMessage(kind, payload) {
|
|
2185
|
-
const envelope = { kind, payload };
|
|
2186
|
-
const body = Buffer.from(JSON.stringify(envelope), "utf-8");
|
|
2187
|
-
const header2 = Buffer.alloc(HEADER_SIZE);
|
|
2188
|
-
header2.writeUInt32BE(body.length, 0);
|
|
2189
|
-
return Buffer.concat([header2, body]);
|
|
2190
|
-
}
|
|
2191
|
-
function encodeAuth(password) {
|
|
2192
|
-
return buildMessage("auth", { password });
|
|
2193
|
-
}
|
|
2194
|
-
function encodeAuthOk() {
|
|
2195
|
-
return buildMessage("auth-ok", { ok: true });
|
|
2196
|
-
}
|
|
2197
|
-
function encodeAuthFail(reason) {
|
|
2198
|
-
return buildMessage("auth-fail", { ok: false, reason });
|
|
2199
|
-
}
|
|
2200
|
-
function encodeData(stats) {
|
|
2201
|
-
return buildMessage("data", { stats });
|
|
2202
|
-
}
|
|
2203
|
-
function encodePing() {
|
|
2204
|
-
return buildMessage("ping", { ts: Date.now() });
|
|
2205
|
-
}
|
|
2206
|
-
function encodePong(ts) {
|
|
2207
|
-
return buildMessage("pong", { ts });
|
|
2208
|
-
}
|
|
2209
|
-
function encodeDisconnect(reason) {
|
|
2210
|
-
return buildMessage("disconnect", { reason });
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
// src/p2p/server.ts
|
|
2214
|
-
var HEADER_SIZE2 = 4;
|
|
2215
|
-
function readMessageFromPeer(peer) {
|
|
2216
|
-
while (peer.readBuffer.length > 0) {
|
|
2217
|
-
if (peer.readExpectingHeader) {
|
|
2218
|
-
if (peer.readBuffer.length >= HEADER_SIZE2) {
|
|
2219
|
-
peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
|
|
2220
|
-
peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE2);
|
|
2221
|
-
peer.readExpectingHeader = false;
|
|
2222
|
-
} else {
|
|
2223
|
-
break;
|
|
2224
|
-
}
|
|
2104
|
+
cleanupPeer(peer) {
|
|
2105
|
+
if (peer.intervalHandle) {
|
|
2106
|
+
clearInterval(peer.intervalHandle);
|
|
2107
|
+
peer.intervalHandle = null;
|
|
2225
2108
|
}
|
|
2226
|
-
if (
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
|
|
2230
|
-
peer.readExpectingHeader = true;
|
|
2231
|
-
return body;
|
|
2232
|
-
} else {
|
|
2233
|
-
break;
|
|
2234
|
-
}
|
|
2109
|
+
if (peer.pingHandle) {
|
|
2110
|
+
clearInterval(peer.pingHandle);
|
|
2111
|
+
peer.pingHandle = null;
|
|
2235
2112
|
}
|
|
2113
|
+
this.peers.delete(peer.socket);
|
|
2236
2114
|
}
|
|
2237
|
-
|
|
2115
|
+
};
|
|
2116
|
+
async function startP2PServer(options) {
|
|
2117
|
+
const server = new P2PServer(options);
|
|
2118
|
+
await server.start();
|
|
2119
|
+
return server;
|
|
2238
2120
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2121
|
+
|
|
2122
|
+
// src/p2p/client.ts
|
|
2123
|
+
import net2 from "net";
|
|
2124
|
+
import tls2 from "tls";
|
|
2125
|
+
import fs3 from "fs";
|
|
2126
|
+
import crypto3 from "crypto";
|
|
2127
|
+
import chalk7 from "chalk";
|
|
2128
|
+
var P2PClient = class {
|
|
2129
|
+
socket = null;
|
|
2242
2130
|
options;
|
|
2243
2131
|
running = false;
|
|
2132
|
+
readState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
|
|
2133
|
+
onDataCallback = null;
|
|
2134
|
+
onStatusCallback = null;
|
|
2135
|
+
reconnectTimer = null;
|
|
2136
|
+
pingHandle = null;
|
|
2137
|
+
pendingNonce = null;
|
|
2138
|
+
auditStream = null;
|
|
2139
|
+
hmacKey;
|
|
2244
2140
|
constructor(options) {
|
|
2245
2141
|
this.options = options;
|
|
2142
|
+
this.hmacKey = deriveHMACKey2(options.password);
|
|
2246
2143
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
host: this.options.host,
|
|
2250
|
-
port: this.options.port,
|
|
2251
|
-
peersConnected: this.peers.size
|
|
2252
|
-
};
|
|
2144
|
+
setOnData(cb) {
|
|
2145
|
+
this.onDataCallback = cb;
|
|
2253
2146
|
}
|
|
2254
|
-
|
|
2147
|
+
setOnStatus(cb) {
|
|
2148
|
+
this.onStatusCallback = cb;
|
|
2149
|
+
}
|
|
2150
|
+
async connect() {
|
|
2151
|
+
if (this.options.auditLog) {
|
|
2152
|
+
try {
|
|
2153
|
+
fs3.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2154
|
+
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
2155
|
+
this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
|
|
2156
|
+
} catch {
|
|
2157
|
+
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2255
2160
|
return new Promise((resolve, reject) => {
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2161
|
+
const useTLS = !!this.options.tls;
|
|
2162
|
+
if (useTLS) {
|
|
2163
|
+
const tlsOptions = {
|
|
2164
|
+
host: this.options.host,
|
|
2165
|
+
port: this.options.port,
|
|
2166
|
+
rejectUnauthorized: !this.options.tlsInsecure
|
|
2167
|
+
};
|
|
2168
|
+
if (this.options.tlsCA) {
|
|
2169
|
+
tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
|
|
2170
|
+
}
|
|
2171
|
+
this.socket = tls2.connect(tlsOptions, () => {
|
|
2172
|
+
this.status("Connecting...");
|
|
2173
|
+
});
|
|
2174
|
+
} else {
|
|
2175
|
+
this.socket = new net2.Socket();
|
|
2176
|
+
this.socket.on("data", (data) => this.onData(data));
|
|
2177
|
+
this.socket.on("close", () => this.onClose());
|
|
2178
|
+
this.socket.on("error", (err) => this.onError(err));
|
|
2179
|
+
this.socket.on("timeout", () => this.onTimeout());
|
|
2180
|
+
this.socket.setTimeout(1e4);
|
|
2181
|
+
this.socket.connect(this.options.port, this.options.host, () => {
|
|
2182
|
+
this.status("Connecting...");
|
|
2183
|
+
});
|
|
2184
|
+
this.socket.once("error", (err) => {
|
|
2185
|
+
reject(err);
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
this.socket.on("data", (data) => this.onData(data));
|
|
2189
|
+
this.socket.on("close", () => this.onClose());
|
|
2190
|
+
this.socket.on("error", (err) => this.onError(err));
|
|
2191
|
+
this.socket.on("timeout", () => this.onTimeout());
|
|
2267
2192
|
});
|
|
2268
2193
|
}
|
|
2269
|
-
|
|
2194
|
+
disconnect() {
|
|
2270
2195
|
this.running = false;
|
|
2271
|
-
|
|
2272
|
-
this.
|
|
2196
|
+
if (this.reconnectTimer) {
|
|
2197
|
+
clearTimeout(this.reconnectTimer);
|
|
2198
|
+
this.reconnectTimer = null;
|
|
2273
2199
|
}
|
|
2274
|
-
this.
|
|
2275
|
-
|
|
2276
|
-
this.
|
|
2277
|
-
this.server = null;
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
onConnection(socket) {
|
|
2281
|
-
const peer = {
|
|
2282
|
-
socket,
|
|
2283
|
-
authenticated: false,
|
|
2284
|
-
intervalHandle: null,
|
|
2285
|
-
pingHandle: null,
|
|
2286
|
-
readBuffer: Buffer.alloc(0),
|
|
2287
|
-
readExpectingHeader: true,
|
|
2288
|
-
readMessageLength: 0
|
|
2289
|
-
};
|
|
2290
|
-
this.peers.set(socket, peer);
|
|
2291
|
-
const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
|
|
2292
|
-
console.log(chalk7.cyan(`Peer connected: ${remoteAddr}`));
|
|
2293
|
-
socket.on("data", (data) => this.onData(peer, data));
|
|
2294
|
-
socket.on("close", () => this.onPeerClose(peer));
|
|
2295
|
-
socket.on("error", (err) => this.onPeerError(peer, err));
|
|
2296
|
-
socket.setTimeout(3e4);
|
|
2297
|
-
socket.on("timeout", () => {
|
|
2298
|
-
if (!peer.authenticated) {
|
|
2299
|
-
this.disconnectPeer(peer, "Authentication timeout");
|
|
2300
|
-
}
|
|
2301
|
-
});
|
|
2302
|
-
}
|
|
2303
|
-
onData(peer, data) {
|
|
2304
|
-
peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
|
|
2305
|
-
if (!peer.authenticated) {
|
|
2306
|
-
const body = readMessageFromPeer(peer);
|
|
2307
|
-
if (body) {
|
|
2308
|
-
this.handleAuthMessage(peer, body);
|
|
2309
|
-
}
|
|
2310
|
-
return;
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
handleAuthMessage(peer, body) {
|
|
2314
|
-
try {
|
|
2315
|
-
const msg = JSON.parse(body.toString("utf-8"));
|
|
2316
|
-
if (msg.kind === "auth" && typeof msg.payload?.password === "string") {
|
|
2317
|
-
if (msg.payload.password === this.options.password) {
|
|
2318
|
-
peer.authenticated = true;
|
|
2319
|
-
peer.socket.write(encodeAuthOk());
|
|
2320
|
-
console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
|
|
2321
|
-
this.startDataStream(peer);
|
|
2322
|
-
this.startPing(peer);
|
|
2323
|
-
} else {
|
|
2324
|
-
peer.socket.write(encodeAuthFail("Invalid password"));
|
|
2325
|
-
this.disconnectPeer(peer, "Authentication failed");
|
|
2326
|
-
}
|
|
2327
|
-
} else {
|
|
2328
|
-
peer.socket.write(encodeAuthFail("Expected auth message first"));
|
|
2329
|
-
this.disconnectPeer(peer, "Expected auth message");
|
|
2330
|
-
}
|
|
2331
|
-
} catch {
|
|
2332
|
-
peer.socket.write(encodeAuthFail("Invalid message format"));
|
|
2333
|
-
this.disconnectPeer(peer, "Invalid message format");
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
startDataStream(peer) {
|
|
2337
|
-
const send = async () => {
|
|
2338
|
-
if (!peer.authenticated || peer.socket.destroyed) return;
|
|
2339
|
-
try {
|
|
2340
|
-
const data = await collectAll({ detailed: this.options.detailed });
|
|
2341
|
-
peer.socket.write(encodeData(data));
|
|
2342
|
-
} catch {
|
|
2343
|
-
}
|
|
2344
|
-
};
|
|
2345
|
-
send();
|
|
2346
|
-
peer.intervalHandle = setInterval(send, this.options.intervalMs);
|
|
2347
|
-
}
|
|
2348
|
-
startPing(peer) {
|
|
2349
|
-
peer.pingHandle = setInterval(() => {
|
|
2350
|
-
if (peer.socket.destroyed) return;
|
|
2351
|
-
peer.socket.write(encodePing());
|
|
2352
|
-
}, 5e3);
|
|
2353
|
-
}
|
|
2354
|
-
onPeerClose(peer) {
|
|
2355
|
-
this.cleanupPeer(peer);
|
|
2356
|
-
console.log(chalk7.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
|
|
2357
|
-
}
|
|
2358
|
-
onPeerError(peer, err) {
|
|
2359
|
-
const code = err.code;
|
|
2360
|
-
if (code === "ECONNRESET") {
|
|
2361
|
-
console.log(chalk7.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
|
|
2362
|
-
} else {
|
|
2363
|
-
console.log(chalk7.red(`Peer error: ${err.message}`));
|
|
2364
|
-
}
|
|
2365
|
-
this.cleanupPeer(peer);
|
|
2366
|
-
}
|
|
2367
|
-
disconnectPeer(peer, reason) {
|
|
2368
|
-
try {
|
|
2369
|
-
peer.socket.write(encodeDisconnect(reason));
|
|
2370
|
-
} catch {
|
|
2371
|
-
}
|
|
2372
|
-
peer.socket.destroy();
|
|
2373
|
-
}
|
|
2374
|
-
cleanupPeer(peer) {
|
|
2375
|
-
if (peer.intervalHandle) {
|
|
2376
|
-
clearInterval(peer.intervalHandle);
|
|
2377
|
-
peer.intervalHandle = null;
|
|
2378
|
-
}
|
|
2379
|
-
if (peer.pingHandle) {
|
|
2380
|
-
clearInterval(peer.pingHandle);
|
|
2381
|
-
peer.pingHandle = null;
|
|
2382
|
-
}
|
|
2383
|
-
this.peers.delete(peer.socket);
|
|
2384
|
-
}
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
// src/p2p/client.ts
|
|
2388
|
-
import net2 from "net";
|
|
2389
|
-
var P2PClient = class {
|
|
2390
|
-
socket = null;
|
|
2391
|
-
options;
|
|
2392
|
-
running = false;
|
|
2393
|
-
readState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
|
|
2394
|
-
onDataCallback = null;
|
|
2395
|
-
onStatusCallback = null;
|
|
2396
|
-
reconnectTimer = null;
|
|
2397
|
-
pingHandle = null;
|
|
2398
|
-
constructor(options) {
|
|
2399
|
-
this.options = options;
|
|
2400
|
-
}
|
|
2401
|
-
setOnData(cb) {
|
|
2402
|
-
this.onDataCallback = cb;
|
|
2403
|
-
}
|
|
2404
|
-
setOnStatus(cb) {
|
|
2405
|
-
this.onStatusCallback = cb;
|
|
2406
|
-
}
|
|
2407
|
-
async connect() {
|
|
2408
|
-
return new Promise((resolve, reject) => {
|
|
2409
|
-
this.socket = new net2.Socket();
|
|
2410
|
-
this.socket.on("data", (data) => this.onData(data));
|
|
2411
|
-
this.socket.on("close", () => this.onClose());
|
|
2412
|
-
this.socket.on("error", (err) => this.onError(err));
|
|
2413
|
-
this.socket.on("timeout", () => this.onTimeout());
|
|
2414
|
-
this.socket.setTimeout(1e4);
|
|
2415
|
-
this.socket.connect(this.options.port, this.options.host, () => {
|
|
2416
|
-
this.running = true;
|
|
2417
|
-
this.socket?.setTimeout(0);
|
|
2418
|
-
this.socket?.write(encodeAuth(this.options.password));
|
|
2419
|
-
this.status("Connecting...");
|
|
2420
|
-
resolve();
|
|
2421
|
-
});
|
|
2422
|
-
this.socket.once("error", (err) => {
|
|
2423
|
-
reject(err);
|
|
2424
|
-
});
|
|
2425
|
-
});
|
|
2426
|
-
}
|
|
2427
|
-
disconnect() {
|
|
2428
|
-
this.running = false;
|
|
2429
|
-
if (this.reconnectTimer) {
|
|
2430
|
-
clearTimeout(this.reconnectTimer);
|
|
2431
|
-
this.reconnectTimer = null;
|
|
2432
|
-
}
|
|
2433
|
-
if (this.pingHandle) {
|
|
2434
|
-
clearInterval(this.pingHandle);
|
|
2435
|
-
this.pingHandle = null;
|
|
2200
|
+
if (this.pingHandle) {
|
|
2201
|
+
clearInterval(this.pingHandle);
|
|
2202
|
+
this.pingHandle = null;
|
|
2436
2203
|
}
|
|
2437
2204
|
if (this.socket) {
|
|
2438
2205
|
try {
|
|
2439
|
-
this.
|
|
2206
|
+
this.sendSigned(encodeDisconnect("Client closing"));
|
|
2440
2207
|
} catch {
|
|
2441
2208
|
}
|
|
2442
2209
|
this.socket.destroy();
|
|
2443
2210
|
this.socket = null;
|
|
2444
2211
|
}
|
|
2212
|
+
if (this.auditStream) {
|
|
2213
|
+
this.auditStream.end();
|
|
2214
|
+
this.auditStream = null;
|
|
2215
|
+
}
|
|
2445
2216
|
this.status("Disconnected");
|
|
2446
2217
|
}
|
|
2447
2218
|
onData(data) {
|
|
@@ -2465,84 +2236,719 @@ var P2PClient = class {
|
|
|
2465
2236
|
} else {
|
|
2466
2237
|
break;
|
|
2467
2238
|
}
|
|
2468
|
-
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
handleMessage(body) {
|
|
2243
|
+
try {
|
|
2244
|
+
const raw = body.toString("utf-8");
|
|
2245
|
+
const msg = decodeMessage(body);
|
|
2246
|
+
if (!verifyHMAC(raw, msg.hmac ?? "", this.hmacKey)) {
|
|
2247
|
+
this.status("Message integrity check failed");
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
switch (msg.kind) {
|
|
2251
|
+
case "challenge":
|
|
2252
|
+
const challengePayload = msg.payload;
|
|
2253
|
+
if (challengePayload.nonce) {
|
|
2254
|
+
this.pendingNonce = challengePayload.nonce;
|
|
2255
|
+
const passwordHash = hashPassword(this.options.password, challengePayload.nonce);
|
|
2256
|
+
this.sendSigned(signMessage("auth", { passwordHash, nonce: challengePayload.nonce }, this.hmacKey));
|
|
2257
|
+
this.status("Authenticating...");
|
|
2258
|
+
}
|
|
2259
|
+
break;
|
|
2260
|
+
case "auth-ok":
|
|
2261
|
+
this.status("Authenticated \u2014 receiving live data");
|
|
2262
|
+
this.startPing();
|
|
2263
|
+
break;
|
|
2264
|
+
case "auth-fail":
|
|
2265
|
+
const failPayload = msg.payload;
|
|
2266
|
+
this.status(`Authentication failed: ${failPayload.reason || "unknown"}`);
|
|
2267
|
+
this.disconnect();
|
|
2268
|
+
break;
|
|
2269
|
+
case "data":
|
|
2270
|
+
if (this.onDataCallback) {
|
|
2271
|
+
const dataPayload = msg.payload;
|
|
2272
|
+
if (dataPayload.stats) {
|
|
2273
|
+
this.onDataCallback(dataPayload.stats);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
break;
|
|
2277
|
+
case "ping":
|
|
2278
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2279
|
+
const ts = msg.payload.ts;
|
|
2280
|
+
this.sendSigned(signMessage("pong", { ts }, this.hmacKey));
|
|
2281
|
+
}
|
|
2282
|
+
break;
|
|
2283
|
+
case "pong":
|
|
2284
|
+
break;
|
|
2285
|
+
case "disconnect":
|
|
2286
|
+
const disconnectPayload = msg.payload;
|
|
2287
|
+
this.status(`Server disconnected: ${disconnectPayload.reason || "unknown"}`);
|
|
2288
|
+
this.disconnect();
|
|
2289
|
+
break;
|
|
2290
|
+
default:
|
|
2291
|
+
break;
|
|
2292
|
+
}
|
|
2293
|
+
} catch {
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
sendSigned(message) {
|
|
2297
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2298
|
+
this.socket.write(message);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
startPing() {
|
|
2302
|
+
this.pingHandle = setInterval(() => {
|
|
2303
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2304
|
+
this.sendSigned(signMessage("ping", { ts: Date.now() }, this.hmacKey));
|
|
2305
|
+
}
|
|
2306
|
+
}, 15e3);
|
|
2307
|
+
}
|
|
2308
|
+
onClose() {
|
|
2309
|
+
if (this.running) {
|
|
2310
|
+
this.status("Connection closed \u2014 reconnecting in 3s");
|
|
2311
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2312
|
+
if (this.running) this.connect().catch(() => {
|
|
2313
|
+
});
|
|
2314
|
+
}, 3e3);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
onError(err) {
|
|
2318
|
+
const code = err.code;
|
|
2319
|
+
if (code === "ECONNREFUSED") {
|
|
2320
|
+
this.status("Connection refused \u2014 is the server running?");
|
|
2321
|
+
} else if (code === "ETIMEDOUT") {
|
|
2322
|
+
this.status("Connection timed out");
|
|
2323
|
+
} else if (code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
|
|
2324
|
+
this.status("TLS certificate verification failed \u2014 use --p2p-insecure to skip");
|
|
2325
|
+
} else {
|
|
2326
|
+
this.status(`Connection error: ${err.message}`);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
onTimeout() {
|
|
2330
|
+
this.status("Connection timed out");
|
|
2331
|
+
this.disconnect();
|
|
2332
|
+
}
|
|
2333
|
+
status(msg) {
|
|
2334
|
+
if (this.onStatusCallback) {
|
|
2335
|
+
this.onStatusCallback(msg);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
audit(event, detail) {
|
|
2339
|
+
if (!this.auditStream) return;
|
|
2340
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2341
|
+
const ip = this.options.host;
|
|
2342
|
+
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
2343
|
+
`);
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
function deriveHMACKey2(password) {
|
|
2347
|
+
return crypto3.createHash("sha256").update(password).digest("hex").slice(0, 32);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/live/input.ts
|
|
2351
|
+
function handleInputModeKey(str, key) {
|
|
2352
|
+
if (key.name === "escape") {
|
|
2353
|
+
state.inputMode = null;
|
|
2354
|
+
state.inputBuffer = "";
|
|
2355
|
+
render();
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
if (state.inputMode === "customizer") {
|
|
2359
|
+
const themesList = Object.keys(THEMES);
|
|
2360
|
+
if (key.name === "up" || str === "k") {
|
|
2361
|
+
state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
|
|
2362
|
+
} else if (key.name === "down" || str === "j") {
|
|
2363
|
+
state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
|
|
2364
|
+
} else if (key.name === "return" || str === " ") {
|
|
2365
|
+
const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
|
|
2366
|
+
if (selected === "Theme") {
|
|
2367
|
+
const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
|
|
2368
|
+
state.currentTheme = themesList[nextIdx];
|
|
2369
|
+
} else if (selected === "Toggle Tree View") {
|
|
2370
|
+
state.treeView = !state.treeView;
|
|
2371
|
+
} else if (selected === "Graph Mode") {
|
|
2372
|
+
state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
|
|
2373
|
+
} else {
|
|
2374
|
+
const itemKey = selected.replace("Toggle ", "").toLowerCase();
|
|
2375
|
+
state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
render();
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
if (state.inputMode === "signal") {
|
|
2382
|
+
if (key.name === "up" || str === "k") {
|
|
2383
|
+
const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
2384
|
+
state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
|
|
2385
|
+
} else if (key.name === "down" || str === "j") {
|
|
2386
|
+
const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
2387
|
+
state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
|
|
2388
|
+
} else if (key.name === "return" || str === " ") {
|
|
2389
|
+
killProcess(state.inputBuffer.trim(), state.inputBuffer.trim());
|
|
2390
|
+
state.inputMode = null;
|
|
2391
|
+
state.inputBuffer = "";
|
|
2392
|
+
render();
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
render();
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (state.inputMode === "p2p") {
|
|
2399
|
+
if (key.name === "escape") {
|
|
2400
|
+
state.inputMode = null;
|
|
2401
|
+
state.inputBuffer = "";
|
|
2402
|
+
render();
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
if (key.name === "return") {
|
|
2406
|
+
const password = state.inputBuffer.trim();
|
|
2407
|
+
if (password) {
|
|
2408
|
+
state.p2pPassword = password;
|
|
2409
|
+
startP2PServerFromTUI();
|
|
2410
|
+
}
|
|
2411
|
+
state.inputMode = null;
|
|
2412
|
+
state.inputBuffer = "";
|
|
2413
|
+
render();
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
if (key.name === "backspace") {
|
|
2417
|
+
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
2418
|
+
render();
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
2422
|
+
state.inputBuffer += str;
|
|
2423
|
+
render();
|
|
2424
|
+
}
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
if (key.name === "return") {
|
|
2428
|
+
if (state.inputMode === "filter") {
|
|
2429
|
+
state.processFilter = state.inputBuffer.trim();
|
|
2430
|
+
setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : "Filter cleared");
|
|
2431
|
+
} else if (state.inputMode === "kill") {
|
|
2432
|
+
killProcess(state.inputBuffer.trim());
|
|
2433
|
+
}
|
|
2434
|
+
state.inputMode = null;
|
|
2435
|
+
state.inputBuffer = "";
|
|
2436
|
+
render();
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
if (key.name === "backspace") {
|
|
2440
|
+
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
2441
|
+
render();
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
2445
|
+
state.inputBuffer += str;
|
|
2446
|
+
render();
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
function killProcess(pidStr, signal = "SIGTERM") {
|
|
2450
|
+
const pid = parseInt(pidStr, 10);
|
|
2451
|
+
if (!pid || pid <= 0) {
|
|
2452
|
+
setStatus(`Invalid PID: ${pidStr}`);
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
try {
|
|
2456
|
+
process.kill(pid, signal);
|
|
2457
|
+
setStatus(`Sent ${signal} to PID ${pid}`);
|
|
2458
|
+
} catch (err) {
|
|
2459
|
+
setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
async function startP2PServerFromTUI() {
|
|
2463
|
+
if (state.p2pServerRunning) return;
|
|
2464
|
+
try {
|
|
2465
|
+
const server = await startP2PServer({
|
|
2466
|
+
host: "0.0.0.0",
|
|
2467
|
+
port: state.p2pPort,
|
|
2468
|
+
password: state.p2pPassword,
|
|
2469
|
+
intervalMs: state.interval * 1e3,
|
|
2470
|
+
detailed: state.detailed,
|
|
2471
|
+
onLog: (msg) => setStatus(msg, 5e3),
|
|
2472
|
+
onPeerEvent: (evt) => {
|
|
2473
|
+
state.p2pEvents.push(evt);
|
|
2474
|
+
if (state.p2pEvents.length > 200) state.p2pEvents.shift();
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
state.p2pServer = server;
|
|
2478
|
+
state.p2pServerRunning = true;
|
|
2479
|
+
state.p2pBind = server.boundAddress || "0.0.0.0";
|
|
2480
|
+
state.p2pEvents = [];
|
|
2481
|
+
setStatus(`P2P server started on ${state.p2pBind}:${state.p2pPort}`);
|
|
2482
|
+
render();
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
setStatus(`P2P server failed: ${err.message}`);
|
|
2485
|
+
state.p2pServerRunning = false;
|
|
2486
|
+
state.p2pServer = null;
|
|
2487
|
+
state.p2pBind = "";
|
|
2488
|
+
render();
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
function syncP2PEvents() {
|
|
2492
|
+
if (!state.p2pServer) return;
|
|
2493
|
+
const history = state.p2pServer.peerEventHistory;
|
|
2494
|
+
if (history && history !== state.p2pEvents) {
|
|
2495
|
+
state.p2pEvents = history;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
function stopP2PServer() {
|
|
2499
|
+
if (!state.p2pServer) return;
|
|
2500
|
+
try {
|
|
2501
|
+
state.p2pServer.stop();
|
|
2502
|
+
} catch {
|
|
2503
|
+
}
|
|
2504
|
+
state.p2pServer = null;
|
|
2505
|
+
state.p2pServerRunning = false;
|
|
2506
|
+
setStatus("P2P server stopped");
|
|
2507
|
+
}
|
|
2508
|
+
function onResize() {
|
|
2509
|
+
state.termWidth = process.stdout.columns || 80;
|
|
2510
|
+
state.termHeight = process.stdout.rows || 24;
|
|
2511
|
+
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
2512
|
+
render();
|
|
2513
|
+
}
|
|
2514
|
+
function restartTicker() {
|
|
2515
|
+
if (state.intervalHandle) clearInterval(state.intervalHandle);
|
|
2516
|
+
state.intervalHandle = setInterval(tick, state.interval * 1e3);
|
|
2517
|
+
}
|
|
2518
|
+
async function tick() {
|
|
2519
|
+
if (state.paused) return;
|
|
2520
|
+
try {
|
|
2521
|
+
const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
|
|
2522
|
+
state.lastData = data;
|
|
2523
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
2524
|
+
state.history.push({
|
|
2525
|
+
cpuUsage: data.cpu.usage,
|
|
2526
|
+
memUsage: data.memory.usagePercent,
|
|
2527
|
+
temp,
|
|
2528
|
+
rxBytes: data.network.rxBytes,
|
|
2529
|
+
txBytes: data.network.txBytes,
|
|
2530
|
+
gpuUtil: data.gpu?.utilization,
|
|
2531
|
+
powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
|
|
2532
|
+
rxPackets: data.network.rxPackets,
|
|
2533
|
+
txPackets: data.network.txPackets,
|
|
2534
|
+
connections: data.network.connections ?? 0
|
|
2535
|
+
});
|
|
2536
|
+
writeLogRow(data);
|
|
2537
|
+
checkAlerts(data);
|
|
2538
|
+
syncP2PEvents();
|
|
2539
|
+
render();
|
|
2540
|
+
} catch {
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
async function startLive(opts2) {
|
|
2544
|
+
if (state.running) return;
|
|
2545
|
+
state.running = true;
|
|
2546
|
+
state.paused = false;
|
|
2547
|
+
state.detailed = !!opts2.detailed;
|
|
2548
|
+
state.interval = opts2.interval;
|
|
2549
|
+
if (opts2.exportDir) state.exportDir = opts2.exportDir;
|
|
2550
|
+
state.termWidth = process.stdout.columns || 80;
|
|
2551
|
+
state.termHeight = process.stdout.rows || 24;
|
|
2552
|
+
state.history.reset();
|
|
2553
|
+
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
2554
|
+
process.stdout.write("\x1B[?1049h");
|
|
2555
|
+
process.stdout.write("\x1B[?25l");
|
|
2556
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2557
|
+
process.title = "pyre";
|
|
2558
|
+
if (state.mouseEnabled) {
|
|
2559
|
+
process.stdout.write("\x1B[?1000h");
|
|
2560
|
+
}
|
|
2561
|
+
await tick();
|
|
2562
|
+
restartTicker();
|
|
2563
|
+
if (opts2.autoLog) startLogging();
|
|
2564
|
+
readline.emitKeypressEvents(process.stdin);
|
|
2565
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
2566
|
+
process.stdin.resume();
|
|
2567
|
+
process.stdout.on("resize", onResize);
|
|
2568
|
+
state.keypressHandler = (str, key) => {
|
|
2569
|
+
if (!key) return;
|
|
2570
|
+
if (key.ctrl && key.name === "c") {
|
|
2571
|
+
stopLive();
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
if (state.inputMode) {
|
|
2575
|
+
handleInputModeKey(str, key);
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
if (key.mouse) {
|
|
2579
|
+
handleMouseClick(key.mouse);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
if (key.sequence && key.sequence.startsWith("\x1B[M")) {
|
|
2583
|
+
handleNormalMouse(key.sequence);
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
if (key.sequence && key.sequence.startsWith("\x1B[<")) {
|
|
2587
|
+
handleSGRMouse(key.sequence);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
switch (key.sequence) {
|
|
2591
|
+
case "+":
|
|
2592
|
+
state.interval += 1;
|
|
2593
|
+
restartTicker();
|
|
2594
|
+
setStatus(`Interval set to ${state.interval}s`);
|
|
2595
|
+
return;
|
|
2596
|
+
case "-":
|
|
2597
|
+
state.interval = Math.max(1, state.interval - 1);
|
|
2598
|
+
restartTicker();
|
|
2599
|
+
setStatus(`Interval set to ${state.interval}s`);
|
|
2600
|
+
return;
|
|
2601
|
+
case "/":
|
|
2602
|
+
state.inputMode = "filter";
|
|
2603
|
+
state.inputBuffer = state.processFilter;
|
|
2604
|
+
render();
|
|
2605
|
+
return;
|
|
2606
|
+
case "0":
|
|
2607
|
+
case "1":
|
|
2608
|
+
case "2":
|
|
2609
|
+
case "3":
|
|
2610
|
+
case "4":
|
|
2611
|
+
case "5":
|
|
2612
|
+
case "6":
|
|
2613
|
+
case "7":
|
|
2614
|
+
case "8":
|
|
2615
|
+
case "9": {
|
|
2616
|
+
const tabMap = {
|
|
2617
|
+
"0": "disk",
|
|
2618
|
+
"1": "cpu",
|
|
2619
|
+
"2": "mem",
|
|
2620
|
+
"3": "gpu",
|
|
2621
|
+
"4": "power",
|
|
2622
|
+
"5": "battery",
|
|
2623
|
+
"6": "thermal",
|
|
2624
|
+
"7": "network",
|
|
2625
|
+
"8": "packets",
|
|
2626
|
+
"9": "tasks"
|
|
2627
|
+
};
|
|
2628
|
+
const panelId = tabMap[key.sequence];
|
|
2629
|
+
if (panelId) {
|
|
2630
|
+
state.activePanel = state.activePanel === panelId ? "grid" : panelId;
|
|
2631
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : `${panelId} panel`);
|
|
2632
|
+
render();
|
|
2633
|
+
}
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
case "P": {
|
|
2637
|
+
state.activePanel = state.activePanel === "process" ? "grid" : "process";
|
|
2638
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : "Process panel");
|
|
2639
|
+
render();
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
switch (key.name) {
|
|
2644
|
+
case "r":
|
|
2645
|
+
if (state.p2pServerRunning) {
|
|
2646
|
+
stopP2PServer();
|
|
2647
|
+
setStatus("P2P server stopped");
|
|
2648
|
+
render();
|
|
2649
|
+
} else {
|
|
2650
|
+
state.inputMode = "p2p";
|
|
2651
|
+
state.inputBuffer = state.p2pPassword;
|
|
2652
|
+
render();
|
|
2653
|
+
}
|
|
2654
|
+
return;
|
|
2655
|
+
case "q":
|
|
2656
|
+
stopLive();
|
|
2657
|
+
break;
|
|
2658
|
+
case "left": {
|
|
2659
|
+
const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
|
|
2660
|
+
if (currentIdx > 0) {
|
|
2661
|
+
state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
|
|
2662
|
+
} else if (currentIdx === -1) {
|
|
2663
|
+
state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
|
|
2664
|
+
}
|
|
2665
|
+
setStatus(`Panel: ${state.activePanel}`);
|
|
2666
|
+
render();
|
|
2667
|
+
break;
|
|
2668
|
+
}
|
|
2669
|
+
case "right": {
|
|
2670
|
+
const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
|
|
2671
|
+
if (currentIdx < state.PANEL_TABS.length - 1) {
|
|
2672
|
+
state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
|
|
2673
|
+
} else if (currentIdx === -1) {
|
|
2674
|
+
state.activePanel = state.PANEL_TABS[0].id;
|
|
2675
|
+
}
|
|
2676
|
+
setStatus(`Panel: ${state.activePanel}`);
|
|
2677
|
+
render();
|
|
2678
|
+
break;
|
|
2679
|
+
}
|
|
2680
|
+
case "c":
|
|
2681
|
+
state.inputMode = "customizer";
|
|
2682
|
+
state.customizerIndex = 0;
|
|
2683
|
+
render();
|
|
2684
|
+
break;
|
|
2685
|
+
case "p":
|
|
2686
|
+
state.paused = !state.paused;
|
|
2687
|
+
setStatus(state.paused ? "Paused" : "Resumed");
|
|
2688
|
+
render();
|
|
2689
|
+
break;
|
|
2690
|
+
case "g":
|
|
2691
|
+
state.showGraphs = !state.showGraphs;
|
|
2692
|
+
render();
|
|
2693
|
+
break;
|
|
2694
|
+
case "b":
|
|
2695
|
+
state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
|
|
2696
|
+
setStatus(`Graph mode: ${state.graphMode}`);
|
|
2697
|
+
render();
|
|
2698
|
+
break;
|
|
2699
|
+
case "d":
|
|
2700
|
+
state.detailed = !state.detailed;
|
|
2701
|
+
setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
|
|
2702
|
+
break;
|
|
2703
|
+
case "s":
|
|
2704
|
+
const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
|
|
2705
|
+
const curIdx = sortCycle.indexOf(state.sortMode);
|
|
2706
|
+
state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
|
|
2707
|
+
setStatus(`Sorting by ${state.sortMode}`);
|
|
2708
|
+
render();
|
|
2709
|
+
break;
|
|
2710
|
+
case "k":
|
|
2711
|
+
state.inputMode = "kill";
|
|
2712
|
+
state.inputBuffer = "";
|
|
2713
|
+
render();
|
|
2714
|
+
break;
|
|
2715
|
+
case "S":
|
|
2716
|
+
state.inputMode = "signal";
|
|
2717
|
+
state.inputBuffer = "SIGTERM";
|
|
2718
|
+
render();
|
|
2719
|
+
break;
|
|
2720
|
+
case "t":
|
|
2721
|
+
state.treeView = !state.treeView;
|
|
2722
|
+
setStatus(state.treeView ? "Tree view enabled" : "Flat view enabled");
|
|
2723
|
+
render();
|
|
2724
|
+
break;
|
|
2725
|
+
case "e":
|
|
2726
|
+
exportSnapshot();
|
|
2727
|
+
render();
|
|
2728
|
+
break;
|
|
2729
|
+
case "l":
|
|
2730
|
+
toggleLogging();
|
|
2731
|
+
render();
|
|
2732
|
+
break;
|
|
2733
|
+
case "f":
|
|
2734
|
+
state.exportFormat = state.exportFormat === "json" ? "csv" : state.exportFormat === "csv" ? "tsv" : "json";
|
|
2735
|
+
setStatus(`Export format: ${state.exportFormat}`);
|
|
2736
|
+
render();
|
|
2737
|
+
break;
|
|
2738
|
+
case "tab":
|
|
2739
|
+
cycleTab(1);
|
|
2740
|
+
break;
|
|
2741
|
+
case "left":
|
|
2742
|
+
cycleTab(-1);
|
|
2743
|
+
break;
|
|
2744
|
+
case "right":
|
|
2745
|
+
cycleTab(1);
|
|
2746
|
+
break;
|
|
2747
|
+
default:
|
|
2748
|
+
if (str && str.length === 1) {
|
|
2749
|
+
const tabId = tabKeyToId(str);
|
|
2750
|
+
if (tabId) {
|
|
2751
|
+
state.activePanel = tabId;
|
|
2752
|
+
setStatus(`Panel: ${tabId.toUpperCase()}`);
|
|
2753
|
+
render();
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2469
2756
|
}
|
|
2757
|
+
};
|
|
2758
|
+
process.stdin.on("keypress", state.keypressHandler);
|
|
2759
|
+
process.once("SIGINT", () => stopLive());
|
|
2760
|
+
}
|
|
2761
|
+
function tabKeyToId(str) {
|
|
2762
|
+
const map = { "1": "cpu", "2": "mem", "3": "gpu", "4": "power", "5": "battery", "6": "thermal", "7": "network", "8": "packets", "9": "tasks", "0": "disk", "p": "process", "r": "p2p" };
|
|
2763
|
+
return map[str] ?? null;
|
|
2764
|
+
}
|
|
2765
|
+
function cycleTab(direction) {
|
|
2766
|
+
const tabs = state.PANEL_TABS.map((t) => t.id);
|
|
2767
|
+
const current = state.activePanel === "grid" ? -1 : tabs.indexOf(state.activePanel);
|
|
2768
|
+
const next = (current + direction + tabs.length) % tabs.length;
|
|
2769
|
+
state.activePanel = tabs[next];
|
|
2770
|
+
setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
|
|
2771
|
+
render();
|
|
2772
|
+
}
|
|
2773
|
+
function handleNormalMouse(seq) {
|
|
2774
|
+
if (seq.length < 6) return;
|
|
2775
|
+
const cb = seq.charCodeAt(3) - 32;
|
|
2776
|
+
const cx = seq.charCodeAt(4) - 32;
|
|
2777
|
+
const cy = seq.charCodeAt(5) - 32;
|
|
2778
|
+
if (cb === 0 || cb === 1) {
|
|
2779
|
+
handleMouseClick(cy, cx);
|
|
2470
2780
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
this.status(`Authentication failed: ${msg.payload.reason}`);
|
|
2481
|
-
this.disconnect();
|
|
2482
|
-
break;
|
|
2483
|
-
case "data":
|
|
2484
|
-
if (this.onDataCallback && msg.payload?.stats) {
|
|
2485
|
-
this.onDataCallback(msg.payload.stats);
|
|
2486
|
-
}
|
|
2487
|
-
break;
|
|
2488
|
-
case "ping":
|
|
2489
|
-
if (this.socket && !this.socket.destroyed) {
|
|
2490
|
-
const ts = msg.payload.ts;
|
|
2491
|
-
this.socket.write(encodePong(ts));
|
|
2492
|
-
}
|
|
2493
|
-
break;
|
|
2494
|
-
case "pong":
|
|
2495
|
-
break;
|
|
2496
|
-
case "disconnect":
|
|
2497
|
-
this.status(`Server disconnected: ${msg.payload.reason}`);
|
|
2498
|
-
this.disconnect();
|
|
2499
|
-
break;
|
|
2500
|
-
default:
|
|
2501
|
-
break;
|
|
2502
|
-
}
|
|
2503
|
-
} catch {
|
|
2504
|
-
}
|
|
2781
|
+
}
|
|
2782
|
+
function handleSGRMouse(seq) {
|
|
2783
|
+
const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
2784
|
+
if (!match) return;
|
|
2785
|
+
const button = parseInt(match[1]);
|
|
2786
|
+
const cx = parseInt(match[2]);
|
|
2787
|
+
const cy = parseInt(match[3]);
|
|
2788
|
+
if (button === 0 || button === 1) {
|
|
2789
|
+
handleMouseClick(cy, cx);
|
|
2505
2790
|
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2791
|
+
}
|
|
2792
|
+
function handleMouseClick(y, x) {
|
|
2793
|
+
if (y !== TAB_BAR_ROW) return;
|
|
2794
|
+
const hitboxes = getTabHitboxes();
|
|
2795
|
+
const hit = hitboxes.find((h) => x >= h.start && x < h.end);
|
|
2796
|
+
if (hit) {
|
|
2797
|
+
state.activePanel = state.activePanel === hit.id ? "grid" : hit.id;
|
|
2798
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : `Panel: ${hit.id.toUpperCase()}`);
|
|
2799
|
+
render();
|
|
2512
2800
|
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
});
|
|
2519
|
-
}, 3e3);
|
|
2520
|
-
}
|
|
2801
|
+
}
|
|
2802
|
+
function stopLive() {
|
|
2803
|
+
if (state.intervalHandle) {
|
|
2804
|
+
clearInterval(state.intervalHandle);
|
|
2805
|
+
state.intervalHandle = null;
|
|
2521
2806
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
this.status("Connection refused \u2014 is the server running?");
|
|
2526
|
-
} else if (code === "ETIMEDOUT") {
|
|
2527
|
-
this.status("Connection timed out");
|
|
2528
|
-
} else {
|
|
2529
|
-
this.status(`Connection error: ${err.message}`);
|
|
2530
|
-
}
|
|
2807
|
+
if (state.statusTimer) {
|
|
2808
|
+
clearTimeout(state.statusTimer);
|
|
2809
|
+
state.statusTimer = null;
|
|
2531
2810
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2811
|
+
if (state.logStream) {
|
|
2812
|
+
state.logStream.end();
|
|
2813
|
+
state.logStream = null;
|
|
2535
2814
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2815
|
+
if (state.p2pServer) {
|
|
2816
|
+
state.p2pServer.stop();
|
|
2817
|
+
state.p2pServer = null;
|
|
2818
|
+
state.p2pServerRunning = false;
|
|
2540
2819
|
}
|
|
2541
|
-
|
|
2820
|
+
if (state.keypressHandler) {
|
|
2821
|
+
process.stdin.removeListener("keypress", state.keypressHandler);
|
|
2822
|
+
state.keypressHandler = null;
|
|
2823
|
+
}
|
|
2824
|
+
process.stdout.removeListener("resize", onResize);
|
|
2825
|
+
state.running = false;
|
|
2826
|
+
state.logging = false;
|
|
2827
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
2828
|
+
process.stdin.pause();
|
|
2829
|
+
if (state.mouseEnabled) {
|
|
2830
|
+
process.stdout.write("\x1B[?1000l");
|
|
2831
|
+
}
|
|
2832
|
+
process.stdout.write("\x1B[?25h");
|
|
2833
|
+
process.stdout.write("\x1B[?1049l");
|
|
2834
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2835
|
+
process.exit(0);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// src/splash.ts
|
|
2839
|
+
import chalk8 from "chalk";
|
|
2840
|
+
var MAXV = 36;
|
|
2841
|
+
var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
|
|
2842
|
+
var palette = [
|
|
2843
|
+
"#0a0505",
|
|
2844
|
+
"#1a0806",
|
|
2845
|
+
"#2a0b06",
|
|
2846
|
+
"#3f1004",
|
|
2847
|
+
"#591404",
|
|
2848
|
+
"#791804",
|
|
2849
|
+
"#9e1c04",
|
|
2850
|
+
"#c92004",
|
|
2851
|
+
"#d63004",
|
|
2852
|
+
"#e34004",
|
|
2853
|
+
"#f05004",
|
|
2854
|
+
"#fd6004",
|
|
2855
|
+
"#ff7004",
|
|
2856
|
+
"#ff8004",
|
|
2857
|
+
"#ff9004",
|
|
2858
|
+
"#ffa004",
|
|
2859
|
+
"#ffb004",
|
|
2860
|
+
"#ffc004",
|
|
2861
|
+
"#ffd004",
|
|
2862
|
+
"#ffe004",
|
|
2863
|
+
"#fff004",
|
|
2864
|
+
"#fff820",
|
|
2865
|
+
"#ffff40",
|
|
2866
|
+
"#ffff60",
|
|
2867
|
+
"#ffff80",
|
|
2868
|
+
"#ffffa0",
|
|
2869
|
+
"#ffffc0",
|
|
2870
|
+
"#ffffe0",
|
|
2871
|
+
"#ffffff",
|
|
2872
|
+
"#ffffff",
|
|
2873
|
+
"#ffffff",
|
|
2874
|
+
"#ffffff",
|
|
2875
|
+
"#ffffff",
|
|
2876
|
+
"#ffffff",
|
|
2877
|
+
"#ffffff",
|
|
2878
|
+
"#ffffff"
|
|
2879
|
+
];
|
|
2880
|
+
function getRgb(hex) {
|
|
2881
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
2882
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
2883
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
2884
|
+
return { r, g, b };
|
|
2885
|
+
}
|
|
2886
|
+
var rgbPalette = palette.map(getRgb);
|
|
2887
|
+
function getChar(val) {
|
|
2888
|
+
const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
|
|
2889
|
+
return CHAR_RAMP[idx];
|
|
2890
|
+
}
|
|
2891
|
+
async function showSplash() {
|
|
2892
|
+
const cols = 60;
|
|
2893
|
+
const rows = 15;
|
|
2894
|
+
let fire = new Uint8Array(cols * rows);
|
|
2895
|
+
process.stdout.write("\x1B[2J");
|
|
2896
|
+
const termRows = process.stdout.rows || 24;
|
|
2897
|
+
const termCols = process.stdout.columns || 80;
|
|
2898
|
+
const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
|
|
2899
|
+
const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
|
|
2900
|
+
return new Promise((resolve) => {
|
|
2901
|
+
const interval = setInterval(() => {
|
|
2902
|
+
for (let x = 0; x < cols; x++) {
|
|
2903
|
+
fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
|
|
2904
|
+
}
|
|
2905
|
+
for (let y = rows - 1; y >= 1; y--) {
|
|
2906
|
+
for (let x = 0; x < cols; x++) {
|
|
2907
|
+
const src = y * cols + x;
|
|
2908
|
+
const val = fire[src];
|
|
2909
|
+
if (val === 0) {
|
|
2910
|
+
fire[(y - 1) * cols + x] = 0;
|
|
2911
|
+
continue;
|
|
2912
|
+
}
|
|
2913
|
+
const decay = Math.floor(Math.random() * 3);
|
|
2914
|
+
const drift = Math.floor(Math.random() * 3) - 1;
|
|
2915
|
+
let nx = x + drift;
|
|
2916
|
+
if (nx < 0) nx = 0;
|
|
2917
|
+
else if (nx >= cols) nx = cols - 1;
|
|
2918
|
+
fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
let output = "";
|
|
2922
|
+
for (let i = 0; i < startRow; i++) output += "\n";
|
|
2923
|
+
for (let y = 0; y < rows; y++) {
|
|
2924
|
+
output += " ".repeat(startCol);
|
|
2925
|
+
for (let x = 0; x < cols; x++) {
|
|
2926
|
+
const v = fire[y * cols + x];
|
|
2927
|
+
const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
|
|
2928
|
+
output += chalk8.rgb(color.r, color.g, color.b)(getChar(v));
|
|
2929
|
+
}
|
|
2930
|
+
output += "\n";
|
|
2931
|
+
}
|
|
2932
|
+
output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk8.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
|
|
2933
|
+
output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk8.dim("[Press Enter to continue]");
|
|
2934
|
+
process.stdout.write(`\x1B[H${output}`);
|
|
2935
|
+
}, 65);
|
|
2936
|
+
const onEnter = () => {
|
|
2937
|
+
process.stdin.removeListener("data", onEnter);
|
|
2938
|
+
clearInterval(interval);
|
|
2939
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2940
|
+
resolve();
|
|
2941
|
+
};
|
|
2942
|
+
process.stdin.setRawMode(true);
|
|
2943
|
+
process.stdin.resume();
|
|
2944
|
+
process.stdin.on("data", onEnter);
|
|
2945
|
+
setTimeout(onEnter, 5e3);
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2542
2948
|
|
|
2543
2949
|
// src/index.ts
|
|
2544
2950
|
var program = new Command();
|
|
2545
|
-
program.name("pyre").version("2.0.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", "cpu").option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)", "0.0.0.0").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication");
|
|
2951
|
+
program.name("pyre").version("2.0.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", "cpu").option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)", "0.0.0.0").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)");
|
|
2546
2952
|
program.parse(process.argv);
|
|
2547
2953
|
var opts = program.opts();
|
|
2548
2954
|
function isExportMode() {
|
|
@@ -2576,7 +2982,7 @@ async function main() {
|
|
|
2576
2982
|
await runP2PConnect();
|
|
2577
2983
|
return;
|
|
2578
2984
|
} else {
|
|
2579
|
-
console.log(
|
|
2985
|
+
console.log(chalk9.red("Usage: pyre p2p <server|connect> [options]"));
|
|
2580
2986
|
process.exit(1);
|
|
2581
2987
|
}
|
|
2582
2988
|
}
|
|
@@ -2590,13 +2996,13 @@ async function main() {
|
|
|
2590
2996
|
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
|
|
2591
2997
|
console.log(output);
|
|
2592
2998
|
if (opts.out) {
|
|
2593
|
-
|
|
2594
|
-
console.log(
|
|
2999
|
+
fs4.writeFileSync(opts.out, stripAnsi(output));
|
|
3000
|
+
console.log(chalk9.dim(`
|
|
2595
3001
|
Saved to ${opts.out}`));
|
|
2596
3002
|
}
|
|
2597
3003
|
}
|
|
2598
3004
|
main().catch((err) => {
|
|
2599
|
-
console.error(
|
|
3005
|
+
console.error(chalk9.red(`Error: ${err.message}`));
|
|
2600
3006
|
process.exit(1);
|
|
2601
3007
|
});
|
|
2602
3008
|
async function runP2PServer() {
|
|
@@ -2605,19 +3011,53 @@ async function runP2PServer() {
|
|
|
2605
3011
|
const password = opts.p2pPassword || "";
|
|
2606
3012
|
const interval = parseFloat(opts.interval) || 2;
|
|
2607
3013
|
const detailed = !!opts.detailed;
|
|
3014
|
+
const tlsCert = opts.p2pCert;
|
|
3015
|
+
const tlsKey = opts.p2pKey;
|
|
3016
|
+
const tlsCA = opts.p2pCA;
|
|
3017
|
+
const rateLimit = parseInt(opts.p2pRateLimit, 10) || 5;
|
|
3018
|
+
const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
3019
|
+
const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
2608
3020
|
if (!password) {
|
|
2609
|
-
console.log(
|
|
3021
|
+
console.log(chalk9.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
|
|
3022
|
+
process.exit(1);
|
|
3023
|
+
}
|
|
3024
|
+
if (tlsCert && !tlsKey) {
|
|
3025
|
+
console.log(chalk9.red("Error: --p2p-key is required when --p2p-cert is specified"));
|
|
2610
3026
|
process.exit(1);
|
|
2611
3027
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
3028
|
+
if (tlsKey && !tlsCert) {
|
|
3029
|
+
console.log(chalk9.red("Error: --p2p-cert is required when --p2p-key is specified"));
|
|
3030
|
+
process.exit(1);
|
|
3031
|
+
}
|
|
3032
|
+
const server = new P2PServer({
|
|
3033
|
+
host,
|
|
3034
|
+
port,
|
|
3035
|
+
password,
|
|
3036
|
+
intervalMs: interval * 1e3,
|
|
3037
|
+
detailed,
|
|
3038
|
+
tlsCert,
|
|
3039
|
+
tlsKey,
|
|
3040
|
+
tlsCA,
|
|
3041
|
+
rateLimitMaxAttempts: rateLimit,
|
|
3042
|
+
rateLimitWindowMs: 6e4,
|
|
3043
|
+
allowedIPs: allowList,
|
|
3044
|
+
deniedIPs: denyList,
|
|
3045
|
+
auditLog: opts.p2pAuditLog,
|
|
3046
|
+
hmacKey: opts.p2pHmacKey
|
|
3047
|
+
});
|
|
3048
|
+
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
3049
|
+
console.log(chalk9.bold(`
|
|
2614
3050
|
pyre P2P Server`));
|
|
2615
|
-
console.log(
|
|
2616
|
-
console.log(
|
|
2617
|
-
console.log(
|
|
2618
|
-
console.log(
|
|
2619
|
-
console.log(
|
|
2620
|
-
console.log(
|
|
3051
|
+
console.log(chalk9.dim(` Host: ${host}`));
|
|
3052
|
+
console.log(chalk9.dim(` Port: ${port}`));
|
|
3053
|
+
console.log(chalk9.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
|
|
3054
|
+
console.log(chalk9.dim(` Password: ${password}`));
|
|
3055
|
+
console.log(chalk9.dim(` Interval: ${interval}s`));
|
|
3056
|
+
console.log(chalk9.dim(` Detailed: ${detailed ? "on" : "off"}`));
|
|
3057
|
+
console.log(chalk9.dim(` Rate limit: ${rateLimit} attempts/min`));
|
|
3058
|
+
if (allowList.length > 0) console.log(chalk9.dim(` Allowed IPs: ${allowList.join(", ")}`));
|
|
3059
|
+
if (denyList.length > 0) console.log(chalk9.dim(` Denied IPs: ${denyList.join(", ")}`));
|
|
3060
|
+
console.log(chalk9.dim(` Waiting for peers...
|
|
2621
3061
|
`));
|
|
2622
3062
|
await server.start();
|
|
2623
3063
|
const rl = readline2.createInterface({ input: process.stdin });
|
|
@@ -2629,7 +3069,7 @@ async function runP2PServer() {
|
|
|
2629
3069
|
process.exit(0);
|
|
2630
3070
|
} else if (trimmed === "status") {
|
|
2631
3071
|
const info = server.info;
|
|
2632
|
-
console.log(
|
|
3072
|
+
console.log(chalk9.dim(` Peers connected: ${info.peersConnected}`));
|
|
2633
3073
|
}
|
|
2634
3074
|
});
|
|
2635
3075
|
process.once("SIGINT", () => {
|
|
@@ -2642,18 +3082,30 @@ async function runP2PConnect() {
|
|
|
2642
3082
|
const host = opts.p2pHost || "127.0.0.1";
|
|
2643
3083
|
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
2644
3084
|
const password = opts.p2pPassword || "";
|
|
3085
|
+
const useTLS = !!opts.p2pTls;
|
|
3086
|
+
const tlsCA = opts.p2pCA;
|
|
3087
|
+
const tlsInsecure = !!opts.p2pInsecure;
|
|
2645
3088
|
if (!password) {
|
|
2646
|
-
console.log(
|
|
3089
|
+
console.log(chalk9.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
|
|
2647
3090
|
process.exit(1);
|
|
2648
3091
|
}
|
|
2649
|
-
const client = new P2PClient({
|
|
3092
|
+
const client = new P2PClient({
|
|
3093
|
+
host,
|
|
3094
|
+
port,
|
|
3095
|
+
password,
|
|
3096
|
+
tls: useTLS,
|
|
3097
|
+
tlsCA,
|
|
3098
|
+
tlsInsecure,
|
|
3099
|
+
auditLog: opts.p2pAuditLog,
|
|
3100
|
+
hmacKey: opts.p2pHmacKey
|
|
3101
|
+
});
|
|
2650
3102
|
client.setOnStatus((msg) => {
|
|
2651
|
-
process.stdout.write(`\r${
|
|
3103
|
+
process.stdout.write(`\r${chalk9.dim(msg)}${" ".repeat(40)}\r`);
|
|
2652
3104
|
});
|
|
2653
3105
|
client.setOnData((data) => {
|
|
2654
3106
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
2655
|
-
console.log(
|
|
2656
|
-
console.log(
|
|
3107
|
+
console.log(chalk9.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
|
|
3108
|
+
console.log(chalk9.dim(` ${data.timestamp}`));
|
|
2657
3109
|
console.log();
|
|
2658
3110
|
console.log(formatTable(data, {
|
|
2659
3111
|
width: process.stdout.columns || 80,
|
|
@@ -2662,15 +3114,17 @@ async function runP2PConnect() {
|
|
|
2662
3114
|
visible: { packets: true }
|
|
2663
3115
|
}));
|
|
2664
3116
|
});
|
|
2665
|
-
|
|
3117
|
+
const protocolLabel = useTLS ? "TLS" : "TCP";
|
|
3118
|
+
console.log(chalk9.bold(`
|
|
2666
3119
|
pyre P2P Client`));
|
|
2667
|
-
console.log(
|
|
2668
|
-
console.log(
|
|
3120
|
+
console.log(chalk9.dim(` Protocol: ${protocolLabel}`));
|
|
3121
|
+
console.log(chalk9.dim(` Connecting to ${host}:${port}...`));
|
|
3122
|
+
console.log(chalk9.dim(` Press Ctrl+C to disconnect
|
|
2669
3123
|
`));
|
|
2670
3124
|
try {
|
|
2671
3125
|
await client.connect();
|
|
2672
3126
|
} catch (err) {
|
|
2673
|
-
console.log(
|
|
3127
|
+
console.log(chalk9.red(`Connection failed: ${err.message}`));
|
|
2674
3128
|
process.exit(1);
|
|
2675
3129
|
}
|
|
2676
3130
|
process.once("SIGINT", () => {
|