peer-term 1.3.0 → 1.3.2
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 +79 -4
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -99,6 +99,10 @@ const RELAY_URLS = argv.relay
|
|
|
99
99
|
const HEARTBEAT_INTERVAL_MS = 5000;
|
|
100
100
|
const MAX_MISSED_PINGS = 2;
|
|
101
101
|
|
|
102
|
+
const UPLOAD_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB max
|
|
103
|
+
const UPLOAD_MAX_CHUNKS_PER_FILE = Math.ceil(UPLOAD_MAX_FILE_SIZE / 16384);
|
|
104
|
+
const UPLOAD_MAX_CONCURRENT = 3;
|
|
105
|
+
|
|
102
106
|
// ─── Path Resolution ─────────────────────────────────────────────────────────
|
|
103
107
|
|
|
104
108
|
function expandTilde(inputPath) {
|
|
@@ -605,6 +609,17 @@ class Session {
|
|
|
605
609
|
return;
|
|
606
610
|
}
|
|
607
611
|
|
|
612
|
+
// Check if decrypted message is a file transfer message (file-start, file-chunk, file-end)
|
|
613
|
+
try {
|
|
614
|
+
const parsedMsg = JSON.parse(plaintext);
|
|
615
|
+
if (parsedMsg && typeof parsedMsg.type === 'string' && parsedMsg.type.startsWith('file-')) {
|
|
616
|
+
this._handleFileMessage(parsedMsg);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
} catch {
|
|
620
|
+
// Not JSON — normal keystroke data, fall through
|
|
621
|
+
}
|
|
622
|
+
|
|
608
623
|
// Normal keystroke — drop if read-only
|
|
609
624
|
if (this.readOnly) return;
|
|
610
625
|
this.ptyProcess.write(plaintext);
|
|
@@ -834,15 +849,32 @@ class Session {
|
|
|
834
849
|
switch (msg.type) {
|
|
835
850
|
case 'file-start': {
|
|
836
851
|
// Validate required fields
|
|
837
|
-
if (!msg.id || !msg.name ||
|
|
852
|
+
if (!msg.id || !msg.name || msg.size === undefined || msg.totalChunks === undefined) {
|
|
838
853
|
this._sendFileError(msg.id, 'Invalid file-start message');
|
|
839
854
|
return;
|
|
840
855
|
}
|
|
856
|
+
|
|
857
|
+
if (msg.size > UPLOAD_MAX_FILE_SIZE) {
|
|
858
|
+
this._sendFileError(msg.id, `File exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (msg.totalChunks > UPLOAD_MAX_CHUNKS_PER_FILE) {
|
|
863
|
+
this._sendFileError(msg.id, 'File requires too many chunks');
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (Object.keys(this._incomingFiles).length >= UPLOAD_MAX_CONCURRENT) {
|
|
868
|
+
this._sendFileError(msg.id, 'Too many concurrent uploads');
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
841
872
|
this.log(`File upload started: "${msg.name}" (${msg.size} bytes, ${msg.totalChunks} chunks)`);
|
|
842
873
|
this._incomingFiles[msg.id] = {
|
|
843
874
|
name: msg.name,
|
|
844
875
|
size: msg.size,
|
|
845
876
|
totalChunks: msg.totalChunks,
|
|
877
|
+
receivedSize: 0,
|
|
846
878
|
chunks: new Map(),
|
|
847
879
|
};
|
|
848
880
|
break;
|
|
@@ -853,8 +885,23 @@ class Session {
|
|
|
853
885
|
this._sendFileError(msg.id, 'Unknown transfer ID');
|
|
854
886
|
return;
|
|
855
887
|
}
|
|
856
|
-
|
|
857
|
-
|
|
888
|
+
|
|
889
|
+
const chunkData = Buffer.from(msg.data, 'base64');
|
|
890
|
+
|
|
891
|
+
if (transfer.chunks.size >= transfer.totalChunks) {
|
|
892
|
+
this._sendFileError(msg.id, 'Too many chunks received');
|
|
893
|
+
delete this._incomingFiles[msg.id];
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (transfer.receivedSize + chunkData.length > transfer.size) {
|
|
898
|
+
this._sendFileError(msg.id, 'Received size exceeds declared size');
|
|
899
|
+
delete this._incomingFiles[msg.id];
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
transfer.chunks.set(msg.index, chunkData);
|
|
904
|
+
transfer.receivedSize += chunkData.length;
|
|
858
905
|
this.logDebug(`File chunk ${msg.index + 1}/${transfer.totalChunks} received for "${transfer.name}"`);
|
|
859
906
|
break;
|
|
860
907
|
}
|
|
@@ -862,6 +909,13 @@ class Session {
|
|
|
862
909
|
this._finalizeFile(msg.id);
|
|
863
910
|
break;
|
|
864
911
|
}
|
|
912
|
+
case 'file-cancel': {
|
|
913
|
+
if (this._incomingFiles[msg.id]) {
|
|
914
|
+
delete this._incomingFiles[msg.id];
|
|
915
|
+
this.log(`File upload cancelled by client: ${msg.id}`);
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
865
919
|
}
|
|
866
920
|
}
|
|
867
921
|
|
|
@@ -896,7 +950,17 @@ class Session {
|
|
|
896
950
|
// Determine save path: ~/peerterm-uploads/<filename>
|
|
897
951
|
const homeDir = os.homedir();
|
|
898
952
|
const uploadDir = path.join(homeDir, 'peerterm-uploads');
|
|
899
|
-
|
|
953
|
+
// Sanitize filename to prevent path traversal (e.g., "../../../etc/passwd")
|
|
954
|
+
const baseName = path.basename(transfer.name);
|
|
955
|
+
if (!baseName || baseName === '.' || baseName === '..') {
|
|
956
|
+
this._sendFileError(id, 'Invalid filename');
|
|
957
|
+
delete this._incomingFiles[id];
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const savePath = path.join(uploadDir, baseName);
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
900
964
|
|
|
901
965
|
try {
|
|
902
966
|
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
@@ -1272,6 +1336,17 @@ class Session {
|
|
|
1272
1336
|
return;
|
|
1273
1337
|
}
|
|
1274
1338
|
|
|
1339
|
+
// Check if decrypted message is a file transfer message
|
|
1340
|
+
try {
|
|
1341
|
+
const parsedMsg = JSON.parse(plaintext);
|
|
1342
|
+
if (parsedMsg && typeof parsedMsg.type === 'string' && parsedMsg.type.startsWith('file-')) {
|
|
1343
|
+
this._handleFileMessage(parsedMsg);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
} catch {
|
|
1347
|
+
// Not JSON — normal keystroke data, fall through
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1275
1350
|
if (this.readOnly) return;
|
|
1276
1351
|
this.ptyProcess.write(plaintext);
|
|
1277
1352
|
} catch (err) {
|