librats 0.5.0 → 0.5.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/README.md +1 -1
- package/binding.gyp +1 -0
- package/lib/index.d.ts +2 -1
- package/native-src/3rdparty/android/ifaddrs-android.c +600 -0
- package/native-src/3rdparty/android/ifaddrs-android.h +54 -0
- package/native-src/CMakeLists.txt +360 -0
- package/native-src/LICENSE +21 -0
- package/native-src/src/bencode.cpp +485 -0
- package/native-src/src/bencode.h +145 -0
- package/native-src/src/bittorrent.cpp +3682 -0
- package/native-src/src/bittorrent.h +731 -0
- package/native-src/src/dht.cpp +2460 -0
- package/native-src/src/dht.h +508 -0
- package/native-src/src/encrypted_socket.cpp +817 -0
- package/native-src/src/encrypted_socket.h +239 -0
- package/native-src/src/file_transfer.cpp +1808 -0
- package/native-src/src/file_transfer.h +567 -0
- package/native-src/src/fs.cpp +639 -0
- package/native-src/src/fs.h +108 -0
- package/native-src/src/gossipsub.cpp +1137 -0
- package/native-src/src/gossipsub.h +403 -0
- package/native-src/src/ice.cpp +1386 -0
- package/native-src/src/ice.h +328 -0
- package/native-src/src/json.hpp +25526 -0
- package/native-src/src/krpc.cpp +558 -0
- package/native-src/src/krpc.h +145 -0
- package/native-src/src/librats.cpp +2735 -0
- package/native-src/src/librats.h +1732 -0
- package/native-src/src/librats_bittorrent.cpp +167 -0
- package/native-src/src/librats_c.cpp +1333 -0
- package/native-src/src/librats_c.h +239 -0
- package/native-src/src/librats_encryption.cpp +123 -0
- package/native-src/src/librats_file_transfer.cpp +226 -0
- package/native-src/src/librats_gossipsub.cpp +293 -0
- package/native-src/src/librats_ice.cpp +515 -0
- package/native-src/src/librats_logging.cpp +158 -0
- package/native-src/src/librats_mdns.cpp +171 -0
- package/native-src/src/librats_nat.cpp +571 -0
- package/native-src/src/librats_persistence.cpp +815 -0
- package/native-src/src/logger.h +412 -0
- package/native-src/src/mdns.cpp +1178 -0
- package/native-src/src/mdns.h +253 -0
- package/native-src/src/network_utils.cpp +598 -0
- package/native-src/src/network_utils.h +162 -0
- package/native-src/src/noise.cpp +981 -0
- package/native-src/src/noise.h +227 -0
- package/native-src/src/os.cpp +371 -0
- package/native-src/src/os.h +40 -0
- package/native-src/src/rats_export.h +17 -0
- package/native-src/src/sha1.cpp +163 -0
- package/native-src/src/sha1.h +42 -0
- package/native-src/src/socket.cpp +1376 -0
- package/native-src/src/socket.h +309 -0
- package/native-src/src/stun.cpp +484 -0
- package/native-src/src/stun.h +349 -0
- package/native-src/src/threadmanager.cpp +105 -0
- package/native-src/src/threadmanager.h +53 -0
- package/native-src/src/tracker.cpp +1110 -0
- package/native-src/src/tracker.h +268 -0
- package/native-src/src/version.cpp +24 -0
- package/native-src/src/version.h.in +45 -0
- package/native-src/version.rc.in +31 -0
- package/package.json +2 -8
- package/scripts/build-librats.js +59 -12
- package/scripts/prepare-package.js +133 -37
- package/src/librats_node.cpp +46 -1
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
#include "librats.h"
|
|
2
|
+
#include "sha1.h"
|
|
3
|
+
#include "os.h"
|
|
4
|
+
#include "fs.h"
|
|
5
|
+
#include "json.hpp" // nlohmann::json
|
|
6
|
+
#include <iostream>
|
|
7
|
+
#include <algorithm>
|
|
8
|
+
#include <chrono>
|
|
9
|
+
#include <memory>
|
|
10
|
+
#include <random>
|
|
11
|
+
#include <sstream>
|
|
12
|
+
#include <iomanip>
|
|
13
|
+
#include <stdexcept>
|
|
14
|
+
|
|
15
|
+
#ifdef TESTING
|
|
16
|
+
#define LOG_CLIENT_DEBUG(message) LOG_DEBUG("client", "[pointer: " << this << "] " << message)
|
|
17
|
+
#define LOG_CLIENT_INFO(message) LOG_INFO("client", "[pointer: " << this << "] " << message)
|
|
18
|
+
#define LOG_CLIENT_WARN(message) LOG_WARN("client", "[pointer: " << this << "] " << message)
|
|
19
|
+
#define LOG_CLIENT_ERROR(message) LOG_ERROR("client", "[pointer: " << this << "] " << message)
|
|
20
|
+
#else
|
|
21
|
+
#define LOG_CLIENT_DEBUG(message) LOG_DEBUG("client", message)
|
|
22
|
+
#define LOG_CLIENT_INFO(message) LOG_INFO("client", message)
|
|
23
|
+
#define LOG_CLIENT_WARN(message) LOG_WARN("client", message)
|
|
24
|
+
#define LOG_CLIENT_ERROR(message) LOG_ERROR("client", message)
|
|
25
|
+
#endif
|
|
26
|
+
|
|
27
|
+
namespace librats {
|
|
28
|
+
|
|
29
|
+
// =========================================================================
|
|
30
|
+
// Configuration Persistence Implementation
|
|
31
|
+
// =========================================================================
|
|
32
|
+
|
|
33
|
+
bool RatsClient::load_configuration() {
|
|
34
|
+
std::lock_guard<std::mutex> lock(config_mutex_);
|
|
35
|
+
|
|
36
|
+
LOG_CLIENT_INFO("Loading configuration from " << get_config_file_path());
|
|
37
|
+
|
|
38
|
+
// Check if config file exists
|
|
39
|
+
if (!file_or_directory_exists(get_config_file_path())) {
|
|
40
|
+
LOG_CLIENT_INFO("No existing configuration found, generating new peer ID");
|
|
41
|
+
our_peer_id_ = generate_persistent_peer_id();
|
|
42
|
+
|
|
43
|
+
// Save the new configuration immediately
|
|
44
|
+
{
|
|
45
|
+
nlohmann::json config;
|
|
46
|
+
config["peer_id"] = our_peer_id_;
|
|
47
|
+
config["version"] = RATS_PROTOCOL_VERSION;
|
|
48
|
+
config["listen_port"] = listen_port_;
|
|
49
|
+
config["max_peers"] = max_peers_;
|
|
50
|
+
|
|
51
|
+
auto now = std::chrono::high_resolution_clock::now();
|
|
52
|
+
auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
|
53
|
+
config["created_at"] = timestamp;
|
|
54
|
+
config["last_updated"] = timestamp;
|
|
55
|
+
|
|
56
|
+
std::string config_data = config.dump(4); // Pretty print with 4 spaces
|
|
57
|
+
if (create_file(get_config_file_path(), config_data)) {
|
|
58
|
+
LOG_CLIENT_INFO("Created new configuration file with peer ID: " << our_peer_id_);
|
|
59
|
+
} else {
|
|
60
|
+
LOG_CLIENT_ERROR("Failed to create configuration file");
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Load existing configuration
|
|
69
|
+
try {
|
|
70
|
+
std::string config_data = read_file_text_cpp(get_config_file_path());
|
|
71
|
+
if (config_data.empty()) {
|
|
72
|
+
LOG_CLIENT_ERROR("Configuration file is empty");
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
nlohmann::json config = nlohmann::json::parse(config_data);
|
|
77
|
+
|
|
78
|
+
// Load peer ID
|
|
79
|
+
our_peer_id_ = config.value("peer_id", "");
|
|
80
|
+
if (our_peer_id_.empty()) {
|
|
81
|
+
LOG_CLIENT_WARN("No peer ID in configuration, generating new one");
|
|
82
|
+
our_peer_id_ = generate_persistent_peer_id();
|
|
83
|
+
return save_configuration(); // Save the new peer ID
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
LOG_CLIENT_INFO("Loaded configuration with peer ID: " << our_peer_id_);
|
|
87
|
+
|
|
88
|
+
// Load encryption settings
|
|
89
|
+
if (config.contains("encryption_enabled")) {
|
|
90
|
+
encryption_enabled_ = config.value("encryption_enabled", true);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (config.contains("encryption_key")) {
|
|
94
|
+
std::string key_hex = config.value("encryption_key", "");
|
|
95
|
+
if (!key_hex.empty()) {
|
|
96
|
+
NoiseKey loaded_key = EncryptedSocket::string_to_key(key_hex);
|
|
97
|
+
// Validate key
|
|
98
|
+
bool is_valid = false;
|
|
99
|
+
for (uint8_t byte : loaded_key) {
|
|
100
|
+
if (byte != 0) {
|
|
101
|
+
is_valid = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (is_valid) {
|
|
106
|
+
static_encryption_key_ = loaded_key;
|
|
107
|
+
LOG_CLIENT_INFO("Loaded encryption key from configuration");
|
|
108
|
+
} else {
|
|
109
|
+
LOG_CLIENT_WARN("Invalid encryption key in configuration, using generated key");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update last_updated timestamp
|
|
115
|
+
auto now = std::chrono::high_resolution_clock::now();
|
|
116
|
+
auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
|
117
|
+
config["last_updated"] = timestamp;
|
|
118
|
+
|
|
119
|
+
// Save updated config
|
|
120
|
+
std::string updated_config_data = config.dump(4);
|
|
121
|
+
create_file(get_config_file_path(), updated_config_data);
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
|
|
125
|
+
} catch (const nlohmann::json::exception& e) {
|
|
126
|
+
LOG_CLIENT_ERROR("Failed to parse configuration file: " << e.what());
|
|
127
|
+
return false;
|
|
128
|
+
} catch (const std::exception& e) {
|
|
129
|
+
LOG_CLIENT_ERROR("Failed to load configuration: " << e.what());
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
bool RatsClient::save_configuration() {
|
|
135
|
+
std::lock_guard<std::mutex> lock(config_mutex_);
|
|
136
|
+
|
|
137
|
+
if (our_peer_id_.empty()) {
|
|
138
|
+
LOG_CLIENT_WARN("No peer ID to save");
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
LOG_CLIENT_DEBUG("Saving configuration to " << get_config_file_path());
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Create configuration JSON
|
|
146
|
+
nlohmann::json config;
|
|
147
|
+
config["peer_id"] = our_peer_id_;
|
|
148
|
+
config["version"] = RATS_PROTOCOL_VERSION;
|
|
149
|
+
config["listen_port"] = listen_port_;
|
|
150
|
+
config["max_peers"] = max_peers_;
|
|
151
|
+
config["encryption_enabled"] = encryption_enabled_;
|
|
152
|
+
config["encryption_key"] = get_encryption_key();
|
|
153
|
+
|
|
154
|
+
auto now = std::chrono::high_resolution_clock::now();
|
|
155
|
+
auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
|
156
|
+
config["last_updated"] = timestamp;
|
|
157
|
+
|
|
158
|
+
// If config file exists, preserve created_at timestamp
|
|
159
|
+
if (file_or_directory_exists(get_config_file_path())) {
|
|
160
|
+
try {
|
|
161
|
+
std::string existing_config_data = read_file_text_cpp(get_config_file_path());
|
|
162
|
+
nlohmann::json existing_config = nlohmann::json::parse(existing_config_data);
|
|
163
|
+
if (existing_config.contains("created_at")) {
|
|
164
|
+
config["created_at"] = existing_config["created_at"];
|
|
165
|
+
}
|
|
166
|
+
} catch (const std::exception&) {
|
|
167
|
+
// If we can't read existing config, just use current timestamp
|
|
168
|
+
config["created_at"] = timestamp;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
config["created_at"] = timestamp;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Save configuration
|
|
175
|
+
std::string config_data = config.dump(4);
|
|
176
|
+
if (create_file(get_config_file_path(), config_data)) {
|
|
177
|
+
LOG_CLIENT_DEBUG("Configuration saved successfully");
|
|
178
|
+
} else {
|
|
179
|
+
LOG_CLIENT_ERROR("Failed to save configuration file");
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Save peers
|
|
184
|
+
return save_peers_to_file();
|
|
185
|
+
|
|
186
|
+
} catch (const nlohmann::json::exception& e) {
|
|
187
|
+
LOG_CLIENT_ERROR("Failed to create configuration JSON: " << e.what());
|
|
188
|
+
return false;
|
|
189
|
+
} catch (const std::exception& e) {
|
|
190
|
+
LOG_CLIENT_ERROR("Failed to save configuration: " << e.what());
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
bool RatsClient::save_peers_to_file() {
|
|
196
|
+
// This method assumes config_mutex_ is already locked by save_configuration()
|
|
197
|
+
|
|
198
|
+
LOG_CLIENT_DEBUG("Saving peers to " << get_peers_file_path());
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
nlohmann::json peers_json = nlohmann::json::array();
|
|
202
|
+
|
|
203
|
+
// Get validated peers for saving
|
|
204
|
+
{
|
|
205
|
+
std::lock_guard<std::mutex> peers_lock(peers_mutex_);
|
|
206
|
+
for (const auto& pair : peers_) {
|
|
207
|
+
const RatsPeer& peer = pair.second;
|
|
208
|
+
// Only save peers that have completed handshake and have valid peer IDs
|
|
209
|
+
if (peer.is_handshake_completed() && !peer.peer_id.empty()) {
|
|
210
|
+
// Don't save ourselves
|
|
211
|
+
if (peer.peer_id != our_peer_id_) {
|
|
212
|
+
peers_json.push_back(serialize_peer_for_persistence(peer));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
LOG_CLIENT_INFO("Saving " << peers_json.size() << " peers to persistence file");
|
|
219
|
+
|
|
220
|
+
// Save peers file
|
|
221
|
+
std::string peers_data = peers_json.dump(4);
|
|
222
|
+
if (create_file(get_peers_file_path(), peers_data)) {
|
|
223
|
+
LOG_CLIENT_DEBUG("Peers saved successfully");
|
|
224
|
+
return true;
|
|
225
|
+
} else {
|
|
226
|
+
LOG_CLIENT_ERROR("Failed to save peers file");
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
} catch (const nlohmann::json::exception& e) {
|
|
231
|
+
LOG_CLIENT_ERROR("Failed to serialize peers: " << e.what());
|
|
232
|
+
return false;
|
|
233
|
+
} catch (const std::exception& e) {
|
|
234
|
+
LOG_CLIENT_ERROR("Failed to save peers: " << e.what());
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
int RatsClient::load_and_reconnect_peers() {
|
|
240
|
+
if (!running_.load()) {
|
|
241
|
+
LOG_CLIENT_DEBUG("Client not running, skipping peer reconnection");
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
std::string peers_file_path;
|
|
246
|
+
{
|
|
247
|
+
std::lock_guard<std::mutex> lock(config_mutex_);
|
|
248
|
+
peers_file_path = get_peers_file_path();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
LOG_CLIENT_INFO("Loading saved peers from " << peers_file_path);
|
|
252
|
+
|
|
253
|
+
// Check if peers file exists
|
|
254
|
+
if (!file_or_directory_exists(peers_file_path)) {
|
|
255
|
+
LOG_CLIENT_INFO("No saved peers file found");
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
std::string peers_data = read_file_text_cpp(peers_file_path);
|
|
261
|
+
if (peers_data.empty()) {
|
|
262
|
+
LOG_CLIENT_INFO("Peers file is empty");
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
nlohmann::json peers_json = nlohmann::json::parse(peers_data);
|
|
267
|
+
|
|
268
|
+
if (!peers_json.is_array()) {
|
|
269
|
+
LOG_CLIENT_ERROR("Invalid peers file format - expected array");
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
int reconnect_attempts = 0;
|
|
274
|
+
|
|
275
|
+
for (const auto& peer_json : peers_json) {
|
|
276
|
+
std::string ip;
|
|
277
|
+
int port;
|
|
278
|
+
std::string peer_id;
|
|
279
|
+
|
|
280
|
+
if (!deserialize_peer_from_persistence(peer_json, ip, port, peer_id)) {
|
|
281
|
+
continue; // Skip invalid or old peers
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Don't connect to ourselves
|
|
285
|
+
if (peer_id == get_our_peer_id()) {
|
|
286
|
+
LOG_CLIENT_DEBUG("Skipping connection to ourselves: " << peer_id);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check if we should ignore this peer (local interface)
|
|
291
|
+
if (should_ignore_peer(ip, port)) {
|
|
292
|
+
LOG_CLIENT_DEBUG("Ignoring saved peer " << ip << ":" << port << " - local interface address");
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check if we're already connected to this peer
|
|
297
|
+
std::string normalized_peer_address = normalize_peer_address(ip, port);
|
|
298
|
+
if (is_already_connected_to_address(normalized_peer_address)) {
|
|
299
|
+
LOG_CLIENT_DEBUG("Already connected to saved peer " << normalized_peer_address);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if peer limit is reached
|
|
304
|
+
if (is_peer_limit_reached()) {
|
|
305
|
+
LOG_CLIENT_DEBUG("Peer limit reached, stopping reconnection attempts");
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
LOG_CLIENT_INFO("Attempting to reconnect to saved peer: " << ip << ":" << port << " (peer_id: " << peer_id << ")");
|
|
310
|
+
|
|
311
|
+
// Attempt to connect (non-blocking)
|
|
312
|
+
add_managed_thread(std::thread([this, ip, port, peer_id]() {
|
|
313
|
+
if (connect_to_peer(ip, port)) {
|
|
314
|
+
LOG_CLIENT_INFO("Successfully reconnected to saved peer: " << ip << ":" << port);
|
|
315
|
+
} else {
|
|
316
|
+
LOG_CLIENT_DEBUG("Failed to reconnect to saved peer: " << ip << ":" << port);
|
|
317
|
+
}
|
|
318
|
+
}), "peer-reconnect-" + peer_id.substr(0, 8));
|
|
319
|
+
|
|
320
|
+
reconnect_attempts++;
|
|
321
|
+
|
|
322
|
+
// Small delay between connection attempts to avoid overwhelming the network
|
|
323
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
LOG_CLIENT_INFO("Processed " << peers_json.size() << " saved peers, attempted " << reconnect_attempts << " reconnections");
|
|
327
|
+
return reconnect_attempts;
|
|
328
|
+
|
|
329
|
+
} catch (const nlohmann::json::exception& e) {
|
|
330
|
+
LOG_CLIENT_ERROR("Failed to parse saved peers file: " << e.what());
|
|
331
|
+
return 0;
|
|
332
|
+
} catch (const std::exception& e) {
|
|
333
|
+
LOG_CLIENT_ERROR("Failed to load saved peers: " << e.what());
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
bool RatsClient::append_peer_to_historical_file(const RatsPeer& peer) {
|
|
339
|
+
// Don't save ourselves
|
|
340
|
+
if (peer.peer_id == our_peer_id_) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Only save peers that have completed handshake and have valid peer IDs
|
|
345
|
+
if (!peer.is_handshake_completed() || peer.peer_id.empty()) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
// Create historical peer entry with timestamp
|
|
351
|
+
nlohmann::json historical_peer = serialize_peer_for_persistence(peer);
|
|
352
|
+
|
|
353
|
+
// Add current timestamp as last_seen
|
|
354
|
+
auto now = std::chrono::high_resolution_clock::now();
|
|
355
|
+
auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
|
356
|
+
historical_peer["last_seen"] = timestamp;
|
|
357
|
+
|
|
358
|
+
// Check if file exists, if not create it as an empty array
|
|
359
|
+
std::string file_path = get_peers_ever_file_path();
|
|
360
|
+
nlohmann::json historical_peers;
|
|
361
|
+
|
|
362
|
+
if (file_or_directory_exists(file_path)) {
|
|
363
|
+
try {
|
|
364
|
+
std::string existing_data = read_file_text_cpp(file_path);
|
|
365
|
+
if (!existing_data.empty()) {
|
|
366
|
+
historical_peers = nlohmann::json::parse(existing_data);
|
|
367
|
+
if (!historical_peers.is_array()) {
|
|
368
|
+
LOG_CLIENT_WARN("Historical peers file format invalid, creating new array");
|
|
369
|
+
historical_peers = nlohmann::json::array();
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
historical_peers = nlohmann::json::array();
|
|
373
|
+
}
|
|
374
|
+
} catch (const std::exception& e) {
|
|
375
|
+
LOG_CLIENT_WARN("Failed to read historical peers file, creating new array: " << e.what());
|
|
376
|
+
historical_peers = nlohmann::json::array();
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
historical_peers = nlohmann::json::array();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check if this peer already exists in historical file
|
|
383
|
+
std::string peer_address = peer.ip + ":" + std::to_string(peer.port);
|
|
384
|
+
bool already_exists = false;
|
|
385
|
+
|
|
386
|
+
for (auto& existing_peer : historical_peers) {
|
|
387
|
+
std::string existing_ip = existing_peer.value("ip", "");
|
|
388
|
+
int existing_port = existing_peer.value("port", 0);
|
|
389
|
+
std::string existing_address = existing_ip + ":" + std::to_string(existing_port);
|
|
390
|
+
|
|
391
|
+
if (existing_address == peer_address || existing_peer.value("peer_id", "") == peer.peer_id) {
|
|
392
|
+
// Update timestamp for existing peer
|
|
393
|
+
existing_peer["last_seen"] = timestamp;
|
|
394
|
+
already_exists = true;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// If peer doesn't exist, add it
|
|
400
|
+
if (!already_exists) {
|
|
401
|
+
historical_peers.push_back(historical_peer);
|
|
402
|
+
LOG_CLIENT_DEBUG("Added new peer to historical file: " << peer.ip << ":" << peer.port << " (peer_id: " << peer.peer_id << ")");
|
|
403
|
+
} else {
|
|
404
|
+
LOG_CLIENT_DEBUG("Updated timestamp for existing historical peer: " << peer.ip << ":" << peer.port);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Save updated historical peers file
|
|
408
|
+
std::string historical_data = historical_peers.dump(4);
|
|
409
|
+
if (create_file(file_path, historical_data)) {
|
|
410
|
+
return true;
|
|
411
|
+
} else {
|
|
412
|
+
LOG_CLIENT_ERROR("Failed to save historical peers file");
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
} catch (const nlohmann::json::exception& e) {
|
|
417
|
+
LOG_CLIENT_ERROR("Failed to process historical peer: " << e.what());
|
|
418
|
+
return false;
|
|
419
|
+
} catch (const std::exception& e) {
|
|
420
|
+
LOG_CLIENT_ERROR("Failed to append peer to historical file: " << e.what());
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
bool RatsClient::load_historical_peers() {
|
|
426
|
+
LOG_CLIENT_INFO("Loading historical peers from " << get_peers_ever_file_path());
|
|
427
|
+
|
|
428
|
+
// Check if historical peers file exists
|
|
429
|
+
if (!file_or_directory_exists(get_peers_ever_file_path())) {
|
|
430
|
+
LOG_CLIENT_INFO("No historical peers file found");
|
|
431
|
+
return true; // Not an error
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
std::string historical_data = read_file_text_cpp(get_peers_ever_file_path());
|
|
436
|
+
if (historical_data.empty()) {
|
|
437
|
+
LOG_CLIENT_INFO("Historical peers file is empty");
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
nlohmann::json historical_peers = nlohmann::json::parse(historical_data);
|
|
442
|
+
|
|
443
|
+
if (!historical_peers.is_array()) {
|
|
444
|
+
LOG_CLIENT_ERROR("Invalid historical peers file format - expected array");
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
LOG_CLIENT_INFO("Loaded " << historical_peers.size() << " historical peers from file");
|
|
449
|
+
return true;
|
|
450
|
+
|
|
451
|
+
} catch (const nlohmann::json::exception& e) {
|
|
452
|
+
LOG_CLIENT_ERROR("Failed to parse historical peers file: " << e.what());
|
|
453
|
+
return false;
|
|
454
|
+
} catch (const std::exception& e) {
|
|
455
|
+
LOG_CLIENT_ERROR("Failed to load historical peers file: " << e.what());
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
bool RatsClient::save_historical_peers() {
|
|
461
|
+
try {
|
|
462
|
+
// Get current peers and save them to historical file
|
|
463
|
+
std::vector<RatsPeer> current_peers = get_validated_peers();
|
|
464
|
+
|
|
465
|
+
for (const auto& peer : current_peers) {
|
|
466
|
+
if (!append_peer_to_historical_file(peer)) {
|
|
467
|
+
LOG_CLIENT_WARN("Failed to save peer to historical file: " << peer.ip << ":" << peer.port);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
LOG_CLIENT_INFO("Saved " << current_peers.size() << " current peers to historical file");
|
|
472
|
+
return true;
|
|
473
|
+
|
|
474
|
+
} catch (const std::exception& e) {
|
|
475
|
+
LOG_CLIENT_ERROR("Failed to save historical peers: " << e.what());
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
void RatsClient::clear_historical_peers() {
|
|
481
|
+
std::string file_path = get_peers_ever_file_path();
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
// Create empty array and save to file
|
|
485
|
+
nlohmann::json empty_array = nlohmann::json::array();
|
|
486
|
+
std::string empty_data = empty_array.dump(4);
|
|
487
|
+
|
|
488
|
+
if (create_file(file_path, empty_data)) {
|
|
489
|
+
LOG_CLIENT_INFO("Cleared historical peers file");
|
|
490
|
+
} else {
|
|
491
|
+
LOG_CLIENT_ERROR("Failed to clear historical peers file");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
} catch (const std::exception& e) {
|
|
495
|
+
LOG_CLIENT_ERROR("Failed to clear historical peers: " << e.what());
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
std::vector<RatsPeer> RatsClient::get_historical_peers() const {
|
|
500
|
+
std::vector<RatsPeer> historical_peers;
|
|
501
|
+
|
|
502
|
+
// Check if historical peers file exists
|
|
503
|
+
if (!file_or_directory_exists(get_peers_ever_file_path())) {
|
|
504
|
+
return historical_peers; // Return empty vector
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
std::string historical_data = read_file_text_cpp(get_peers_ever_file_path());
|
|
509
|
+
if (historical_data.empty()) {
|
|
510
|
+
return historical_peers;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
nlohmann::json peers_json = nlohmann::json::parse(historical_data);
|
|
514
|
+
|
|
515
|
+
if (!peers_json.is_array()) {
|
|
516
|
+
LOG_CLIENT_ERROR("Invalid historical peers file format - expected array");
|
|
517
|
+
return historical_peers;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
for (const auto& peer_json : peers_json) {
|
|
521
|
+
std::string ip;
|
|
522
|
+
int port;
|
|
523
|
+
std::string peer_id;
|
|
524
|
+
|
|
525
|
+
if (deserialize_peer_from_persistence(peer_json, ip, port, peer_id)) {
|
|
526
|
+
// Create a RatsPeer object for the historical peer
|
|
527
|
+
// Note: This won't have all the runtime fields populated
|
|
528
|
+
RatsPeer historical_peer(peer_id, ip, static_cast<uint16_t>(port),
|
|
529
|
+
INVALID_SOCKET_VALUE, ip + ":" + std::to_string(port), false);
|
|
530
|
+
|
|
531
|
+
// Set additional fields from JSON if available
|
|
532
|
+
historical_peer.version = peer_json.value("version", "");
|
|
533
|
+
|
|
534
|
+
historical_peers.push_back(historical_peer);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
LOG_CLIENT_DEBUG("Retrieved " << historical_peers.size() << " historical peers");
|
|
539
|
+
|
|
540
|
+
} catch (const nlohmann::json::exception& e) {
|
|
541
|
+
LOG_CLIENT_ERROR("Failed to parse historical peers file: " << e.what());
|
|
542
|
+
} catch (const std::exception& e) {
|
|
543
|
+
LOG_CLIENT_ERROR("Failed to read historical peers file: " << e.what());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return historical_peers;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
int RatsClient::load_and_reconnect_historical_peers() {
|
|
550
|
+
if (!running_.load()) {
|
|
551
|
+
LOG_CLIENT_DEBUG("Client not running, skipping historical peer reconnection");
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
LOG_CLIENT_INFO("Loading historical peers from " << get_peers_ever_file_path());
|
|
556
|
+
|
|
557
|
+
// Check if historical peers file exists
|
|
558
|
+
if (!file_or_directory_exists(get_peers_ever_file_path())) {
|
|
559
|
+
LOG_CLIENT_INFO("No historical peers file found");
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
std::string historical_data = read_file_text_cpp(get_peers_ever_file_path());
|
|
565
|
+
if (historical_data.empty()) {
|
|
566
|
+
LOG_CLIENT_INFO("Historical peers file is empty");
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
nlohmann::json historical_peers = nlohmann::json::parse(historical_data);
|
|
571
|
+
|
|
572
|
+
if (!historical_peers.is_array()) {
|
|
573
|
+
LOG_CLIENT_ERROR("Invalid historical peers file format - expected array");
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
int reconnect_attempts = 0;
|
|
578
|
+
|
|
579
|
+
for (const auto& peer_json : historical_peers) {
|
|
580
|
+
std::string ip;
|
|
581
|
+
int port;
|
|
582
|
+
std::string peer_id;
|
|
583
|
+
|
|
584
|
+
if (!deserialize_peer_from_persistence(peer_json, ip, port, peer_id)) {
|
|
585
|
+
continue; // Skip invalid or old peers
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Don't connect to ourselves
|
|
589
|
+
if (peer_id == get_our_peer_id()) {
|
|
590
|
+
LOG_CLIENT_DEBUG("Skipping connection to ourselves: " << peer_id);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Check if we should ignore this peer (local interface)
|
|
595
|
+
if (should_ignore_peer(ip, port)) {
|
|
596
|
+
LOG_CLIENT_DEBUG("Ignoring historical peer " << ip << ":" << port << " - local interface address");
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check if we're already connected to this peer
|
|
601
|
+
std::string normalized_peer_address = normalize_peer_address(ip, port);
|
|
602
|
+
if (is_already_connected_to_address(normalized_peer_address)) {
|
|
603
|
+
LOG_CLIENT_DEBUG("Already connected to historical peer " << normalized_peer_address);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Check if peer limit is reached
|
|
608
|
+
if (is_peer_limit_reached()) {
|
|
609
|
+
LOG_CLIENT_DEBUG("Peer limit reached, stopping historical reconnection attempts");
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
LOG_CLIENT_INFO("Attempting to reconnect to historical peer: " << ip << ":" << port << " (peer_id: " << peer_id << ")");
|
|
614
|
+
|
|
615
|
+
// Attempt to connect (non-blocking)
|
|
616
|
+
add_managed_thread(std::thread([this, ip, port, peer_id]() {
|
|
617
|
+
if (connect_to_peer(ip, port)) {
|
|
618
|
+
LOG_CLIENT_INFO("Successfully reconnected to historical peer: " << ip << ":" << port);
|
|
619
|
+
} else {
|
|
620
|
+
LOG_CLIENT_DEBUG("Failed to reconnect to historical peer: " << ip << ":" << port);
|
|
621
|
+
}
|
|
622
|
+
}), "historical-reconnect-" + peer_id.substr(0, 8));
|
|
623
|
+
|
|
624
|
+
reconnect_attempts++;
|
|
625
|
+
|
|
626
|
+
// Small delay between connection attempts to avoid overwhelming the network
|
|
627
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(150));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
LOG_CLIENT_INFO("Processed " << historical_peers.size() << " historical peers, attempted " << reconnect_attempts << " reconnections");
|
|
631
|
+
return reconnect_attempts;
|
|
632
|
+
|
|
633
|
+
} catch (const nlohmann::json::exception& e) {
|
|
634
|
+
LOG_CLIENT_ERROR("Failed to parse historical peers file: " << e.what());
|
|
635
|
+
return 0;
|
|
636
|
+
} catch (const std::exception& e) {
|
|
637
|
+
LOG_CLIENT_ERROR("Failed to load historical peers: " << e.what());
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Configuration persistence implementation
|
|
643
|
+
std::string RatsClient::generate_persistent_peer_id() const {
|
|
644
|
+
// Generate a unique peer ID using SHA1 hash of timestamp, random data, and hostname
|
|
645
|
+
auto now = std::chrono::high_resolution_clock::now();
|
|
646
|
+
auto timestamp = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
|
|
647
|
+
|
|
648
|
+
// Get system information for uniqueness
|
|
649
|
+
SystemInfo sys_info = get_system_info();
|
|
650
|
+
|
|
651
|
+
// Create random component
|
|
652
|
+
std::random_device rd;
|
|
653
|
+
std::mt19937 gen(rd());
|
|
654
|
+
std::uniform_int_distribution<> dis(0, 255);
|
|
655
|
+
|
|
656
|
+
// Build unique string
|
|
657
|
+
std::ostringstream unique_stream;
|
|
658
|
+
unique_stream << timestamp << "_" << sys_info.hostname << "_" << listen_port_ << "_";
|
|
659
|
+
|
|
660
|
+
// Add random component
|
|
661
|
+
for (int i = 0; i < 16; ++i) {
|
|
662
|
+
unique_stream << std::setfill('0') << std::setw(2) << std::hex << dis(gen);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Generate SHA1 hash of the unique string
|
|
666
|
+
std::string unique_string = unique_stream.str();
|
|
667
|
+
std::string peer_id = SHA1::hash(unique_string);
|
|
668
|
+
|
|
669
|
+
LOG_CLIENT_INFO("Generated new persistent peer ID: " << peer_id);
|
|
670
|
+
return peer_id;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
nlohmann::json RatsClient::serialize_peer_for_persistence(const RatsPeer& peer) const {
|
|
674
|
+
nlohmann::json peer_json;
|
|
675
|
+
peer_json["ip"] = peer.ip;
|
|
676
|
+
peer_json["port"] = peer.port;
|
|
677
|
+
peer_json["peer_id"] = peer.peer_id;
|
|
678
|
+
peer_json["normalized_address"] = peer.normalized_address;
|
|
679
|
+
peer_json["is_outgoing"] = peer.is_outgoing;
|
|
680
|
+
peer_json["version"] = peer.version;
|
|
681
|
+
|
|
682
|
+
// Add timestamp for cleanup of old peers
|
|
683
|
+
auto now = std::chrono::high_resolution_clock::now();
|
|
684
|
+
auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
|
685
|
+
peer_json["last_seen"] = timestamp;
|
|
686
|
+
|
|
687
|
+
return peer_json;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
bool RatsClient::deserialize_peer_from_persistence(const nlohmann::json& json, std::string& ip, int& port, std::string& peer_id) const {
|
|
691
|
+
try {
|
|
692
|
+
ip = json.value("ip", "");
|
|
693
|
+
port = json.value("port", 0);
|
|
694
|
+
peer_id = json.value("peer_id", "");
|
|
695
|
+
|
|
696
|
+
// Validate required fields
|
|
697
|
+
if (ip.empty() || port <= 0 || port > 65535 || peer_id.empty()) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Check if peer data is not too old (optional - remove peers older than 7 days)
|
|
702
|
+
if (json.contains("last_seen")) {
|
|
703
|
+
auto now = std::chrono::high_resolution_clock::now();
|
|
704
|
+
auto current_timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
|
705
|
+
int64_t last_seen = json.value("last_seen", current_timestamp);
|
|
706
|
+
|
|
707
|
+
const int64_t MAX_PEER_AGE_SECONDS = 7 * 24 * 60 * 60; // 7 days
|
|
708
|
+
if (current_timestamp - last_seen > MAX_PEER_AGE_SECONDS) {
|
|
709
|
+
LOG_CLIENT_DEBUG("Skipping old peer " << ip << ":" << port << " (last seen " << (current_timestamp - last_seen) << " seconds ago)");
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return true;
|
|
715
|
+
|
|
716
|
+
} catch (const nlohmann::json::exception& e) {
|
|
717
|
+
LOG_CLIENT_ERROR("Failed to deserialize peer: " << e.what());
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
std::string RatsClient::get_config_file_path() const {
|
|
723
|
+
#ifdef TESTING
|
|
724
|
+
// For testing with ephemeral ports (port 0), use a unique identifier to avoid conflicts
|
|
725
|
+
if (listen_port_ == 0) {
|
|
726
|
+
// Generate a unique file path based on object pointer to ensure uniqueness during testing
|
|
727
|
+
std::ostringstream oss;
|
|
728
|
+
oss << "config_" << this << ".json";
|
|
729
|
+
return oss.str();
|
|
730
|
+
}
|
|
731
|
+
return "config_" + std::to_string(listen_port_) + ".json";
|
|
732
|
+
#else
|
|
733
|
+
return data_directory_ + "/" + CONFIG_FILE_NAME;
|
|
734
|
+
#endif
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
std::string RatsClient::get_peers_file_path() const {
|
|
738
|
+
#ifdef TESTING
|
|
739
|
+
// For testing with ephemeral ports (port 0), use a unique identifier to avoid conflicts
|
|
740
|
+
if (listen_port_ == 0) {
|
|
741
|
+
// Generate a unique file path based on object pointer to ensure uniqueness during testing
|
|
742
|
+
std::ostringstream oss;
|
|
743
|
+
oss << "peers_" << this << ".json";
|
|
744
|
+
return oss.str();
|
|
745
|
+
}
|
|
746
|
+
return "peers_" + std::to_string(listen_port_) + ".json";
|
|
747
|
+
#else
|
|
748
|
+
return data_directory_ + "/" + PEERS_FILE_NAME;
|
|
749
|
+
#endif
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
std::string RatsClient::get_peers_ever_file_path() const {
|
|
753
|
+
#ifdef TESTING
|
|
754
|
+
// For testing with ephemeral ports (port 0), use a unique identifier to avoid conflicts
|
|
755
|
+
if (listen_port_ == 0) {
|
|
756
|
+
// Generate a unique file path based on object pointer to ensure uniqueness during testing
|
|
757
|
+
std::ostringstream oss;
|
|
758
|
+
oss << "peers_ever_" << this << ".json";
|
|
759
|
+
return oss.str();
|
|
760
|
+
}
|
|
761
|
+
return "peers_ever_" + std::to_string(listen_port_) + ".json";
|
|
762
|
+
#else
|
|
763
|
+
return data_directory_ + "/" + PEERS_EVER_FILE_NAME;
|
|
764
|
+
#endif
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// =========================================================================
|
|
768
|
+
// Data directory management
|
|
769
|
+
// =========================================================================
|
|
770
|
+
|
|
771
|
+
bool RatsClient::set_data_directory(const std::string& directory_path) {
|
|
772
|
+
std::lock_guard<std::mutex> lock(config_mutex_);
|
|
773
|
+
|
|
774
|
+
// Normalize the path (remove trailing slashes)
|
|
775
|
+
std::string normalized_path = directory_path;
|
|
776
|
+
while (!normalized_path.empty() && (normalized_path.back() == '/' || normalized_path.back() == '\\')) {
|
|
777
|
+
normalized_path.pop_back();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Use current directory if empty
|
|
781
|
+
if (normalized_path.empty()) {
|
|
782
|
+
normalized_path = ".";
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Check if directory exists
|
|
786
|
+
if (!directory_exists(normalized_path)) {
|
|
787
|
+
// Try to create the directory
|
|
788
|
+
if (!create_directories(normalized_path.c_str())) {
|
|
789
|
+
LOG_CLIENT_ERROR("Failed to create data directory: " << normalized_path);
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
LOG_CLIENT_INFO("Created data directory: " << normalized_path);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Test if we can write to the directory by creating a temporary file
|
|
796
|
+
std::string test_file = normalized_path + "/test_write_access.tmp";
|
|
797
|
+
if (!create_file(test_file, "test")) {
|
|
798
|
+
LOG_CLIENT_ERROR("Cannot write to data directory: " << normalized_path);
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Clean up test file
|
|
803
|
+
delete_file(test_file.c_str());
|
|
804
|
+
|
|
805
|
+
data_directory_ = normalized_path;
|
|
806
|
+
LOG_CLIENT_INFO("Data directory set to: " << data_directory_);
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
std::string RatsClient::get_data_directory() const {
|
|
811
|
+
std::lock_guard<std::mutex> lock(config_mutex_);
|
|
812
|
+
return data_directory_;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
} // namespace librats
|