@balaji003/lantransfer 1.0.8 → 1.0.10
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 +64 -1
- package/server.js +138 -16
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -186,6 +186,28 @@
|
|
|
186
186
|
}
|
|
187
187
|
.btn-change-dir:hover { background: var(--surface-hover); border-color: var(--accent); }
|
|
188
188
|
|
|
189
|
+
/* ── Update banner ── */
|
|
190
|
+
.update-banner {
|
|
191
|
+
display: none; align-items: center; justify-content: center; gap: 12px;
|
|
192
|
+
padding: 8px 24px; background: rgba(74, 158, 255, 0.1);
|
|
193
|
+
border-bottom: 1px solid var(--accent); font-size: 13px; flex-shrink: 0;
|
|
194
|
+
}
|
|
195
|
+
.update-banner.show { display: flex; }
|
|
196
|
+
.update-banner span { color: var(--text); }
|
|
197
|
+
.update-banner .version-new { color: var(--green); font-weight: 600; }
|
|
198
|
+
.update-banner .version-old { color: var(--text-dim); }
|
|
199
|
+
.btn-update {
|
|
200
|
+
background: var(--accent); color: #fff; border: none; border-radius: 4px;
|
|
201
|
+
padding: 3px 12px; font-size: 12px; font-weight: 500; cursor: pointer;
|
|
202
|
+
transition: background 0.12s;
|
|
203
|
+
}
|
|
204
|
+
.btn-update:hover { background: var(--accent-dim); }
|
|
205
|
+
.btn-dismiss {
|
|
206
|
+
background: none; border: none; color: var(--text-dim); cursor: pointer;
|
|
207
|
+
font-size: 16px; padding: 0 4px; line-height: 1;
|
|
208
|
+
}
|
|
209
|
+
.btn-dismiss:hover { color: var(--text); }
|
|
210
|
+
|
|
189
211
|
/* ── Transfers ── */
|
|
190
212
|
.transfers-section { flex: 1; overflow-y: auto; padding: 16px 24px; }
|
|
191
213
|
.transfer {
|
|
@@ -284,6 +306,13 @@
|
|
|
284
306
|
</div>
|
|
285
307
|
</div>
|
|
286
308
|
|
|
309
|
+
<!-- Update banner -->
|
|
310
|
+
<div class="update-banner" id="update-banner">
|
|
311
|
+
<span>Update available: <span class="version-old" id="ver-current"></span> → <span class="version-new" id="ver-latest"></span></span>
|
|
312
|
+
<button class="btn-update" id="update-btn" onclick="doUpdate()">Update Now</button>
|
|
313
|
+
<button class="btn-dismiss" onclick="dismissUpdate()" title="Dismiss">×</button>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
287
316
|
<!-- Download location -->
|
|
288
317
|
<div class="download-dir">
|
|
289
318
|
<span>Save to:</span>
|
|
@@ -419,6 +448,28 @@
|
|
|
419
448
|
api('open-folder', { transferId: transferId });
|
|
420
449
|
}
|
|
421
450
|
|
|
451
|
+
let updateDismissed = false;
|
|
452
|
+
|
|
453
|
+
function doUpdate() {
|
|
454
|
+
const btn = document.getElementById('update-btn');
|
|
455
|
+
btn.textContent = 'Updating...';
|
|
456
|
+
btn.disabled = true;
|
|
457
|
+
api('do-update');
|
|
458
|
+
// Server will restart — poll until it's back
|
|
459
|
+
setTimeout(function poll() {
|
|
460
|
+
fetch('/api/ping').then(() => {
|
|
461
|
+
window.location.reload();
|
|
462
|
+
}).catch(() => {
|
|
463
|
+
setTimeout(poll, 1000);
|
|
464
|
+
});
|
|
465
|
+
}, 3000);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function dismissUpdate() {
|
|
469
|
+
updateDismissed = true;
|
|
470
|
+
document.getElementById('update-banner').classList.remove('show');
|
|
471
|
+
}
|
|
472
|
+
|
|
422
473
|
function doShutdown() {
|
|
423
474
|
if (confirm('Stop the server? This will close LAN Transfer.')) {
|
|
424
475
|
api('shutdown');
|
|
@@ -450,6 +501,16 @@
|
|
|
450
501
|
}
|
|
451
502
|
document.getElementById('local-ip').textContent = state.localIP || '';
|
|
452
503
|
|
|
504
|
+
// Update banner
|
|
505
|
+
const banner = document.getElementById('update-banner');
|
|
506
|
+
if (!updateDismissed && state.latestVersion && state.currentVersion && state.latestVersion !== state.currentVersion) {
|
|
507
|
+
document.getElementById('ver-current').textContent = 'v' + state.currentVersion;
|
|
508
|
+
document.getElementById('ver-latest').textContent = 'v' + state.latestVersion;
|
|
509
|
+
banner.classList.add('show');
|
|
510
|
+
} else {
|
|
511
|
+
banner.classList.remove('show');
|
|
512
|
+
}
|
|
513
|
+
|
|
453
514
|
// Download dir
|
|
454
515
|
const dirEl = document.getElementById('download-dir');
|
|
455
516
|
dirEl.textContent = state.downloadDir || '';
|
|
@@ -586,11 +647,13 @@
|
|
|
586
647
|
extra += '</div>';
|
|
587
648
|
}
|
|
588
649
|
|
|
650
|
+
const encrypted = (t.status === 'in_progress' || t.status === 'completed') ? ' \uD83D\uDD12' : '';
|
|
651
|
+
|
|
589
652
|
return '<div class="transfer">'
|
|
590
653
|
+ '<div class="transfer-top">'
|
|
591
654
|
+ '<div class="transfer-info">'
|
|
592
655
|
+ '<span class="transfer-icon">' + icon + '</span>'
|
|
593
|
-
+ '<span>' + esc(t.filename) + ' ' + arrow + ' ' + esc(peer) + '</span>'
|
|
656
|
+
+ '<span>' + esc(t.filename) + ' ' + arrow + ' ' + esc(peer) + encrypted + '</span>'
|
|
594
657
|
+ '</div>'
|
|
595
658
|
+ '<span class="badge ' + badgeClass + '">' + badgeText + '</span>'
|
|
596
659
|
+ '</div>'
|
package/server.js
CHANGED
|
@@ -4,18 +4,19 @@
|
|
|
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.
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
const http = require('http');
|
|
16
|
+
const https = require('https');
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
|
+
const crypto = require('crypto');
|
|
19
20
|
const dgram = require('dgram');
|
|
20
21
|
const net = require('net');
|
|
21
22
|
const os = require('os');
|
|
@@ -27,9 +28,13 @@ const { exec } = require('child_process');
|
|
|
27
28
|
const DISCOVERY_PORT = 34254;
|
|
28
29
|
const TRANSFER_PORT = 34255;
|
|
29
30
|
const HTTP_PORT = 3000;
|
|
30
|
-
const XOR_KEY = Buffer.from('LAN-XFER-KEY-2024');
|
|
31
31
|
const PEER_TIMEOUT = 10_000; // ms
|
|
32
32
|
const BROADCAST_INTERVAL = 3_000; // ms
|
|
33
|
+
const ECDH_CURVE = 'prime256v1'; // NIST P-256
|
|
34
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
35
|
+
const CURRENT_VERSION = PKG.version;
|
|
36
|
+
const PKG_NAME = PKG.name;
|
|
37
|
+
let latestVersion = null; // filled by update check
|
|
33
38
|
|
|
34
39
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
35
40
|
// Persistent config (device name)
|
|
@@ -101,6 +106,8 @@ function serializeState() {
|
|
|
101
106
|
isDiscoverable,
|
|
102
107
|
deviceName,
|
|
103
108
|
downloadDir,
|
|
109
|
+
currentVersion: CURRENT_VERSION,
|
|
110
|
+
latestVersion,
|
|
104
111
|
localIP: getLocalIP(),
|
|
105
112
|
peers: [...peers.entries()].map(([id, p]) => ({
|
|
106
113
|
id, name: p.device_name, ip: p.ip,
|
|
@@ -128,14 +135,18 @@ function broadcast() {
|
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
131
|
-
// Helpers
|
|
138
|
+
// Helpers — E2E encryption (ECDH + AES-256-CTR)
|
|
132
139
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
133
|
-
function
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
function createKeyPair() {
|
|
141
|
+
const ecdh = crypto.createECDH(ECDH_CURVE);
|
|
142
|
+
ecdh.generateKeys();
|
|
143
|
+
return ecdh;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function deriveKey(ecdh, peerPubKey) {
|
|
147
|
+
const shared = ecdh.computeSecret(peerPubKey);
|
|
148
|
+
// SHA-256 the shared secret to get a 32-byte AES key
|
|
149
|
+
return crypto.createHash('sha256').update(shared).digest();
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
function readBody(req) {
|
|
@@ -350,6 +361,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
350
361
|
console.log(` [open] Failed — transferId: ${transferId}, found: ${!!t}, savePath: ${t ? t.savePath : 'N/A'}`);
|
|
351
362
|
}
|
|
352
363
|
}
|
|
364
|
+
else if (req.url === '/api/check-update') {
|
|
365
|
+
checkForUpdate();
|
|
366
|
+
}
|
|
367
|
+
else if (req.url === '/api/do-update') {
|
|
368
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
369
|
+
res.end('ok');
|
|
370
|
+
console.log(` [update] Running: npm install -g ${PKG_NAME}`);
|
|
371
|
+
exec(`npm install -g ${PKG_NAME}`, { timeout: 120_000 }, (err, stdout, stderr) => {
|
|
372
|
+
if (err) {
|
|
373
|
+
console.error(` [update] Failed: ${err.message}`);
|
|
374
|
+
console.error(stderr);
|
|
375
|
+
} else {
|
|
376
|
+
console.log(` [update] Success! Restarting...`);
|
|
377
|
+
console.log(stdout);
|
|
378
|
+
// Restart the process
|
|
379
|
+
setTimeout(() => {
|
|
380
|
+
const args = process.argv.slice(1);
|
|
381
|
+
const child = require('child_process').spawn(process.execPath, args, {
|
|
382
|
+
detached: true,
|
|
383
|
+
stdio: 'ignore',
|
|
384
|
+
});
|
|
385
|
+
child.unref();
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}, 500);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
353
392
|
else if (req.url === '/api/shutdown') {
|
|
354
393
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
355
394
|
res.end('ok');
|
|
@@ -536,12 +575,32 @@ async function handleIncoming(socket) {
|
|
|
536
575
|
t.savePath = savePath;
|
|
537
576
|
console.log(` [recv] Saving to: ${savePath}`);
|
|
538
577
|
|
|
578
|
+
// ── E2E key exchange: receive sender's public key, send ours ──
|
|
579
|
+
const pubKeyLenBuf = await readExact(socket, 2);
|
|
580
|
+
const pubKeyLen = pubKeyLenBuf.readUInt16BE(0);
|
|
581
|
+
const senderPubKey = await readExact(socket, pubKeyLen);
|
|
582
|
+
console.log(` [recv] Got sender public key (${pubKeyLen} bytes)`);
|
|
583
|
+
|
|
584
|
+
const ecdh = createKeyPair();
|
|
585
|
+
const myPubKey = ecdh.getPublicKey();
|
|
586
|
+
const keyLenBuf = Buffer.alloc(2);
|
|
587
|
+
keyLenBuf.writeUInt16BE(myPubKey.length);
|
|
588
|
+
socket.write(keyLenBuf);
|
|
589
|
+
socket.write(myPubKey);
|
|
590
|
+
console.log(` [recv] Sent our public key (${myPubKey.length} bytes)`);
|
|
591
|
+
|
|
592
|
+
// Derive AES key + receive IV
|
|
593
|
+
const aesKey = deriveKey(ecdh, senderPubKey);
|
|
594
|
+
const iv = await readExact(socket, 16);
|
|
595
|
+
console.log(` [recv] AES key derived, IV received — decrypting with AES-256-CTR`);
|
|
596
|
+
|
|
597
|
+
const decipher = crypto.createDecipheriv('aes-256-ctr', aesKey, iv);
|
|
539
598
|
const ws = fs.createWriteStream(savePath);
|
|
540
599
|
let received = 0;
|
|
541
600
|
let lastBc = 0;
|
|
542
601
|
|
|
543
602
|
socket.on('data', chunk => {
|
|
544
|
-
const decrypted =
|
|
603
|
+
const decrypted = decipher.update(chunk);
|
|
545
604
|
ws.write(decrypted);
|
|
546
605
|
received += chunk.length;
|
|
547
606
|
|
|
@@ -558,6 +617,8 @@ async function handleIncoming(socket) {
|
|
|
558
617
|
|
|
559
618
|
await new Promise((resolve, reject) => {
|
|
560
619
|
socket.on('end', () => {
|
|
620
|
+
const final = decipher.final();
|
|
621
|
+
if (final.length > 0) ws.write(final);
|
|
561
622
|
ws.end();
|
|
562
623
|
t.status = 'completed';
|
|
563
624
|
t.progress = 100;
|
|
@@ -595,6 +656,7 @@ async function sendFile(peer, file) {
|
|
|
595
656
|
socket.connect(peer.tcp_port, peer.ip, res);
|
|
596
657
|
socket.once('error', rej);
|
|
597
658
|
});
|
|
659
|
+
console.log(` [send] Connected to ${peer.ip}:${peer.tcp_port}`);
|
|
598
660
|
|
|
599
661
|
// Send header
|
|
600
662
|
const headerJSON = JSON.stringify({
|
|
@@ -613,20 +675,44 @@ async function sendFile(peer, file) {
|
|
|
613
675
|
t.error = 'Rejected by recipient';
|
|
614
676
|
broadcast();
|
|
615
677
|
socket.end();
|
|
678
|
+
console.log(` [send] Rejected by ${peer.device_name}`);
|
|
616
679
|
return;
|
|
617
680
|
}
|
|
618
681
|
|
|
682
|
+
console.log(` [send] Accepted — starting E2E key exchange`);
|
|
683
|
+
|
|
684
|
+
// ── E2E key exchange: send our public key, receive receiver's ──
|
|
685
|
+
const ecdh = createKeyPair();
|
|
686
|
+
const myPubKey = ecdh.getPublicKey();
|
|
687
|
+
const keyLenBuf = Buffer.alloc(2);
|
|
688
|
+
keyLenBuf.writeUInt16BE(myPubKey.length);
|
|
689
|
+
socket.write(keyLenBuf);
|
|
690
|
+
socket.write(myPubKey);
|
|
691
|
+
console.log(` [send] Sent our public key (${myPubKey.length} bytes)`);
|
|
692
|
+
|
|
693
|
+
const recvKeyLenBuf = await readExact(socket, 2);
|
|
694
|
+
const recvKeyLen = recvKeyLenBuf.readUInt16BE(0);
|
|
695
|
+
const recvPubKey = await readExact(socket, recvKeyLen);
|
|
696
|
+
console.log(` [send] Got receiver public key (${recvKeyLen} bytes)`);
|
|
697
|
+
|
|
698
|
+
// Derive AES key, generate IV, send IV
|
|
699
|
+
const aesKey = deriveKey(ecdh, recvPubKey);
|
|
700
|
+
const iv = crypto.randomBytes(16);
|
|
701
|
+
socket.write(iv);
|
|
702
|
+
console.log(` [send] AES key derived, IV sent — encrypting with AES-256-CTR`);
|
|
703
|
+
|
|
619
704
|
t.status = 'in_progress';
|
|
620
705
|
t._start = Date.now();
|
|
621
706
|
broadcast();
|
|
622
707
|
|
|
623
|
-
// Stream file with
|
|
708
|
+
// Stream file with AES-256-CTR encryption
|
|
709
|
+
const cipher = crypto.createCipheriv('aes-256-ctr', aesKey, iv);
|
|
624
710
|
const rs = fs.createReadStream(file.path, { highWaterMark: 65536 });
|
|
625
711
|
let sent = 0;
|
|
626
712
|
let lastBc = 0;
|
|
627
713
|
|
|
628
714
|
for await (const chunk of rs) {
|
|
629
|
-
const encrypted =
|
|
715
|
+
const encrypted = cipher.update(Buffer.from(chunk));
|
|
630
716
|
const ok = socket.write(encrypted);
|
|
631
717
|
sent += chunk.length;
|
|
632
718
|
|
|
@@ -649,19 +735,50 @@ async function sendFile(peer, file) {
|
|
|
649
735
|
}
|
|
650
736
|
}
|
|
651
737
|
|
|
738
|
+
// Write final cipher block
|
|
739
|
+
const final = cipher.final();
|
|
740
|
+
if (final.length > 0) socket.write(final);
|
|
741
|
+
|
|
652
742
|
socket.end();
|
|
653
743
|
t.status = 'completed';
|
|
654
744
|
t.progress = 100;
|
|
655
745
|
broadcast();
|
|
656
|
-
console.log(`
|
|
746
|
+
console.log(` [send] Complete: ${file.name} → ${peer.device_name}`);
|
|
657
747
|
} catch (e) {
|
|
658
748
|
t.status = 'failed';
|
|
659
749
|
t.error = e.message;
|
|
660
750
|
broadcast();
|
|
661
|
-
console.error(`
|
|
751
|
+
console.error(` [send] Error: ${e.message}`);
|
|
662
752
|
}
|
|
663
753
|
}
|
|
664
754
|
|
|
755
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
756
|
+
// Update checker
|
|
757
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
758
|
+
function checkForUpdate() {
|
|
759
|
+
const url = `https://registry.npmjs.org/${PKG_NAME}/latest`;
|
|
760
|
+
console.log(` [update] Checking ${url}`);
|
|
761
|
+
https.get(url, { timeout: 5000 }, res => {
|
|
762
|
+
let body = '';
|
|
763
|
+
res.on('data', c => body += c);
|
|
764
|
+
res.on('end', () => {
|
|
765
|
+
try {
|
|
766
|
+
const data = JSON.parse(body);
|
|
767
|
+
latestVersion = data.version || null;
|
|
768
|
+
console.log(` [update] Current: ${CURRENT_VERSION}, Latest: ${latestVersion}`);
|
|
769
|
+
if (latestVersion && latestVersion !== CURRENT_VERSION) {
|
|
770
|
+
console.log(` [update] Update available! Run: npm install -g ${PKG_NAME}`);
|
|
771
|
+
}
|
|
772
|
+
broadcast();
|
|
773
|
+
} catch (e) {
|
|
774
|
+
console.error(` [update] Parse error: ${e.message}`);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}).on('error', e => {
|
|
778
|
+
console.error(` [update] Check failed: ${e.message}`);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
665
782
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
666
783
|
// Utility
|
|
667
784
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -770,9 +887,14 @@ server.listen(HTTP_PORT, () => {
|
|
|
770
887
|
console.log(` Discovery HTTP scan (no firewall needed)`);
|
|
771
888
|
console.log(` Discovery UDP :${DISCOVERY_PORT} (fallback)`);
|
|
772
889
|
console.log(` Transfer TCP :${TRANSFER_PORT}`);
|
|
890
|
+
console.log(` Encryption ECDH + AES-256-CTR (E2E)`);
|
|
891
|
+
console.log(` Version ${CURRENT_VERSION}`);
|
|
773
892
|
console.log(` UI: http://localhost:${HTTP_PORT}`);
|
|
774
893
|
console.log('');
|
|
775
894
|
|
|
895
|
+
// Check for updates on startup
|
|
896
|
+
checkForUpdate();
|
|
897
|
+
|
|
776
898
|
// Auto-open browser
|
|
777
899
|
const url = `http://localhost:${HTTP_PORT}`;
|
|
778
900
|
if (process.platform === 'win32') exec(`start "" "${url}"`);
|