@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.
package/dist/index.js CHANGED
@@ -1066,6 +1066,7 @@ var P2P_END_ACK_RETRY_DELAY_MS = 100;
1066
1066
  var P2P_CLOSE_GRACE_PERIOD_MS = 2e3;
1067
1067
 
1068
1068
  // src/p2p/send.ts
1069
+ var P2P_UNACKED_CHUNK_TIMEOUT_MS = 3e4;
1069
1070
  function generateSessionId() {
1070
1071
  return crypto.randomUUID();
1071
1072
  }
@@ -1155,6 +1156,10 @@ async function startP2PSend(opts) {
1155
1156
  const unackedChunks = /* @__PURE__ */ new Map();
1156
1157
  let nextSeq = 0;
1157
1158
  let ackResolvers = [];
1159
+ let transferEverStarted = false;
1160
+ const connectionAttempts = [];
1161
+ const MAX_CONNECTION_ATTEMPTS = 10;
1162
+ const CONNECTION_RATE_WINDOW_MS = 1e4;
1158
1163
  const transitionTo = (newState) => {
1159
1164
  if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
1160
1165
  console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
@@ -1265,6 +1270,12 @@ async function startP2PSend(opts) {
1265
1270
  const sendChunk = async (conn, data, offset, fileTotal) => {
1266
1271
  if (chunkAcknowledgments) {
1267
1272
  while (unackedChunks.size >= maxUnackedChunks) {
1273
+ const now = Date.now();
1274
+ for (const [_seq, chunk] of unackedChunks) {
1275
+ if (now - chunk.sentAt > P2P_UNACKED_CHUNK_TIMEOUT_MS) {
1276
+ throw new DropgateNetworkError("Receiver stopped acknowledging chunks");
1277
+ }
1278
+ }
1268
1279
  await Promise.race([
1269
1280
  waitForAck(),
1270
1281
  sleep(1e3)
@@ -1321,6 +1332,23 @@ async function startP2PSend(opts) {
1321
1332
  };
1322
1333
  peer.on("connection", (conn) => {
1323
1334
  if (isStopped()) return;
1335
+ const now = Date.now();
1336
+ while (connectionAttempts.length > 0 && connectionAttempts[0] < now - CONNECTION_RATE_WINDOW_MS) {
1337
+ connectionAttempts.shift();
1338
+ }
1339
+ if (connectionAttempts.length >= MAX_CONNECTION_ATTEMPTS) {
1340
+ console.warn("[P2P Send] Connection rate limit exceeded, rejecting connection");
1341
+ try {
1342
+ conn.send({ t: "error", message: "Too many connection attempts. Please wait." });
1343
+ } catch {
1344
+ }
1345
+ try {
1346
+ conn.close();
1347
+ } catch {
1348
+ }
1349
+ return;
1350
+ }
1351
+ connectionAttempts.push(now);
1324
1352
  if (activeConn) {
1325
1353
  const isOldConnOpen = activeConn.open !== false;
1326
1354
  if (isOldConnOpen && state === "transferring") {
@@ -1339,6 +1367,17 @@ async function startP2PSend(opts) {
1339
1367
  } catch {
1340
1368
  }
1341
1369
  activeConn = null;
1370
+ if (transferEverStarted) {
1371
+ try {
1372
+ conn.send({ t: "error", message: "Transfer already started with another receiver. Cannot reconnect." });
1373
+ } catch {
1374
+ }
1375
+ try {
1376
+ conn.close();
1377
+ } catch {
1378
+ }
1379
+ return;
1380
+ }
1342
1381
  state = "listening";
1343
1382
  sentBytes = 0;
1344
1383
  nextSeq = 0;
@@ -1468,6 +1507,7 @@ async function startP2PSend(opts) {
1468
1507
  }, heartbeatIntervalMs);
1469
1508
  }
1470
1509
  transitionTo("transferring");
1510
+ transferEverStarted = true;
1471
1511
  let overallSentBytes = 0;
1472
1512
  for (let fi = 0; fi < files.length; fi++) {
1473
1513
  const currentFile = files[fi];
@@ -1639,6 +1679,10 @@ async function startP2PReceive(opts) {
1639
1679
  let fileList = null;
1640
1680
  let currentFileReceived = 0;
1641
1681
  let totalReceivedAllFiles = 0;
1682
+ let expectedChunkSeq = 0;
1683
+ let writeQueueDepth = 0;
1684
+ const MAX_WRITE_QUEUE_DEPTH = 100;
1685
+ const MAX_FILE_COUNT = 1e4;
1642
1686
  const transitionTo = (newState) => {
1643
1687
  if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
1644
1688
  console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
@@ -1739,8 +1783,16 @@ async function startP2PReceive(opts) {
1739
1783
  });
1740
1784
  conn.on("data", async (data) => {
1741
1785
  try {
1742
- resetWatchdog();
1743
1786
  if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
1787
+ if (state !== "transferring") {
1788
+ throw new DropgateValidationError(
1789
+ "Received binary data before transfer was accepted. Possible malicious sender."
1790
+ );
1791
+ }
1792
+ resetWatchdog();
1793
+ if (writeQueueDepth >= MAX_WRITE_QUEUE_DEPTH) {
1794
+ throw new DropgateNetworkError("Write queue overflow - receiver cannot keep up");
1795
+ }
1744
1796
  let bufPromise;
1745
1797
  if (data instanceof ArrayBuffer) {
1746
1798
  bufPromise = Promise.resolve(new Uint8Array(data));
@@ -1754,9 +1806,22 @@ async function startP2PReceive(opts) {
1754
1806
  return;
1755
1807
  }
1756
1808
  const chunkSeq = pendingChunk?.seq ?? -1;
1809
+ const expectedSize = pendingChunk?.size;
1757
1810
  pendingChunk = null;
1811
+ writeQueueDepth++;
1758
1812
  writeQueue = writeQueue.then(async () => {
1759
1813
  const buf = await bufPromise;
1814
+ if (expectedSize !== void 0 && buf.byteLength !== expectedSize) {
1815
+ throw new DropgateValidationError(
1816
+ `Chunk size mismatch: expected ${expectedSize}, got ${buf.byteLength}`
1817
+ );
1818
+ }
1819
+ const newReceived = received + buf.byteLength;
1820
+ if (total > 0 && newReceived > total) {
1821
+ throw new DropgateValidationError(
1822
+ `Received more data than expected: ${newReceived} > ${total}`
1823
+ );
1824
+ }
1760
1825
  if (onData) {
1761
1826
  await onData(buf);
1762
1827
  }
@@ -1778,6 +1843,8 @@ async function startP2PReceive(opts) {
1778
1843
  } catch {
1779
1844
  }
1780
1845
  safeError(err2);
1846
+ }).finally(() => {
1847
+ writeQueueDepth--;
1781
1848
  });
1782
1849
  return;
1783
1850
  }
@@ -1789,10 +1856,21 @@ async function startP2PReceive(opts) {
1789
1856
  transitionTo("negotiating");
1790
1857
  onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
1791
1858
  break;
1792
- case "file_list":
1793
- fileList = msg;
1794
- total = fileList.totalSize;
1859
+ case "file_list": {
1860
+ const fileListMsg = msg;
1861
+ if (fileListMsg.fileCount > MAX_FILE_COUNT) {
1862
+ throw new DropgateValidationError(`Too many files: ${fileListMsg.fileCount}`);
1863
+ }
1864
+ const sumSize = fileListMsg.files.reduce((sum, f) => sum + f.size, 0);
1865
+ if (sumSize !== fileListMsg.totalSize) {
1866
+ throw new DropgateValidationError(
1867
+ `File list size mismatch: declared ${fileListMsg.totalSize}, actual sum ${sumSize}`
1868
+ );
1869
+ }
1870
+ fileList = fileListMsg;
1871
+ total = fileListMsg.totalSize;
1795
1872
  break;
1873
+ }
1796
1874
  case "meta": {
1797
1875
  if (state !== "negotiating" && !(state === "transferring" && fileList)) {
1798
1876
  return;
@@ -1854,9 +1932,22 @@ async function startP2PReceive(opts) {
1854
1932
  }
1855
1933
  break;
1856
1934
  }
1857
- case "chunk":
1858
- pendingChunk = msg;
1935
+ case "chunk": {
1936
+ const chunkMsg = msg;
1937
+ if (state !== "transferring") {
1938
+ throw new DropgateValidationError(
1939
+ "Received chunk message before transfer was accepted."
1940
+ );
1941
+ }
1942
+ if (chunkMsg.seq !== expectedChunkSeq) {
1943
+ throw new DropgateValidationError(
1944
+ `Chunk sequence error: expected ${expectedChunkSeq}, got ${chunkMsg.seq}`
1945
+ );
1946
+ }
1947
+ expectedChunkSeq++;
1948
+ pendingChunk = chunkMsg;
1859
1949
  break;
1950
+ }
1860
1951
  case "ping":
1861
1952
  try {
1862
1953
  conn.send({ t: "pong", timestamp: Date.now() });