@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/index.cjs CHANGED
@@ -1129,6 +1129,7 @@ var P2P_END_ACK_RETRY_DELAY_MS = 100;
1129
1129
  var P2P_CLOSE_GRACE_PERIOD_MS = 2e3;
1130
1130
 
1131
1131
  // src/p2p/send.ts
1132
+ var P2P_UNACKED_CHUNK_TIMEOUT_MS = 3e4;
1132
1133
  function generateSessionId() {
1133
1134
  return crypto.randomUUID();
1134
1135
  }
@@ -1218,6 +1219,10 @@ async function startP2PSend(opts) {
1218
1219
  const unackedChunks = /* @__PURE__ */ new Map();
1219
1220
  let nextSeq = 0;
1220
1221
  let ackResolvers = [];
1222
+ let transferEverStarted = false;
1223
+ const connectionAttempts = [];
1224
+ const MAX_CONNECTION_ATTEMPTS = 10;
1225
+ const CONNECTION_RATE_WINDOW_MS = 1e4;
1221
1226
  const transitionTo = (newState) => {
1222
1227
  if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
1223
1228
  console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
@@ -1328,6 +1333,12 @@ async function startP2PSend(opts) {
1328
1333
  const sendChunk = async (conn, data, offset, fileTotal) => {
1329
1334
  if (chunkAcknowledgments) {
1330
1335
  while (unackedChunks.size >= maxUnackedChunks) {
1336
+ const now = Date.now();
1337
+ for (const [_seq, chunk] of unackedChunks) {
1338
+ if (now - chunk.sentAt > P2P_UNACKED_CHUNK_TIMEOUT_MS) {
1339
+ throw new DropgateNetworkError("Receiver stopped acknowledging chunks");
1340
+ }
1341
+ }
1331
1342
  await Promise.race([
1332
1343
  waitForAck(),
1333
1344
  sleep(1e3)
@@ -1384,6 +1395,23 @@ async function startP2PSend(opts) {
1384
1395
  };
1385
1396
  peer.on("connection", (conn) => {
1386
1397
  if (isStopped()) return;
1398
+ const now = Date.now();
1399
+ while (connectionAttempts.length > 0 && connectionAttempts[0] < now - CONNECTION_RATE_WINDOW_MS) {
1400
+ connectionAttempts.shift();
1401
+ }
1402
+ if (connectionAttempts.length >= MAX_CONNECTION_ATTEMPTS) {
1403
+ console.warn("[P2P Send] Connection rate limit exceeded, rejecting connection");
1404
+ try {
1405
+ conn.send({ t: "error", message: "Too many connection attempts. Please wait." });
1406
+ } catch {
1407
+ }
1408
+ try {
1409
+ conn.close();
1410
+ } catch {
1411
+ }
1412
+ return;
1413
+ }
1414
+ connectionAttempts.push(now);
1387
1415
  if (activeConn) {
1388
1416
  const isOldConnOpen = activeConn.open !== false;
1389
1417
  if (isOldConnOpen && state === "transferring") {
@@ -1402,6 +1430,17 @@ async function startP2PSend(opts) {
1402
1430
  } catch {
1403
1431
  }
1404
1432
  activeConn = null;
1433
+ if (transferEverStarted) {
1434
+ try {
1435
+ conn.send({ t: "error", message: "Transfer already started with another receiver. Cannot reconnect." });
1436
+ } catch {
1437
+ }
1438
+ try {
1439
+ conn.close();
1440
+ } catch {
1441
+ }
1442
+ return;
1443
+ }
1405
1444
  state = "listening";
1406
1445
  sentBytes = 0;
1407
1446
  nextSeq = 0;
@@ -1531,6 +1570,7 @@ async function startP2PSend(opts) {
1531
1570
  }, heartbeatIntervalMs);
1532
1571
  }
1533
1572
  transitionTo("transferring");
1573
+ transferEverStarted = true;
1534
1574
  let overallSentBytes = 0;
1535
1575
  for (let fi = 0; fi < files.length; fi++) {
1536
1576
  const currentFile = files[fi];
@@ -1702,6 +1742,10 @@ async function startP2PReceive(opts) {
1702
1742
  let fileList = null;
1703
1743
  let currentFileReceived = 0;
1704
1744
  let totalReceivedAllFiles = 0;
1745
+ let expectedChunkSeq = 0;
1746
+ let writeQueueDepth = 0;
1747
+ const MAX_WRITE_QUEUE_DEPTH = 100;
1748
+ const MAX_FILE_COUNT = 1e4;
1705
1749
  const transitionTo = (newState) => {
1706
1750
  if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
1707
1751
  console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
@@ -1802,8 +1846,16 @@ async function startP2PReceive(opts) {
1802
1846
  });
1803
1847
  conn.on("data", async (data) => {
1804
1848
  try {
1805
- resetWatchdog();
1806
1849
  if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
1850
+ if (state !== "transferring") {
1851
+ throw new DropgateValidationError(
1852
+ "Received binary data before transfer was accepted. Possible malicious sender."
1853
+ );
1854
+ }
1855
+ resetWatchdog();
1856
+ if (writeQueueDepth >= MAX_WRITE_QUEUE_DEPTH) {
1857
+ throw new DropgateNetworkError("Write queue overflow - receiver cannot keep up");
1858
+ }
1807
1859
  let bufPromise;
1808
1860
  if (data instanceof ArrayBuffer) {
1809
1861
  bufPromise = Promise.resolve(new Uint8Array(data));
@@ -1817,9 +1869,22 @@ async function startP2PReceive(opts) {
1817
1869
  return;
1818
1870
  }
1819
1871
  const chunkSeq = pendingChunk?.seq ?? -1;
1872
+ const expectedSize = pendingChunk?.size;
1820
1873
  pendingChunk = null;
1874
+ writeQueueDepth++;
1821
1875
  writeQueue = writeQueue.then(async () => {
1822
1876
  const buf = await bufPromise;
1877
+ if (expectedSize !== void 0 && buf.byteLength !== expectedSize) {
1878
+ throw new DropgateValidationError(
1879
+ `Chunk size mismatch: expected ${expectedSize}, got ${buf.byteLength}`
1880
+ );
1881
+ }
1882
+ const newReceived = received + buf.byteLength;
1883
+ if (total > 0 && newReceived > total) {
1884
+ throw new DropgateValidationError(
1885
+ `Received more data than expected: ${newReceived} > ${total}`
1886
+ );
1887
+ }
1823
1888
  if (onData) {
1824
1889
  await onData(buf);
1825
1890
  }
@@ -1841,6 +1906,8 @@ async function startP2PReceive(opts) {
1841
1906
  } catch {
1842
1907
  }
1843
1908
  safeError(err2);
1909
+ }).finally(() => {
1910
+ writeQueueDepth--;
1844
1911
  });
1845
1912
  return;
1846
1913
  }
@@ -1852,10 +1919,21 @@ async function startP2PReceive(opts) {
1852
1919
  transitionTo("negotiating");
1853
1920
  onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
1854
1921
  break;
1855
- case "file_list":
1856
- fileList = msg;
1857
- total = fileList.totalSize;
1922
+ case "file_list": {
1923
+ const fileListMsg = msg;
1924
+ if (fileListMsg.fileCount > MAX_FILE_COUNT) {
1925
+ throw new DropgateValidationError(`Too many files: ${fileListMsg.fileCount}`);
1926
+ }
1927
+ const sumSize = fileListMsg.files.reduce((sum, f) => sum + f.size, 0);
1928
+ if (sumSize !== fileListMsg.totalSize) {
1929
+ throw new DropgateValidationError(
1930
+ `File list size mismatch: declared ${fileListMsg.totalSize}, actual sum ${sumSize}`
1931
+ );
1932
+ }
1933
+ fileList = fileListMsg;
1934
+ total = fileListMsg.totalSize;
1858
1935
  break;
1936
+ }
1859
1937
  case "meta": {
1860
1938
  if (state !== "negotiating" && !(state === "transferring" && fileList)) {
1861
1939
  return;
@@ -1917,9 +1995,22 @@ async function startP2PReceive(opts) {
1917
1995
  }
1918
1996
  break;
1919
1997
  }
1920
- case "chunk":
1921
- pendingChunk = msg;
1998
+ case "chunk": {
1999
+ const chunkMsg = msg;
2000
+ if (state !== "transferring") {
2001
+ throw new DropgateValidationError(
2002
+ "Received chunk message before transfer was accepted."
2003
+ );
2004
+ }
2005
+ if (chunkMsg.seq !== expectedChunkSeq) {
2006
+ throw new DropgateValidationError(
2007
+ `Chunk sequence error: expected ${expectedChunkSeq}, got ${chunkMsg.seq}`
2008
+ );
2009
+ }
2010
+ expectedChunkSeq++;
2011
+ pendingChunk = chunkMsg;
1922
2012
  break;
2013
+ }
1923
2014
  case "ping":
1924
2015
  try {
1925
2016
  conn.send({ t: "pong", timestamp: Date.now() });