@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balaji003/lantransfer",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "LAN File Transfer — peer-to-peer file sharing over local network. Zero dependencies.",
5
5
  "main": "server.js",
6
6
  "bin": {
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">&times;</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
- * Protocol-compatible with the original Rust version:
8
- * - UDP discovery on port 34254
7
+ * - HTTP + UDP discovery
9
8
  * - TCP file transfer on port 34255
10
- * - XOR obfuscation with key "LAN-XFER-KEY-2024"
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 xorCrypt(buf, offset) {
134
- const out = Buffer.allocUnsafe(buf.length);
135
- for (let i = 0; i < buf.length; i++) {
136
- out[i] = buf[i] ^ XOR_KEY[(offset + i) % XOR_KEY.length];
137
- }
138
- return out;
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 = xorCrypt(chunk, received);
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 XOR encryption
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 = xorCrypt(Buffer.from(chunk), sent);
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(` Sent: ${file.name} → ${peer.device_name}`);
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(` Send error: ${e.message}`);
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}"`);