librats 2.3.1 → 2.3.3

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/lib/index.d.ts CHANGED
@@ -127,6 +127,7 @@ declare module 'librats' {
127
127
  ) => void;
128
128
  export type FileCompleteHandler = (
129
129
  transferId: number,
130
+ peerId: string,
130
131
  success: boolean,
131
132
  path: string
132
133
  ) => void;
@@ -574,8 +574,9 @@ rats_error_t rats_on_file_complete(rats_t node, rats_file_complete_cb cb, void*
574
574
  if (!cb) return RATS_ERR_INVALID_ARG;
575
575
  auto* h = as_handle(node);
576
576
  if (!h->files) return RATS_ERR_NOT_ENABLED;
577
- h->files->on_complete([cb, user](uint64_t id, bool success, const std::string& path) {
578
- cb(user, id, success ? 1 : 0, path.c_str());
577
+ h->files->on_complete([cb, user](const PeerId& peer, uint64_t id, bool success, const std::string& path) {
578
+ const std::string hex = peer.to_hex();
579
+ cb(user, id, hex.c_str(), success ? 1 : 0, path.c_str());
579
580
  });
580
581
  return RATS_OK;
581
582
  }
@@ -345,7 +345,8 @@ typedef void (*rats_file_offer_cb)(void* user, const char* peer_id_hex, uint64_t
345
345
  const char* name, uint64_t size, int is_directory);
346
346
  typedef void (*rats_file_progress_cb)(void* user, uint64_t transfer_id, const char* peer_id_hex,
347
347
  uint64_t bytes_transferred, uint64_t total_bytes, int status);
348
- typedef void (*rats_file_complete_cb)(void* user, uint64_t transfer_id, int success, const char* path);
348
+ typedef void (*rats_file_complete_cb)(void* user, uint64_t transfer_id, const char* peer_id_hex,
349
+ int success, const char* path);
349
350
 
350
351
  /** Enable the file-transfer subsystem. `temp_dir` holds in-progress downloads
351
352
  * (NULL → current directory). Call before start(). */
@@ -143,12 +143,23 @@ FileTransfer::Stats FileTransfer::stats() const {
143
143
 
144
144
  // ── lookups ───────────────────────────────────────────────────────────────────
145
145
 
146
- std::shared_ptr<FileTransfer::Outgoing> FileTransfer::find_outgoing(uint64_t id) const {
146
+ std::shared_ptr<FileTransfer::Outgoing> FileTransfer::find_own_outgoing(uint64_t id) const {
147
147
  std::lock_guard<std::mutex> lock(mutex_);
148
148
  auto it = outgoing_.find(id);
149
149
  return it == outgoing_.end() ? nullptr : it->second;
150
150
  }
151
151
 
152
+ std::shared_ptr<FileTransfer::Outgoing> FileTransfer::find_outgoing(const PeerId& peer, uint64_t id) const {
153
+ std::lock_guard<std::mutex> lock(mutex_);
154
+ auto it = outgoing_.find(id);
155
+ if (it == outgoing_.end()) return nullptr;
156
+ // The id alone found it; the peer is what proves the sender is entitled to it.
157
+ // Outgoing ids come from one node-wide counter, so a peer that never received
158
+ // this id can still name it — and every control opcode below would otherwise
159
+ // act on a stranger's transfer.
160
+ return it->second->peer == peer ? it->second : nullptr;
161
+ }
162
+
152
163
  std::shared_ptr<FileTransfer::Incoming> FileTransfer::find_incoming(const PeerId& peer, uint64_t id) const {
153
164
  std::lock_guard<std::mutex> lock(mutex_);
154
165
  auto pit = incoming_.find(peer);
@@ -241,7 +252,7 @@ void FileTransfer::worker_loop() {
241
252
  id = send_queue_.front();
242
253
  send_queue_.pop();
243
254
  }
244
- auto t = find_outgoing(id);
255
+ auto t = find_own_outgoing(id); // id came from our queue, not from a peer
245
256
  if (!t) continue;
246
257
  {
247
258
  std::lock_guard<std::mutex> lk(t->mtx);
@@ -380,7 +391,7 @@ void FileTransfer::finish_outgoing(const std::shared_ptr<Outgoing>& t, bool succ
380
391
  if (cancelled) LOG_INFO("filexfer", "Send [" << t->id << "] cancelled");
381
392
  else if (success) LOG_INFO("filexfer", "Send [" << t->id << "] completed (" << t->root << ")");
382
393
  else LOG_WARN("filexfer", "Send [" << t->id << "] failed");
383
- if (complete_handler_) complete_handler_(t->id, success, t->root);
394
+ if (complete_handler_) complete_handler_(t->peer, t->id, success, t->root);
384
395
  }
385
396
 
386
397
  // ── Receiver ──────────────────────────────────────────────────────────────────
@@ -428,7 +439,7 @@ void FileTransfer::reject(const PeerId& from, uint64_t id) {
428
439
  if (pit != incoming_.end()) pit->second.erase(id);
429
440
  }
430
441
  Bytes m; m.push_back(OP_RESPONSE); put_u64(m, id); m.push_back(0); send_to(from, m);
431
- if (t && complete_handler_) complete_handler_(id, false, "");
442
+ if (t && complete_handler_) complete_handler_(from, id, false, "");
432
443
  }
433
444
 
434
445
  // ── Receive-side disk writer (off the reactor thread) ─────────────────────────
@@ -627,7 +638,7 @@ void FileTransfer::finish_incoming(const std::shared_ptr<Incoming>& t, bool succ
627
638
  else if (success) LOG_INFO("filexfer", "Receive [" << t->id << "] completed -> " << dest);
628
639
  else if (error.empty()) LOG_WARN("filexfer", "Receive [" << t->id << "] failed");
629
640
  else LOG_WARN("filexfer", "Receive [" << t->id << "] failed: " << error);
630
- if (complete_handler_) complete_handler_(t->id, success, dest);
641
+ if (complete_handler_) complete_handler_(t->peer, t->id, success, dest);
631
642
  }
632
643
 
633
644
  // ── Message dispatch (reactor thread) ────────────────────────────────────────
@@ -675,7 +686,7 @@ void FileTransfer::on_message(const Peer& peer, ByteView payload) {
675
686
  const uint64_t id = r.u64();
676
687
  const uint8_t accepted = r.u8();
677
688
  if (!r.ok) return;
678
- auto t = find_outgoing(id);
689
+ auto t = find_outgoing(from, id);
679
690
  if (!t) return;
680
691
  if (!accepted) { finish_outgoing(t, false); return; }
681
692
  { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Pending) t->status = Status::Active; t->last_activity = std::chrono::steady_clock::now(); }
@@ -703,7 +714,7 @@ void FileTransfer::on_message(const Peer& peer, ByteView payload) {
703
714
  const uint64_t id = r.u64();
704
715
  const uint64_t received = r.u64();
705
716
  if (!r.ok) return;
706
- auto t = find_outgoing(id);
717
+ auto t = find_outgoing(from, id);
707
718
  if (!t) return;
708
719
  { std::lock_guard<std::mutex> lk(t->mtx); t->acked = received; t->last_activity = std::chrono::steady_clock::now(); }
709
720
  t->cv.notify_all();
@@ -713,13 +724,13 @@ void FileTransfer::on_message(const Peer& peer, ByteView payload) {
713
724
  const uint64_t id = r.u64();
714
725
  const uint8_t ok = r.u8();
715
726
  if (!r.ok) return;
716
- if (auto t = find_outgoing(id)) finish_outgoing(t, ok != 0);
727
+ if (auto t = find_outgoing(from, id)) finish_outgoing(t, ok != 0);
717
728
  return;
718
729
  }
719
730
  case OP_CANCEL: {
720
731
  const uint64_t id = r.u64();
721
732
  if (!r.ok) return;
722
- if (auto t = find_outgoing(id)) {
733
+ if (auto t = find_outgoing(from, id)) {
723
734
  { std::lock_guard<std::mutex> lk(t->mtx); t->status = Status::Cancelled; t->cv.notify_all(); }
724
735
  finish_outgoing(t, false);
725
736
  }
@@ -732,14 +743,14 @@ void FileTransfer::on_message(const Peer& peer, ByteView payload) {
732
743
  case OP_PAUSE: {
733
744
  const uint64_t id = r.u64();
734
745
  if (!r.ok) return;
735
- if (auto t = find_outgoing(id)) { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Active) t->status = Status::Paused; t->cv.notify_all(); }
746
+ if (auto t = find_outgoing(from, id)) { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Active) t->status = Status::Paused; t->cv.notify_all(); }
736
747
  if (auto t = find_incoming(from, id)) { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Active) t->status = Status::Paused; }
737
748
  return;
738
749
  }
739
750
  case OP_RESUME: {
740
751
  const uint64_t id = r.u64();
741
752
  if (!r.ok) return;
742
- if (auto t = find_outgoing(id)) {
753
+ if (auto t = find_outgoing(from, id)) {
743
754
  bool requeue = false;
744
755
  { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Paused) { t->status = Status::Active; t->last_activity = std::chrono::steady_clock::now(); requeue = true; } t->cv.notify_all(); }
745
756
  if (requeue) queue_send(id);
@@ -860,7 +871,7 @@ void FileTransfer::handle_file_end(const PeerId& from, uint64_t id, uint32_t fid
860
871
 
861
872
  bool FileTransfer::cancel(const PeerId& peer, uint64_t id) {
862
873
  bool acted = false;
863
- if (auto t = find_outgoing(id)) {
874
+ if (auto t = find_outgoing(peer, id)) {
864
875
  { std::lock_guard<std::mutex> lk(t->mtx); if (!t->finished) { t->status = Status::Cancelled; t->cv.notify_all(); acted = true; } }
865
876
  if (acted) { send_simple(t->peer, OP_CANCEL, id); finish_outgoing(t, false); }
866
877
  }
@@ -873,7 +884,7 @@ bool FileTransfer::cancel(const PeerId& peer, uint64_t id) {
873
884
  }
874
885
 
875
886
  bool FileTransfer::pause(const PeerId& peer, uint64_t id) {
876
- if (auto t = find_outgoing(id)) {
887
+ if (auto t = find_outgoing(peer, id)) {
877
888
  bool ok = false;
878
889
  { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Active) { t->status = Status::Paused; ok = true; t->cv.notify_all(); } }
879
890
  if (ok) { send_simple(t->peer, OP_PAUSE, id); return true; }
@@ -887,7 +898,7 @@ bool FileTransfer::pause(const PeerId& peer, uint64_t id) {
887
898
  }
888
899
 
889
900
  bool FileTransfer::resume(const PeerId& peer, uint64_t id) {
890
- if (auto t = find_outgoing(id)) {
901
+ if (auto t = find_outgoing(peer, id)) {
891
902
  bool ok = false;
892
903
  { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Paused) { t->status = Status::Active; t->last_activity = std::chrono::steady_clock::now(); ok = true; t->cv.notify_all(); } }
893
904
  if (ok) { send_simple(t->peer, OP_RESUME, id); queue_send(id); return true; }
@@ -940,7 +951,16 @@ void FileTransfer::maintenance_loop() {
940
951
  if (!running_.load()) return;
941
952
 
942
953
  const auto now = std::chrono::steady_clock::now();
943
- const uint32_t timeout = config_.transfer_timeout_secs;
954
+
955
+ // Two deadlines, because Pending and Active are waiting on different
956
+ // things. An Active transfer is waiting on the wire, so silence means the
957
+ // link is gone. A Pending one is waiting on the far side's application to
958
+ // answer the offer, which legitimately takes longer than a network
959
+ // round-trip — the app may be prompting a user or sitting behind a busy
960
+ // event loop. Sharing one deadline meant an offer nobody had answered yet
961
+ // was killed as if the peer had vanished.
962
+ const uint32_t idle_timeout = config_.transfer_timeout_secs;
963
+ const uint32_t offer_timeout = config_.offer_timeout_secs;
944
964
 
945
965
  std::vector<std::shared_ptr<Outgoing>> out;
946
966
  std::vector<std::shared_ptr<Incoming>> in;
@@ -950,21 +970,35 @@ void FileTransfer::maintenance_loop() {
950
970
  for (auto& [peer, m] : incoming_) for (auto& [id, t] : m) in.push_back(t);
951
971
  }
952
972
 
973
+ auto expired = [&](Status status, std::chrono::steady_clock::time_point since) {
974
+ const uint32_t limit = (status == Status::Pending) ? offer_timeout : idle_timeout;
975
+ return limit > 0
976
+ && std::chrono::duration_cast<std::chrono::seconds>(now - since).count() > limit;
977
+ };
978
+
953
979
  for (auto& t : out) {
954
980
  bool stale = false;
955
981
  { std::lock_guard<std::mutex> lk(t->mtx);
956
- if (!t->finished && t->status != Status::Paused &&
957
- std::chrono::duration_cast<std::chrono::seconds>(now - t->last_activity).count() > timeout)
982
+ if (!t->finished && t->status != Status::Paused && expired(t->status, t->last_activity))
958
983
  stale = true; }
959
984
  if (stale) { LOG_WARN("filexfer", "outgoing transfer " << t->id << " timed out"); send_complete(t->peer, t->id, false); finish_outgoing(t, false); }
960
985
  }
961
986
  for (auto& t : in) {
962
987
  bool stale = false;
963
988
  { std::lock_guard<std::mutex> lk(t->mtx);
964
- if (!t->finished && t->status != Status::Paused && t->status != Status::Pending &&
965
- std::chrono::duration_cast<std::chrono::seconds>(now - t->last_activity).count() > timeout)
989
+ if (!t->finished && t->status != Status::Paused && expired(t->status, t->last_activity))
966
990
  stale = true; }
967
- if (stale) { LOG_WARN("filexfer", "incoming transfer " << t->id << " timed out"); send_complete(t->peer, t->id, false); finish_incoming(t, false, "timed out"); }
991
+ // A Pending incoming transfer is an offer our own app never answered.
992
+ // It used to be exempt from every deadline and so leaked for the life
993
+ // of the process; reclaiming it is a reject, not a network failure.
994
+ bool pending = false;
995
+ { std::lock_guard<std::mutex> lk(t->mtx); pending = (t->status == Status::Pending); }
996
+ if (stale && pending) {
997
+ LOG_WARN("filexfer", "incoming offer " << t->id << " was never answered; rejecting");
998
+ reject(t->peer, t->id);
999
+ } else if (stale) {
1000
+ LOG_WARN("filexfer", "incoming transfer " << t->id << " timed out"); send_complete(t->peer, t->id, false); finish_incoming(t, false, "timed out");
1001
+ }
968
1002
  }
969
1003
  }
970
1004
  }
@@ -77,7 +77,12 @@ public:
77
77
  uint32_t chunk_size = 64 * 1024; ///< payload bytes per chunk
78
78
  uint32_t window_bytes = 4 * 1024 * 1024; ///< max un-acked bytes in flight
79
79
  uint32_t progress_interval = 256 * 1024; ///< receiver acks every N bytes
80
- uint32_t transfer_timeout_secs = 60; ///< abort a transfer idle this long
80
+ uint32_t transfer_timeout_secs = 60; ///< abort an *active* transfer idle this long
81
+ // An offered transfer is waiting on a human or an app callback, not on the
82
+ // wire, so it gets its own (longer) deadline. Applied to both sides: the
83
+ // sender drops an offer nobody answered, and the receiver reclaims an offer
84
+ // its app never accepted or rejected.
85
+ uint32_t offer_timeout_secs = 300;
81
86
  uint32_t worker_threads = 4; ///< concurrent outgoing transfers
82
87
  uint32_t disk_threads = 4; ///< receive-side disk-writer pool size
83
88
  bool verify_integrity = true; ///< whole-file SHA-256 end-to-end check
@@ -134,7 +139,12 @@ public:
134
139
 
135
140
  using OfferHandler = std::function<void(const Offer&)>;
136
141
  using ProgressHandler = std::function<void(const Progress&)>;
137
- using CompleteHandler = std::function<void(uint64_t id, bool success, const std::string& path)>;
142
+ // The peer is part of the identity of a transfer, not decoration: incoming ids
143
+ // are allocated by the *sender*, so every node's first offer is id 1 and two
144
+ // peers routinely have a transfer of the same id in flight at once. A handler
145
+ // given only the id cannot tell which of them finished.
146
+ using CompleteHandler =
147
+ std::function<void(const PeerId& peer, uint64_t id, bool success, const std::string& path)>;
138
148
 
139
149
  explicit FileTransfer(std::string temp_dir = ".");
140
150
  explicit FileTransfer(Config config);
@@ -318,7 +328,15 @@ private:
318
328
  void emit_progress(const std::shared_ptr<Outgoing>& t);
319
329
  void emit_progress(const std::shared_ptr<Incoming>& t);
320
330
 
321
- std::shared_ptr<Outgoing> find_outgoing(uint64_t id) const;
331
+ // Both lookups are peer-scoped. Outgoing ids are unique on this node, so the
332
+ // peer is not needed to *find* the transfer — it is needed to establish that
333
+ // the sender of the message is the transfer's counterparty. Without that check
334
+ // any connected peer can cancel, complete or stall somebody else's transfer by
335
+ // naming an id it was never given.
336
+ std::shared_ptr<Outgoing> find_outgoing(const PeerId& peer, uint64_t id) const;
337
+ // Unchecked lookup, for ids that came off our own send queue rather than off
338
+ // the wire. Never call it with an id a peer supplied.
339
+ std::shared_ptr<Outgoing> find_own_outgoing(uint64_t id) const;
322
340
  std::shared_ptr<Incoming> find_incoming(const PeerId& peer, uint64_t id) const;
323
341
 
324
342
  void send_to(const PeerId& peer, const Bytes& msg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librats",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "Node.js bindings for librats - A high-performance peer-to-peer networking library",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -855,7 +855,7 @@ void RatsNode::OnFileProgress(const Napi::CallbackInfo& info) {
855
855
  throw_on_error(env, rats_on_file_progress(node_, trampoline, on_file_progress_.get()));
856
856
  }
857
857
 
858
- // rats_file_complete_cb(user, transfer_id, success, path)
858
+ // rats_file_complete_cb(user, transfer_id, peer_id_hex, success, path)
859
859
  void RatsNode::OnFileComplete(const Napi::CallbackInfo& info) {
860
860
  RATS_REQUIRE_NODE();
861
861
  Napi::Env env = info.Env();
@@ -865,12 +865,15 @@ void RatsNode::OnFileComplete(const Napi::CallbackInfo& info) {
865
865
  }
866
866
  on_file_complete_ = std::make_unique<CbContext>();
867
867
  on_file_complete_->init(env, info[0].As<Napi::Function>(), "on_file_complete");
868
- auto trampoline = [](void* user, uint64_t transfer_id, int success, const char* path) {
868
+ auto trampoline = [](void* user, uint64_t transfer_id, const char* peer_id_hex, int success,
869
+ const char* path) {
869
870
  auto* c = static_cast<CbContext*>(user);
870
871
  std::string p = path ? path : "";
872
+ std::string peer = peer_id_hex ? peer_id_hex : "";
871
873
  bool ok = success != 0;
872
- c->tsfn.BlockingCall([transfer_id, ok, p](Napi::Env env, Napi::Function js) {
874
+ c->tsfn.BlockingCall([transfer_id, peer, ok, p](Napi::Env env, Napi::Function js) {
873
875
  js.Call({Napi::Number::New(env, static_cast<double>(transfer_id)),
876
+ Napi::String::New(env, peer),
874
877
  Napi::Boolean::New(env, ok),
875
878
  Napi::String::New(env, p)});
876
879
  });