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