peer-term 1.2.14 → 1.3.0
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 +145 -2
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -336,6 +336,10 @@ class Session {
|
|
|
336
336
|
this.webrtc = null;
|
|
337
337
|
this.useDataChannel = false;
|
|
338
338
|
|
|
339
|
+
// File upload: in-progress file transfers from client
|
|
340
|
+
// Shape: { [id]: { name, size, totalChunks, chunks: Map<index, Buffer> } }
|
|
341
|
+
this._incomingFiles = {};
|
|
342
|
+
|
|
339
343
|
// Local TCP viewer (new terminal window)
|
|
340
344
|
this.viewerServer = null;
|
|
341
345
|
this.viewerSocket = null;
|
|
@@ -820,6 +824,129 @@ class Session {
|
|
|
820
824
|
}
|
|
821
825
|
}
|
|
822
826
|
|
|
827
|
+
// ─── File Upload Handling ─────────────────────────────────────────────
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Route file transfer messages (file-start, file-chunk, file-end).
|
|
831
|
+
* All file messages arrive over the DataChannel only.
|
|
832
|
+
*/
|
|
833
|
+
_handleFileMessage(msg) {
|
|
834
|
+
switch (msg.type) {
|
|
835
|
+
case 'file-start': {
|
|
836
|
+
// Validate required fields
|
|
837
|
+
if (!msg.id || !msg.name || !msg.size || !msg.totalChunks) {
|
|
838
|
+
this._sendFileError(msg.id, 'Invalid file-start message');
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
this.log(`File upload started: "${msg.name}" (${msg.size} bytes, ${msg.totalChunks} chunks)`);
|
|
842
|
+
this._incomingFiles[msg.id] = {
|
|
843
|
+
name: msg.name,
|
|
844
|
+
size: msg.size,
|
|
845
|
+
totalChunks: msg.totalChunks,
|
|
846
|
+
chunks: new Map(),
|
|
847
|
+
};
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
case 'file-chunk': {
|
|
851
|
+
const transfer = this._incomingFiles[msg.id];
|
|
852
|
+
if (!transfer) {
|
|
853
|
+
this._sendFileError(msg.id, 'Unknown transfer ID');
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
// data is base64-encoded raw bytes
|
|
857
|
+
transfer.chunks.set(msg.index, Buffer.from(msg.data, 'base64'));
|
|
858
|
+
this.logDebug(`File chunk ${msg.index + 1}/${transfer.totalChunks} received for "${transfer.name}"`);
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
case 'file-end': {
|
|
862
|
+
this._finalizeFile(msg.id);
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Reassemble all chunks in order and write the completed file to disk.
|
|
870
|
+
* Saves to ~/peerterm-uploads/<filename>.
|
|
871
|
+
*/
|
|
872
|
+
async _finalizeFile(id) {
|
|
873
|
+
const transfer = this._incomingFiles[id];
|
|
874
|
+
if (!transfer) {
|
|
875
|
+
this._sendFileError(id, 'Unknown transfer ID');
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Check all chunks are present
|
|
880
|
+
for (let i = 0; i < transfer.totalChunks; i++) {
|
|
881
|
+
if (!transfer.chunks.has(i)) {
|
|
882
|
+
this.log(`File "${transfer.name}" missing chunk ${i}`);
|
|
883
|
+
this._sendFileError(id, `Missing chunk ${i}`);
|
|
884
|
+
delete this._incomingFiles[id];
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Reassemble in order using Buffer.concat
|
|
890
|
+
const ordered = [];
|
|
891
|
+
for (let i = 0; i < transfer.totalChunks; i++) {
|
|
892
|
+
ordered.push(transfer.chunks.get(i));
|
|
893
|
+
}
|
|
894
|
+
const fileData = Buffer.concat(ordered);
|
|
895
|
+
|
|
896
|
+
// Determine save path: ~/peerterm-uploads/<filename>
|
|
897
|
+
const homeDir = os.homedir();
|
|
898
|
+
const uploadDir = path.join(homeDir, 'peerterm-uploads');
|
|
899
|
+
const savePath = path.join(uploadDir, transfer.name);
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
903
|
+
await fs.promises.writeFile(savePath, fileData);
|
|
904
|
+
|
|
905
|
+
this.log(`File saved: ${savePath}`);
|
|
906
|
+
|
|
907
|
+
// Write green ANSI message into the PTY to show the save path
|
|
908
|
+
if (this.ptyProcess) {
|
|
909
|
+
this.ptyProcess.write(`\r\n\x1b[32m[PeerTerm] File saved: ${savePath}\x1b[0m\r\n`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Send ack to client
|
|
913
|
+
this._sendFileAck(id, transfer.name);
|
|
914
|
+
} catch (err) {
|
|
915
|
+
this.log(`Failed to save file "${transfer.name}": ${err.message}`);
|
|
916
|
+
this._sendFileError(id, `Save failed: ${err.message}`);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
delete this._incomingFiles[id];
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Send file-ack to the client confirming successful file save.
|
|
924
|
+
*/
|
|
925
|
+
async _sendFileAck(id, name) {
|
|
926
|
+
if (!this.sharedKey) return;
|
|
927
|
+
try {
|
|
928
|
+
const ackJson = JSON.stringify({ type: 'file-ack', id, name });
|
|
929
|
+
const payload = await encrypt(this.sharedKey, ackJson);
|
|
930
|
+
this._sendEncryptedToClient(payload);
|
|
931
|
+
} catch (err) {
|
|
932
|
+
this.log(`Failed to send file-ack: ${err.message}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Send file-error to the client with an error message.
|
|
938
|
+
*/
|
|
939
|
+
async _sendFileError(id, msg) {
|
|
940
|
+
if (!this.sharedKey) return;
|
|
941
|
+
try {
|
|
942
|
+
const errJson = JSON.stringify({ type: 'file-error', id, msg });
|
|
943
|
+
const payload = await encrypt(this.sharedKey, errJson);
|
|
944
|
+
this._sendEncryptedToClient(payload);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
this.log(`Failed to send file-error: ${err.message}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
823
950
|
_startViewerServer() {
|
|
824
951
|
// Generate a random nonce for viewer authentication
|
|
825
952
|
this.viewerToken = crypto.randomBytes(16).toString('hex');
|
|
@@ -1221,6 +1348,8 @@ class Session {
|
|
|
1221
1348
|
this.intentionalClose = true;
|
|
1222
1349
|
this.stopHeartbeat();
|
|
1223
1350
|
this._stopReconnecting();
|
|
1351
|
+
// Clean up in-progress file uploads
|
|
1352
|
+
this._incomingFiles = {};
|
|
1224
1353
|
// Clean up viewer
|
|
1225
1354
|
if (this.viewerSocket) {
|
|
1226
1355
|
try { this.viewerSocket.destroy(); } catch {}
|
|
@@ -1267,7 +1396,7 @@ class Session {
|
|
|
1267
1396
|
});
|
|
1268
1397
|
|
|
1269
1398
|
this.webrtc.onMessage(async (data) => {
|
|
1270
|
-
if (!this.sharedKey
|
|
1399
|
+
if (!this.sharedKey) return;
|
|
1271
1400
|
try {
|
|
1272
1401
|
// Check if this is a tagged message (resize)
|
|
1273
1402
|
let parsed;
|
|
@@ -1286,8 +1415,22 @@ class Session {
|
|
|
1286
1415
|
return;
|
|
1287
1416
|
}
|
|
1288
1417
|
|
|
1289
|
-
// Normal encrypted
|
|
1418
|
+
// Normal encrypted data — decrypt first
|
|
1290
1419
|
const plaintext = await decrypt(this.sharedKey, data);
|
|
1420
|
+
|
|
1421
|
+
// Check if decrypted message is a file transfer message (file-start, file-chunk, file-end)
|
|
1422
|
+
try {
|
|
1423
|
+
const msg = JSON.parse(plaintext);
|
|
1424
|
+
if (msg && typeof msg.type === 'string' && msg.type.startsWith('file-')) {
|
|
1425
|
+
this._handleFileMessage(msg);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
} catch {
|
|
1429
|
+
// Not JSON — normal keystroke data, fall through
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Normal keystroke — drop if read-only or no PTY
|
|
1433
|
+
if (!this.ptyProcess) return;
|
|
1291
1434
|
if (this.readOnly) return;
|
|
1292
1435
|
this.ptyProcess.write(plaintext);
|
|
1293
1436
|
} catch (err) {
|