@balaji003/lantransfer 1.0.7 → 1.0.9
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/package.json +1 -1
- package/public/index.html +28 -18
- package/server.js +168 -57
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -347,6 +347,8 @@
|
|
|
347
347
|
evtSource = new EventSource('/events');
|
|
348
348
|
evtSource.onmessage = e => {
|
|
349
349
|
state = JSON.parse(e.data);
|
|
350
|
+
console.log('[sse] State update — peers:', state.peers?.length, 'files:', state.sharedFiles?.length,
|
|
351
|
+
'transfers:', state.transfers?.length, 'incoming:', !!state.incomingRequest);
|
|
350
352
|
// Auto-select the latest file if none selected
|
|
351
353
|
if (!selectedFileId && state.sharedFiles && state.sharedFiles.length > 0) {
|
|
352
354
|
selectedFileId = state.sharedFiles[state.sharedFiles.length - 1].id;
|
|
@@ -354,9 +356,11 @@
|
|
|
354
356
|
render();
|
|
355
357
|
};
|
|
356
358
|
evtSource.onopen = () => {
|
|
359
|
+
console.log('[sse] Connected');
|
|
357
360
|
document.getElementById('conn-status').textContent = '';
|
|
358
361
|
};
|
|
359
|
-
evtSource.onerror = () => {
|
|
362
|
+
evtSource.onerror = (e) => {
|
|
363
|
+
console.error('[sse] Error/disconnected', e);
|
|
360
364
|
document.getElementById('conn-status').textContent = 'Reconnecting...';
|
|
361
365
|
};
|
|
362
366
|
}
|
|
@@ -366,10 +370,16 @@
|
|
|
366
370
|
// API
|
|
367
371
|
// ──────────────────────────────────────────────────────────────────────────
|
|
368
372
|
function api(endpoint, data) {
|
|
373
|
+
console.log('[api]', endpoint, data || '');
|
|
369
374
|
return fetch('/api/' + endpoint, {
|
|
370
375
|
method: 'POST',
|
|
371
376
|
headers: { 'Content-Type': 'application/json' },
|
|
372
377
|
body: JSON.stringify(data || {}),
|
|
378
|
+
}).then(r => {
|
|
379
|
+
console.log('[api]', endpoint, 'response:', r.status);
|
|
380
|
+
return r;
|
|
381
|
+
}).catch(err => {
|
|
382
|
+
console.error('[api]', endpoint, 'error:', err);
|
|
373
383
|
});
|
|
374
384
|
}
|
|
375
385
|
|
|
@@ -388,13 +398,9 @@
|
|
|
388
398
|
if (selectedFileId === id) selectedFileId = null;
|
|
389
399
|
}
|
|
390
400
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
document.getElementById('browse-btn').textContent = 'Opening...';
|
|
395
|
-
await api('browse');
|
|
396
|
-
browsing = false;
|
|
397
|
-
document.getElementById('browse-btn').textContent = 'Browse';
|
|
401
|
+
function doBrowse() {
|
|
402
|
+
console.log('[browse] Clicked');
|
|
403
|
+
api('browse');
|
|
398
404
|
}
|
|
399
405
|
|
|
400
406
|
function doSend() {
|
|
@@ -403,12 +409,14 @@
|
|
|
403
409
|
}
|
|
404
410
|
}
|
|
405
411
|
|
|
406
|
-
function respondTransfer(
|
|
407
|
-
|
|
412
|
+
function respondTransfer(accepted) {
|
|
413
|
+
console.log('[respondTransfer] accepted:', accepted);
|
|
414
|
+
api('respond', { accepted: accepted });
|
|
408
415
|
}
|
|
409
416
|
|
|
410
|
-
function openFolder(
|
|
411
|
-
|
|
417
|
+
function openFolder(transferId) {
|
|
418
|
+
console.log('[openFolder] transferId:', transferId);
|
|
419
|
+
api('open-folder', { transferId: transferId });
|
|
412
420
|
}
|
|
413
421
|
|
|
414
422
|
function doShutdown() {
|
|
@@ -531,8 +539,8 @@
|
|
|
531
539
|
// Modal
|
|
532
540
|
const overlay = document.getElementById('modal-overlay');
|
|
533
541
|
const modal = document.getElementById('modal');
|
|
534
|
-
if (state.
|
|
535
|
-
const req = state.
|
|
542
|
+
if (state.incomingRequest) {
|
|
543
|
+
const req = state.incomingRequest;
|
|
536
544
|
overlay.classList.add('show');
|
|
537
545
|
modal.innerHTML =
|
|
538
546
|
'<div class="modal-title">Incoming File</div>'
|
|
@@ -540,8 +548,8 @@
|
|
|
540
548
|
+ '<div class="modal-file">' + esc(req.filename) + '</div>'
|
|
541
549
|
+ '<div class="modal-size">' + formatSize(req.size) + '</div>'
|
|
542
550
|
+ '<div class="modal-buttons">'
|
|
543
|
-
+ '<button class="btn-accept" onclick="respondTransfer(
|
|
544
|
-
+ '<button class="btn-reject" onclick="respondTransfer(
|
|
551
|
+
+ '<button class="btn-accept" onclick="respondTransfer(true)">Accept</button>'
|
|
552
|
+
+ '<button class="btn-reject" onclick="respondTransfer(false)">Reject</button>'
|
|
545
553
|
+ '</div>';
|
|
546
554
|
} else {
|
|
547
555
|
overlay.classList.remove('show');
|
|
@@ -573,16 +581,18 @@
|
|
|
573
581
|
extra = '<div class="transfer-actions">'
|
|
574
582
|
+ '<span class="transfer-size">' + formatSize(t.size) + '</span>';
|
|
575
583
|
if (t.status === 'completed' && t.direction === 'receive' && t.savePath) {
|
|
576
|
-
extra += '<button class="btn-open-folder" onclick="openFolder(\'' + esc(t.
|
|
584
|
+
extra += '<button class="btn-open-folder" onclick="openFolder(\'' + esc(t.id) + '\')">Open Folder</button>';
|
|
577
585
|
}
|
|
578
586
|
extra += '</div>';
|
|
579
587
|
}
|
|
580
588
|
|
|
589
|
+
const encrypted = (t.status === 'in_progress' || t.status === 'completed') ? ' \uD83D\uDD12' : '';
|
|
590
|
+
|
|
581
591
|
return '<div class="transfer">'
|
|
582
592
|
+ '<div class="transfer-top">'
|
|
583
593
|
+ '<div class="transfer-info">'
|
|
584
594
|
+ '<span class="transfer-icon">' + icon + '</span>'
|
|
585
|
-
+ '<span>' + esc(t.filename) + ' ' + arrow + ' ' + esc(peer) + '</span>'
|
|
595
|
+
+ '<span>' + esc(t.filename) + ' ' + arrow + ' ' + esc(peer) + encrypted + '</span>'
|
|
586
596
|
+ '</div>'
|
|
587
597
|
+ '<span class="badge ' + badgeClass + '">' + badgeText + '</span>'
|
|
588
598
|
+ '</div>'
|
package/server.js
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
* server.js — LAN File Transfer (Node.js rewrite)
|
|
5
5
|
*
|
|
6
6
|
* Zero external dependencies. Uses built-in modules only.
|
|
7
|
-
*
|
|
8
|
-
* - UDP discovery on port 34254
|
|
7
|
+
* - HTTP + UDP discovery
|
|
9
8
|
* - TCP file transfer on port 34255
|
|
10
|
-
* -
|
|
9
|
+
* - E2E encryption: ECDH key exchange + AES-256-CTR
|
|
11
10
|
*
|
|
12
11
|
* Run: node server.js
|
|
13
12
|
* Then open http://localhost:3000 in a browser.
|
|
@@ -16,6 +15,7 @@
|
|
|
16
15
|
const http = require('http');
|
|
17
16
|
const fs = require('fs');
|
|
18
17
|
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
19
|
const dgram = require('dgram');
|
|
20
20
|
const net = require('net');
|
|
21
21
|
const os = require('os');
|
|
@@ -27,9 +27,9 @@ const { exec } = require('child_process');
|
|
|
27
27
|
const DISCOVERY_PORT = 34254;
|
|
28
28
|
const TRANSFER_PORT = 34255;
|
|
29
29
|
const HTTP_PORT = 3000;
|
|
30
|
-
const XOR_KEY = Buffer.from('LAN-XFER-KEY-2024');
|
|
31
30
|
const PEER_TIMEOUT = 10_000; // ms
|
|
32
31
|
const BROADCAST_INTERVAL = 3_000; // ms
|
|
32
|
+
const ECDH_CURVE = 'prime256v1'; // NIST P-256
|
|
33
33
|
|
|
34
34
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
35
35
|
// Persistent config (device name)
|
|
@@ -62,7 +62,7 @@ let downloadDir = config.downloadDir || path.join(os.homedir(), 'Downloads');
|
|
|
62
62
|
const sharedFiles = []; // [ { id, path, name, size } ]
|
|
63
63
|
const peers = new Map(); // id → { device_name, ip, tcp_port, last_seen }
|
|
64
64
|
const transfers = []; // [ TransferEntry ]
|
|
65
|
-
|
|
65
|
+
let pendingRequest = null; // { filename, size, senderName, resolve } or null
|
|
66
66
|
let idCounter = 0;
|
|
67
67
|
let browseInProgress = false;
|
|
68
68
|
|
|
@@ -114,9 +114,9 @@ function serializeState() {
|
|
|
114
114
|
speed: t.speed, error: t.error,
|
|
115
115
|
savePath: t.savePath || null,
|
|
116
116
|
})),
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
incomingRequest: pendingRequest
|
|
118
|
+
? { filename: pendingRequest.filename, size: pendingRequest.size, senderName: pendingRequest.senderName }
|
|
119
|
+
: null,
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -128,14 +128,18 @@ function broadcast() {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
131
|
-
// Helpers
|
|
131
|
+
// Helpers — E2E encryption (ECDH + AES-256-CTR)
|
|
132
132
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
133
|
-
function
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
function createKeyPair() {
|
|
134
|
+
const ecdh = crypto.createECDH(ECDH_CURVE);
|
|
135
|
+
ecdh.generateKeys();
|
|
136
|
+
return ecdh;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function deriveKey(ecdh, peerPubKey) {
|
|
140
|
+
const shared = ecdh.computeSecret(peerPubKey);
|
|
141
|
+
// SHA-256 the shared secret to get a 32-byte AES key
|
|
142
|
+
return crypto.createHash('sha256').update(shared).digest();
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
function readBody(req) {
|
|
@@ -180,6 +184,8 @@ function readExact(socket, n) {
|
|
|
180
184
|
const htmlPath = path.join(__dirname, 'public', 'index.html');
|
|
181
185
|
|
|
182
186
|
const server = http.createServer(async (req, res) => {
|
|
187
|
+
console.log(` [http] ${req.method} ${req.url}`);
|
|
188
|
+
|
|
183
189
|
// ── Serve the web UI ──
|
|
184
190
|
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
|
|
185
191
|
fs.readFile(htmlPath, (err, data) => {
|
|
@@ -217,7 +223,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
217
223
|
'Connection': 'keep-alive',
|
|
218
224
|
});
|
|
219
225
|
sseClients.add(res);
|
|
220
|
-
|
|
226
|
+
console.log(` [sse] Client connected (total: ${sseClients.size})`);
|
|
227
|
+
req.on('close', () => {
|
|
228
|
+
sseClients.delete(res);
|
|
229
|
+
console.log(` [sse] Client disconnected (total: ${sseClients.size})`);
|
|
230
|
+
});
|
|
221
231
|
res.write(`data: ${serializeState()}\n\n`);
|
|
222
232
|
return;
|
|
223
233
|
}
|
|
@@ -233,32 +243,40 @@ const server = http.createServer(async (req, res) => {
|
|
|
233
243
|
console.log(` [toggle] Discoverable: ${isDiscoverable}`);
|
|
234
244
|
}
|
|
235
245
|
else if (req.url === '/api/browse') {
|
|
246
|
+
// Respond immediately so we don't block the HTTP connection
|
|
247
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
248
|
+
res.end('ok');
|
|
236
249
|
if (browseInProgress) {
|
|
237
250
|
console.log(' [browse] Dialog already open, ignoring');
|
|
238
251
|
} else {
|
|
239
252
|
browseInProgress = true;
|
|
240
253
|
console.log(' [browse] Opening file dialog...');
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
try {
|
|
255
|
+
const fp = await openFileDialog();
|
|
256
|
+
browseInProgress = false;
|
|
257
|
+
if (fp) {
|
|
245
258
|
const s = fs.statSync(fp);
|
|
246
259
|
const file = { id: String(idCounter++), path: fp, name: path.basename(fp), size: s.size };
|
|
247
260
|
sharedFiles.push(file);
|
|
248
261
|
console.log(` [browse] Added: ${file.name} (${formatSize(file.size)})`);
|
|
249
|
-
}
|
|
250
|
-
console.
|
|
262
|
+
} else {
|
|
263
|
+
console.log(' [browse] Cancelled by user');
|
|
251
264
|
}
|
|
252
|
-
}
|
|
253
|
-
|
|
265
|
+
} catch (e) {
|
|
266
|
+
browseInProgress = false;
|
|
267
|
+
console.error(` [browse] Error: ${e.message}`);
|
|
254
268
|
}
|
|
269
|
+
broadcast();
|
|
255
270
|
}
|
|
271
|
+
return; // already responded
|
|
256
272
|
}
|
|
257
273
|
else if (req.url === '/api/remove-file') {
|
|
258
274
|
const idx = sharedFiles.findIndex(f => f.id === data.fileId);
|
|
259
275
|
if (idx !== -1) {
|
|
260
276
|
console.log(` [files] Removed: ${sharedFiles[idx].name}`);
|
|
261
277
|
sharedFiles.splice(idx, 1);
|
|
278
|
+
} else {
|
|
279
|
+
console.log(` [files] Remove failed — fileId not found: ${data.fileId}`);
|
|
262
280
|
}
|
|
263
281
|
}
|
|
264
282
|
else if (req.url === '/api/send') {
|
|
@@ -266,9 +284,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
266
284
|
const file = sharedFiles.find(f => f.id === data.fileId);
|
|
267
285
|
if (peer && file) {
|
|
268
286
|
console.log(` [send] ${file.name} -> ${peer.device_name} (${peer.ip})`);
|
|
269
|
-
sendFile(peer, { ...file });
|
|
287
|
+
sendFile(peer, { ...file }).catch(e => console.error(` [send] Unhandled: ${e.message}`));
|
|
270
288
|
} else {
|
|
271
|
-
console.log(` [send] Failed — peer: ${!!peer}, file: ${!!file}`);
|
|
289
|
+
console.log(` [send] Failed — peer: ${!!peer} (${data.peerId}), file: ${!!file} (${data.fileId})`);
|
|
272
290
|
}
|
|
273
291
|
}
|
|
274
292
|
else if (req.url === '/api/rename') {
|
|
@@ -278,34 +296,62 @@ const server = http.createServer(async (req, res) => {
|
|
|
278
296
|
config.deviceName = newName;
|
|
279
297
|
saveConfig(config);
|
|
280
298
|
console.log(` [rename] Device name set to: ${deviceName}`);
|
|
299
|
+
} else {
|
|
300
|
+
console.log(` [rename] Invalid name: "${data.name}"`);
|
|
281
301
|
}
|
|
282
302
|
}
|
|
283
303
|
else if (req.url === '/api/set-download-dir') {
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
304
|
+
// Respond immediately so we don't block the HTTP connection
|
|
305
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
306
|
+
res.end('ok');
|
|
307
|
+
console.log(' [config] Opening folder dialog...');
|
|
308
|
+
try {
|
|
309
|
+
const dir = await openFolderDialog();
|
|
310
|
+
if (dir) {
|
|
311
|
+
downloadDir = dir;
|
|
312
|
+
config.downloadDir = dir;
|
|
313
|
+
saveConfig(config);
|
|
314
|
+
console.log(` [config] Download dir set to: ${downloadDir}`);
|
|
315
|
+
} else {
|
|
316
|
+
console.log(' [config] Folder dialog cancelled');
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
console.error(` [config] Folder dialog error: ${e.message}`);
|
|
291
320
|
}
|
|
321
|
+
broadcast();
|
|
322
|
+
return; // already responded
|
|
292
323
|
}
|
|
293
324
|
else if (req.url === '/api/respond') {
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
325
|
+
console.log(` [respond] Received: accepted=${data.accepted}`);
|
|
326
|
+
if (pendingRequest) {
|
|
327
|
+
pendingRequest.resolve(!!data.accepted);
|
|
328
|
+
console.log(` [respond] ${data.accepted ? 'Accepted' : 'Rejected'}: ${pendingRequest.filename}`);
|
|
329
|
+
pendingRequest = null;
|
|
330
|
+
} else {
|
|
331
|
+
console.log(` [respond] WARNING: No pending request`);
|
|
299
332
|
}
|
|
300
333
|
}
|
|
301
334
|
else if (req.url === '/api/open-folder') {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
335
|
+
const transferId = data.transferId;
|
|
336
|
+
const t = transfers.find(tr => tr.id === transferId);
|
|
337
|
+
if (t && t.savePath) {
|
|
338
|
+
console.log(` [open] Opening folder for: ${t.savePath}`);
|
|
339
|
+
if (fs.existsSync(t.savePath)) {
|
|
340
|
+
if (process.platform === 'win32') exec(`explorer /select,"${t.savePath}"`);
|
|
341
|
+
else if (process.platform === 'darwin') exec(`open -R "${t.savePath}"`);
|
|
342
|
+
else exec(`xdg-open "${path.dirname(t.savePath)}"`);
|
|
343
|
+
} else {
|
|
344
|
+
console.log(` [open] File no longer exists: ${t.savePath}`);
|
|
345
|
+
// Open the directory instead
|
|
346
|
+
const dir = path.dirname(t.savePath);
|
|
347
|
+
if (fs.existsSync(dir)) {
|
|
348
|
+
if (process.platform === 'win32') exec(`explorer "${dir}"`);
|
|
349
|
+
else if (process.platform === 'darwin') exec(`open "${dir}"`);
|
|
350
|
+
else exec(`xdg-open "${dir}"`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
console.log(` [open] Failed — transferId: ${transferId}, found: ${!!t}, savePath: ${t ? t.savePath : 'N/A'}`);
|
|
309
355
|
}
|
|
310
356
|
}
|
|
311
357
|
else if (req.url === '/api/shutdown') {
|
|
@@ -315,6 +361,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
315
361
|
setTimeout(() => process.exit(0), 200);
|
|
316
362
|
return;
|
|
317
363
|
}
|
|
364
|
+
else {
|
|
365
|
+
console.log(` [http] Unknown POST endpoint: ${req.url}`);
|
|
366
|
+
}
|
|
318
367
|
|
|
319
368
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
320
369
|
res.end('ok');
|
|
@@ -428,33 +477,41 @@ tcpServer.on('error', err => {
|
|
|
428
477
|
tcpServer.listen(TRANSFER_PORT);
|
|
429
478
|
|
|
430
479
|
async function handleIncoming(socket) {
|
|
480
|
+
const remoteAddr = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
481
|
+
console.log(` [recv] TCP connection from ${remoteAddr}`);
|
|
482
|
+
|
|
431
483
|
// Read header length (8 bytes, little-endian u64)
|
|
432
484
|
const lenBuf = await readExact(socket, 8);
|
|
433
485
|
const headerLen = Number(lenBuf.readBigUInt64LE(0));
|
|
486
|
+
console.log(` [recv] Header length: ${headerLen} bytes`);
|
|
434
487
|
|
|
435
488
|
// Read header JSON
|
|
436
489
|
const headerBuf = await readExact(socket, headerLen);
|
|
437
490
|
const header = JSON.parse(headerBuf.toString('utf-8'));
|
|
438
491
|
const { filename, size, sender_name } = header;
|
|
439
492
|
|
|
440
|
-
console.log(` Incoming: ${filename} (${formatSize(size)}) from ${sender_name}`);
|
|
493
|
+
console.log(` [recv] Incoming: "${filename}" (${formatSize(size)}) from ${sender_name}`);
|
|
441
494
|
|
|
442
495
|
// Prompt user for accept / reject
|
|
443
|
-
|
|
496
|
+
console.log(` [recv] Waiting for user decision...`);
|
|
497
|
+
|
|
444
498
|
const accepted = await new Promise(resolve => {
|
|
445
|
-
|
|
499
|
+
pendingRequest = { filename, size, senderName: sender_name, resolve };
|
|
446
500
|
broadcast();
|
|
447
501
|
|
|
448
502
|
// Auto-reject if sender disconnects while waiting
|
|
449
503
|
socket.once('close', () => {
|
|
450
|
-
if (
|
|
451
|
-
|
|
504
|
+
if (pendingRequest && pendingRequest.resolve === resolve) {
|
|
505
|
+
console.log(` [recv] Sender disconnected while waiting, auto-rejecting`);
|
|
506
|
+
pendingRequest = null;
|
|
452
507
|
resolve(false);
|
|
453
508
|
broadcast();
|
|
454
509
|
}
|
|
455
510
|
});
|
|
456
511
|
});
|
|
457
512
|
|
|
513
|
+
console.log(` [recv] User decision: ${accepted ? 'ACCEPTED' : 'REJECTED'}`);
|
|
514
|
+
|
|
458
515
|
// Send decision byte
|
|
459
516
|
socket.write(Buffer.from([accepted ? 1 : 0]));
|
|
460
517
|
if (!accepted) { socket.end(); return; }
|
|
@@ -481,13 +538,34 @@ async function handleIncoming(socket) {
|
|
|
481
538
|
}
|
|
482
539
|
|
|
483
540
|
t.savePath = savePath;
|
|
484
|
-
|
|
541
|
+
console.log(` [recv] Saving to: ${savePath}`);
|
|
542
|
+
|
|
543
|
+
// ── E2E key exchange: receive sender's public key, send ours ──
|
|
544
|
+
const pubKeyLenBuf = await readExact(socket, 2);
|
|
545
|
+
const pubKeyLen = pubKeyLenBuf.readUInt16BE(0);
|
|
546
|
+
const senderPubKey = await readExact(socket, pubKeyLen);
|
|
547
|
+
console.log(` [recv] Got sender public key (${pubKeyLen} bytes)`);
|
|
548
|
+
|
|
549
|
+
const ecdh = createKeyPair();
|
|
550
|
+
const myPubKey = ecdh.getPublicKey();
|
|
551
|
+
const keyLenBuf = Buffer.alloc(2);
|
|
552
|
+
keyLenBuf.writeUInt16BE(myPubKey.length);
|
|
553
|
+
socket.write(keyLenBuf);
|
|
554
|
+
socket.write(myPubKey);
|
|
555
|
+
console.log(` [recv] Sent our public key (${myPubKey.length} bytes)`);
|
|
556
|
+
|
|
557
|
+
// Derive AES key + receive IV
|
|
558
|
+
const aesKey = deriveKey(ecdh, senderPubKey);
|
|
559
|
+
const iv = await readExact(socket, 16);
|
|
560
|
+
console.log(` [recv] AES key derived, IV received — decrypting with AES-256-CTR`);
|
|
561
|
+
|
|
562
|
+
const decipher = crypto.createDecipheriv('aes-256-ctr', aesKey, iv);
|
|
485
563
|
const ws = fs.createWriteStream(savePath);
|
|
486
564
|
let received = 0;
|
|
487
565
|
let lastBc = 0;
|
|
488
566
|
|
|
489
567
|
socket.on('data', chunk => {
|
|
490
|
-
const decrypted =
|
|
568
|
+
const decrypted = decipher.update(chunk);
|
|
491
569
|
ws.write(decrypted);
|
|
492
570
|
received += chunk.length;
|
|
493
571
|
|
|
@@ -504,11 +582,13 @@ async function handleIncoming(socket) {
|
|
|
504
582
|
|
|
505
583
|
await new Promise((resolve, reject) => {
|
|
506
584
|
socket.on('end', () => {
|
|
585
|
+
const final = decipher.final();
|
|
586
|
+
if (final.length > 0) ws.write(final);
|
|
507
587
|
ws.end();
|
|
508
588
|
t.status = 'completed';
|
|
509
589
|
t.progress = 100;
|
|
510
590
|
broadcast();
|
|
511
|
-
console.log(`
|
|
591
|
+
console.log(` [recv] Complete: ${savePath} (${formatSize(received)})`);
|
|
512
592
|
resolve();
|
|
513
593
|
});
|
|
514
594
|
socket.on('error', err => {
|
|
@@ -516,6 +596,7 @@ async function handleIncoming(socket) {
|
|
|
516
596
|
t.status = 'failed';
|
|
517
597
|
t.error = err.message;
|
|
518
598
|
broadcast();
|
|
599
|
+
console.error(` [recv] Socket error: ${err.message}`);
|
|
519
600
|
reject(err);
|
|
520
601
|
});
|
|
521
602
|
});
|
|
@@ -540,6 +621,7 @@ async function sendFile(peer, file) {
|
|
|
540
621
|
socket.connect(peer.tcp_port, peer.ip, res);
|
|
541
622
|
socket.once('error', rej);
|
|
542
623
|
});
|
|
624
|
+
console.log(` [send] Connected to ${peer.ip}:${peer.tcp_port}`);
|
|
543
625
|
|
|
544
626
|
// Send header
|
|
545
627
|
const headerJSON = JSON.stringify({
|
|
@@ -558,20 +640,44 @@ async function sendFile(peer, file) {
|
|
|
558
640
|
t.error = 'Rejected by recipient';
|
|
559
641
|
broadcast();
|
|
560
642
|
socket.end();
|
|
643
|
+
console.log(` [send] Rejected by ${peer.device_name}`);
|
|
561
644
|
return;
|
|
562
645
|
}
|
|
563
646
|
|
|
647
|
+
console.log(` [send] Accepted — starting E2E key exchange`);
|
|
648
|
+
|
|
649
|
+
// ── E2E key exchange: send our public key, receive receiver's ──
|
|
650
|
+
const ecdh = createKeyPair();
|
|
651
|
+
const myPubKey = ecdh.getPublicKey();
|
|
652
|
+
const keyLenBuf = Buffer.alloc(2);
|
|
653
|
+
keyLenBuf.writeUInt16BE(myPubKey.length);
|
|
654
|
+
socket.write(keyLenBuf);
|
|
655
|
+
socket.write(myPubKey);
|
|
656
|
+
console.log(` [send] Sent our public key (${myPubKey.length} bytes)`);
|
|
657
|
+
|
|
658
|
+
const recvKeyLenBuf = await readExact(socket, 2);
|
|
659
|
+
const recvKeyLen = recvKeyLenBuf.readUInt16BE(0);
|
|
660
|
+
const recvPubKey = await readExact(socket, recvKeyLen);
|
|
661
|
+
console.log(` [send] Got receiver public key (${recvKeyLen} bytes)`);
|
|
662
|
+
|
|
663
|
+
// Derive AES key, generate IV, send IV
|
|
664
|
+
const aesKey = deriveKey(ecdh, recvPubKey);
|
|
665
|
+
const iv = crypto.randomBytes(16);
|
|
666
|
+
socket.write(iv);
|
|
667
|
+
console.log(` [send] AES key derived, IV sent — encrypting with AES-256-CTR`);
|
|
668
|
+
|
|
564
669
|
t.status = 'in_progress';
|
|
565
670
|
t._start = Date.now();
|
|
566
671
|
broadcast();
|
|
567
672
|
|
|
568
|
-
// Stream file with
|
|
673
|
+
// Stream file with AES-256-CTR encryption
|
|
674
|
+
const cipher = crypto.createCipheriv('aes-256-ctr', aesKey, iv);
|
|
569
675
|
const rs = fs.createReadStream(file.path, { highWaterMark: 65536 });
|
|
570
676
|
let sent = 0;
|
|
571
677
|
let lastBc = 0;
|
|
572
678
|
|
|
573
679
|
for await (const chunk of rs) {
|
|
574
|
-
const encrypted =
|
|
680
|
+
const encrypted = cipher.update(Buffer.from(chunk));
|
|
575
681
|
const ok = socket.write(encrypted);
|
|
576
682
|
sent += chunk.length;
|
|
577
683
|
|
|
@@ -594,16 +700,20 @@ async function sendFile(peer, file) {
|
|
|
594
700
|
}
|
|
595
701
|
}
|
|
596
702
|
|
|
703
|
+
// Write final cipher block
|
|
704
|
+
const final = cipher.final();
|
|
705
|
+
if (final.length > 0) socket.write(final);
|
|
706
|
+
|
|
597
707
|
socket.end();
|
|
598
708
|
t.status = 'completed';
|
|
599
709
|
t.progress = 100;
|
|
600
710
|
broadcast();
|
|
601
|
-
console.log(`
|
|
711
|
+
console.log(` [send] Complete: ${file.name} → ${peer.device_name}`);
|
|
602
712
|
} catch (e) {
|
|
603
713
|
t.status = 'failed';
|
|
604
714
|
t.error = e.message;
|
|
605
715
|
broadcast();
|
|
606
|
-
console.error(`
|
|
716
|
+
console.error(` [send] Error: ${e.message}`);
|
|
607
717
|
}
|
|
608
718
|
}
|
|
609
719
|
|
|
@@ -715,6 +825,7 @@ server.listen(HTTP_PORT, () => {
|
|
|
715
825
|
console.log(` Discovery HTTP scan (no firewall needed)`);
|
|
716
826
|
console.log(` Discovery UDP :${DISCOVERY_PORT} (fallback)`);
|
|
717
827
|
console.log(` Transfer TCP :${TRANSFER_PORT}`);
|
|
828
|
+
console.log(` Encryption ECDH + AES-256-CTR (E2E)`);
|
|
718
829
|
console.log(` UI: http://localhost:${HTTP_PORT}`);
|
|
719
830
|
console.log('');
|
|
720
831
|
|