librats 0.9.0 → 0.9.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.
@@ -0,0 +1,163 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file upnp.h
5
+ * @brief UPnP Internet Gateway Device (IGD) port mapping client
6
+ *
7
+ * Implements the client side of automatic port forwarding via UPnP:
8
+ * 1. Discover the IGD on the LAN with an SSDP M-SEARCH (UDP multicast to
9
+ * 239.255.255.250:1900).
10
+ * 2. Fetch and parse the device description XML to locate the
11
+ * WANIPConnection / WANPPPConnection service control URL.
12
+ * 3. Issue SOAP actions (AddPortMapping / DeletePortMapping /
13
+ * GetExternalIPAddress) over HTTP to the control URL.
14
+ * 4. Periodically refresh the mappings so the lease never expires.
15
+ *
16
+ * All network activity runs on a dedicated worker thread. Results are reported
17
+ * through a @ref PortMapCallback. The implementation is self-contained (no
18
+ * external XML/HTTP libraries) using librats' own socket primitives.
19
+ */
20
+
21
+ #include "port_mapping.h"
22
+ #include "wakeup_pipe.h"
23
+
24
+ #include <atomic>
25
+ #include <condition_variable>
26
+ #include <mutex>
27
+ #include <string>
28
+ #include <thread>
29
+ #include <vector>
30
+ #include <chrono>
31
+
32
+ namespace librats {
33
+
34
+ /**
35
+ * Pure parsing helpers used by the UPnP client. Exposed (rather than file-local)
36
+ * so the SSDP/SOAP XML and URL handling — the most error-prone part of the
37
+ * protocol — can be unit tested directly.
38
+ */
39
+ namespace upnp_detail {
40
+
41
+ /// Extract the trimmed text of the first <tag ...>...</tag> (case-insensitive),
42
+ /// searching from @p from. Returns "" when the tag is absent.
43
+ std::string extract_xml_tag(const std::string& xml, const std::string& tag, size_t from = 0);
44
+
45
+ /// Parse "http://host[:port][/path]". Defaults port to 80 and path to "/".
46
+ /// Returns false if the scheme is missing or the host is empty.
47
+ bool parse_http_url(const std::string& url, std::string& host, uint16_t& port, std::string& path);
48
+
49
+ /// Resolve a device's controlURL (which may be absolute, root-relative or
50
+ /// path-relative) into an absolute http URL. @p url_base is the device's optional
51
+ /// <URLBase>; @p desc_host / @p desc_port are the host the description was fetched
52
+ /// from, used when no URLBase is present.
53
+ std::string resolve_control_url(std::string control_url, std::string url_base,
54
+ const std::string& desc_host, uint16_t desc_port);
55
+
56
+ } // namespace upnp_detail
57
+
58
+ /// SSDP multicast address / port used to discover UPnP devices.
59
+ constexpr const char* SSDP_MULTICAST_ADDR = "239.255.255.250";
60
+ constexpr uint16_t SSDP_PORT = 1900;
61
+
62
+ /// Default UPnP lease duration in seconds (0 means request a permanent mapping).
63
+ constexpr uint32_t UPNP_DEFAULT_LEASE = 3600;
64
+
65
+ /**
66
+ * UPnP IGD port mapping client. Thread-safe public API; all SSDP/HTTP/SOAP
67
+ * traffic happens on the internal worker thread started by @ref start().
68
+ */
69
+ class UpnpClient {
70
+ public:
71
+ UpnpClient();
72
+ ~UpnpClient();
73
+
74
+ UpnpClient(const UpnpClient&) = delete;
75
+ UpnpClient& operator=(const UpnpClient&) = delete;
76
+
77
+ /**
78
+ * Register a mapping to install. External port defaults to the internal port.
79
+ * Safe to call before or after @ref start().
80
+ */
81
+ void add_mapping(PortMapProtocol protocol, uint16_t internal_port, uint16_t external_port = 0,
82
+ const std::string& description = "librats");
83
+
84
+ /// Request lease duration in seconds (0 = permanent). Effective on next refresh.
85
+ void set_lease_duration(uint32_t seconds) { lease_duration_ = seconds; }
86
+
87
+ /// Set the result callback (invoked from the worker thread).
88
+ void set_callback(PortMapCallback cb) { callback_ = std::move(cb); }
89
+
90
+ /// Start discovery + mapping on the worker thread. Returns false if running.
91
+ bool start();
92
+
93
+ /// Remove installed mappings (best-effort) and stop the worker thread.
94
+ void stop();
95
+
96
+ bool is_running() const { return running_.load(); }
97
+
98
+ /// Discovered external IP address reported by the IGD, or empty.
99
+ std::string external_ip() const;
100
+
101
+ private:
102
+ struct Mapping {
103
+ PortMapProtocol protocol;
104
+ uint16_t internal_port;
105
+ uint16_t external_port;
106
+ std::string description;
107
+ bool active = false;
108
+ std::chrono::steady_clock::time_point expires{};
109
+ };
110
+
111
+ // Parsed IGD endpoint discovered via SSDP + device description.
112
+ struct Device {
113
+ std::string control_url; // absolute http URL of the control endpoint
114
+ std::string service_type; // urn:schemas-upnp-org:service:WANIPConnection:1 ...
115
+ std::string control_host; // host of control_url
116
+ uint16_t control_port = 0; // port of control_url
117
+ std::string control_path; // path of control_url
118
+ std::string local_ip; // our LAN IP facing this device
119
+ bool valid() const { return !control_url.empty() && !service_type.empty(); }
120
+ };
121
+
122
+ void worker_loop();
123
+ bool discover_device(Device& out); // SSDP + description fetch/parse
124
+ bool fetch_description(const std::string& location, const std::string& local_ip, Device& out);
125
+ bool add_port_mapping(const Device& dev, Mapping& m);
126
+ bool delete_port_mapping(const Device& dev, const Mapping& m);
127
+ bool query_external_ip(const Device& dev);
128
+ void remove_all_mappings(const Device& dev);
129
+ void notify(const Mapping& m, bool success, const std::string& error);
130
+
131
+ // SOAP helper: POST an action to the device control URL. Returns true only on
132
+ // a successful (HTTP 200, no SOAP fault) response. When the IGD reports a UPnP
133
+ // error, *upnp_error receives its numeric code (e.g. 718 conflict, 725 permanent
134
+ // lease only) so the caller can react; it is set to 0 on success.
135
+ bool soap_action(const Device& dev, const std::string& action,
136
+ const std::string& body_args, std::string& response_body,
137
+ int* upnp_error = nullptr);
138
+
139
+ PortMapCallback callback_;
140
+ uint32_t lease_duration_ = UPNP_DEFAULT_LEASE;
141
+ // Set once an IGD rejects a timed lease with error 725; subsequent requests ask
142
+ // for a permanent (0) lease. Touched only from the worker thread.
143
+ bool permanent_lease_only_ = false;
144
+
145
+ mutable std::mutex mutex_; // guards mappings_, external_ip_, device_
146
+ std::vector<Mapping> mappings_;
147
+ std::string external_ip_;
148
+ Device device_;
149
+ bool device_found_ = false;
150
+
151
+ std::atomic<bool> running_{false};
152
+ std::atomic<bool> stop_requested_{false};
153
+ std::condition_variable cv_;
154
+ std::mutex cv_mutex_;
155
+ // Set under cv_mutex_ to break the refresh sleep early when a mapping is added
156
+ // or stop() is requested. Guarding it with the same mutex the worker waits on
157
+ // is what makes the wakeup race-free (no lost notifications).
158
+ bool wake_worker_ = false;
159
+ WakeupPipe wakeup_; // interrupts blocking SSDP receives on stop()
160
+ std::thread worker_;
161
+ };
162
+
163
+ } // namespace librats
@@ -0,0 +1,60 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file wakeup_pipe.h
5
+ * @brief Loopback-UDP self-pipe for interrupting a blocking select()/receive.
6
+ *
7
+ * A worker thread that blocks in receive_udp_data() can only react to a stop
8
+ * request after its socket timeout expires. WakeupPipe provides a second socket
9
+ * to add to that select() set: stop() sends a one-byte datagram to it, which
10
+ * wakes the select() immediately so the worker can observe stop_requested_ and
11
+ * exit without waiting out the timeout.
12
+ *
13
+ * UDP loopback is used (rather than a pipe) because it is selectable on every
14
+ * platform, including Windows. If socket creation fails the pipe degrades
15
+ * gracefully: fd() returns an invalid socket (treated as "no interrupt") and
16
+ * signal() is a no-op, so callers simply fall back to timeout-based wakeups.
17
+ */
18
+
19
+ #include "socket.h"
20
+
21
+ #include <vector>
22
+
23
+ namespace librats {
24
+
25
+ class WakeupPipe {
26
+ public:
27
+ WakeupPipe() {
28
+ sock_ = create_udp_socket(0, "127.0.0.1", AddressFamily::IPv4);
29
+ if (is_valid_socket(sock_)) {
30
+ sockaddr_in addr;
31
+ socklen_t len = sizeof(addr);
32
+ if (getsockname(sock_, reinterpret_cast<sockaddr*>(&addr), &len) == 0) {
33
+ port_ = ntohs(addr.sin_port);
34
+ }
35
+ }
36
+ }
37
+
38
+ ~WakeupPipe() {
39
+ if (is_valid_socket(sock_)) close_socket(sock_);
40
+ }
41
+
42
+ WakeupPipe(const WakeupPipe&) = delete;
43
+ WakeupPipe& operator=(const WakeupPipe&) = delete;
44
+
45
+ /// Socket to add to a select() set (pass as receive_udp_data's interrupt_fd).
46
+ socket_t fd() const { return sock_; }
47
+
48
+ /// Wake any select() watching fd(). Idempotent; only meant to be called from stop().
49
+ void signal() {
50
+ if (is_valid_socket(sock_) && port_ != 0) {
51
+ send_udp_data(sock_, std::vector<uint8_t>{1}, "127.0.0.1", port_, AddressFamily::IPv4);
52
+ }
53
+ }
54
+
55
+ private:
56
+ socket_t sock_ = INVALID_SOCKET_VALUE;
57
+ uint16_t port_ = 0;
58
+ };
59
+
60
+ } // namespace librats
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librats",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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",