peer-term 1.2.12 → 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.12",
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) {
@@ -819,6 +821,9 @@ class Session {
819
821
  }
820
822
 
821
823
  _startViewerServer() {
824
+ // Generate a random nonce for viewer authentication
825
+ this.viewerToken = crypto.randomBytes(16).toString('hex');
826
+
822
827
  this.viewerServer = net.createServer((socket) => {
823
828
  // Close any existing viewer connection before accepting a new one
824
829
  if (this.viewerSocket) {
@@ -826,66 +831,119 @@ class Session {
826
831
  this.viewerSocket.removeAllListeners();
827
832
  try { this.viewerSocket.destroy(); } catch {}
828
833
  }
829
- this.viewerSocket = socket;
830
- this.log('Host viewer connected.');
831
-
832
- let recvBuf = '';
833
-
834
- socket.on('data', (data) => {
835
- const chunk = data.toString();
836
-
837
- // Control messages start with \x00{
838
- if (recvBuf.length > 0 || chunk.startsWith('\x00{')) {
839
- recvBuf += chunk;
840
- let newlineIdx;
841
- while ((newlineIdx = recvBuf.indexOf('\n')) !== -1) {
842
- const line = recvBuf.slice(0, newlineIdx).trim();
843
- recvBuf = recvBuf.slice(newlineIdx + 1);
844
-
845
- if (line.startsWith('\x00')) {
846
- try {
847
- const msg = JSON.parse(line.slice(1));
848
- if (msg.type === 'resize' &&
849
- Number.isInteger(msg.cols) && msg.cols > 0 &&
850
- Number.isInteger(msg.rows) && msg.rows > 0 &&
851
- this.ptyProcess) {
852
- this.ptyProcess.resize(msg.cols, msg.rows);
853
- this._sendResize(msg.cols, msg.rows);
854
- this.logDebug(`Viewer resized to ${msg.cols}x${msg.rows}`);
855
- }
856
- } catch {}
857
- } else if (this.ptyProcess) {
858
- this.ptyProcess.write(line + '\n');
859
- }
860
- }
861
- // Flush any non-control data left in the buffer
862
- if (recvBuf.length > 0 && !recvBuf.startsWith('\x00')) {
863
- if (this.ptyProcess) this.ptyProcess.write(recvBuf);
864
- 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 {}
865
857
  }
866
858
  return;
867
859
  }
868
860
 
869
- // Host keystrokes → PTY (host always has access)
870
- if (this.ptyProcess) {
871
- 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;
872
870
  }
873
- });
874
871
 
875
- socket.on('close', () => {
876
- this.viewerSocket = null;
877
- this.logDebug('Host viewer disconnected.');
878
- });
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);
879
937
 
880
938
  socket.on('error', () => {
881
- this.viewerSocket = null;
939
+ clearTimeout(authTimeout);
882
940
  });
883
941
  });
884
942
 
885
943
  this.viewerServer.listen(0, '127.0.0.1', () => {
886
944
  const port = this.viewerServer.address().port;
887
945
  this.logDebug(`Viewer server on port ${port}`);
888
- this._openViewerTerminal(port);
946
+ this._openViewerTerminal(port, this.viewerToken);
889
947
  });
890
948
 
891
949
  this.viewerServer.on('error', (err) => {
@@ -894,7 +952,7 @@ class Session {
894
952
  });
895
953
  }
896
954
 
897
- _openViewerTerminal(port) {
955
+ _openViewerTerminal(port, token) {
898
956
  if (!/^[A-Za-z0-9_-]+$/.test(this.code)) {
899
957
  this.log('Invalid session code format. Aborting viewer terminal.');
900
958
  return;
@@ -904,12 +962,12 @@ class Session {
904
962
  const platform = os.platform();
905
963
 
906
964
  if (platform === 'win32') {
907
- 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}`);
908
966
  } else if (platform === 'darwin') {
909
- 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}"'`);
910
968
  } else {
911
969
  // Linux: try common terminal emulators
912
- 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}"`);
913
971
  }
914
972
  this.log('Opening terminal viewer...');
915
973
  }
@@ -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;