@dropgate/core 3.0.0 → 3.0.1

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.
@@ -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
- fileList = msg;
944
- total = fileList.totalSize;
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
- pendingChunk = msg;
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() });