@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.js
CHANGED
|
@@ -177,6 +177,7 @@ var P2P_END_ACK_RETRY_DELAY_MS = 100;
|
|
|
177
177
|
var P2P_CLOSE_GRACE_PERIOD_MS = 2e3;
|
|
178
178
|
|
|
179
179
|
// src/p2p/send.ts
|
|
180
|
+
var P2P_UNACKED_CHUNK_TIMEOUT_MS = 3e4;
|
|
180
181
|
function generateSessionId() {
|
|
181
182
|
return crypto.randomUUID();
|
|
182
183
|
}
|
|
@@ -266,6 +267,10 @@ async function startP2PSend(opts) {
|
|
|
266
267
|
const unackedChunks = /* @__PURE__ */ new Map();
|
|
267
268
|
let nextSeq = 0;
|
|
268
269
|
let ackResolvers = [];
|
|
270
|
+
let transferEverStarted = false;
|
|
271
|
+
const connectionAttempts = [];
|
|
272
|
+
const MAX_CONNECTION_ATTEMPTS = 10;
|
|
273
|
+
const CONNECTION_RATE_WINDOW_MS = 1e4;
|
|
269
274
|
const transitionTo = (newState) => {
|
|
270
275
|
if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
|
|
271
276
|
console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
|
|
@@ -376,6 +381,12 @@ async function startP2PSend(opts) {
|
|
|
376
381
|
const sendChunk = async (conn, data, offset, fileTotal) => {
|
|
377
382
|
if (chunkAcknowledgments) {
|
|
378
383
|
while (unackedChunks.size >= maxUnackedChunks) {
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
for (const [_seq, chunk] of unackedChunks) {
|
|
386
|
+
if (now - chunk.sentAt > P2P_UNACKED_CHUNK_TIMEOUT_MS) {
|
|
387
|
+
throw new DropgateNetworkError("Receiver stopped acknowledging chunks");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
379
390
|
await Promise.race([
|
|
380
391
|
waitForAck(),
|
|
381
392
|
sleep(1e3)
|
|
@@ -432,6 +443,23 @@ async function startP2PSend(opts) {
|
|
|
432
443
|
};
|
|
433
444
|
peer.on("connection", (conn) => {
|
|
434
445
|
if (isStopped()) return;
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
while (connectionAttempts.length > 0 && connectionAttempts[0] < now - CONNECTION_RATE_WINDOW_MS) {
|
|
448
|
+
connectionAttempts.shift();
|
|
449
|
+
}
|
|
450
|
+
if (connectionAttempts.length >= MAX_CONNECTION_ATTEMPTS) {
|
|
451
|
+
console.warn("[P2P Send] Connection rate limit exceeded, rejecting connection");
|
|
452
|
+
try {
|
|
453
|
+
conn.send({ t: "error", message: "Too many connection attempts. Please wait." });
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
conn.close();
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
connectionAttempts.push(now);
|
|
435
463
|
if (activeConn) {
|
|
436
464
|
const isOldConnOpen = activeConn.open !== false;
|
|
437
465
|
if (isOldConnOpen && state === "transferring") {
|
|
@@ -450,6 +478,17 @@ async function startP2PSend(opts) {
|
|
|
450
478
|
} catch {
|
|
451
479
|
}
|
|
452
480
|
activeConn = null;
|
|
481
|
+
if (transferEverStarted) {
|
|
482
|
+
try {
|
|
483
|
+
conn.send({ t: "error", message: "Transfer already started with another receiver. Cannot reconnect." });
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
conn.close();
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
453
492
|
state = "listening";
|
|
454
493
|
sentBytes = 0;
|
|
455
494
|
nextSeq = 0;
|
|
@@ -579,6 +618,7 @@ async function startP2PSend(opts) {
|
|
|
579
618
|
}, heartbeatIntervalMs);
|
|
580
619
|
}
|
|
581
620
|
transitionTo("transferring");
|
|
621
|
+
transferEverStarted = true;
|
|
582
622
|
let overallSentBytes = 0;
|
|
583
623
|
for (let fi = 0; fi < files.length; fi++) {
|
|
584
624
|
const currentFile = files[fi];
|
|
@@ -750,6 +790,10 @@ async function startP2PReceive(opts) {
|
|
|
750
790
|
let fileList = null;
|
|
751
791
|
let currentFileReceived = 0;
|
|
752
792
|
let totalReceivedAllFiles = 0;
|
|
793
|
+
let expectedChunkSeq = 0;
|
|
794
|
+
let writeQueueDepth = 0;
|
|
795
|
+
const MAX_WRITE_QUEUE_DEPTH = 100;
|
|
796
|
+
const MAX_FILE_COUNT = 1e4;
|
|
753
797
|
const transitionTo = (newState) => {
|
|
754
798
|
if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
|
|
755
799
|
console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
|
|
@@ -850,8 +894,16 @@ async function startP2PReceive(opts) {
|
|
|
850
894
|
});
|
|
851
895
|
conn.on("data", async (data) => {
|
|
852
896
|
try {
|
|
853
|
-
resetWatchdog();
|
|
854
897
|
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
|
|
898
|
+
if (state !== "transferring") {
|
|
899
|
+
throw new DropgateValidationError(
|
|
900
|
+
"Received binary data before transfer was accepted. Possible malicious sender."
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
resetWatchdog();
|
|
904
|
+
if (writeQueueDepth >= MAX_WRITE_QUEUE_DEPTH) {
|
|
905
|
+
throw new DropgateNetworkError("Write queue overflow - receiver cannot keep up");
|
|
906
|
+
}
|
|
855
907
|
let bufPromise;
|
|
856
908
|
if (data instanceof ArrayBuffer) {
|
|
857
909
|
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
@@ -865,9 +917,22 @@ async function startP2PReceive(opts) {
|
|
|
865
917
|
return;
|
|
866
918
|
}
|
|
867
919
|
const chunkSeq = pendingChunk?.seq ?? -1;
|
|
920
|
+
const expectedSize = pendingChunk?.size;
|
|
868
921
|
pendingChunk = null;
|
|
922
|
+
writeQueueDepth++;
|
|
869
923
|
writeQueue = writeQueue.then(async () => {
|
|
870
924
|
const buf = await bufPromise;
|
|
925
|
+
if (expectedSize !== void 0 && buf.byteLength !== expectedSize) {
|
|
926
|
+
throw new DropgateValidationError(
|
|
927
|
+
`Chunk size mismatch: expected ${expectedSize}, got ${buf.byteLength}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const newReceived = received + buf.byteLength;
|
|
931
|
+
if (total > 0 && newReceived > total) {
|
|
932
|
+
throw new DropgateValidationError(
|
|
933
|
+
`Received more data than expected: ${newReceived} > ${total}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
871
936
|
if (onData) {
|
|
872
937
|
await onData(buf);
|
|
873
938
|
}
|
|
@@ -889,6 +954,8 @@ async function startP2PReceive(opts) {
|
|
|
889
954
|
} catch {
|
|
890
955
|
}
|
|
891
956
|
safeError(err);
|
|
957
|
+
}).finally(() => {
|
|
958
|
+
writeQueueDepth--;
|
|
892
959
|
});
|
|
893
960
|
return;
|
|
894
961
|
}
|
|
@@ -900,10 +967,21 @@ async function startP2PReceive(opts) {
|
|
|
900
967
|
transitionTo("negotiating");
|
|
901
968
|
onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
|
|
902
969
|
break;
|
|
903
|
-
case "file_list":
|
|
904
|
-
|
|
905
|
-
|
|
970
|
+
case "file_list": {
|
|
971
|
+
const fileListMsg = msg;
|
|
972
|
+
if (fileListMsg.fileCount > MAX_FILE_COUNT) {
|
|
973
|
+
throw new DropgateValidationError(`Too many files: ${fileListMsg.fileCount}`);
|
|
974
|
+
}
|
|
975
|
+
const sumSize = fileListMsg.files.reduce((sum, f) => sum + f.size, 0);
|
|
976
|
+
if (sumSize !== fileListMsg.totalSize) {
|
|
977
|
+
throw new DropgateValidationError(
|
|
978
|
+
`File list size mismatch: declared ${fileListMsg.totalSize}, actual sum ${sumSize}`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
fileList = fileListMsg;
|
|
982
|
+
total = fileListMsg.totalSize;
|
|
906
983
|
break;
|
|
984
|
+
}
|
|
907
985
|
case "meta": {
|
|
908
986
|
if (state !== "negotiating" && !(state === "transferring" && fileList)) {
|
|
909
987
|
return;
|
|
@@ -965,9 +1043,22 @@ async function startP2PReceive(opts) {
|
|
|
965
1043
|
}
|
|
966
1044
|
break;
|
|
967
1045
|
}
|
|
968
|
-
case "chunk":
|
|
969
|
-
|
|
1046
|
+
case "chunk": {
|
|
1047
|
+
const chunkMsg = msg;
|
|
1048
|
+
if (state !== "transferring") {
|
|
1049
|
+
throw new DropgateValidationError(
|
|
1050
|
+
"Received chunk message before transfer was accepted."
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
if (chunkMsg.seq !== expectedChunkSeq) {
|
|
1054
|
+
throw new DropgateValidationError(
|
|
1055
|
+
`Chunk sequence error: expected ${expectedChunkSeq}, got ${chunkMsg.seq}`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
expectedChunkSeq++;
|
|
1059
|
+
pendingChunk = chunkMsg;
|
|
970
1060
|
break;
|
|
1061
|
+
}
|
|
971
1062
|
case "ping":
|
|
972
1063
|
try {
|
|
973
1064
|
conn.send({ t: "pong", timestamp: Date.now() });
|