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/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -37,7 +37,17 @@ program
|
|
|
37
37
|
.option('--limit <n>', 'Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)', '10')
|
|
38
38
|
.option('--p2p-host <host>', 'P2P host address (server: bind address, client: server address)', '0.0.0.0')
|
|
39
39
|
.option('--p2p-port <port>', 'P2P port number', '9876')
|
|
40
|
-
.option('--p2p-password <password>', 'Password for P2P authentication')
|
|
40
|
+
.option('--p2p-password <password>', 'Password for P2P authentication')
|
|
41
|
+
.option('--p2p-tls', 'Enable TLS encryption for P2P connections')
|
|
42
|
+
.option('--p2p-cert <file>', 'TLS certificate file (PEM) for P2P server')
|
|
43
|
+
.option('--p2p-key <file>', 'TLS private key file (PEM) for P2P server')
|
|
44
|
+
.option('--p2p-ca <file>', 'TLS CA certificate file (PEM) for P2P client')
|
|
45
|
+
.option('--p2p-insecure', 'Skip TLS certificate verification (client only)')
|
|
46
|
+
.option('--p2p-rate-limit <n>', 'Max auth attempts per IP per minute (default: 5)', '5')
|
|
47
|
+
.option('--p2p-allow <ips>', 'Comma-separated list of allowed IPs (empty = all)')
|
|
48
|
+
.option('--p2p-deny <ips>', 'Comma-separated list of denied IPs')
|
|
49
|
+
.option('--p2p-audit-log <dir>', 'Directory for P2P audit logs')
|
|
50
|
+
.option('--p2p-hmac-key <key>', 'HMAC key for message signing (default: derived from password)');
|
|
41
51
|
|
|
42
52
|
program.parse(process.argv);
|
|
43
53
|
|
|
@@ -116,20 +126,56 @@ async function runP2PServer(): Promise<void> {
|
|
|
116
126
|
const password = opts.p2pPassword || '';
|
|
117
127
|
const interval = parseFloat(opts.interval) || 2;
|
|
118
128
|
const detailed = !!opts.detailed;
|
|
129
|
+
const tlsCert = opts.p2pCert;
|
|
130
|
+
const tlsKey = opts.p2pKey;
|
|
131
|
+
const tlsCA = opts.p2pCA;
|
|
132
|
+
const rateLimit = parseInt(opts.p2pRateLimit, 10) || 5;
|
|
133
|
+
const allowList = opts.p2pAllow ? opts.p2pAllow.split(',').map((s: string) => s.trim()).filter(Boolean) : [];
|
|
134
|
+
const denyList = opts.p2pDeny ? opts.p2pDeny.split(',').map((s: string) => s.trim()).filter(Boolean) : [];
|
|
119
135
|
|
|
120
136
|
if (!password) {
|
|
121
137
|
console.log(chalk.red('Error: a password is required for P2P server mode. Use --p2p-password <password>'));
|
|
122
138
|
process.exit(1);
|
|
123
139
|
}
|
|
124
140
|
|
|
125
|
-
|
|
141
|
+
if (tlsCert && !tlsKey) {
|
|
142
|
+
console.log(chalk.red('Error: --p2p-key is required when --p2p-cert is specified'));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
if (tlsKey && !tlsCert) {
|
|
146
|
+
console.log(chalk.red('Error: --p2p-cert is required when --p2p-key is specified'));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const server = new P2PServer({
|
|
151
|
+
host,
|
|
152
|
+
port,
|
|
153
|
+
password,
|
|
154
|
+
intervalMs: interval * 1000,
|
|
155
|
+
detailed,
|
|
156
|
+
tlsCert,
|
|
157
|
+
tlsKey,
|
|
158
|
+
tlsCA,
|
|
159
|
+
rateLimitMaxAttempts: rateLimit,
|
|
160
|
+
rateLimitWindowMs: 60000,
|
|
161
|
+
allowedIPs: allowList,
|
|
162
|
+
deniedIPs: denyList,
|
|
163
|
+
auditLog: opts.p2pAuditLog,
|
|
164
|
+
hmacKey: opts.p2pHmacKey,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
126
168
|
|
|
127
169
|
console.log(chalk.bold(`\n pyre P2P Server`));
|
|
128
170
|
console.log(chalk.dim(` Host: ${host}`));
|
|
129
171
|
console.log(chalk.dim(` Port: ${port}`));
|
|
172
|
+
console.log(chalk.dim(` Protocol: ${tlsEnabled ? 'TLS' : 'TCP'}`));
|
|
130
173
|
console.log(chalk.dim(` Password: ${password}`));
|
|
131
174
|
console.log(chalk.dim(` Interval: ${interval}s`));
|
|
132
175
|
console.log(chalk.dim(` Detailed: ${detailed ? 'on' : 'off'}`));
|
|
176
|
+
console.log(chalk.dim(` Rate limit: ${rateLimit} attempts/min`));
|
|
177
|
+
if (allowList.length > 0) console.log(chalk.dim(` Allowed IPs: ${allowList.join(', ')}`));
|
|
178
|
+
if (denyList.length > 0) console.log(chalk.dim(` Denied IPs: ${denyList.join(', ')}`));
|
|
133
179
|
console.log(chalk.dim(` Waiting for peers...\n`));
|
|
134
180
|
|
|
135
181
|
await server.start();
|
|
@@ -158,13 +204,25 @@ async function runP2PConnect(): Promise<void> {
|
|
|
158
204
|
const host = opts.p2pHost || '127.0.0.1';
|
|
159
205
|
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
160
206
|
const password = opts.p2pPassword || '';
|
|
207
|
+
const useTLS = !!opts.p2pTls;
|
|
208
|
+
const tlsCA = opts.p2pCA;
|
|
209
|
+
const tlsInsecure = !!opts.p2pInsecure;
|
|
161
210
|
|
|
162
211
|
if (!password) {
|
|
163
212
|
console.log(chalk.red('Error: a password is required for P2P connect mode. Use --p2p-password <password>'));
|
|
164
213
|
process.exit(1);
|
|
165
214
|
}
|
|
166
215
|
|
|
167
|
-
const client = new P2PClient({
|
|
216
|
+
const client = new P2PClient({
|
|
217
|
+
host,
|
|
218
|
+
port,
|
|
219
|
+
password,
|
|
220
|
+
tls: useTLS,
|
|
221
|
+
tlsCA,
|
|
222
|
+
tlsInsecure,
|
|
223
|
+
auditLog: opts.p2pAuditLog,
|
|
224
|
+
hmacKey: opts.p2pHmacKey,
|
|
225
|
+
});
|
|
168
226
|
|
|
169
227
|
client.setOnStatus((msg) => {
|
|
170
228
|
process.stdout.write(`\r${chalk.dim(msg)}${' '.repeat(40)}\r`);
|
|
@@ -183,7 +241,9 @@ async function runP2PConnect(): Promise<void> {
|
|
|
183
241
|
}));
|
|
184
242
|
});
|
|
185
243
|
|
|
244
|
+
const protocolLabel = useTLS ? 'TLS' : 'TCP';
|
|
186
245
|
console.log(chalk.bold(`\n pyre P2P Client`));
|
|
246
|
+
console.log(chalk.dim(` Protocol: ${protocolLabel}`));
|
|
187
247
|
console.log(chalk.dim(` Connecting to ${host}:${port}...`));
|
|
188
248
|
console.log(chalk.dim(` Press Ctrl+C to disconnect\n`));
|
|
189
249
|
|
|
@@ -627,7 +627,7 @@ export async function collectPackets(): Promise<PacketData | null> {
|
|
|
627
627
|
let topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[] = [];
|
|
628
628
|
let allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[] = [];
|
|
629
629
|
try {
|
|
630
|
-
|
|
630
|
+
const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '')).trim();
|
|
631
631
|
const lsofLines = lsof.split('\n');
|
|
632
632
|
topProcesses = lsofLines.map(line => {
|
|
633
633
|
const parts = line.trim().split(/\s+/);
|
package/src/p2p/client.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
|
+
import tls from 'node:tls';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
2
5
|
import chalk from 'chalk';
|
|
3
|
-
import { encodeAuth, encodePing, encodePong, encodeDisconnect } from './protocol.js';
|
|
6
|
+
import { encodeChallenge, encodeAuth, encodePing, encodePong, encodeDisconnect, hashPassword, computeHMAC, verifyHMAC, signMessage, decodeMessage, generateNonce } from './protocol.js';
|
|
4
7
|
import type { P2PClientOptions } from './types.js';
|
|
5
8
|
import type { StatsData } from '../monitors/types.js';
|
|
6
9
|
|
|
@@ -11,7 +14,7 @@ interface ReadState {
|
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export class P2PClient {
|
|
14
|
-
private socket: net.Socket | null = null;
|
|
17
|
+
private socket: net.Socket | tls.TLSSocket | null = null;
|
|
15
18
|
private options: P2PClientOptions;
|
|
16
19
|
private running = false;
|
|
17
20
|
private readState: ReadState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
|
|
@@ -19,9 +22,13 @@ export class P2PClient {
|
|
|
19
22
|
private onStatusCallback: ((msg: string) => void) | null = null;
|
|
20
23
|
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
21
24
|
private pingHandle: NodeJS.Timeout | null = null;
|
|
25
|
+
private pendingNonce: string | null = null;
|
|
26
|
+
private auditStream: fs.WriteStream | null = null;
|
|
27
|
+
private hmacKey: string;
|
|
22
28
|
|
|
23
29
|
constructor(options: P2PClientOptions) {
|
|
24
30
|
this.options = options;
|
|
31
|
+
this.hmacKey = deriveHMACKey(options.password);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
setOnData(cb: (data: StatsData) => void): void {
|
|
@@ -33,27 +40,50 @@ export class P2PClient {
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
async connect(): Promise<void> {
|
|
43
|
+
if (this.options.auditLog) {
|
|
44
|
+
try {
|
|
45
|
+
fs.mkdirSync(this.options.auditLog, { recursive: true });
|
|
46
|
+
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
47
|
+
this.auditStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
48
|
+
} catch {
|
|
49
|
+
console.log(chalk.yellow('Warning: could not open audit log'));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
return new Promise((resolve, reject) => {
|
|
37
|
-
|
|
54
|
+
const useTLS = !!this.options.tls;
|
|
55
|
+
|
|
56
|
+
if (useTLS) {
|
|
57
|
+
const tlsOptions: tls.ConnectionOptions = {
|
|
58
|
+
host: this.options.host,
|
|
59
|
+
port: this.options.port,
|
|
60
|
+
rejectUnauthorized: !this.options.tlsInsecure,
|
|
61
|
+
};
|
|
62
|
+
if (this.options.tlsCA) {
|
|
63
|
+
tlsOptions.ca = fs.readFileSync(this.options.tlsCA);
|
|
64
|
+
}
|
|
65
|
+
this.socket = tls.connect(tlsOptions, () => {
|
|
66
|
+
this.status('Connecting...');
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
this.socket = new net.Socket();
|
|
70
|
+
this.socket.on('data', (data) => this.onData(data));
|
|
71
|
+
this.socket.on('close', () => this.onClose());
|
|
72
|
+
this.socket.on('error', (err) => this.onError(err));
|
|
73
|
+
this.socket.on('timeout', () => this.onTimeout());
|
|
74
|
+
this.socket.setTimeout(10000);
|
|
75
|
+
this.socket.connect(this.options.port, this.options.host, () => {
|
|
76
|
+
this.status('Connecting...');
|
|
77
|
+
});
|
|
78
|
+
this.socket.once('error', (err) => {
|
|
79
|
+
reject(err);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
38
82
|
|
|
39
83
|
this.socket.on('data', (data) => this.onData(data));
|
|
40
84
|
this.socket.on('close', () => this.onClose());
|
|
41
85
|
this.socket.on('error', (err) => this.onError(err));
|
|
42
86
|
this.socket.on('timeout', () => this.onTimeout());
|
|
43
|
-
|
|
44
|
-
this.socket.setTimeout(10000);
|
|
45
|
-
|
|
46
|
-
this.socket.connect(this.options.port, this.options.host, () => {
|
|
47
|
-
this.running = true;
|
|
48
|
-
this.socket?.setTimeout(0);
|
|
49
|
-
this.socket?.write(encodeAuth(this.options.password));
|
|
50
|
-
this.status('Connecting...');
|
|
51
|
-
resolve();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
this.socket.once('error', (err) => {
|
|
55
|
-
reject(err);
|
|
56
|
-
});
|
|
57
87
|
});
|
|
58
88
|
}
|
|
59
89
|
|
|
@@ -69,13 +99,17 @@ export class P2PClient {
|
|
|
69
99
|
}
|
|
70
100
|
if (this.socket) {
|
|
71
101
|
try {
|
|
72
|
-
this.
|
|
102
|
+
this.sendSigned(encodeDisconnect('Client closing'));
|
|
73
103
|
} catch {
|
|
74
104
|
// ignore
|
|
75
105
|
}
|
|
76
106
|
this.socket.destroy();
|
|
77
107
|
this.socket = null;
|
|
78
108
|
}
|
|
109
|
+
if (this.auditStream) {
|
|
110
|
+
this.auditStream.end();
|
|
111
|
+
this.auditStream = null;
|
|
112
|
+
}
|
|
79
113
|
this.status('Disconnected');
|
|
80
114
|
}
|
|
81
115
|
|
|
@@ -108,29 +142,49 @@ export class P2PClient {
|
|
|
108
142
|
|
|
109
143
|
private handleMessage(body: Buffer): void {
|
|
110
144
|
try {
|
|
111
|
-
const
|
|
145
|
+
const raw = body.toString('utf-8');
|
|
146
|
+
const msg = decodeMessage(body);
|
|
147
|
+
|
|
148
|
+
if (!verifyHMAC(raw, msg.hmac ?? '', this.hmacKey)) {
|
|
149
|
+
this.status('Message integrity check failed');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
112
152
|
|
|
113
153
|
switch (msg.kind) {
|
|
154
|
+
case 'challenge':
|
|
155
|
+
const challengePayload = msg.payload as { nonce?: string };
|
|
156
|
+
if (challengePayload.nonce) {
|
|
157
|
+
this.pendingNonce = challengePayload.nonce;
|
|
158
|
+
const passwordHash = hashPassword(this.options.password, challengePayload.nonce);
|
|
159
|
+
this.sendSigned(signMessage('auth', { passwordHash, nonce: challengePayload.nonce }, this.hmacKey));
|
|
160
|
+
this.status('Authenticating...');
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
|
|
114
164
|
case 'auth-ok':
|
|
115
165
|
this.status('Authenticated — receiving live data');
|
|
116
166
|
this.startPing();
|
|
117
167
|
break;
|
|
118
168
|
|
|
119
169
|
case 'auth-fail':
|
|
120
|
-
|
|
170
|
+
const failPayload = msg.payload as { reason?: string };
|
|
171
|
+
this.status(`Authentication failed: ${failPayload.reason || 'unknown'}`);
|
|
121
172
|
this.disconnect();
|
|
122
173
|
break;
|
|
123
174
|
|
|
124
175
|
case 'data':
|
|
125
|
-
if (this.onDataCallback
|
|
126
|
-
|
|
176
|
+
if (this.onDataCallback) {
|
|
177
|
+
const dataPayload = msg.payload as { stats?: StatsData };
|
|
178
|
+
if (dataPayload.stats) {
|
|
179
|
+
this.onDataCallback(dataPayload.stats);
|
|
180
|
+
}
|
|
127
181
|
}
|
|
128
182
|
break;
|
|
129
183
|
|
|
130
184
|
case 'ping':
|
|
131
185
|
if (this.socket && !this.socket.destroyed) {
|
|
132
186
|
const ts = (msg.payload as { ts: number }).ts;
|
|
133
|
-
this.
|
|
187
|
+
this.sendSigned(signMessage('pong', { ts }, this.hmacKey));
|
|
134
188
|
}
|
|
135
189
|
break;
|
|
136
190
|
|
|
@@ -138,7 +192,8 @@ export class P2PClient {
|
|
|
138
192
|
break;
|
|
139
193
|
|
|
140
194
|
case 'disconnect':
|
|
141
|
-
|
|
195
|
+
const disconnectPayload = msg.payload as { reason?: string };
|
|
196
|
+
this.status(`Server disconnected: ${disconnectPayload.reason || 'unknown'}`);
|
|
142
197
|
this.disconnect();
|
|
143
198
|
break;
|
|
144
199
|
|
|
@@ -150,10 +205,16 @@ export class P2PClient {
|
|
|
150
205
|
}
|
|
151
206
|
}
|
|
152
207
|
|
|
208
|
+
private sendSigned(message: Buffer): void {
|
|
209
|
+
if (this.socket && !this.socket.destroyed) {
|
|
210
|
+
this.socket.write(message);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
153
214
|
private startPing(): void {
|
|
154
215
|
this.pingHandle = setInterval(() => {
|
|
155
216
|
if (this.socket && !this.socket.destroyed) {
|
|
156
|
-
this.
|
|
217
|
+
this.sendSigned(signMessage('ping', { ts: Date.now() }, this.hmacKey));
|
|
157
218
|
}
|
|
158
219
|
}, 15000);
|
|
159
220
|
}
|
|
@@ -173,6 +234,8 @@ export class P2PClient {
|
|
|
173
234
|
this.status('Connection refused — is the server running?');
|
|
174
235
|
} else if (code === 'ETIMEDOUT') {
|
|
175
236
|
this.status('Connection timed out');
|
|
237
|
+
} else if (code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
238
|
+
this.status('TLS certificate verification failed — use --p2p-insecure to skip');
|
|
176
239
|
} else {
|
|
177
240
|
this.status(`Connection error: ${err.message}`);
|
|
178
241
|
}
|
|
@@ -188,6 +251,17 @@ export class P2PClient {
|
|
|
188
251
|
this.onStatusCallback(msg);
|
|
189
252
|
}
|
|
190
253
|
}
|
|
254
|
+
|
|
255
|
+
audit(event: string, detail: string): void {
|
|
256
|
+
if (!this.auditStream) return;
|
|
257
|
+
const ts = new Date().toISOString();
|
|
258
|
+
const ip = this.options.host;
|
|
259
|
+
this.auditStream.write(`${ts} ${ip} ${event} ${detail}\n`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function deriveHMACKey(password: string): string {
|
|
264
|
+
return crypto.createHash('sha256').update(password).digest('hex').slice(0, 32);
|
|
191
265
|
}
|
|
192
266
|
|
|
193
267
|
export async function connectP2PClient(options: P2PClientOptions): Promise<P2PClient> {
|
package/src/p2p/protocol.ts
CHANGED
|
@@ -1,19 +1,53 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import type { P2PMessage, P2PMessageKind, P2PPayload, ChallengeMessage, AuthMessage } from './types.js';
|
|
2
3
|
import type { StatsData } from '../monitors/types.js';
|
|
3
4
|
|
|
4
5
|
const HEADER_SIZE = 4;
|
|
5
6
|
const MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
7
|
+
const HMAC_ALGO = 'sha256';
|
|
6
8
|
|
|
7
|
-
function buildMessage(kind: P2PMessageKind, payload: P2PPayload): Buffer {
|
|
9
|
+
function buildMessage(kind: P2PMessageKind, payload: P2PPayload, hmac?: string): Buffer {
|
|
8
10
|
const envelope: P2PMessage = { kind, payload };
|
|
11
|
+
if (hmac) envelope.hmac = hmac;
|
|
9
12
|
const body = Buffer.from(JSON.stringify(envelope), 'utf-8');
|
|
10
13
|
const header = Buffer.alloc(HEADER_SIZE);
|
|
11
14
|
header.writeUInt32BE(body.length, 0);
|
|
12
15
|
return Buffer.concat([header, body]);
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
export function
|
|
16
|
-
return
|
|
18
|
+
export function generateNonce(): string {
|
|
19
|
+
return crypto.randomBytes(32).toString('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hashPassword(password: string, nonce: string): string {
|
|
23
|
+
return crypto.createHash('sha256').update(`${password}:${nonce}`).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function computeHMAC(body: string, key?: string): string | undefined {
|
|
27
|
+
if (!key) return undefined;
|
|
28
|
+
return crypto.createHmac(HMAC_ALGO, key).update(body).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function verifyHMAC(body: string, expectedHmac: string, key?: string): boolean {
|
|
32
|
+
if (!key) return expectedHmac === undefined;
|
|
33
|
+
if (!expectedHmac) return false;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(body) as P2PMessage;
|
|
36
|
+
const payloadWithoutHmac = { ...parsed, hmac: undefined };
|
|
37
|
+
const canonical = JSON.stringify(payloadWithoutHmac);
|
|
38
|
+
const computed = crypto.createHmac(HMAC_ALGO, key).update(canonical).digest('hex');
|
|
39
|
+
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expectedHmac));
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function encodeChallenge(nonce: string): Buffer {
|
|
46
|
+
return buildMessage('challenge', { nonce });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function encodeAuth(passwordHash: string, nonce: string): Buffer {
|
|
50
|
+
return buildMessage('auth', { passwordHash, nonce });
|
|
17
51
|
}
|
|
18
52
|
|
|
19
53
|
export function encodeAuthOk(): Buffer {
|
|
@@ -46,6 +80,13 @@ export function decodeMessage(buf: Buffer): P2PMessage {
|
|
|
46
80
|
return msg;
|
|
47
81
|
}
|
|
48
82
|
|
|
83
|
+
export function signMessage(kind: P2PMessageKind, payload: P2PPayload, key?: string): Buffer {
|
|
84
|
+
const envelope: P2PMessage = { kind, payload };
|
|
85
|
+
const bodyJson = JSON.stringify(envelope);
|
|
86
|
+
const hmac = computeHMAC(bodyJson, key);
|
|
87
|
+
return buildMessage(kind, payload, hmac);
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
export function readMessage(stream: { read(n: number): Buffer | null }): Buffer | null {
|
|
50
91
|
const header = stream.read(HEADER_SIZE);
|
|
51
92
|
if (!header || header.length < HEADER_SIZE) return null;
|