@dropgate/core 3.0.0 → 3.0.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/LICENSE +201 -0
- package/README.md +4 -4
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +97 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +97 -6
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +97 -6
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.js +97 -6
- package/dist/p2p/index.js.map +1 -1
- package/package.json +2 -2
package/dist/p2p/index.cjs
CHANGED
|
@@ -216,6 +216,7 @@ var P2P_END_ACK_RETRY_DELAY_MS = 100;
|
|
|
216
216
|
var P2P_CLOSE_GRACE_PERIOD_MS = 2e3;
|
|
217
217
|
|
|
218
218
|
// src/p2p/send.ts
|
|
219
|
+
var P2P_UNACKED_CHUNK_TIMEOUT_MS = 3e4;
|
|
219
220
|
function generateSessionId() {
|
|
220
221
|
return crypto.randomUUID();
|
|
221
222
|
}
|
|
@@ -305,6 +306,10 @@ async function startP2PSend(opts) {
|
|
|
305
306
|
const unackedChunks = /* @__PURE__ */ new Map();
|
|
306
307
|
let nextSeq = 0;
|
|
307
308
|
let ackResolvers = [];
|
|
309
|
+
let transferEverStarted = false;
|
|
310
|
+
const connectionAttempts = [];
|
|
311
|
+
const MAX_CONNECTION_ATTEMPTS = 10;
|
|
312
|
+
const CONNECTION_RATE_WINDOW_MS = 1e4;
|
|
308
313
|
const transitionTo = (newState) => {
|
|
309
314
|
if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
|
|
310
315
|
console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
|
|
@@ -415,6 +420,12 @@ async function startP2PSend(opts) {
|
|
|
415
420
|
const sendChunk = async (conn, data, offset, fileTotal) => {
|
|
416
421
|
if (chunkAcknowledgments) {
|
|
417
422
|
while (unackedChunks.size >= maxUnackedChunks) {
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
for (const [_seq, chunk] of unackedChunks) {
|
|
425
|
+
if (now - chunk.sentAt > P2P_UNACKED_CHUNK_TIMEOUT_MS) {
|
|
426
|
+
throw new DropgateNetworkError("Receiver stopped acknowledging chunks");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
418
429
|
await Promise.race([
|
|
419
430
|
waitForAck(),
|
|
420
431
|
sleep(1e3)
|
|
@@ -471,6 +482,23 @@ async function startP2PSend(opts) {
|
|
|
471
482
|
};
|
|
472
483
|
peer.on("connection", (conn) => {
|
|
473
484
|
if (isStopped()) return;
|
|
485
|
+
const now = Date.now();
|
|
486
|
+
while (connectionAttempts.length > 0 && connectionAttempts[0] < now - CONNECTION_RATE_WINDOW_MS) {
|
|
487
|
+
connectionAttempts.shift();
|
|
488
|
+
}
|
|
489
|
+
if (connectionAttempts.length >= MAX_CONNECTION_ATTEMPTS) {
|
|
490
|
+
console.warn("[P2P Send] Connection rate limit exceeded, rejecting connection");
|
|
491
|
+
try {
|
|
492
|
+
conn.send({ t: "error", message: "Too many connection attempts. Please wait." });
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
conn.close();
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
connectionAttempts.push(now);
|
|
474
502
|
if (activeConn) {
|
|
475
503
|
const isOldConnOpen = activeConn.open !== false;
|
|
476
504
|
if (isOldConnOpen && state === "transferring") {
|
|
@@ -489,6 +517,17 @@ async function startP2PSend(opts) {
|
|
|
489
517
|
} catch {
|
|
490
518
|
}
|
|
491
519
|
activeConn = null;
|
|
520
|
+
if (transferEverStarted) {
|
|
521
|
+
try {
|
|
522
|
+
conn.send({ t: "error", message: "Transfer already started with another receiver. Cannot reconnect." });
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
conn.close();
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
492
531
|
state = "listening";
|
|
493
532
|
sentBytes = 0;
|
|
494
533
|
nextSeq = 0;
|
|
@@ -618,6 +657,7 @@ async function startP2PSend(opts) {
|
|
|
618
657
|
}, heartbeatIntervalMs);
|
|
619
658
|
}
|
|
620
659
|
transitionTo("transferring");
|
|
660
|
+
transferEverStarted = true;
|
|
621
661
|
let overallSentBytes = 0;
|
|
622
662
|
for (let fi = 0; fi < files.length; fi++) {
|
|
623
663
|
const currentFile = files[fi];
|
|
@@ -789,6 +829,10 @@ async function startP2PReceive(opts) {
|
|
|
789
829
|
let fileList = null;
|
|
790
830
|
let currentFileReceived = 0;
|
|
791
831
|
let totalReceivedAllFiles = 0;
|
|
832
|
+
let expectedChunkSeq = 0;
|
|
833
|
+
let writeQueueDepth = 0;
|
|
834
|
+
const MAX_WRITE_QUEUE_DEPTH = 100;
|
|
835
|
+
const MAX_FILE_COUNT = 1e4;
|
|
792
836
|
const transitionTo = (newState) => {
|
|
793
837
|
if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
|
|
794
838
|
console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
|
|
@@ -889,8 +933,16 @@ async function startP2PReceive(opts) {
|
|
|
889
933
|
});
|
|
890
934
|
conn.on("data", async (data) => {
|
|
891
935
|
try {
|
|
892
|
-
resetWatchdog();
|
|
893
936
|
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
|
|
937
|
+
if (state !== "transferring") {
|
|
938
|
+
throw new DropgateValidationError(
|
|
939
|
+
"Received binary data before transfer was accepted. Possible malicious sender."
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
resetWatchdog();
|
|
943
|
+
if (writeQueueDepth >= MAX_WRITE_QUEUE_DEPTH) {
|
|
944
|
+
throw new DropgateNetworkError("Write queue overflow - receiver cannot keep up");
|
|
945
|
+
}
|
|
894
946
|
let bufPromise;
|
|
895
947
|
if (data instanceof ArrayBuffer) {
|
|
896
948
|
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
@@ -904,9 +956,22 @@ async function startP2PReceive(opts) {
|
|
|
904
956
|
return;
|
|
905
957
|
}
|
|
906
958
|
const chunkSeq = pendingChunk?.seq ?? -1;
|
|
959
|
+
const expectedSize = pendingChunk?.size;
|
|
907
960
|
pendingChunk = null;
|
|
961
|
+
writeQueueDepth++;
|
|
908
962
|
writeQueue = writeQueue.then(async () => {
|
|
909
963
|
const buf = await bufPromise;
|
|
964
|
+
if (expectedSize !== void 0 && buf.byteLength !== expectedSize) {
|
|
965
|
+
throw new DropgateValidationError(
|
|
966
|
+
`Chunk size mismatch: expected ${expectedSize}, got ${buf.byteLength}`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
const newReceived = received + buf.byteLength;
|
|
970
|
+
if (total > 0 && newReceived > total) {
|
|
971
|
+
throw new DropgateValidationError(
|
|
972
|
+
`Received more data than expected: ${newReceived} > ${total}`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
910
975
|
if (onData) {
|
|
911
976
|
await onData(buf);
|
|
912
977
|
}
|
|
@@ -928,6 +993,8 @@ async function startP2PReceive(opts) {
|
|
|
928
993
|
} catch {
|
|
929
994
|
}
|
|
930
995
|
safeError(err);
|
|
996
|
+
}).finally(() => {
|
|
997
|
+
writeQueueDepth--;
|
|
931
998
|
});
|
|
932
999
|
return;
|
|
933
1000
|
}
|
|
@@ -939,10 +1006,21 @@ async function startP2PReceive(opts) {
|
|
|
939
1006
|
transitionTo("negotiating");
|
|
940
1007
|
onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
|
|
941
1008
|
break;
|
|
942
|
-
case "file_list":
|
|
943
|
-
|
|
944
|
-
|
|
1009
|
+
case "file_list": {
|
|
1010
|
+
const fileListMsg = msg;
|
|
1011
|
+
if (fileListMsg.fileCount > MAX_FILE_COUNT) {
|
|
1012
|
+
throw new DropgateValidationError(`Too many files: ${fileListMsg.fileCount}`);
|
|
1013
|
+
}
|
|
1014
|
+
const sumSize = fileListMsg.files.reduce((sum, f) => sum + f.size, 0);
|
|
1015
|
+
if (sumSize !== fileListMsg.totalSize) {
|
|
1016
|
+
throw new DropgateValidationError(
|
|
1017
|
+
`File list size mismatch: declared ${fileListMsg.totalSize}, actual sum ${sumSize}`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
fileList = fileListMsg;
|
|
1021
|
+
total = fileListMsg.totalSize;
|
|
945
1022
|
break;
|
|
1023
|
+
}
|
|
946
1024
|
case "meta": {
|
|
947
1025
|
if (state !== "negotiating" && !(state === "transferring" && fileList)) {
|
|
948
1026
|
return;
|
|
@@ -1004,9 +1082,22 @@ async function startP2PReceive(opts) {
|
|
|
1004
1082
|
}
|
|
1005
1083
|
break;
|
|
1006
1084
|
}
|
|
1007
|
-
case "chunk":
|
|
1008
|
-
|
|
1085
|
+
case "chunk": {
|
|
1086
|
+
const chunkMsg = msg;
|
|
1087
|
+
if (state !== "transferring") {
|
|
1088
|
+
throw new DropgateValidationError(
|
|
1089
|
+
"Received chunk message before transfer was accepted."
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
if (chunkMsg.seq !== expectedChunkSeq) {
|
|
1093
|
+
throw new DropgateValidationError(
|
|
1094
|
+
`Chunk sequence error: expected ${expectedChunkSeq}, got ${chunkMsg.seq}`
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
expectedChunkSeq++;
|
|
1098
|
+
pendingChunk = chunkMsg;
|
|
1009
1099
|
break;
|
|
1100
|
+
}
|
|
1010
1101
|
case "ping":
|
|
1011
1102
|
try {
|
|
1012
1103
|
conn.send({ t: "pong", timestamp: Date.now() });
|