peer-term 1.2.11 → 1.2.13

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": "peer-term",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "license": "MIT",
5
5
  "description": "Share your terminal instantly using a 6-digit code. Encrypted. No config.",
6
6
  "keywords": [
package/src/index.js CHANGED
@@ -22,6 +22,7 @@
22
22
  import * as dotenv from 'dotenv';
23
23
  import os from 'os';
24
24
  import fs from 'fs';
25
+ import crypto from 'crypto';
25
26
  import path from 'path';
26
27
  import net from 'net';
27
28
  import readline from 'readline';
@@ -338,6 +339,7 @@ class Session {
338
339
  // Local TCP viewer (new terminal window)
339
340
  this.viewerServer = null;
340
341
  this.viewerSocket = null;
342
+ this.viewerToken = null; // Random nonce for viewer auth
341
343
  }
342
344
 
343
345
  log(msg) {
@@ -368,25 +370,54 @@ class Session {
368
370
 
369
371
  async _completeKeyExchange(clientPublicKeyBase64) {
370
372
  if (!this.keyPair || !this.hostPublicKeyBase64) return;
373
+ if (typeof clientPublicKeyBase64 !== 'string' || clientPublicKeyBase64.length === 0) {
374
+ this._rejectProtocolMessage('Invalid key-exchange public key');
375
+ return;
376
+ }
371
377
 
372
- this.logDebug('Deriving shared secret...');
373
- this.clientPublicKeyBase64 = clientPublicKeyBase64;
378
+ try {
379
+ this.logDebug('Deriving shared secret...');
380
+ this.clientPublicKeyBase64 = clientPublicKeyBase64;
381
+
382
+ const peerPublicKey = await importPublicKey(clientPublicKeyBase64);
383
+ this.sharedKey = await deriveSharedKey(this.keyPair.privateKey, peerPublicKey);
384
+
385
+ if (this.secureMode) {
386
+ this.securityFingerprint = await fingerprintPublicKeys(
387
+ this.code,
388
+ this.hostPublicKeyBase64,
389
+ this.clientPublicKeyBase64
390
+ );
391
+ this.fingerprintAuthorized = false;
392
+ this._printFingerprintAuthorization();
393
+ } else {
394
+ this.fingerprintAuthorized = true;
395
+ await this._activateSecureSession();
396
+ }
397
+ } catch (err) {
398
+ this._rejectProtocolMessage(`Invalid key exchange: ${err.message}`);
399
+ }
400
+ }
374
401
 
375
- const peerPublicKey = await importPublicKey(clientPublicKeyBase64);
376
- this.sharedKey = await deriveSharedKey(this.keyPair.privateKey, peerPublicKey);
402
+ _rejectProtocolMessage(reason) {
403
+ this.log(`Protocol error: ${reason}`);
404
+ this.isClientConnected = false;
405
+ this.awaitingRejoin = false;
406
+ this.sharedKey = null;
407
+ this.keyPair = null;
408
+ this.hostPublicKeyBase64 = null;
409
+ this.clientPublicKeyBase64 = null;
410
+ this.securityFingerprint = null;
411
+ this.fingerprintAuthorized = false;
412
+ this.stopHeartbeat();
413
+ this._cleanupWebRTC();
414
+ this.destroy();
415
+ }
377
416
 
378
- if (this.secureMode) {
379
- this.securityFingerprint = await fingerprintPublicKeys(
380
- this.code,
381
- this.hostPublicKeyBase64,
382
- this.clientPublicKeyBase64
383
- );
384
- this.fingerprintAuthorized = false;
385
- this._printFingerprintAuthorization();
386
- } else {
387
- this.fingerprintAuthorized = true;
388
- await this._activateSecureSession();
389
- }
417
+ _handleMessageError(err) {
418
+ const message = err instanceof Error ? err.message : String(err);
419
+ this.log(`Protocol handler error: ${message}`);
420
+ this.destroy();
390
421
  }
391
422
 
392
423
  _printFingerprintAuthorization() {
@@ -481,6 +512,7 @@ class Session {
481
512
  });
482
513
 
483
514
  this.ws.on('message', async (raw) => {
515
+ try {
484
516
  let msg;
485
517
  try {
486
518
  msg = JSON.parse(raw.toString());
@@ -488,6 +520,10 @@ class Session {
488
520
  this.log('Invalid message from relay');
489
521
  return;
490
522
  }
523
+ if (!msg || typeof msg.type !== 'string') {
524
+ this._rejectProtocolMessage('Malformed relay message');
525
+ return;
526
+ }
491
527
 
492
528
  switch (msg.type) {
493
529
  case 'code': {
@@ -614,6 +650,9 @@ class Session {
614
650
  break;
615
651
  }
616
652
  }
653
+ } catch (err) {
654
+ this._handleMessageError(err);
655
+ }
617
656
  });
618
657
 
619
658
  this.ws.on('close', () => {
@@ -782,6 +821,9 @@ class Session {
782
821
  }
783
822
 
784
823
  _startViewerServer() {
824
+ // Generate a random nonce for viewer authentication
825
+ this.viewerToken = crypto.randomBytes(16).toString('hex');
826
+
785
827
  this.viewerServer = net.createServer((socket) => {
786
828
  // Close any existing viewer connection before accepting a new one
787
829
  if (this.viewerSocket) {
@@ -789,66 +831,119 @@ class Session {
789
831
  this.viewerSocket.removeAllListeners();
790
832
  try { this.viewerSocket.destroy(); } catch {}
791
833
  }
792
- this.viewerSocket = socket;
793
- this.log('Host viewer connected.');
794
-
795
- let recvBuf = '';
796
-
797
- socket.on('data', (data) => {
798
- const chunk = data.toString();
799
-
800
- // Control messages start with \x00{
801
- if (recvBuf.length > 0 || chunk.startsWith('\x00{')) {
802
- recvBuf += chunk;
803
- let newlineIdx;
804
- while ((newlineIdx = recvBuf.indexOf('\n')) !== -1) {
805
- const line = recvBuf.slice(0, newlineIdx).trim();
806
- recvBuf = recvBuf.slice(newlineIdx + 1);
807
-
808
- if (line.startsWith('\x00')) {
809
- try {
810
- const msg = JSON.parse(line.slice(1));
811
- if (msg.type === 'resize' &&
812
- Number.isInteger(msg.cols) && msg.cols > 0 &&
813
- Number.isInteger(msg.rows) && msg.rows > 0 &&
814
- this.ptyProcess) {
815
- this.ptyProcess.resize(msg.cols, msg.rows);
816
- this._sendResize(msg.cols, msg.rows);
817
- this.logDebug(`Viewer resized to ${msg.cols}x${msg.rows}`);
818
- }
819
- } catch {}
820
- } else if (this.ptyProcess) {
821
- this.ptyProcess.write(line + '\n');
822
- }
823
- }
824
- // Flush any non-control data left in the buffer
825
- if (recvBuf.length > 0 && !recvBuf.startsWith('\x00')) {
826
- if (this.ptyProcess) this.ptyProcess.write(recvBuf);
827
- recvBuf = '';
834
+
835
+ // ── Token authentication gate ──────────────────────────────────
836
+ // The first data packet must be the viewer token followed by a newline.
837
+ // Reject the connection if no valid token arrives within 2 seconds.
838
+ let authenticated = false;
839
+ let authBuf = '';
840
+
841
+ const authTimeout = setTimeout(() => {
842
+ if (!authenticated) {
843
+ this.logDebug('Viewer auth timeout — closing connection.');
844
+ try { socket.destroy(); } catch {}
845
+ }
846
+ }, 2000);
847
+
848
+ const onAuthData = (data) => {
849
+ authBuf += data.toString();
850
+ const newlineIdx = authBuf.indexOf('\n');
851
+ if (newlineIdx === -1) {
852
+ // Accumulated too much data without a newline — reject
853
+ if (authBuf.length > 256) {
854
+ clearTimeout(authTimeout);
855
+ this.logDebug('Viewer auth buffer overflow — closing connection.');
856
+ try { socket.destroy(); } catch {}
828
857
  }
829
858
  return;
830
859
  }
831
860
 
832
- // Host keystrokes → PTY (host always has access)
833
- if (this.ptyProcess) {
834
- this.ptyProcess.write(data);
861
+ const token = authBuf.slice(0, newlineIdx).trim();
862
+ const remaining = authBuf.slice(newlineIdx + 1);
863
+ clearTimeout(authTimeout);
864
+ socket.removeListener('data', onAuthData);
865
+
866
+ if (token !== this.viewerToken) {
867
+ this.logDebug('Viewer auth failed — wrong token.');
868
+ try { socket.destroy(); } catch {}
869
+ return;
835
870
  }
836
- });
837
871
 
838
- socket.on('close', () => {
839
- this.viewerSocket = null;
840
- this.logDebug('Host viewer disconnected.');
841
- });
872
+ // Authenticated — promote to active viewer
873
+ authenticated = true;
874
+ this.viewerSocket = socket;
875
+ this.log('Host viewer connected (authenticated).');
876
+
877
+ // Install the real data handler
878
+ let recvBuf = '';
879
+
880
+ // Process any leftover data that arrived after the token line
881
+ if (remaining.length > 0) {
882
+ handleViewerData(remaining);
883
+ }
884
+
885
+ socket.on('data', (chunk) => handleViewerData(chunk.toString()));
886
+
887
+ const handleViewerData = (chunk) => {
888
+ // Control messages start with \x00{
889
+ if (recvBuf.length > 0 || chunk.startsWith('\x00{')) {
890
+ recvBuf += chunk;
891
+ let nlIdx;
892
+ while ((nlIdx = recvBuf.indexOf('\n')) !== -1) {
893
+ const line = recvBuf.slice(0, nlIdx).trim();
894
+ recvBuf = recvBuf.slice(nlIdx + 1);
895
+
896
+ if (line.startsWith('\x00')) {
897
+ try {
898
+ const msg = JSON.parse(line.slice(1));
899
+ if (msg.type === 'resize' &&
900
+ Number.isInteger(msg.cols) && msg.cols > 0 &&
901
+ Number.isInteger(msg.rows) && msg.rows > 0 &&
902
+ this.ptyProcess) {
903
+ this.ptyProcess.resize(msg.cols, msg.rows);
904
+ this._sendResize(msg.cols, msg.rows);
905
+ this.logDebug(`Viewer resized to ${msg.cols}x${msg.rows}`);
906
+ }
907
+ } catch {}
908
+ } else if (this.ptyProcess) {
909
+ this.ptyProcess.write(line + '\n');
910
+ }
911
+ }
912
+ // Flush any non-control data left in the buffer
913
+ if (recvBuf.length > 0 && !recvBuf.startsWith('\x00')) {
914
+ if (this.ptyProcess) this.ptyProcess.write(recvBuf);
915
+ recvBuf = '';
916
+ }
917
+ return;
918
+ }
919
+
920
+ // Host keystrokes → PTY (host always has access)
921
+ if (this.ptyProcess) {
922
+ this.ptyProcess.write(chunk);
923
+ }
924
+ };
925
+
926
+ socket.on('close', () => {
927
+ this.viewerSocket = null;
928
+ this.logDebug('Host viewer disconnected.');
929
+ });
930
+
931
+ socket.on('error', () => {
932
+ this.viewerSocket = null;
933
+ });
934
+ };
935
+
936
+ socket.on('data', onAuthData);
842
937
 
843
938
  socket.on('error', () => {
844
- this.viewerSocket = null;
939
+ clearTimeout(authTimeout);
845
940
  });
846
941
  });
847
942
 
848
943
  this.viewerServer.listen(0, '127.0.0.1', () => {
849
944
  const port = this.viewerServer.address().port;
850
945
  this.logDebug(`Viewer server on port ${port}`);
851
- this._openViewerTerminal(port);
946
+ this._openViewerTerminal(port, this.viewerToken);
852
947
  });
853
948
 
854
949
  this.viewerServer.on('error', (err) => {
@@ -857,7 +952,7 @@ class Session {
857
952
  });
858
953
  }
859
954
 
860
- _openViewerTerminal(port) {
955
+ _openViewerTerminal(port, token) {
861
956
  if (!/^[A-Za-z0-9_-]+$/.test(this.code)) {
862
957
  this.log('Invalid session code format. Aborting viewer terminal.');
863
958
  return;
@@ -867,12 +962,12 @@ class Session {
867
962
  const platform = os.platform();
868
963
 
869
964
  if (platform === 'win32') {
870
- cpExec(`start "PeerTerm - ${this.code}" cmd /c node "${viewerScript}" ${port} ${this.code}`);
965
+ cpExec(`start "PeerTerm - ${this.code}" cmd /c node "${viewerScript}" ${port} ${this.code} ${token}`);
871
966
  } else if (platform === 'darwin') {
872
- cpExec(`osascript -e 'tell app "Terminal" to do script "node \"${viewerScript}\" ${port} ${this.code}"'`);
967
+ cpExec(`osascript -e 'tell app "Terminal" to do script "node \"${viewerScript}\" ${port} ${this.code} ${token}"'`);
873
968
  } else {
874
969
  // Linux: try common terminal emulators
875
- cpExec(`x-terminal-emulator -e "node '${viewerScript}' ${port} ${this.code}" 2>/dev/null || gnome-terminal -- node "${viewerScript}" ${port} ${this.code} 2>/dev/null || xterm -e "node '${viewerScript}' ${port} ${this.code}"`);
970
+ cpExec(`x-terminal-emulator -e "node '${viewerScript}' ${port} ${this.code} ${token}" 2>/dev/null || gnome-terminal -- node "${viewerScript}" ${port} ${this.code} ${token} 2>/dev/null || xterm -e "node '${viewerScript}' ${port} ${this.code} ${token}"`);
876
971
  }
877
972
  this.log('Opening terminal viewer...');
878
973
  }
@@ -910,12 +1005,17 @@ class Session {
910
1005
  });
911
1006
 
912
1007
  newWs.on('message', async (raw) => {
1008
+ try {
913
1009
  let msg;
914
1010
  try {
915
1011
  msg = JSON.parse(raw.toString());
916
1012
  } catch {
917
1013
  return;
918
1014
  }
1015
+ if (!msg || typeof msg.type !== 'string') {
1016
+ this._rejectProtocolMessage('Malformed relay message');
1017
+ return;
1018
+ }
919
1019
 
920
1020
  if (msg.type === 'rejoined') {
921
1021
  // Success — replace the old ws with this new one
@@ -935,6 +1035,9 @@ class Session {
935
1035
  this.destroy();
936
1036
  try { newWs.close(); } catch {}
937
1037
  }
1038
+ } catch (err) {
1039
+ this._handleMessageError(err);
1040
+ }
938
1041
  });
939
1042
 
940
1043
  newWs.on('error', () => {
@@ -970,6 +1073,7 @@ class Session {
970
1073
  */
971
1074
  _attachWsHandlers(newWs) {
972
1075
  newWs.on('message', async (raw) => {
1076
+ try {
973
1077
  let msg;
974
1078
  try {
975
1079
  msg = JSON.parse(raw.toString());
@@ -977,6 +1081,10 @@ class Session {
977
1081
  this.log('Invalid message from relay');
978
1082
  return;
979
1083
  }
1084
+ if (!msg || typeof msg.type !== 'string') {
1085
+ this._rejectProtocolMessage('Malformed relay message');
1086
+ return;
1087
+ }
980
1088
 
981
1089
  switch (msg.type) {
982
1090
  case 'client-connected': {
@@ -1084,6 +1192,9 @@ class Session {
1084
1192
  break;
1085
1193
  }
1086
1194
  }
1195
+ } catch (err) {
1196
+ this._handleMessageError(err);
1197
+ }
1087
1198
  });
1088
1199
 
1089
1200
  newWs.on('close', () => {
@@ -1414,7 +1525,6 @@ process.on('uncaughtException', (err) => {
1414
1525
  process.on('unhandledRejection', (reason) => {
1415
1526
  const err = reason instanceof Error ? reason : new Error(String(reason));
1416
1527
  logger.error(`Unhandled rejection: ${err.message}`, err);
1417
- process.exit(1);
1418
1528
  });
1419
1529
 
1420
1530
  // ─── Run ─────────────────────────────────────────────────────────────────────
@@ -4,14 +4,23 @@
4
4
  *
5
5
  * Opens in a new terminal window and connects to the host's PTY
6
6
  * via a local TCP socket. Gives the host a direct terminal view.
7
+ *
8
+ * Usage: session-viewer.js <port> <code> <token>
9
+ * The token is a random nonce generated by the host for authentication.
7
10
  */
8
11
 
9
12
  import net from 'net';
10
13
 
11
- const port = parseInt(process.argv[2], 10);
14
+ const port = parseInt(process.argv[2], 10);
15
+ const token = process.argv[4] || '';
12
16
 
13
17
  if (!port || isNaN(port) || port < 1 || port > 65535) {
14
- console.error('Usage: session-viewer.js <port>\nPort must be a number between 1 and 65535.');
18
+ console.error('Usage: session-viewer.js <port> <code> <token>\nPort must be a number between 1 and 65535.');
19
+ process.exit(1);
20
+ }
21
+
22
+ if (!token) {
23
+ console.error('Missing viewer token. Cannot authenticate.');
15
24
  process.exit(1);
16
25
  }
17
26
 
@@ -33,6 +42,10 @@ function formatResizeMessage(cols, rows) {
33
42
 
34
43
  const client = net.connect({ port, host: '127.0.0.1' }, () => {
35
44
  client.setTimeout(0); // Clear timeout once connected
45
+
46
+ // Send the authentication token as the very first message
47
+ client.write(token + '\n');
48
+
36
49
  // Send initial terminal size
37
50
  const cols = process.stdout.isTTY ? (process.stdout.columns || 80) : 80;
38
51
  const rows = process.stdout.isTTY ? (process.stdout.rows || 24) : 24;