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 +1 -1
- package/src/index.js +108 -50
- package/src/session-viewer.js +15 -2
package/package.json
CHANGED
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
this.
|
|
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
|
-
|
|
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
|
}
|
package/src/session-viewer.js
CHANGED
|
@@ -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
|
|
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;
|