pyre-cli 2.0.0 → 3.0.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 +0 -1
- package/dist/index.js +365 -83
- package/package.json +1 -1
- package/src/index.ts +63 -3
- 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 +194 -23
- package/src/p2p/types.ts +28 -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+/);
|
|
@@ -2176,42 +2176,67 @@ async function showSplash() {
|
|
|
2176
2176
|
|
|
2177
2177
|
// src/p2p/server.ts
|
|
2178
2178
|
import net from "net";
|
|
2179
|
+
import tls from "tls";
|
|
2180
|
+
import fs2 from "fs";
|
|
2181
|
+
import crypto2 from "crypto";
|
|
2179
2182
|
import chalk7 from "chalk";
|
|
2180
2183
|
|
|
2181
2184
|
// src/p2p/protocol.ts
|
|
2185
|
+
import crypto from "crypto";
|
|
2182
2186
|
var HEADER_SIZE = 4;
|
|
2183
2187
|
var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
2184
|
-
|
|
2188
|
+
var HMAC_ALGO = "sha256";
|
|
2189
|
+
function buildMessage(kind, payload, hmac) {
|
|
2185
2190
|
const envelope = { kind, payload };
|
|
2191
|
+
if (hmac) envelope.hmac = hmac;
|
|
2186
2192
|
const body = Buffer.from(JSON.stringify(envelope), "utf-8");
|
|
2187
2193
|
const header2 = Buffer.alloc(HEADER_SIZE);
|
|
2188
2194
|
header2.writeUInt32BE(body.length, 0);
|
|
2189
2195
|
return Buffer.concat([header2, body]);
|
|
2190
2196
|
}
|
|
2191
|
-
function
|
|
2192
|
-
return
|
|
2197
|
+
function generateNonce() {
|
|
2198
|
+
return crypto.randomBytes(32).toString("hex");
|
|
2193
2199
|
}
|
|
2194
|
-
function
|
|
2195
|
-
return
|
|
2200
|
+
function hashPassword(password, nonce) {
|
|
2201
|
+
return crypto.createHash("sha256").update(`${password}:${nonce}`).digest("hex");
|
|
2196
2202
|
}
|
|
2197
|
-
function
|
|
2198
|
-
|
|
2203
|
+
function computeHMAC(body, key) {
|
|
2204
|
+
if (!key) return void 0;
|
|
2205
|
+
return crypto.createHmac(HMAC_ALGO, key).update(body).digest("hex");
|
|
2199
2206
|
}
|
|
2200
|
-
function
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2207
|
+
function verifyHMAC(body, expectedHmac, key) {
|
|
2208
|
+
if (!key) return expectedHmac === void 0;
|
|
2209
|
+
if (!expectedHmac) return false;
|
|
2210
|
+
try {
|
|
2211
|
+
const parsed = JSON.parse(body);
|
|
2212
|
+
const payloadWithoutHmac = { ...parsed, hmac: void 0 };
|
|
2213
|
+
const canonical = JSON.stringify(payloadWithoutHmac);
|
|
2214
|
+
const computed = crypto.createHmac(HMAC_ALGO, key).update(canonical).digest("hex");
|
|
2215
|
+
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expectedHmac));
|
|
2216
|
+
} catch {
|
|
2217
|
+
return false;
|
|
2218
|
+
}
|
|
2208
2219
|
}
|
|
2209
2220
|
function encodeDisconnect(reason) {
|
|
2210
2221
|
return buildMessage("disconnect", { reason });
|
|
2211
2222
|
}
|
|
2223
|
+
function decodeMessage(buf) {
|
|
2224
|
+
const str = buf.toString("utf-8");
|
|
2225
|
+
const msg = JSON.parse(str);
|
|
2226
|
+
return msg;
|
|
2227
|
+
}
|
|
2228
|
+
function signMessage(kind, payload, key) {
|
|
2229
|
+
const envelope = { kind, payload };
|
|
2230
|
+
const bodyJson = JSON.stringify(envelope);
|
|
2231
|
+
const hmac = computeHMAC(bodyJson, key);
|
|
2232
|
+
return buildMessage(kind, payload, hmac);
|
|
2233
|
+
}
|
|
2212
2234
|
|
|
2213
2235
|
// src/p2p/server.ts
|
|
2214
2236
|
var HEADER_SIZE2 = 4;
|
|
2237
|
+
var DEFAULT_RATE_LIMIT_MAX = 5;
|
|
2238
|
+
var DEFAULT_RATE_LIMIT_WINDOW = 6e4;
|
|
2239
|
+
var HMAC_KEY_LENGTH = 32;
|
|
2215
2240
|
function readMessageFromPeer(peer) {
|
|
2216
2241
|
while (peer.readBuffer.length > 0) {
|
|
2217
2242
|
if (peer.readExpectingHeader) {
|
|
@@ -2236,13 +2261,36 @@ function readMessageFromPeer(peer) {
|
|
|
2236
2261
|
}
|
|
2237
2262
|
return null;
|
|
2238
2263
|
}
|
|
2264
|
+
function isIPAllowed(ip, allowedIPs, deniedIPs) {
|
|
2265
|
+
for (const denied of deniedIPs) {
|
|
2266
|
+
if (ip === denied || ip.startsWith(denied)) {
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
if (allowedIPs.length === 0) {
|
|
2271
|
+
return true;
|
|
2272
|
+
}
|
|
2273
|
+
for (const allowed of allowedIPs) {
|
|
2274
|
+
if (ip === allowed || ip.startsWith(allowed)) {
|
|
2275
|
+
return true;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return false;
|
|
2279
|
+
}
|
|
2280
|
+
function deriveHMACKey(password) {
|
|
2281
|
+
return crypto2.createHash("sha256").update(password).digest("hex").slice(0, HMAC_KEY_LENGTH);
|
|
2282
|
+
}
|
|
2239
2283
|
var P2PServer = class {
|
|
2240
2284
|
server = null;
|
|
2241
2285
|
peers = /* @__PURE__ */ new Map();
|
|
2242
2286
|
options;
|
|
2243
2287
|
running = false;
|
|
2288
|
+
rateLimitMap = /* @__PURE__ */ new Map();
|
|
2289
|
+
auditStream = null;
|
|
2290
|
+
hmacKey;
|
|
2244
2291
|
constructor(options) {
|
|
2245
2292
|
this.options = options;
|
|
2293
|
+
this.hmacKey = deriveHMACKey(options.password);
|
|
2246
2294
|
}
|
|
2247
2295
|
get info() {
|
|
2248
2296
|
return {
|
|
@@ -2252,16 +2300,42 @@ var P2PServer = class {
|
|
|
2252
2300
|
};
|
|
2253
2301
|
}
|
|
2254
2302
|
async start() {
|
|
2303
|
+
if (this.options.auditLog) {
|
|
2304
|
+
try {
|
|
2305
|
+
fs2.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2306
|
+
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
2307
|
+
this.auditStream = fs2.createWriteStream(logFile, { flags: "a" });
|
|
2308
|
+
} catch {
|
|
2309
|
+
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2255
2312
|
return new Promise((resolve, reject) => {
|
|
2256
|
-
|
|
2313
|
+
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
2314
|
+
if (useTLS) {
|
|
2315
|
+
const tlsOptions = {
|
|
2316
|
+
key: fs2.readFileSync(this.options.tlsKey),
|
|
2317
|
+
cert: fs2.readFileSync(this.options.tlsCert)
|
|
2318
|
+
};
|
|
2319
|
+
if (this.options.tlsCA) {
|
|
2320
|
+
tlsOptions.ca = fs2.readFileSync(this.options.tlsCA);
|
|
2321
|
+
}
|
|
2322
|
+
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
2323
|
+
} else {
|
|
2324
|
+
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
2325
|
+
}
|
|
2257
2326
|
this.server.on("error", (err) => reject(err));
|
|
2258
2327
|
this.server.listen(this.options.port, this.options.host, () => {
|
|
2259
2328
|
this.running = true;
|
|
2260
2329
|
const addr = this.server?.address();
|
|
2261
2330
|
const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
|
|
2262
|
-
|
|
2331
|
+
const protocol = useTLS ? "TLS" : "TCP";
|
|
2332
|
+
console.log(chalk7.green(`P2P server listening on ${bound} (${protocol})`));
|
|
2263
2333
|
console.log(chalk7.dim(` Password: ${this.options.password}`));
|
|
2264
2334
|
console.log(chalk7.dim(` Interval: ${this.options.intervalMs}ms`));
|
|
2335
|
+
const allowedIPs = this.options.allowedIPs ?? [];
|
|
2336
|
+
const deniedIPs = this.options.deniedIPs ?? [];
|
|
2337
|
+
if (allowedIPs.length > 0) console.log(chalk7.dim(` Allowed IPs: ${allowedIPs.join(", ")}`));
|
|
2338
|
+
if (deniedIPs.length > 0) console.log(chalk7.dim(` Denied IPs: ${deniedIPs.join(", ")}`));
|
|
2265
2339
|
resolve();
|
|
2266
2340
|
});
|
|
2267
2341
|
});
|
|
@@ -2272,12 +2346,68 @@ var P2PServer = class {
|
|
|
2272
2346
|
this.disconnectPeer(peer, "Server shutting down");
|
|
2273
2347
|
}
|
|
2274
2348
|
this.peers.clear();
|
|
2349
|
+
this.rateLimitMap.clear();
|
|
2275
2350
|
if (this.server) {
|
|
2276
2351
|
this.server.close();
|
|
2277
2352
|
this.server = null;
|
|
2278
2353
|
}
|
|
2354
|
+
if (this.auditStream) {
|
|
2355
|
+
this.auditStream.end();
|
|
2356
|
+
this.auditStream = null;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
audit(ip, event, detail) {
|
|
2360
|
+
if (!this.auditStream) return;
|
|
2361
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2362
|
+
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
2363
|
+
`);
|
|
2364
|
+
}
|
|
2365
|
+
getPeerIP(socket) {
|
|
2366
|
+
const addr = socket.remoteAddress;
|
|
2367
|
+
if (!addr) return "unknown";
|
|
2368
|
+
if (addr.startsWith("::ffff:")) {
|
|
2369
|
+
return addr.slice(7);
|
|
2370
|
+
}
|
|
2371
|
+
return addr;
|
|
2372
|
+
}
|
|
2373
|
+
checkRateLimit(ip) {
|
|
2374
|
+
const maxAttempts = this.options.rateLimitMaxAttempts ?? DEFAULT_RATE_LIMIT_MAX;
|
|
2375
|
+
const windowMs = this.options.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW;
|
|
2376
|
+
const now = Date.now();
|
|
2377
|
+
const entry = this.rateLimitMap.get(ip);
|
|
2378
|
+
if (!entry) {
|
|
2379
|
+
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
2380
|
+
return true;
|
|
2381
|
+
}
|
|
2382
|
+
if (now - entry.firstAttempt > windowMs) {
|
|
2383
|
+
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
2384
|
+
return true;
|
|
2385
|
+
}
|
|
2386
|
+
entry.attempts++;
|
|
2387
|
+
if (entry.attempts > maxAttempts) {
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
return true;
|
|
2391
|
+
}
|
|
2392
|
+
cleanupRateLimit(ip) {
|
|
2393
|
+
this.rateLimitMap.delete(ip);
|
|
2279
2394
|
}
|
|
2280
2395
|
onConnection(socket) {
|
|
2396
|
+
const ip = this.getPeerIP(socket);
|
|
2397
|
+
const allowedIPs = this.options.allowedIPs ?? [];
|
|
2398
|
+
const deniedIPs = this.options.deniedIPs ?? [];
|
|
2399
|
+
if (!isIPAllowed(ip, allowedIPs, deniedIPs)) {
|
|
2400
|
+
this.audit(ip, "BLOCKED", "IP not allowed");
|
|
2401
|
+
console.log(chalk7.yellow(`Blocked IP: ${ip}`));
|
|
2402
|
+
socket.destroy();
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
if (!this.checkRateLimit(ip)) {
|
|
2406
|
+
this.audit(ip, "RATE_LIMITED", "Too many auth attempts");
|
|
2407
|
+
console.log(chalk7.yellow(`Rate limit exceeded for IP: ${ip}`));
|
|
2408
|
+
socket.destroy();
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2281
2411
|
const peer = {
|
|
2282
2412
|
socket,
|
|
2283
2413
|
authenticated: false,
|
|
@@ -2285,11 +2415,14 @@ var P2PServer = class {
|
|
|
2285
2415
|
pingHandle: null,
|
|
2286
2416
|
readBuffer: Buffer.alloc(0),
|
|
2287
2417
|
readExpectingHeader: true,
|
|
2288
|
-
readMessageLength: 0
|
|
2418
|
+
readMessageLength: 0,
|
|
2419
|
+
ip,
|
|
2420
|
+
pendingNonce: null
|
|
2289
2421
|
};
|
|
2290
2422
|
this.peers.set(socket, peer);
|
|
2291
2423
|
const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
|
|
2292
2424
|
console.log(chalk7.cyan(`Peer connected: ${remoteAddr}`));
|
|
2425
|
+
this.audit(ip, "CONNECT", remoteAddr);
|
|
2293
2426
|
socket.on("data", (data) => this.onData(peer, data));
|
|
2294
2427
|
socket.on("close", () => this.onPeerClose(peer));
|
|
2295
2428
|
socket.on("error", (err) => this.onPeerError(peer, err));
|
|
@@ -2299,6 +2432,9 @@ var P2PServer = class {
|
|
|
2299
2432
|
this.disconnectPeer(peer, "Authentication timeout");
|
|
2300
2433
|
}
|
|
2301
2434
|
});
|
|
2435
|
+
const nonce = generateNonce();
|
|
2436
|
+
this.sendSigned(peer, signMessage("challenge", { nonce }, this.hmacKey));
|
|
2437
|
+
peer.pendingNonce = nonce;
|
|
2302
2438
|
}
|
|
2303
2439
|
onData(peer, data) {
|
|
2304
2440
|
peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
|
|
@@ -2312,33 +2448,56 @@ var P2PServer = class {
|
|
|
2312
2448
|
}
|
|
2313
2449
|
handleAuthMessage(peer, body) {
|
|
2314
2450
|
try {
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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"));
|
|
2451
|
+
const raw = body.toString("utf-8");
|
|
2452
|
+
const msg = decodeMessage(body);
|
|
2453
|
+
if (msg.kind !== "auth") {
|
|
2454
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Expected auth message first" }, this.hmacKey));
|
|
2329
2455
|
this.disconnectPeer(peer, "Expected auth message");
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
const authPayload = msg.payload;
|
|
2459
|
+
if (!authPayload.passwordHash || !authPayload.nonce) {
|
|
2460
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid auth payload" }, this.hmacKey));
|
|
2461
|
+
this.disconnectPeer(peer, "Invalid auth payload");
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
if (peer.pendingNonce && authPayload.nonce !== peer.pendingNonce) {
|
|
2465
|
+
this.audit(peer.ip, "AUTH_FAIL", "Nonce mismatch");
|
|
2466
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid nonce" }, this.hmacKey));
|
|
2467
|
+
this.disconnectPeer(peer, "Invalid nonce");
|
|
2468
|
+
return;
|
|
2330
2469
|
}
|
|
2470
|
+
const expectedHash = hashPassword(this.options.password, authPayload.nonce);
|
|
2471
|
+
if (authPayload.passwordHash !== expectedHash) {
|
|
2472
|
+
this.audit(peer.ip, "AUTH_FAIL", "Invalid password hash");
|
|
2473
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid password" }, this.hmacKey));
|
|
2474
|
+
this.disconnectPeer(peer, "Authentication failed");
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
peer.authenticated = true;
|
|
2478
|
+
this.cleanupRateLimit(peer.ip);
|
|
2479
|
+
peer.pendingNonce = null;
|
|
2480
|
+
peer.socket.write(signMessage("auth-ok", { ok: true }, this.hmacKey));
|
|
2481
|
+
console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
|
|
2482
|
+
this.audit(peer.ip, "AUTH_OK", "Challenge-response successful");
|
|
2483
|
+
this.startDataStream(peer);
|
|
2484
|
+
this.startPing(peer);
|
|
2331
2485
|
} catch {
|
|
2332
|
-
peer.socket.write(
|
|
2486
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid message format" }, this.hmacKey));
|
|
2333
2487
|
this.disconnectPeer(peer, "Invalid message format");
|
|
2334
2488
|
}
|
|
2335
2489
|
}
|
|
2490
|
+
sendSigned(peer, message) {
|
|
2491
|
+
if (!peer.socket.destroyed) {
|
|
2492
|
+
peer.socket.write(message);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2336
2495
|
startDataStream(peer) {
|
|
2337
2496
|
const send = async () => {
|
|
2338
2497
|
if (!peer.authenticated || peer.socket.destroyed) return;
|
|
2339
2498
|
try {
|
|
2340
2499
|
const data = await collectAll({ detailed: this.options.detailed });
|
|
2341
|
-
|
|
2500
|
+
this.sendSigned(peer, signMessage("data", { stats: data }, this.hmacKey));
|
|
2342
2501
|
} catch {
|
|
2343
2502
|
}
|
|
2344
2503
|
};
|
|
@@ -2348,25 +2507,28 @@ var P2PServer = class {
|
|
|
2348
2507
|
startPing(peer) {
|
|
2349
2508
|
peer.pingHandle = setInterval(() => {
|
|
2350
2509
|
if (peer.socket.destroyed) return;
|
|
2351
|
-
peer.
|
|
2510
|
+
this.sendSigned(peer, signMessage("ping", { ts: Date.now() }, this.hmacKey));
|
|
2352
2511
|
}, 5e3);
|
|
2353
2512
|
}
|
|
2354
2513
|
onPeerClose(peer) {
|
|
2355
2514
|
this.cleanupPeer(peer);
|
|
2356
2515
|
console.log(chalk7.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
|
|
2516
|
+
this.audit(peer.ip, "DISCONNECT", "Peer closed connection");
|
|
2357
2517
|
}
|
|
2358
2518
|
onPeerError(peer, err) {
|
|
2359
2519
|
const code = err.code;
|
|
2360
2520
|
if (code === "ECONNRESET") {
|
|
2361
2521
|
console.log(chalk7.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
|
|
2522
|
+
this.audit(peer.ip, "ERROR", "Connection reset");
|
|
2362
2523
|
} else {
|
|
2363
2524
|
console.log(chalk7.red(`Peer error: ${err.message}`));
|
|
2525
|
+
this.audit(peer.ip, "ERROR", err.message);
|
|
2364
2526
|
}
|
|
2365
2527
|
this.cleanupPeer(peer);
|
|
2366
2528
|
}
|
|
2367
2529
|
disconnectPeer(peer, reason) {
|
|
2368
2530
|
try {
|
|
2369
|
-
|
|
2531
|
+
this.sendSigned(peer, signMessage("disconnect", { reason }, this.hmacKey));
|
|
2370
2532
|
} catch {
|
|
2371
2533
|
}
|
|
2372
2534
|
peer.socket.destroy();
|
|
@@ -2386,6 +2548,10 @@ var P2PServer = class {
|
|
|
2386
2548
|
|
|
2387
2549
|
// src/p2p/client.ts
|
|
2388
2550
|
import net2 from "net";
|
|
2551
|
+
import tls2 from "tls";
|
|
2552
|
+
import fs3 from "fs";
|
|
2553
|
+
import crypto3 from "crypto";
|
|
2554
|
+
import chalk8 from "chalk";
|
|
2389
2555
|
var P2PClient = class {
|
|
2390
2556
|
socket = null;
|
|
2391
2557
|
options;
|
|
@@ -2395,8 +2561,12 @@ var P2PClient = class {
|
|
|
2395
2561
|
onStatusCallback = null;
|
|
2396
2562
|
reconnectTimer = null;
|
|
2397
2563
|
pingHandle = null;
|
|
2564
|
+
pendingNonce = null;
|
|
2565
|
+
auditStream = null;
|
|
2566
|
+
hmacKey;
|
|
2398
2567
|
constructor(options) {
|
|
2399
2568
|
this.options = options;
|
|
2569
|
+
this.hmacKey = deriveHMACKey2(options.password);
|
|
2400
2570
|
}
|
|
2401
2571
|
setOnData(cb) {
|
|
2402
2572
|
this.onDataCallback = cb;
|
|
@@ -2405,23 +2575,47 @@ var P2PClient = class {
|
|
|
2405
2575
|
this.onStatusCallback = cb;
|
|
2406
2576
|
}
|
|
2407
2577
|
async connect() {
|
|
2578
|
+
if (this.options.auditLog) {
|
|
2579
|
+
try {
|
|
2580
|
+
fs3.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2581
|
+
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
2582
|
+
this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
|
|
2583
|
+
} catch {
|
|
2584
|
+
console.log(chalk8.yellow("Warning: could not open audit log"));
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2408
2587
|
return new Promise((resolve, reject) => {
|
|
2409
|
-
|
|
2588
|
+
const useTLS = !!this.options.tls;
|
|
2589
|
+
if (useTLS) {
|
|
2590
|
+
const tlsOptions = {
|
|
2591
|
+
host: this.options.host,
|
|
2592
|
+
port: this.options.port,
|
|
2593
|
+
rejectUnauthorized: !this.options.tlsInsecure
|
|
2594
|
+
};
|
|
2595
|
+
if (this.options.tlsCA) {
|
|
2596
|
+
tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
|
|
2597
|
+
}
|
|
2598
|
+
this.socket = tls2.connect(tlsOptions, () => {
|
|
2599
|
+
this.status("Connecting...");
|
|
2600
|
+
});
|
|
2601
|
+
} else {
|
|
2602
|
+
this.socket = new net2.Socket();
|
|
2603
|
+
this.socket.on("data", (data) => this.onData(data));
|
|
2604
|
+
this.socket.on("close", () => this.onClose());
|
|
2605
|
+
this.socket.on("error", (err) => this.onError(err));
|
|
2606
|
+
this.socket.on("timeout", () => this.onTimeout());
|
|
2607
|
+
this.socket.setTimeout(1e4);
|
|
2608
|
+
this.socket.connect(this.options.port, this.options.host, () => {
|
|
2609
|
+
this.status("Connecting...");
|
|
2610
|
+
});
|
|
2611
|
+
this.socket.once("error", (err) => {
|
|
2612
|
+
reject(err);
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2410
2615
|
this.socket.on("data", (data) => this.onData(data));
|
|
2411
2616
|
this.socket.on("close", () => this.onClose());
|
|
2412
2617
|
this.socket.on("error", (err) => this.onError(err));
|
|
2413
2618
|
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
2619
|
});
|
|
2426
2620
|
}
|
|
2427
2621
|
disconnect() {
|
|
@@ -2436,12 +2630,16 @@ var P2PClient = class {
|
|
|
2436
2630
|
}
|
|
2437
2631
|
if (this.socket) {
|
|
2438
2632
|
try {
|
|
2439
|
-
this.
|
|
2633
|
+
this.sendSigned(encodeDisconnect("Client closing"));
|
|
2440
2634
|
} catch {
|
|
2441
2635
|
}
|
|
2442
2636
|
this.socket.destroy();
|
|
2443
2637
|
this.socket = null;
|
|
2444
2638
|
}
|
|
2639
|
+
if (this.auditStream) {
|
|
2640
|
+
this.auditStream.end();
|
|
2641
|
+
this.auditStream = null;
|
|
2642
|
+
}
|
|
2445
2643
|
this.status("Disconnected");
|
|
2446
2644
|
}
|
|
2447
2645
|
onData(data) {
|
|
@@ -2470,31 +2668,50 @@ var P2PClient = class {
|
|
|
2470
2668
|
}
|
|
2471
2669
|
handleMessage(body) {
|
|
2472
2670
|
try {
|
|
2473
|
-
const
|
|
2671
|
+
const raw = body.toString("utf-8");
|
|
2672
|
+
const msg = decodeMessage(body);
|
|
2673
|
+
if (!verifyHMAC(raw, msg.hmac ?? "", this.hmacKey)) {
|
|
2674
|
+
this.status("Message integrity check failed");
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2474
2677
|
switch (msg.kind) {
|
|
2678
|
+
case "challenge":
|
|
2679
|
+
const challengePayload = msg.payload;
|
|
2680
|
+
if (challengePayload.nonce) {
|
|
2681
|
+
this.pendingNonce = challengePayload.nonce;
|
|
2682
|
+
const passwordHash = hashPassword(this.options.password, challengePayload.nonce);
|
|
2683
|
+
this.sendSigned(signMessage("auth", { passwordHash, nonce: challengePayload.nonce }, this.hmacKey));
|
|
2684
|
+
this.status("Authenticating...");
|
|
2685
|
+
}
|
|
2686
|
+
break;
|
|
2475
2687
|
case "auth-ok":
|
|
2476
2688
|
this.status("Authenticated \u2014 receiving live data");
|
|
2477
2689
|
this.startPing();
|
|
2478
2690
|
break;
|
|
2479
2691
|
case "auth-fail":
|
|
2480
|
-
|
|
2692
|
+
const failPayload = msg.payload;
|
|
2693
|
+
this.status(`Authentication failed: ${failPayload.reason || "unknown"}`);
|
|
2481
2694
|
this.disconnect();
|
|
2482
2695
|
break;
|
|
2483
2696
|
case "data":
|
|
2484
|
-
if (this.onDataCallback
|
|
2485
|
-
|
|
2697
|
+
if (this.onDataCallback) {
|
|
2698
|
+
const dataPayload = msg.payload;
|
|
2699
|
+
if (dataPayload.stats) {
|
|
2700
|
+
this.onDataCallback(dataPayload.stats);
|
|
2701
|
+
}
|
|
2486
2702
|
}
|
|
2487
2703
|
break;
|
|
2488
2704
|
case "ping":
|
|
2489
2705
|
if (this.socket && !this.socket.destroyed) {
|
|
2490
2706
|
const ts = msg.payload.ts;
|
|
2491
|
-
this.
|
|
2707
|
+
this.sendSigned(signMessage("pong", { ts }, this.hmacKey));
|
|
2492
2708
|
}
|
|
2493
2709
|
break;
|
|
2494
2710
|
case "pong":
|
|
2495
2711
|
break;
|
|
2496
2712
|
case "disconnect":
|
|
2497
|
-
|
|
2713
|
+
const disconnectPayload = msg.payload;
|
|
2714
|
+
this.status(`Server disconnected: ${disconnectPayload.reason || "unknown"}`);
|
|
2498
2715
|
this.disconnect();
|
|
2499
2716
|
break;
|
|
2500
2717
|
default:
|
|
@@ -2503,10 +2720,15 @@ var P2PClient = class {
|
|
|
2503
2720
|
} catch {
|
|
2504
2721
|
}
|
|
2505
2722
|
}
|
|
2723
|
+
sendSigned(message) {
|
|
2724
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2725
|
+
this.socket.write(message);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2506
2728
|
startPing() {
|
|
2507
2729
|
this.pingHandle = setInterval(() => {
|
|
2508
2730
|
if (this.socket && !this.socket.destroyed) {
|
|
2509
|
-
this.
|
|
2731
|
+
this.sendSigned(signMessage("ping", { ts: Date.now() }, this.hmacKey));
|
|
2510
2732
|
}
|
|
2511
2733
|
}, 15e3);
|
|
2512
2734
|
}
|
|
@@ -2525,6 +2747,8 @@ var P2PClient = class {
|
|
|
2525
2747
|
this.status("Connection refused \u2014 is the server running?");
|
|
2526
2748
|
} else if (code === "ETIMEDOUT") {
|
|
2527
2749
|
this.status("Connection timed out");
|
|
2750
|
+
} else if (code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
|
|
2751
|
+
this.status("TLS certificate verification failed \u2014 use --p2p-insecure to skip");
|
|
2528
2752
|
} else {
|
|
2529
2753
|
this.status(`Connection error: ${err.message}`);
|
|
2530
2754
|
}
|
|
@@ -2538,11 +2762,21 @@ var P2PClient = class {
|
|
|
2538
2762
|
this.onStatusCallback(msg);
|
|
2539
2763
|
}
|
|
2540
2764
|
}
|
|
2765
|
+
audit(event, detail) {
|
|
2766
|
+
if (!this.auditStream) return;
|
|
2767
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2768
|
+
const ip = this.options.host;
|
|
2769
|
+
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
2770
|
+
`);
|
|
2771
|
+
}
|
|
2541
2772
|
};
|
|
2773
|
+
function deriveHMACKey2(password) {
|
|
2774
|
+
return crypto3.createHash("sha256").update(password).digest("hex").slice(0, 32);
|
|
2775
|
+
}
|
|
2542
2776
|
|
|
2543
2777
|
// src/index.ts
|
|
2544
2778
|
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");
|
|
2779
|
+
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
2780
|
program.parse(process.argv);
|
|
2547
2781
|
var opts = program.opts();
|
|
2548
2782
|
function isExportMode() {
|
|
@@ -2576,7 +2810,7 @@ async function main() {
|
|
|
2576
2810
|
await runP2PConnect();
|
|
2577
2811
|
return;
|
|
2578
2812
|
} else {
|
|
2579
|
-
console.log(
|
|
2813
|
+
console.log(chalk9.red("Usage: pyre p2p <server|connect> [options]"));
|
|
2580
2814
|
process.exit(1);
|
|
2581
2815
|
}
|
|
2582
2816
|
}
|
|
@@ -2590,13 +2824,13 @@ async function main() {
|
|
|
2590
2824
|
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
|
|
2591
2825
|
console.log(output);
|
|
2592
2826
|
if (opts.out) {
|
|
2593
|
-
|
|
2594
|
-
console.log(
|
|
2827
|
+
fs4.writeFileSync(opts.out, stripAnsi(output));
|
|
2828
|
+
console.log(chalk9.dim(`
|
|
2595
2829
|
Saved to ${opts.out}`));
|
|
2596
2830
|
}
|
|
2597
2831
|
}
|
|
2598
2832
|
main().catch((err) => {
|
|
2599
|
-
console.error(
|
|
2833
|
+
console.error(chalk9.red(`Error: ${err.message}`));
|
|
2600
2834
|
process.exit(1);
|
|
2601
2835
|
});
|
|
2602
2836
|
async function runP2PServer() {
|
|
@@ -2605,19 +2839,53 @@ async function runP2PServer() {
|
|
|
2605
2839
|
const password = opts.p2pPassword || "";
|
|
2606
2840
|
const interval = parseFloat(opts.interval) || 2;
|
|
2607
2841
|
const detailed = !!opts.detailed;
|
|
2842
|
+
const tlsCert = opts.p2pCert;
|
|
2843
|
+
const tlsKey = opts.p2pKey;
|
|
2844
|
+
const tlsCA = opts.p2pCA;
|
|
2845
|
+
const rateLimit = parseInt(opts.p2pRateLimit, 10) || 5;
|
|
2846
|
+
const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
2847
|
+
const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
2608
2848
|
if (!password) {
|
|
2609
|
-
console.log(
|
|
2849
|
+
console.log(chalk9.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
|
|
2850
|
+
process.exit(1);
|
|
2851
|
+
}
|
|
2852
|
+
if (tlsCert && !tlsKey) {
|
|
2853
|
+
console.log(chalk9.red("Error: --p2p-key is required when --p2p-cert is specified"));
|
|
2854
|
+
process.exit(1);
|
|
2855
|
+
}
|
|
2856
|
+
if (tlsKey && !tlsCert) {
|
|
2857
|
+
console.log(chalk9.red("Error: --p2p-cert is required when --p2p-key is specified"));
|
|
2610
2858
|
process.exit(1);
|
|
2611
2859
|
}
|
|
2612
|
-
const server = new P2PServer({
|
|
2613
|
-
|
|
2860
|
+
const server = new P2PServer({
|
|
2861
|
+
host,
|
|
2862
|
+
port,
|
|
2863
|
+
password,
|
|
2864
|
+
intervalMs: interval * 1e3,
|
|
2865
|
+
detailed,
|
|
2866
|
+
tlsCert,
|
|
2867
|
+
tlsKey,
|
|
2868
|
+
tlsCA,
|
|
2869
|
+
rateLimitMaxAttempts: rateLimit,
|
|
2870
|
+
rateLimitWindowMs: 6e4,
|
|
2871
|
+
allowedIPs: allowList,
|
|
2872
|
+
deniedIPs: denyList,
|
|
2873
|
+
auditLog: opts.p2pAuditLog,
|
|
2874
|
+
hmacKey: opts.p2pHmacKey
|
|
2875
|
+
});
|
|
2876
|
+
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
2877
|
+
console.log(chalk9.bold(`
|
|
2614
2878
|
pyre P2P Server`));
|
|
2615
|
-
console.log(
|
|
2616
|
-
console.log(
|
|
2617
|
-
console.log(
|
|
2618
|
-
console.log(
|
|
2619
|
-
console.log(
|
|
2620
|
-
console.log(
|
|
2879
|
+
console.log(chalk9.dim(` Host: ${host}`));
|
|
2880
|
+
console.log(chalk9.dim(` Port: ${port}`));
|
|
2881
|
+
console.log(chalk9.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
|
|
2882
|
+
console.log(chalk9.dim(` Password: ${password}`));
|
|
2883
|
+
console.log(chalk9.dim(` Interval: ${interval}s`));
|
|
2884
|
+
console.log(chalk9.dim(` Detailed: ${detailed ? "on" : "off"}`));
|
|
2885
|
+
console.log(chalk9.dim(` Rate limit: ${rateLimit} attempts/min`));
|
|
2886
|
+
if (allowList.length > 0) console.log(chalk9.dim(` Allowed IPs: ${allowList.join(", ")}`));
|
|
2887
|
+
if (denyList.length > 0) console.log(chalk9.dim(` Denied IPs: ${denyList.join(", ")}`));
|
|
2888
|
+
console.log(chalk9.dim(` Waiting for peers...
|
|
2621
2889
|
`));
|
|
2622
2890
|
await server.start();
|
|
2623
2891
|
const rl = readline2.createInterface({ input: process.stdin });
|
|
@@ -2629,7 +2897,7 @@ async function runP2PServer() {
|
|
|
2629
2897
|
process.exit(0);
|
|
2630
2898
|
} else if (trimmed === "status") {
|
|
2631
2899
|
const info = server.info;
|
|
2632
|
-
console.log(
|
|
2900
|
+
console.log(chalk9.dim(` Peers connected: ${info.peersConnected}`));
|
|
2633
2901
|
}
|
|
2634
2902
|
});
|
|
2635
2903
|
process.once("SIGINT", () => {
|
|
@@ -2642,18 +2910,30 @@ async function runP2PConnect() {
|
|
|
2642
2910
|
const host = opts.p2pHost || "127.0.0.1";
|
|
2643
2911
|
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
2644
2912
|
const password = opts.p2pPassword || "";
|
|
2913
|
+
const useTLS = !!opts.p2pTls;
|
|
2914
|
+
const tlsCA = opts.p2pCA;
|
|
2915
|
+
const tlsInsecure = !!opts.p2pInsecure;
|
|
2645
2916
|
if (!password) {
|
|
2646
|
-
console.log(
|
|
2917
|
+
console.log(chalk9.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
|
|
2647
2918
|
process.exit(1);
|
|
2648
2919
|
}
|
|
2649
|
-
const client = new P2PClient({
|
|
2920
|
+
const client = new P2PClient({
|
|
2921
|
+
host,
|
|
2922
|
+
port,
|
|
2923
|
+
password,
|
|
2924
|
+
tls: useTLS,
|
|
2925
|
+
tlsCA,
|
|
2926
|
+
tlsInsecure,
|
|
2927
|
+
auditLog: opts.p2pAuditLog,
|
|
2928
|
+
hmacKey: opts.p2pHmacKey
|
|
2929
|
+
});
|
|
2650
2930
|
client.setOnStatus((msg) => {
|
|
2651
|
-
process.stdout.write(`\r${
|
|
2931
|
+
process.stdout.write(`\r${chalk9.dim(msg)}${" ".repeat(40)}\r`);
|
|
2652
2932
|
});
|
|
2653
2933
|
client.setOnData((data) => {
|
|
2654
2934
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
2655
|
-
console.log(
|
|
2656
|
-
console.log(
|
|
2935
|
+
console.log(chalk9.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
|
|
2936
|
+
console.log(chalk9.dim(` ${data.timestamp}`));
|
|
2657
2937
|
console.log();
|
|
2658
2938
|
console.log(formatTable(data, {
|
|
2659
2939
|
width: process.stdout.columns || 80,
|
|
@@ -2662,15 +2942,17 @@ async function runP2PConnect() {
|
|
|
2662
2942
|
visible: { packets: true }
|
|
2663
2943
|
}));
|
|
2664
2944
|
});
|
|
2665
|
-
|
|
2945
|
+
const protocolLabel = useTLS ? "TLS" : "TCP";
|
|
2946
|
+
console.log(chalk9.bold(`
|
|
2666
2947
|
pyre P2P Client`));
|
|
2667
|
-
console.log(
|
|
2668
|
-
console.log(
|
|
2948
|
+
console.log(chalk9.dim(` Protocol: ${protocolLabel}`));
|
|
2949
|
+
console.log(chalk9.dim(` Connecting to ${host}:${port}...`));
|
|
2950
|
+
console.log(chalk9.dim(` Press Ctrl+C to disconnect
|
|
2669
2951
|
`));
|
|
2670
2952
|
try {
|
|
2671
2953
|
await client.connect();
|
|
2672
2954
|
} catch (err) {
|
|
2673
|
-
console.log(
|
|
2955
|
+
console.log(chalk9.red(`Connection failed: ${err.message}`));
|
|
2674
2956
|
process.exit(1);
|
|
2675
2957
|
}
|
|
2676
2958
|
process.once("SIGINT", () => {
|