blob: afb110a8778731d9bea7803cb76ed16d485143e1 [file] [log] [blame]
Adrien Béraud612b55b2023-05-29 10:42:04 -04001/*
2 * Copyright (C) 2004-2023 Savoir-faire Linux Inc.
3 *
Adrien Béraudcb753622023-07-17 22:32:49 -04004 * This program is free software: you can redistribute it and/or modify
Adrien Béraud612b55b2023-05-29 10:42:04 -04005 * it under the terms of the GNU General Public License as published by
Adrien Béraudcb753622023-07-17 22:32:49 -04006 * the Free Software Foundation, either version 3 of the License, or
Adrien Béraud612b55b2023-05-29 10:42:04 -04007 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
Adrien Béraudcb753622023-07-17 22:32:49 -040011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Adrien Béraud612b55b2023-05-29 10:42:04 -040012 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
Adrien Béraudcb753622023-07-17 22:32:49 -040015 * along with this program. If not, see <https://www.gnu.org/licenses/>.
Adrien Béraud612b55b2023-05-29 10:42:04 -040016 */
Adrien Béraud612b55b2023-05-29 10:42:04 -040017#include "pupnp.h"
Adrien Béraud370257c2023-08-15 20:53:09 -040018#include "string_utils.h"
Adrien Béraud612b55b2023-05-29 10:42:04 -040019
Adrien Béraud612b55b2023-05-29 10:42:04 -040020#include <opendht/http.h>
21
Adrien Béraud1ae60aa2023-07-07 09:55:09 -040022namespace dhtnet {
Adrien Béraud612b55b2023-05-29 10:42:04 -040023namespace upnp {
24
25// Action identifiers.
26constexpr static const char* ACTION_ADD_PORT_MAPPING {"AddPortMapping"};
27constexpr static const char* ACTION_DELETE_PORT_MAPPING {"DeletePortMapping"};
28constexpr static const char* ACTION_GET_GENERIC_PORT_MAPPING_ENTRY {"GetGenericPortMappingEntry"};
29constexpr static const char* ACTION_GET_STATUS_INFO {"GetStatusInfo"};
30constexpr static const char* ACTION_GET_EXTERNAL_IP_ADDRESS {"GetExternalIPAddress"};
31
32// Error codes returned by router when trying to remove ports.
33constexpr static int ARRAY_IDX_INVALID = 713;
34constexpr static int CONFLICT_IN_MAPPING = 718;
35
36// Max number of IGD search attempts before failure.
37constexpr static unsigned int PUPNP_MAX_RESTART_SEARCH_RETRIES {3};
38// IGD search timeout (in seconds).
39constexpr static unsigned int SEARCH_TIMEOUT {60};
40// Base unit for the timeout between two successive IGD search.
41constexpr static auto PUPNP_SEARCH_RETRY_UNIT {std::chrono::seconds(10)};
42
43// Helper functions for xml parsing.
44static std::string_view
45getElementText(IXML_Node* node)
46{
47 if (node) {
48 if (IXML_Node* textNode = ixmlNode_getFirstChild(node))
49 if (const char* value = ixmlNode_getNodeValue(textNode))
50 return std::string_view(value);
51 }
52 return {};
53}
54
55static std::string_view
56getFirstDocItem(IXML_Document* doc, const char* item)
57{
58 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
59 nodeList(ixmlDocument_getElementsByTagName(doc, item), ixmlNodeList_free);
60 if (nodeList) {
61 // If there are several nodes which match the tag, we only want the first one.
62 return getElementText(ixmlNodeList_item(nodeList.get(), 0));
63 }
64 return {};
65}
66
67static std::string_view
68getFirstElementItem(IXML_Element* element, const char* item)
69{
70 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
71 nodeList(ixmlElement_getElementsByTagName(element, item), ixmlNodeList_free);
72 if (nodeList) {
73 // If there are several nodes which match the tag, we only want the first one.
74 return getElementText(ixmlNodeList_item(nodeList.get(), 0));
75 }
76 return {};
77}
78
79static bool
Adrien Béraudd78d1ac2023-08-25 10:43:33 -040080errorOnResponse(IXML_Document* doc, const std::shared_ptr<dht::log::Logger>& logger)
Adrien Béraud612b55b2023-05-29 10:42:04 -040081{
82 if (not doc)
83 return true;
84
85 auto errorCode = getFirstDocItem(doc, "errorCode");
86 if (not errorCode.empty()) {
87 auto errorDescription = getFirstDocItem(doc, "errorDescription");
Adrien Béraudd78d1ac2023-08-25 10:43:33 -040088 if (logger) logger->warn("PUPnP: Response contains error: {:s}: {:s}",
89 errorCode,
90 errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -040091 return true;
92 }
93 return false;
94}
95
96// UPNP class implementation
97
Adrien Béraud370257c2023-08-15 20:53:09 -040098PUPnP::PUPnP(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger)
99 : UPnPProtocol(logger), ioContext(ctx), searchForIgdTimer_(*ctx)
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400100 , ongoingOpsThreadPool_(1, 64)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400101{
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400102 if (logger_) logger_->debug("PUPnP: Creating instance [{}] ...", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400103}
104
105PUPnP::~PUPnP()
106{
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400107 if (logger_) logger_->debug("PUPnP: Instance [{}] destroyed", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400108}
109
110void
111PUPnP::initUpnpLib()
112{
113 assert(not initialized_);
Adrien Bérauda61adb52023-08-23 09:31:02 -0400114 auto hostinfo = ip_utils::getHostName();
Adrien Bérauda61adb52023-08-23 09:31:02 -0400115 int upnp_err = UpnpInit2(hostinfo.interface.empty() ? nullptr : hostinfo.interface.c_str(), 0);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400116 if (upnp_err != UPNP_E_SUCCESS) {
Amna7a33c7c2024-08-16 12:26:04 -0400117 if (upnp_err == UPNP_E_INIT) {
118 if (logger_) logger_->warn("PUPnP: libupnp already initialized");
119 initialized_ = true;
120 return;
121 }else {
122 if (logger_) logger_->error("PUPnP: Can't initialize libupnp: {}", UpnpGetErrorMessage(upnp_err));
123 UpnpFinish();
124 initialized_ = false;
125 return;
126 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400127 }
128
129 // Disable embedded WebServer if any.
130 if (UpnpIsWebserverEnabled() == 1) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400131 if (logger_) logger_->warn("PUPnP: Web-server is enabled. Disabling");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400132 UpnpEnableWebserver(0);
133 if (UpnpIsWebserverEnabled() == 1) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400134 if (logger_) logger_->error("PUPnP: Could not disable Web-server!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400135 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400136 if (logger_) logger_->debug("PUPnP: Web-server successfully disabled");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400137 }
138 }
139
140 char* ip_address = UpnpGetServerIpAddress();
141 char* ip_address6 = nullptr;
142 unsigned short port = UpnpGetServerPort();
143 unsigned short port6 = 0;
144#if UPNP_ENABLE_IPV6
145 ip_address6 = UpnpGetServerIp6Address();
146 port6 = UpnpGetServerPort6();
147#endif
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400148 if (logger_) {
149 if (ip_address6 and port6)
150 logger_->debug("PUPnP: Initialized on {}:{:d} | {}:{:d}", ip_address, port, ip_address6, port6);
151 else
152 logger_->debug("PUPnP: Initialized on {}:{:d}", ip_address, port);
153 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400154
155 // Relax the parser to allow malformed XML text.
156 ixmlRelaxParser(1);
157
158 initialized_ = true;
159}
160
161bool
162PUPnP::isRunning() const
163{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500164 std::unique_lock lk(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400165 return not shutdownComplete_;
166}
167
168void
169PUPnP::registerClient()
170{
171 assert(not clientRegistered_);
172
Adrien Béraud612b55b2023-05-29 10:42:04 -0400173 // Register Upnp control point.
174 int upnp_err = UpnpRegisterClient(ctrlPtCallback, this, &ctrlptHandle_);
175 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400176 if (logger_) logger_->error("PUPnP: Can't register client: {}", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400177 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400178 if (logger_) logger_->debug("PUPnP: Successfully registered client");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400179 clientRegistered_ = true;
180 }
181}
182
183void
François-Simon Fauteux-Chapleaud7976982024-04-26 16:06:23 -0400184PUPnP::unregisterClient()
185{
186 int upnp_err = UpnpUnRegisterClient(ctrlptHandle_);
187 if (upnp_err != UPNP_E_SUCCESS) {
188 if (logger_) logger_->error("PUPnP: Failed to unregister client: {}", UpnpGetErrorMessage(upnp_err));
189 } else {
190 if (logger_) logger_->debug("PUPnP: Successfully unregistered client");
191 clientRegistered_ = false;
192 }
193}
194
195void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400196PUPnP::setObserver(UpnpMappingObserver* obs)
197{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400198 observer_ = obs;
199}
200
201const IpAddr
202PUPnP::getHostAddress() const
203{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500204 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400205 return hostAddress_;
206}
207
208void
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400209PUPnP::terminate()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400210{
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400211 if (logger_) logger_->debug("PUPnP: Terminate instance {}", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400212
213 clientRegistered_ = false;
214 observer_ = nullptr;
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400215 {
216 std::lock_guard lk(ongoingOpsMtx_);
217 destroying_ = true;
218 if (ongoingOps_ > 0) {
219 if (logger_) logger_->debug("PUPnP: {} ongoing operations, detaching corresponding threads", ongoingOps_);
220 ongoingOpsThreadPool_.detach();
221 }
222 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400223
224 UpnpUnRegisterClient(ctrlptHandle_);
225
226 if (initialized_) {
227 if (UpnpFinish() != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400228 if (logger_) logger_->error("PUPnP: Failed to properly close lib-upnp");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400229 }
230
231 initialized_ = false;
232 }
233
234 // Clear all the lists.
235 discoveredIgdList_.clear();
236
Adrien Béraud024c46f2024-03-02 23:53:18 -0500237 std::lock_guard lock(pupnpMutex_);
Adrien Béraud7a82bee2023-08-30 10:26:45 -0400238 validIgdList_.clear();
239 shutdownComplete_ = true;
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400240 if (logger_) logger_->debug("PUPnP: Instance {} terminated", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400241}
242
243void
Amna0d215232024-08-27 17:57:45 -0400244PUPnP::searchForDeviceAsync(const std::string& deviceType)
245{
246 // Despite its name and the claim in the libupnp documentation that it "returns immediately",
247 // the UpnpSearchAsync function isn't really async. This is because it tries to send multiple
248 // copies of each search message and waits for a certain amount of time after sending each
249 // copy. The number of copies is given by the NUM_SSDP_COPY macro, whose default value is 2,
250 // and the waiting time is determined by the SSDP_PAUSE macro, whose default value is 100 (ms).
251 // If both IPv4 and IPv6 are enabled, then UpnpSearchAsync sends 3 distinct messages (2 for IPv6
252 // and 1 for IPv4), resulting in a total of 3 * 2 * 100 = 600 ms spent waiting by default.
253 // This is why we put the call to UpnpSearchAsync on its own thread.
254 dht::ThreadPool::io().run([w = weak_from_this(), deviceType] {
255 auto sthis = std::static_pointer_cast<PUPnP>(w.lock());
256 if (!sthis)
257 return;
258
259 auto err = UpnpSearchAsync(sthis->ctrlptHandle_,
260 SEARCH_TIMEOUT,
261 deviceType.c_str(),
262 sthis.get());
263 if (err != UPNP_E_SUCCESS) {
264 if (sthis->logger_)
265 sthis->logger_->warn("PUPnP: Send search for {} failed. Error {:d}: {}",
266 deviceType,
267 err,
268 UpnpGetErrorMessage(err));
269 }
270 });
271}
272void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400273PUPnP::searchForDevices()
274{
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400275 if (logger_) logger_->debug("PUPnP: Send IGD search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400276
277 // Send out search for multiple types of devices, as some routers may possibly
278 // only reply to one.
Amna0d215232024-08-27 17:57:45 -0400279 searchForDeviceAsync(UPNP_ROOT_DEVICE);
280 searchForDeviceAsync(UPNP_IGD_DEVICE);
281 searchForDeviceAsync(UPNP_WANIP_SERVICE);
282 searchForDeviceAsync(UPNP_WANPPP_SERVICE);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400283}
284
285void
286PUPnP::clearIgds()
287{
Morteza Namvar5f639522023-07-04 17:08:58 -0400288 // JAMI_DBG("PUPnP: clearing IGDs and devices lists");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400289
François-Simon Fauteux-Chapleaud7976982024-04-26 16:06:23 -0400290 // We need to unregister the client to make sure that we don't keep receiving and
291 // processing IGD-related events unnecessarily, see:
292 // https://git.jami.net/savoirfairelinux/dhtnet/-/issues/29
293 if (clientRegistered_)
294 unregisterClient();
295
Adrien Béraud370257c2023-08-15 20:53:09 -0400296 searchForIgdTimer_.cancel();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400297
298 igdSearchCounter_ = 0;
299
300 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500301 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400302 for (auto const& igd : validIgdList_) {
303 igd->setValid(false);
304 }
305 validIgdList_.clear();
306 hostAddress_ = {};
307 }
308
309 discoveredIgdList_.clear();
310}
311
312void
313PUPnP::searchForIgd()
314{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400315 // Update local address before searching.
316 updateHostAddress();
317
318 if (isReady()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400319 if (logger_) logger_->debug("PUPnP: Already have a valid IGD. Skip the search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400320 return;
321 }
322
323 if (igdSearchCounter_++ >= PUPNP_MAX_RESTART_SEARCH_RETRIES) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400324 if (logger_) logger_->warn("PUPnP: Setup failed after {:d} trials. PUPnP will be disabled!",
325 PUPNP_MAX_RESTART_SEARCH_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400326 return;
327 }
328
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400329 if (logger_) logger_->debug("PUPnP: Start search for IGD: attempt {:d}", igdSearchCounter_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400330
331 // Do not init if the host is not valid. Otherwise, the init will fail
332 // anyway and may put libupnp in an unstable state (mainly deadlocks)
333 // even if the UpnpFinish() method is called.
334 if (not hasValidHostAddress()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400335 if (logger_) logger_->warn("PUPnP: Host address is invalid. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400336 } else {
337 // Init and register if needed
338 if (not initialized_) {
339 initUpnpLib();
340 }
341 if (initialized_ and not clientRegistered_) {
342 registerClient();
343 }
344 // Start searching
345 if (clientRegistered_) {
346 assert(initialized_);
347 searchForDevices();
Amna0d215232024-08-27 17:57:45 -0400348 observer_->onIgdDiscoveryStarted();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400349 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400350 if (logger_) logger_->warn("PUPnP: PUPNP not fully setup. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400351 }
352 }
353
354 // Cancel the current timer (if any) and re-schedule.
355 // The connectivity change may be received while the the local
356 // interface is not fully setup. The rescheduling typically
357 // usefull to mitigate this race.
Adrien Béraud370257c2023-08-15 20:53:09 -0400358 searchForIgdTimer_.expires_after(PUPNP_SEARCH_RETRY_UNIT * igdSearchCounter_);
359 searchForIgdTimer_.async_wait([w = weak()] (const asio::error_code& ec) {
360 if (not ec) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400361 if (auto upnpThis = w.lock())
362 upnpThis->searchForIgd();
Adrien Béraud370257c2023-08-15 20:53:09 -0400363 }
364 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400365}
366
367std::list<std::shared_ptr<IGD>>
368PUPnP::getIgdList() const
369{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500370 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400371 std::list<std::shared_ptr<IGD>> igdList;
372 for (auto& it : validIgdList_) {
373 // Return only active IGDs.
374 if (it->isValid()) {
375 igdList.emplace_back(it);
376 }
377 }
378 return igdList;
379}
380
381bool
382PUPnP::isReady() const
383{
384 // Must at least have a valid local address.
385 if (not getHostAddress() or getHostAddress().isLoopback())
386 return false;
387
388 return hasValidIgd();
389}
390
391bool
392PUPnP::hasValidIgd() const
393{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500394 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400395 for (auto& it : validIgdList_) {
396 if (it->isValid()) {
397 return true;
398 }
399 }
400 return false;
401}
402
403void
404PUPnP::updateHostAddress()
405{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500406 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400407 hostAddress_ = ip_utils::getLocalAddr(AF_INET);
408}
409
410bool
411PUPnP::hasValidHostAddress()
412{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500413 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400414 return hostAddress_ and not hostAddress_.isLoopback();
415}
416
417void
418PUPnP::incrementErrorsCounter(const std::shared_ptr<IGD>& igd)
419{
420 if (not igd or not igd->isValid())
421 return;
422 if (not igd->incrementErrorsCounter()) {
423 // Disable this IGD.
424 igd->setValid(false);
425 // Notify the listener.
426 if (observer_)
427 observer_->onIgdUpdated(igd, UpnpIgdEvent::INVALID_STATE);
428 }
429}
430
431bool
432PUPnP::validateIgd(const std::string& location, IXML_Document* doc_container_ptr)
433{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400434 assert(doc_container_ptr != nullptr);
435
436 XMLDocument document(doc_container_ptr, ixmlDocument_free);
437 auto descDoc = document.get();
438 // Check device type.
439 auto deviceType = getFirstDocItem(descDoc, "deviceType");
440 if (deviceType != UPNP_IGD_DEVICE) {
441 // Device type not IGD.
442 return false;
443 }
444
445 std::shared_ptr<UPnPIGD> igd_candidate = parseIgd(descDoc, location);
446 if (not igd_candidate) {
447 // No valid IGD candidate.
448 return false;
449 }
450
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400451 if (logger_) logger_->debug("PUPnP: Validating the IGD candidate [UDN: {}]\n"
452 " Name : {}\n"
453 " Service Type : {}\n"
454 " Service ID : {}\n"
455 " Base URL : {}\n"
456 " Location URL : {}\n"
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400457 " Control URL : {}\n"
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400458 " Event URL : {}",
459 igd_candidate->getUID(),
460 igd_candidate->getFriendlyName(),
461 igd_candidate->getServiceType(),
462 igd_candidate->getServiceId(),
463 igd_candidate->getBaseURL(),
464 igd_candidate->getLocationURL(),
465 igd_candidate->getControlURL(),
466 igd_candidate->getEventSubURL());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400467
468 // Check if IGD is connected.
469 if (not actionIsIgdConnected(*igd_candidate)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400470 if (logger_) logger_->warn("PUPnP: IGD candidate {} is not connected", igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400471 return false;
472 }
473
474 // Validate external Ip.
475 igd_candidate->setPublicIp(actionGetExternalIP(*igd_candidate));
476 if (igd_candidate->getPublicIp().toString().empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400477 if (logger_) logger_->warn("PUPnP: IGD candidate {} has no valid external Ip",
478 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400479 return false;
480 }
481
482 // Validate internal Ip.
483 if (igd_candidate->getBaseURL().empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400484 if (logger_) logger_->warn("PUPnP: IGD candidate {} has no valid internal Ip",
485 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400486 return false;
487 }
488
489 // Typically the IGD local address should be extracted from the XML
490 // document (e.g. parsing the base URL). For simplicity, we assume
491 // that it matches the gateway as seen by the local interface.
492 if (const auto& localGw = ip_utils::getLocalGateway()) {
493 igd_candidate->setLocalIp(localGw);
494 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400495 if (logger_) logger_->warn("PUPnP: Could not set internal address for IGD candidate {}",
496 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400497 return false;
498 }
499
500 // Store info for subscription.
501 std::string eventSub = igd_candidate->getEventSubURL();
502
503 {
504 // Add the IGD if not already present in the list.
Adrien Béraud024c46f2024-03-02 23:53:18 -0500505 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400506 for (auto& igd : validIgdList_) {
507 // Must not be a null pointer
508 assert(igd.get() != nullptr);
509 if (*igd == *igd_candidate) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400510 if (logger_) logger_->debug("PUPnP: Device [{}] with int/ext addresses [{}:{}] is already in the list of valid IGDs",
511 igd_candidate->getUID(),
512 igd_candidate->toString(),
513 igd_candidate->getPublicIp().toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400514 return true;
515 }
516 }
517 }
518
519 // We have a valid IGD
520 igd_candidate->setValid(true);
521
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400522 if (logger_) logger_->debug("PUPnP: Added a new IGD [{}] to the list of valid IGDs",
523 igd_candidate->getUID());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400524
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400525 if (logger_) logger_->debug("PUPnP: New IGD addresses [int: {} - ext: {}]",
526 igd_candidate->toString(),
527 igd_candidate->getPublicIp().toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400528
529 // Subscribe to IGD events.
530 int upnp_err = UpnpSubscribeAsync(ctrlptHandle_,
531 eventSub.c_str(),
532 UPNP_INFINITE,
533 subEventCallback,
534 this);
535 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400536 if (logger_) logger_->warn("PUPnP: Failed to send subscribe request to {}: error %i - {}",
537 igd_candidate->getUID(),
538 upnp_err,
539 UpnpGetErrorMessage(upnp_err));
540 return false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400541 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400542 if (logger_) logger_->debug("PUPnP: Successfully subscribed to IGD {}", igd_candidate->getUID());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400543 }
544
545 {
546 // This is a new (and hopefully valid) IGD.
Adrien Béraud024c46f2024-03-02 23:53:18 -0500547 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400548 validIgdList_.emplace_back(igd_candidate);
549 }
550
551 // Report to the listener.
Adrien Béraud370257c2023-08-15 20:53:09 -0400552 ioContext->post([w = weak(), igd_candidate] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400553 if (auto upnpThis = w.lock()) {
554 if (upnpThis->observer_)
555 upnpThis->observer_->onIgdUpdated(igd_candidate, UpnpIgdEvent::ADDED);
556 }
557 });
558
559 return true;
560}
561
562void
563PUPnP::requestMappingAdd(const Mapping& mapping)
564{
Adrien Béraud370257c2023-08-15 20:53:09 -0400565 ioContext->post([w = weak(), mapping] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400566 if (auto upnpThis = w.lock()) {
567 if (not upnpThis->isRunning())
568 return;
569 Mapping mapRes(mapping);
570 if (upnpThis->actionAddPortMapping(mapRes)) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400571 auto now = sys_clock::now();
572 mapRes.setRenewalTime(now + std::chrono::seconds(MAPPING_LEASE_DURATION / 2));
573 mapRes.setExpiryTime(now + std::chrono::seconds(MAPPING_LEASE_DURATION));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400574 mapRes.setState(MappingState::OPEN);
575 mapRes.setInternalAddress(upnpThis->getHostAddress().toString());
576 upnpThis->processAddMapAction(mapRes);
577 } else {
578 upnpThis->incrementErrorsCounter(mapRes.getIgd());
579 mapRes.setState(MappingState::FAILED);
580 upnpThis->processRequestMappingFailure(mapRes);
581 }
582 }
583 });
584}
585
586void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400587PUPnP::requestMappingRenew(const Mapping& mapping)
588{
589 ioContext->post([w = weak(), mapping] {
590 if (auto upnpThis = w.lock()) {
591 if (not upnpThis->isRunning())
592 return;
593 Mapping mapRes(mapping);
594 if (upnpThis->actionAddPortMapping(mapRes)) {
595 if (upnpThis->logger_)
596 upnpThis->logger_->debug("PUPnP: Renewal request for mapping {} on {} succeeded",
597 mapRes.toString(),
598 mapRes.getIgd()->toString());
599 auto now = sys_clock::now();
600 mapRes.setRenewalTime(now + std::chrono::seconds(MAPPING_LEASE_DURATION / 2));
601 mapRes.setExpiryTime(now + std::chrono::seconds(MAPPING_LEASE_DURATION));
602 mapRes.setState(MappingState::OPEN);
603 mapRes.setInternalAddress(upnpThis->getHostAddress().toString());
604 upnpThis->processMappingRenewed(mapRes);
605 } else {
606 if (upnpThis->logger_)
607 upnpThis->logger_->debug("PUPnP: Renewal request for mapping {} on {} failed",
608 mapRes.toString(),
609 mapRes.getIgd()->toString());
610 upnpThis->incrementErrorsCounter(mapRes.getIgd());
611 mapRes.setState(MappingState::FAILED);
612 upnpThis->processRequestMappingFailure(mapRes);
613 }
614 }
615 });
616}
617
618void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400619PUPnP::requestMappingRemove(const Mapping& mapping)
620{
621 // Send remove request using the matching IGD
Adrien Béraud370257c2023-08-15 20:53:09 -0400622 ioContext->dispatch([w = weak(), mapping] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400623 if (auto upnpThis = w.lock()) {
624 // Abort if we are shutting down.
625 if (not upnpThis->isRunning())
626 return;
627 if (upnpThis->actionDeletePortMapping(mapping)) {
628 upnpThis->processRemoveMapAction(mapping);
629 } else {
630 assert(mapping.getIgd());
631 // Dont need to report in case of failure.
632 upnpThis->incrementErrorsCounter(mapping.getIgd());
633 }
634 }
635 });
636}
637
638std::shared_ptr<UPnPIGD>
639PUPnP::findMatchingIgd(const std::string& ctrlURL) const
640{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500641 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400642
643 auto iter = std::find_if(validIgdList_.begin(),
644 validIgdList_.end(),
645 [&ctrlURL](const std::shared_ptr<IGD>& igd) {
646 if (auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd)) {
647 return upnpIgd->getControlURL() == ctrlURL;
648 }
649 return false;
650 });
651
652 if (iter == validIgdList_.end()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400653 if (logger_) logger_->warn("PUPnP: Did not find the IGD matching ctrl URL [{}]", ctrlURL);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400654 return {};
655 }
656
657 return std::dynamic_pointer_cast<UPnPIGD>(*iter);
658}
659
660void
661PUPnP::processAddMapAction(const Mapping& map)
662{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400663 if (observer_ == nullptr)
664 return;
665
Adrien Béraud370257c2023-08-15 20:53:09 -0400666 ioContext->post([w = weak(), map] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400667 if (auto upnpThis = w.lock()) {
668 if (upnpThis->observer_)
669 upnpThis->observer_->onMappingAdded(map.getIgd(), std::move(map));
670 }
671 });
672}
673
674void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400675PUPnP::processMappingRenewed(const Mapping& map)
676{
677 if (observer_ == nullptr)
678 return;
679
680 ioContext->post([w = weak(), map] {
681 if (auto upnpThis = w.lock()) {
682 if (upnpThis->observer_)
683 upnpThis->observer_->onMappingRenewed(map.getIgd(), std::move(map));
684 }
685 });
686}
687
688void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400689PUPnP::processRequestMappingFailure(const Mapping& map)
690{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400691 if (observer_ == nullptr)
692 return;
693
Adrien Béraud370257c2023-08-15 20:53:09 -0400694 ioContext->post([w = weak(), map] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400695 if (auto upnpThis = w.lock()) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400696 if (upnpThis->observer_)
697 upnpThis->observer_->onMappingRequestFailed(map);
698 }
699 });
700}
701
702void
703PUPnP::processRemoveMapAction(const Mapping& map)
704{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400705 if (observer_ == nullptr)
706 return;
707
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400708 if (logger_) logger_->warn("PUPnP: Closed mapping {}", map.toString());
Adrien Béraud370257c2023-08-15 20:53:09 -0400709 ioContext->post([map, obs = observer_] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400710 obs->onMappingRemoved(map.getIgd(), std::move(map));
711 });
712}
713
714const char*
715PUPnP::eventTypeToString(Upnp_EventType eventType)
716{
717 switch (eventType) {
718 case UPNP_CONTROL_ACTION_REQUEST:
719 return "UPNP_CONTROL_ACTION_REQUEST";
720 case UPNP_CONTROL_ACTION_COMPLETE:
721 return "UPNP_CONTROL_ACTION_COMPLETE";
722 case UPNP_CONTROL_GET_VAR_REQUEST:
723 return "UPNP_CONTROL_GET_VAR_REQUEST";
724 case UPNP_CONTROL_GET_VAR_COMPLETE:
725 return "UPNP_CONTROL_GET_VAR_COMPLETE";
726 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
727 return "UPNP_DISCOVERY_ADVERTISEMENT_ALIVE";
728 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
729 return "UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE";
730 case UPNP_DISCOVERY_SEARCH_RESULT:
731 return "UPNP_DISCOVERY_SEARCH_RESULT";
732 case UPNP_DISCOVERY_SEARCH_TIMEOUT:
733 return "UPNP_DISCOVERY_SEARCH_TIMEOUT";
734 case UPNP_EVENT_SUBSCRIPTION_REQUEST:
735 return "UPNP_EVENT_SUBSCRIPTION_REQUEST";
736 case UPNP_EVENT_RECEIVED:
737 return "UPNP_EVENT_RECEIVED";
738 case UPNP_EVENT_RENEWAL_COMPLETE:
739 return "UPNP_EVENT_RENEWAL_COMPLETE";
740 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
741 return "UPNP_EVENT_SUBSCRIBE_COMPLETE";
742 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE:
743 return "UPNP_EVENT_UNSUBSCRIBE_COMPLETE";
744 case UPNP_EVENT_AUTORENEWAL_FAILED:
745 return "UPNP_EVENT_AUTORENEWAL_FAILED";
746 case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
747 return "UPNP_EVENT_SUBSCRIPTION_EXPIRED";
748 default:
749 return "Unknown UPNP Event";
750 }
751}
752
753int
754PUPnP::ctrlPtCallback(Upnp_EventType event_type, const void* event, void* user_data)
755{
756 auto pupnp = static_cast<PUPnP*>(user_data);
757
758 if (pupnp == nullptr) {
Adrien Bérauda61adb52023-08-23 09:31:02 -0400759 fmt::print(stderr, "PUPnP: Control point callback without PUPnP");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400760 return UPNP_E_SUCCESS;
761 }
762
763 auto upnpThis = pupnp->weak().lock();
Adrien Bérauda61adb52023-08-23 09:31:02 -0400764 if (not upnpThis) {
765 fmt::print(stderr, "PUPnP: Control point callback without PUPnP");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400766 return UPNP_E_SUCCESS;
Adrien Bérauda61adb52023-08-23 09:31:02 -0400767 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400768
769 // Ignore if already unregistered.
770 if (not upnpThis->clientRegistered_)
771 return UPNP_E_SUCCESS;
772
773 // Process the callback.
774 return upnpThis->handleCtrlPtUPnPEvents(event_type, event);
775}
776
777PUPnP::CtrlAction
778PUPnP::getAction(const char* xmlNode)
779{
780 if (strstr(xmlNode, ACTION_ADD_PORT_MAPPING)) {
781 return CtrlAction::ADD_PORT_MAPPING;
782 } else if (strstr(xmlNode, ACTION_DELETE_PORT_MAPPING)) {
783 return CtrlAction::DELETE_PORT_MAPPING;
784 } else if (strstr(xmlNode, ACTION_GET_GENERIC_PORT_MAPPING_ENTRY)) {
785 return CtrlAction::GET_GENERIC_PORT_MAPPING_ENTRY;
786 } else if (strstr(xmlNode, ACTION_GET_STATUS_INFO)) {
787 return CtrlAction::GET_STATUS_INFO;
788 } else if (strstr(xmlNode, ACTION_GET_EXTERNAL_IP_ADDRESS)) {
789 return CtrlAction::GET_EXTERNAL_IP_ADDRESS;
790 } else {
791 return CtrlAction::UNKNOWN;
792 }
793}
794
795void
796PUPnP::processDiscoverySearchResult(const std::string& cpDeviceId,
797 const std::string& igdLocationUrl,
798 const IpAddr& dstAddr)
799{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400800 // Update host address if needed.
801 if (not hasValidHostAddress())
802 updateHostAddress();
803
804 // The host address must be valid to proceed.
805 if (not hasValidHostAddress()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400806 if (logger_) logger_->warn("PUPnP: Local address is invalid. Ignore search result for now!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400807 return;
808 }
809
810 // Use the device ID and the URL as ID. This is necessary as some
811 // IGDs may have the same device ID but different URLs.
812
813 auto igdId = cpDeviceId + " url: " + igdLocationUrl;
814
815 if (not discoveredIgdList_.emplace(igdId).second) {
Adrien Beraud64bb00f2023-08-23 19:06:46 -0400816 //if (logger_) logger_->debug("PUPnP: IGD [{}] already in the list", igdId);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400817 return;
818 }
819
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400820 if (logger_) logger_->debug("PUPnP: Discovered a new IGD [{}]", igdId);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400821
822 // NOTE: here, we check if the location given is related to the source address.
823 // If it's not the case, it's certainly a router plugged in the network, but not
824 // related to this network. So the given location will be unreachable and this
825 // will cause some timeout.
826
827 // Only check the IP address (ignore the port number).
828 dht::http::Url url(igdLocationUrl);
829 if (IpAddr(url.host).toString(false) != dstAddr.toString(false)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400830 if (logger_) logger_->debug("PUPnP: Returned location {} does not match the source address {}",
831 IpAddr(url.host).toString(true, true),
832 dstAddr.toString(true, true));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400833 return;
834 }
835
836 // Run a separate thread to prevent blocking this thread
837 // if the IGD HTTP server is not responsive.
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400838 ongoingOpsThreadPool_.run([w = weak(), url=igdLocationUrl] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400839 if (auto upnpThis = w.lock()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400840 upnpThis->downLoadIgdDescription(url);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400841 }
842 });
843}
844
845void
846PUPnP::downLoadIgdDescription(const std::string& locationUrl)
847{
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400848 if(logger_) logger_->debug("PUPnP: downLoadIgdDescription {}", locationUrl);
Sébastien Blind14fc352023-10-06 15:21:53 -0400849 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500850 std::lock_guard lk(ongoingOpsMtx_);
Sébastien Blind14fc352023-10-06 15:21:53 -0400851 if (destroying_)
852 return;
853 ongoingOps_++;
854 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400855 IXML_Document* doc_container_ptr = nullptr;
856 int upnp_err = UpnpDownloadXmlDoc(locationUrl.c_str(), &doc_container_ptr);
857
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400858 std::lock_guard lk(ongoingOpsMtx_);
859 // Trying to use libupnp functions after UpnpFinish has been called (which may
860 // be the case if destroying_ is true) can cause errors. It's probably not a
861 // problem here, but return early just in case.
862 if (destroying_)
863 return;
864
Adrien Béraud612b55b2023-05-29 10:42:04 -0400865 if (upnp_err != UPNP_E_SUCCESS or not doc_container_ptr) {
Adrien Béraud370257c2023-08-15 20:53:09 -0400866 if(logger_) logger_->warn("PUPnP: Error downloading device XML document from {} -> {}",
867 locationUrl,
868 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400869 } else {
Adrien Béraud370257c2023-08-15 20:53:09 -0400870 if(logger_) logger_->debug("PUPnP: Succeeded to download device XML document from {}", locationUrl);
871 ioContext->post([w = weak(), url = locationUrl, doc_container_ptr] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400872 if (auto upnpThis = w.lock()) {
873 upnpThis->validateIgd(url, doc_container_ptr);
874 }
875 });
876 }
Sébastien Blind14fc352023-10-06 15:21:53 -0400877 ongoingOps_--;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400878}
879
880void
881PUPnP::processDiscoveryAdvertisementByebye(const std::string& cpDeviceId)
882{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400883 discoveredIgdList_.erase(cpDeviceId);
884
885 std::shared_ptr<IGD> igd;
886 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500887 std::lock_guard lk(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400888 for (auto it = validIgdList_.begin(); it != validIgdList_.end();) {
889 if ((*it)->getUID() == cpDeviceId) {
890 igd = *it;
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400891 if (logger_) logger_->debug("PUPnP: Received [{}] for IGD [{}] {}. Will be removed.",
892 PUPnP::eventTypeToString(UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE),
893 igd->getUID(),
894 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400895 igd->setValid(false);
896 // Remove the IGD.
897 it = validIgdList_.erase(it);
898 break;
899 } else {
900 it++;
901 }
902 }
903 }
904
905 // Notify the listener.
906 if (observer_ and igd) {
907 observer_->onIgdUpdated(igd, UpnpIgdEvent::REMOVED);
908 }
909}
910
911void
912PUPnP::processDiscoverySubscriptionExpired(Upnp_EventType event_type, const std::string& eventSubUrl)
913{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500914 std::lock_guard lk(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400915 for (auto& it : validIgdList_) {
916 if (auto igd = std::dynamic_pointer_cast<UPnPIGD>(it)) {
917 if (igd->getEventSubURL() == eventSubUrl) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400918 if (logger_) logger_->debug("PUPnP: Received [{}] event for IGD [{}] {}. Request a new subscribe.",
919 PUPnP::eventTypeToString(event_type),
920 igd->getUID(),
921 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400922 UpnpSubscribeAsync(ctrlptHandle_,
923 eventSubUrl.c_str(),
924 UPNP_INFINITE,
925 subEventCallback,
926 this);
927 break;
928 }
929 }
930 }
931}
932
933int
934PUPnP::handleCtrlPtUPnPEvents(Upnp_EventType event_type, const void* event)
935{
936 switch (event_type) {
937 // "ALIVE" events are processed as "SEARCH RESULT". It might be usefull
938 // if "SEARCH RESULT" was missed.
939 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
940 case UPNP_DISCOVERY_SEARCH_RESULT: {
941 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
942
943 // First check the error code.
944 auto upnp_status = UpnpDiscovery_get_ErrCode(d_event);
945 if (upnp_status != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400946 if (logger_) logger_->error("PUPnP: UPNP discovery is in erroneous state: %s",
947 UpnpGetErrorMessage(upnp_status));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400948 break;
949 }
950
951 // Parse the event's data.
952 std::string deviceId {UpnpDiscovery_get_DeviceID_cstr(d_event)};
953 std::string location {UpnpDiscovery_get_Location_cstr(d_event)};
954 IpAddr dstAddr(*(const pj_sockaddr*) (UpnpDiscovery_get_DestAddr(d_event)));
Adrien Béraud370257c2023-08-15 20:53:09 -0400955 ioContext->post([w = weak(),
Adrien Béraud612b55b2023-05-29 10:42:04 -0400956 deviceId = std::move(deviceId),
957 location = std::move(location),
958 dstAddr = std::move(dstAddr)] {
959 if (auto upnpThis = w.lock()) {
960 upnpThis->processDiscoverySearchResult(deviceId, location, dstAddr);
961 }
962 });
963 break;
964 }
965 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: {
966 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
967
968 std::string deviceId(UpnpDiscovery_get_DeviceID_cstr(d_event));
969
970 // Process the response on the main thread.
Adrien Béraud370257c2023-08-15 20:53:09 -0400971 ioContext->post([w = weak(), deviceId = std::move(deviceId)] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400972 if (auto upnpThis = w.lock()) {
973 upnpThis->processDiscoveryAdvertisementByebye(deviceId);
974 }
975 });
976 break;
977 }
978 case UPNP_DISCOVERY_SEARCH_TIMEOUT: {
979 // Even if the discovery search is successful, it's normal to receive
980 // time-out events. This because we send search requests using various
981 // device types, which some of them may not return a response.
982 break;
983 }
984 case UPNP_EVENT_RECEIVED: {
985 // Nothing to do.
986 break;
987 }
988 // Treat failed autorenewal like an expired subscription.
989 case UPNP_EVENT_AUTORENEWAL_FAILED:
990 case UPNP_EVENT_SUBSCRIPTION_EXPIRED: // This event will occur only if autorenewal is disabled.
991 {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400992 if (logger_) logger_->warn("PUPnP: Received Subscription Event {}", eventTypeToString(event_type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400993 const UpnpEventSubscribe* es_event = (const UpnpEventSubscribe*) event;
994 if (es_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400995 if (logger_) logger_->warn("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400996 break;
997 }
998 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
999
1000 // Process the response on the main thread.
Adrien Béraud370257c2023-08-15 20:53:09 -04001001 ioContext->post([w = weak(), event_type, publisherUrl = std::move(publisherUrl)] {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001002 if (auto upnpThis = w.lock()) {
1003 upnpThis->processDiscoverySubscriptionExpired(event_type, publisherUrl);
1004 }
1005 });
1006 break;
1007 }
1008 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
1009 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE: {
1010 UpnpEventSubscribe* es_event = (UpnpEventSubscribe*) event;
1011 if (es_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001012 if (logger_) logger_->warn("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001013 } else {
1014 UpnpEventSubscribe_delete(es_event);
1015 }
1016 break;
1017 }
1018 case UPNP_CONTROL_ACTION_COMPLETE: {
1019 const UpnpActionComplete* a_event = (const UpnpActionComplete*) event;
1020 if (a_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001021 if (logger_) logger_->warn("PUPnP: Received Action Complete Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001022 break;
1023 }
1024 auto res = UpnpActionComplete_get_ErrCode(a_event);
1025 if (res != UPNP_E_SUCCESS and res != UPNP_E_TIMEDOUT) {
1026 auto err = UpnpActionComplete_get_ErrCode(a_event);
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001027 if (logger_) logger_->warn("PUPnP: Received Action Complete error %i %s", err, UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001028 } else {
1029 auto actionRequest = UpnpActionComplete_get_ActionRequest(a_event);
1030 // Abort if there is no action to process.
1031 if (actionRequest == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001032 if (logger_) logger_->warn("PUPnP: Can't get the Action Request data from the event");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001033 break;
1034 }
1035
1036 auto actionResult = UpnpActionComplete_get_ActionResult(a_event);
1037 if (actionResult != nullptr) {
1038 ixmlDocument_free(actionResult);
1039 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001040 if (logger_) logger_->warn("PUPnP: Action Result document not found");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001041 }
1042 }
1043 break;
1044 }
1045 default: {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001046 if (logger_) logger_->warn("PUPnP: Unhandled Control Point event");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001047 break;
1048 }
1049 }
1050
1051 return UPNP_E_SUCCESS;
1052}
1053
1054int
1055PUPnP::subEventCallback(Upnp_EventType event_type, const void* event, void* user_data)
1056{
1057 if (auto pupnp = static_cast<PUPnP*>(user_data))
1058 return pupnp->handleSubscriptionUPnPEvent(event_type, event);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001059 return 0;
1060}
1061
1062int
1063PUPnP::handleSubscriptionUPnPEvent(Upnp_EventType, const void* event)
1064{
1065 UpnpEventSubscribe* es_event = static_cast<UpnpEventSubscribe*>(const_cast<void*>(event));
1066
1067 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001068 // JAMI_ERR("PUPnP: Unexpected null pointer!");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001069 return UPNP_E_INVALID_ARGUMENT;
1070 }
1071 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
1072 int upnp_err = UpnpEventSubscribe_get_ErrCode(es_event);
1073 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001074 if (logger_) logger_->warn("PUPnP: Subscription error {} from {}",
1075 UpnpGetErrorMessage(upnp_err),
1076 publisherUrl);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001077 return upnp_err;
1078 }
1079
1080 return UPNP_E_SUCCESS;
1081}
1082
1083std::unique_ptr<UPnPIGD>
1084PUPnP::parseIgd(IXML_Document* doc, std::string locationUrl)
1085{
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001086 if (not(doc and !locationUrl.empty()))
Adrien Béraud612b55b2023-05-29 10:42:04 -04001087 return nullptr;
1088
1089 // Check the UDN to see if its already in our device list.
1090 std::string UDN(getFirstDocItem(doc, "UDN"));
1091 if (UDN.empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001092 if (logger_) logger_->warn("PUPnP: could not find UDN in description document of device");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001093 return nullptr;
1094 } else {
Adrien Béraud024c46f2024-03-02 23:53:18 -05001095 std::lock_guard lk(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001096 for (auto& it : validIgdList_) {
1097 if (it->getUID() == UDN) {
1098 // We already have this device in our list.
1099 return nullptr;
1100 }
1101 }
1102 }
1103
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001104 if (logger_) logger_->debug("PUPnP: Found new device [{}]", UDN);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001105
1106 std::unique_ptr<UPnPIGD> new_igd;
1107 int upnp_err;
1108
1109 // Get friendly name.
1110 std::string friendlyName(getFirstDocItem(doc, "friendlyName"));
1111
1112 // Get base URL.
1113 std::string baseURL(getFirstDocItem(doc, "URLBase"));
1114 if (baseURL.empty())
1115 baseURL = locationUrl;
1116
1117 // Get list of services defined by serviceType.
1118 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> serviceList(nullptr,
1119 ixmlNodeList_free);
1120 serviceList.reset(ixmlDocument_getElementsByTagName(doc, "serviceType"));
1121 unsigned long list_length = ixmlNodeList_length(serviceList.get());
1122
1123 // Go through the "serviceType" nodes until we find the the correct service type.
1124 for (unsigned long node_idx = 0; node_idx < list_length; node_idx++) {
1125 IXML_Node* serviceType_node = ixmlNodeList_item(serviceList.get(), node_idx);
1126 std::string serviceType(getElementText(serviceType_node));
1127
1128 // Only check serviceType of WANIPConnection or WANPPPConnection.
1129 if (serviceType != UPNP_WANIP_SERVICE
1130 && serviceType != UPNP_WANPPP_SERVICE) {
1131 // IGD is not WANIP or WANPPP service. Going to next node.
1132 continue;
1133 }
1134
1135 // Get parent node.
1136 IXML_Node* service_node = ixmlNode_getParentNode(serviceType_node);
1137 if (not service_node) {
1138 // IGD serviceType has no parent node. Going to next node.
1139 continue;
1140 }
1141
1142 // Perform sanity check. The parent node should be called "service".
1143 if (strcmp(ixmlNode_getNodeName(service_node), "service") != 0) {
1144 // IGD "serviceType" parent node is not called "service". Going to next node.
1145 continue;
1146 }
1147
1148 // Get serviceId.
1149 IXML_Element* service_element = (IXML_Element*) service_node;
1150 std::string serviceId(getFirstElementItem(service_element, "serviceId"));
1151 if (serviceId.empty()) {
1152 // IGD "serviceId" is empty. Going to next node.
1153 continue;
1154 }
1155
1156 // Get the relative controlURL and turn it into absolute address using the URLBase.
1157 std::string controlURL(getFirstElementItem(service_element, "controlURL"));
1158 if (controlURL.empty()) {
1159 // IGD control URL is empty. Going to next node.
1160 continue;
1161 }
1162
1163 char* absolute_control_url = nullptr;
1164 upnp_err = UpnpResolveURL2(baseURL.c_str(), controlURL.c_str(), &absolute_control_url);
1165 if (upnp_err == UPNP_E_SUCCESS)
1166 controlURL = absolute_control_url;
1167 else
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001168 if (logger_) logger_->warn("PUPnP: Error resolving absolute controlURL -> {}",
1169 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001170
1171 std::free(absolute_control_url);
1172
1173 // Get the relative eventSubURL and turn it into absolute address using the URLBase.
1174 std::string eventSubURL(getFirstElementItem(service_element, "eventSubURL"));
1175 if (eventSubURL.empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001176 if (logger_) logger_->warn("PUPnP: IGD event sub URL is empty. Going to next node");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001177 continue;
1178 }
1179
1180 char* absolute_event_sub_url = nullptr;
1181 upnp_err = UpnpResolveURL2(baseURL.c_str(), eventSubURL.c_str(), &absolute_event_sub_url);
1182 if (upnp_err == UPNP_E_SUCCESS)
1183 eventSubURL = absolute_event_sub_url;
1184 else
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001185 if (logger_) logger_->warn("PUPnP: Error resolving absolute eventSubURL -> {}",
1186 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001187
1188 std::free(absolute_event_sub_url);
1189
1190 new_igd.reset(new UPnPIGD(std::move(UDN),
1191 std::move(baseURL),
1192 std::move(friendlyName),
1193 std::move(serviceType),
1194 std::move(serviceId),
1195 std::move(locationUrl),
1196 std::move(controlURL),
1197 std::move(eventSubURL)));
1198
1199 return new_igd;
1200 }
1201
1202 return nullptr;
1203}
1204
1205bool
1206PUPnP::actionIsIgdConnected(const UPnPIGD& igd)
1207{
1208 if (not clientRegistered_)
1209 return false;
1210
1211 // Set action name.
1212 IXML_Document* action_container_ptr = UpnpMakeAction("GetStatusInfo",
1213 igd.getServiceType().c_str(),
1214 0,
1215 nullptr);
1216 if (not action_container_ptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001217 if (logger_) logger_->warn("PUPnP: Failed to make GetStatusInfo action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001218 return false;
1219 }
1220 XMLDocument action(action_container_ptr, ixmlDocument_free); // Action pointer.
1221
1222 IXML_Document* response_container_ptr = nullptr;
1223 int upnp_err = UpnpSendAction(ctrlptHandle_,
1224 igd.getControlURL().c_str(),
1225 igd.getServiceType().c_str(),
1226 nullptr,
1227 action.get(),
1228 &response_container_ptr);
Sébastien Blin45b50692024-03-06 11:18:50 -05001229 if (upnp_err == 401) {
1230 // YET ANOTHER UPNP HACK: MiniUpnp on some routers seems to not recognize this action, sending a 401: Invalid Action.
1231 // So even if mapping succeeds, the router was considered as not connected.
1232 // Returning true here works around this issue.
1233 // E.g. https://community.tp-link.com/us/home/forum/topic/577840
1234 return true;
1235 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001236 if (not response_container_ptr or upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001237 if (logger_) logger_->warn("PUPnP: Failed to send GetStatusInfo action -> {}", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001238 return false;
1239 }
1240 XMLDocument response(response_container_ptr, ixmlDocument_free);
1241
Adrien Béraudd78d1ac2023-08-25 10:43:33 -04001242 if (errorOnResponse(response.get(), logger_)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001243 if (logger_) logger_->warn("PUPnP: Failed to get GetStatusInfo from {} -> {:d}: {}",
1244 igd.getServiceType().c_str(),
1245 upnp_err,
1246 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001247 return false;
1248 }
1249
1250 // Parse response.
1251 auto status = getFirstDocItem(response.get(), "NewConnectionStatus");
1252 return status == "Connected";
1253}
1254
1255IpAddr
1256PUPnP::actionGetExternalIP(const UPnPIGD& igd)
1257{
1258 if (not clientRegistered_)
1259 return {};
1260
1261 // Action and response pointers.
1262 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1263 action(nullptr, ixmlDocument_free); // Action pointer.
1264 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1265 response(nullptr, ixmlDocument_free); // Response pointer.
1266
1267 // Set action name.
1268 static constexpr const char* action_name {"GetExternalIPAddress"};
1269
1270 IXML_Document* action_container_ptr = nullptr;
1271 action_container_ptr = UpnpMakeAction(action_name, igd.getServiceType().c_str(), 0, nullptr);
1272 action.reset(action_container_ptr);
1273
1274 if (not action) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001275 if (logger_) logger_->warn("PUPnP: Failed to make GetExternalIPAddress action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001276 return {};
1277 }
1278
1279 IXML_Document* response_container_ptr = nullptr;
1280 int upnp_err = UpnpSendAction(ctrlptHandle_,
1281 igd.getControlURL().c_str(),
1282 igd.getServiceType().c_str(),
1283 nullptr,
1284 action.get(),
1285 &response_container_ptr);
1286 response.reset(response_container_ptr);
1287
1288 if (not response or upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001289 if (logger_) logger_->warn("PUPnP: Failed to send GetExternalIPAddress action -> {}",
1290 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001291 return {};
1292 }
1293
Adrien Béraudd78d1ac2023-08-25 10:43:33 -04001294 if (errorOnResponse(response.get(), logger_)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001295 if (logger_) logger_->warn("PUPnP: Failed to get GetExternalIPAddress from {} -> {:d}: {}",
1296 igd.getServiceType(),
1297 upnp_err,
1298 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001299 return {};
1300 }
1301
1302 return {getFirstDocItem(response.get(), "NewExternalIPAddress")};
1303}
1304
1305std::map<Mapping::key_t, Mapping>
1306PUPnP::getMappingsListByDescr(const std::shared_ptr<IGD>& igd, const std::string& description) const
1307{
1308 auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd);
1309 assert(upnpIgd);
1310
1311 std::map<Mapping::key_t, Mapping> mapList;
1312
1313 if (not clientRegistered_ or not upnpIgd->isValid() or not upnpIgd->getLocalIp())
1314 return mapList;
1315
1316 // Set action name.
1317 static constexpr const char* action_name {"GetGenericPortMappingEntry"};
1318
1319 for (int entry_idx = 0;; entry_idx++) {
1320 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1321 action(nullptr, ixmlDocument_free); // Action pointer.
1322 IXML_Document* action_container_ptr = nullptr;
1323
1324 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1325 response(nullptr, ixmlDocument_free); // Response pointer.
1326 IXML_Document* response_container_ptr = nullptr;
1327
1328 UpnpAddToAction(&action_container_ptr,
1329 action_name,
1330 upnpIgd->getServiceType().c_str(),
1331 "NewPortMappingIndex",
1332 std::to_string(entry_idx).c_str());
1333 action.reset(action_container_ptr);
1334
1335 if (not action) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001336 // JAMI_WARN("PUPnP: Failed to add NewPortMappingIndex action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001337 break;
1338 }
1339
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001340 auto timeIgdRequestSent = sys_clock::now();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001341 int upnp_err = UpnpSendAction(ctrlptHandle_,
1342 upnpIgd->getControlURL().c_str(),
1343 upnpIgd->getServiceType().c_str(),
1344 nullptr,
1345 action.get(),
1346 &response_container_ptr);
1347 response.reset(response_container_ptr);
1348
1349 if (not response) {
1350 // No existing mapping. Abort silently.
1351 break;
1352 }
1353
1354 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001355 // JAMI_ERR("PUPnP: GetGenericPortMappingEntry returned with error: %i", upnp_err);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001356 break;
1357 }
1358
1359 // Check error code.
1360 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1361 if (not errorCode.empty()) {
1362 auto error = to_int<int>(errorCode);
1363 if (error == ARRAY_IDX_INVALID or error == CONFLICT_IN_MAPPING) {
1364 // No more port mapping entries in the response.
Morteza Namvar5f639522023-07-04 17:08:58 -04001365 // JAMI_DBG("PUPnP: No more mappings (found a total of %i mappings", entry_idx);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001366 break;
1367 } else {
1368 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
Adrien Béraud370257c2023-08-15 20:53:09 -04001369 if (logger_) logger_->error("PUPnP: GetGenericPortMappingEntry returned with error: {:s}: {:s}",
Adrien Béraud612b55b2023-05-29 10:42:04 -04001370 errorCode,
1371 errorDescription);
1372 break;
1373 }
1374 }
1375
1376 // Parse the response.
1377 auto desc_actual = getFirstDocItem(response.get(), "NewPortMappingDescription");
1378 auto client_ip = getFirstDocItem(response.get(), "NewInternalClient");
1379
1380 if (client_ip != getHostAddress().toString()) {
1381 // Silently ignore un-matching addresses.
1382 continue;
1383 }
1384
1385 if (desc_actual.find(description) == std::string::npos)
1386 continue;
1387
1388 auto port_internal = getFirstDocItem(response.get(), "NewInternalPort");
1389 auto port_external = getFirstDocItem(response.get(), "NewExternalPort");
1390 std::string transport(getFirstDocItem(response.get(), "NewProtocol"));
1391
1392 if (port_internal.empty() || port_external.empty() || transport.empty()) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001393 // Invalid entry, ignore
Adrien Béraud612b55b2023-05-29 10:42:04 -04001394 continue;
1395 }
1396
1397 std::transform(transport.begin(), transport.end(), transport.begin(), ::toupper);
1398 PortType type = transport.find("TCP") != std::string::npos ? PortType::TCP : PortType::UDP;
1399 auto ePort = to_int<uint16_t>(port_external);
1400 auto iPort = to_int<uint16_t>(port_internal);
1401
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001402 auto leaseDurationStr = getFirstDocItem(response.get(), "NewLeaseDuration");
1403 auto leaseDuration = to_int<uint32_t>(leaseDurationStr);
1404 auto expiryTime = (leaseDuration == 0) ? sys_clock::time_point::max()
1405 : timeIgdRequestSent + std::chrono::seconds(leaseDuration);
1406
Adrien Béraud612b55b2023-05-29 10:42:04 -04001407 Mapping map(type, ePort, iPort);
1408 map.setIgd(igd);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001409 map.setExpiryTime(expiryTime);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001410
1411 mapList.emplace(map.getMapKey(), std::move(map));
1412 }
1413
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001414 if (logger_) logger_->debug("PUPnP: Found {:d} allocated mappings on IGD {:s}",
1415 mapList.size(),
1416 upnpIgd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001417
1418 return mapList;
1419}
1420
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -04001421std::vector<MappingInfo>
1422PUPnP::getMappingsInfo(const std::shared_ptr<IGD>& igd) const
1423{
1424 auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd);
1425 assert(upnpIgd);
1426
1427 std::vector<MappingInfo> mappingInfoList;
1428
1429 if (not clientRegistered_ or not upnpIgd->isValid() or not upnpIgd->getLocalIp())
1430 return mappingInfoList;
1431
1432 static constexpr const char* action_name {"GetGenericPortMappingEntry"};
1433
1434 for (int entry_idx = 0;; entry_idx++) {
1435 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1436 action(nullptr, ixmlDocument_free); // Action pointer.
1437 IXML_Document* action_container_ptr = nullptr;
1438
1439 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1440 response(nullptr, ixmlDocument_free); // Response pointer.
1441 IXML_Document* response_container_ptr = nullptr;
1442
1443 UpnpAddToAction(&action_container_ptr,
1444 action_name,
1445 upnpIgd->getServiceType().c_str(),
1446 "NewPortMappingIndex",
1447 std::to_string(entry_idx).c_str());
1448 action.reset(action_container_ptr);
1449
1450 int upnp_err = UpnpSendAction(ctrlptHandle_,
1451 upnpIgd->getControlURL().c_str(),
1452 upnpIgd->getServiceType().c_str(),
1453 nullptr,
1454 action.get(),
1455 &response_container_ptr);
1456 response.reset(response_container_ptr);
1457
1458 if (!response || upnp_err != UPNP_E_SUCCESS) {
1459 break;
1460 }
1461
1462 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1463 if (not errorCode.empty()) {
1464 auto error = to_int<int>(errorCode);
1465 if (error == ARRAY_IDX_INVALID or error == CONFLICT_IN_MAPPING) {
1466 // No more port mapping entries in the response.
1467 break;
1468 } else {
1469 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
1470 if (logger_) logger_->error("PUPnP: GetGenericPortMappingEntry returned with error: {:s}: {:s}",
1471 errorCode,
1472 errorDescription);
1473 break;
1474 }
1475 }
1476
1477 // Parse the response.
1478 MappingInfo info;
1479 info.remoteHost = getFirstDocItem(response.get(), "NewRemoteHost");
1480 info.protocol = getFirstDocItem(response.get(), "NewProtocol");
1481 info.internalClient = getFirstDocItem(response.get(), "NewInternalClient");
1482 info.enabled = getFirstDocItem(response.get(), "NewEnabled");
1483 info.description = getFirstDocItem(response.get(), "NewPortMappingDescription");
1484
1485 auto externalPort = getFirstDocItem(response.get(), "NewExternalPort");
1486 info.externalPort = to_int<uint16_t>(externalPort);
1487
1488 auto internalPort = getFirstDocItem(response.get(), "NewInternalPort");
1489 info.internalPort = to_int<uint16_t>(internalPort);
1490
1491 auto leaseDuration = getFirstDocItem(response.get(), "NewLeaseDuration");
1492 info.leaseDuration = to_int<uint32_t>(leaseDuration);
1493
1494 mappingInfoList.push_back(std::move(info));
1495 }
1496
1497 return mappingInfoList;
1498}
1499
Adrien Béraud612b55b2023-05-29 10:42:04 -04001500void
1501PUPnP::deleteMappingsByDescription(const std::shared_ptr<IGD>& igd, const std::string& description)
1502{
1503 if (not(clientRegistered_ and igd->getLocalIp()))
1504 return;
1505
François-Simon Fauteux-Chapleau1b3aba22024-05-23 11:51:48 -04001506 if (logger_) logger_->debug("PUPnP: Remove all mappings (if any) on IGD {} matching description prefix {}",
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001507 igd->toString(),
François-Simon Fauteux-Chapleau1b3aba22024-05-23 11:51:48 -04001508 description);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001509
Adrien Béraud370257c2023-08-15 20:53:09 -04001510 ioContext->post([w=weak(), igd, description]{
1511 if (auto sthis = w.lock()) {
1512 auto mapList = sthis->getMappingsListByDescr(igd, description);
1513 for (auto const& [_, map] : mapList) {
1514 sthis->requestMappingRemove(map);
1515 }
1516 }
1517 });
Adrien Béraud612b55b2023-05-29 10:42:04 -04001518}
1519
1520bool
1521PUPnP::actionAddPortMapping(const Mapping& mapping)
1522{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001523 if (not clientRegistered_)
1524 return false;
1525
1526 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1527 if (not igdIn)
1528 return false;
1529
1530 // The requested IGD must be present in the list of local valid IGDs.
1531 auto igd = findMatchingIgd(igdIn->getControlURL());
1532
1533 if (not igd or not igd->isValid())
1534 return false;
1535
1536 // Action and response pointers.
1537 XMLDocument action(nullptr, ixmlDocument_free);
1538 IXML_Document* action_container_ptr = nullptr;
1539 XMLDocument response(nullptr, ixmlDocument_free);
1540 IXML_Document* response_container_ptr = nullptr;
1541
1542 // Set action sequence.
1543 UpnpAddToAction(&action_container_ptr,
1544 ACTION_ADD_PORT_MAPPING,
1545 igd->getServiceType().c_str(),
1546 "NewRemoteHost",
1547 "");
1548 UpnpAddToAction(&action_container_ptr,
1549 ACTION_ADD_PORT_MAPPING,
1550 igd->getServiceType().c_str(),
1551 "NewExternalPort",
1552 mapping.getExternalPortStr().c_str());
1553 UpnpAddToAction(&action_container_ptr,
1554 ACTION_ADD_PORT_MAPPING,
1555 igd->getServiceType().c_str(),
1556 "NewProtocol",
1557 mapping.getTypeStr());
1558 UpnpAddToAction(&action_container_ptr,
1559 ACTION_ADD_PORT_MAPPING,
1560 igd->getServiceType().c_str(),
1561 "NewInternalPort",
1562 mapping.getInternalPortStr().c_str());
1563 UpnpAddToAction(&action_container_ptr,
1564 ACTION_ADD_PORT_MAPPING,
1565 igd->getServiceType().c_str(),
1566 "NewInternalClient",
1567 getHostAddress().toString().c_str());
1568 UpnpAddToAction(&action_container_ptr,
1569 ACTION_ADD_PORT_MAPPING,
1570 igd->getServiceType().c_str(),
1571 "NewEnabled",
1572 "1");
1573 UpnpAddToAction(&action_container_ptr,
1574 ACTION_ADD_PORT_MAPPING,
1575 igd->getServiceType().c_str(),
1576 "NewPortMappingDescription",
1577 mapping.toString().c_str());
1578 UpnpAddToAction(&action_container_ptr,
1579 ACTION_ADD_PORT_MAPPING,
1580 igd->getServiceType().c_str(),
1581 "NewLeaseDuration",
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001582 std::to_string(MAPPING_LEASE_DURATION).c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001583
1584 action.reset(action_container_ptr);
1585
1586 int upnp_err = UpnpSendAction(ctrlptHandle_,
1587 igd->getControlURL().c_str(),
1588 igd->getServiceType().c_str(),
1589 nullptr,
1590 action.get(),
1591 &response_container_ptr);
1592 response.reset(response_container_ptr);
1593
1594 bool success = true;
1595
1596 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001597 if (logger_) {
1598 logger_->warn("PUPnP: Failed to send action {} for mapping {}. {:d}: {}",
1599 ACTION_ADD_PORT_MAPPING,
1600 mapping.toString(),
1601 upnp_err,
1602 UpnpGetErrorMessage(upnp_err));
1603 logger_->warn("PUPnP: IGD ctrlUrl {}", igd->getControlURL());
1604 logger_->warn("PUPnP: IGD service type {}", igd->getServiceType());
1605 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001606
1607 success = false;
1608 }
1609
1610 // Check if an error has occurred.
1611 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1612 if (not errorCode.empty()) {
1613 success = false;
1614 // Try to get the error description.
1615 std::string errorDescription;
1616 if (response) {
1617 errorDescription = getFirstDocItem(response.get(), "errorDescription");
1618 }
1619
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001620 if (logger_) logger_->warn("PUPnP: {:s} returned with error: {:s} {:s}",
1621 ACTION_ADD_PORT_MAPPING,
1622 errorCode,
1623 errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001624 }
1625 return success;
1626}
1627
1628bool
1629PUPnP::actionDeletePortMapping(const Mapping& mapping)
1630{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001631 if (not clientRegistered_)
1632 return false;
1633
1634 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1635 if (not igdIn)
1636 return false;
1637
1638 // The requested IGD must be present in the list of local valid IGDs.
1639 auto igd = findMatchingIgd(igdIn->getControlURL());
1640
1641 if (not igd or not igd->isValid())
1642 return false;
1643
1644 // Action and response pointers.
1645 XMLDocument action(nullptr, ixmlDocument_free);
1646 IXML_Document* action_container_ptr = nullptr;
1647 XMLDocument response(nullptr, ixmlDocument_free);
1648 IXML_Document* response_container_ptr = nullptr;
1649
1650 // Set action sequence.
1651 UpnpAddToAction(&action_container_ptr,
1652 ACTION_DELETE_PORT_MAPPING,
1653 igd->getServiceType().c_str(),
1654 "NewRemoteHost",
1655 "");
1656 UpnpAddToAction(&action_container_ptr,
1657 ACTION_DELETE_PORT_MAPPING,
1658 igd->getServiceType().c_str(),
1659 "NewExternalPort",
1660 mapping.getExternalPortStr().c_str());
1661 UpnpAddToAction(&action_container_ptr,
1662 ACTION_DELETE_PORT_MAPPING,
1663 igd->getServiceType().c_str(),
1664 "NewProtocol",
1665 mapping.getTypeStr());
1666
1667 action.reset(action_container_ptr);
1668
1669 int upnp_err = UpnpSendAction(ctrlptHandle_,
1670 igd->getControlURL().c_str(),
1671 igd->getServiceType().c_str(),
1672 nullptr,
1673 action.get(),
1674 &response_container_ptr);
1675 response.reset(response_container_ptr);
1676
1677 bool success = true;
1678
1679 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001680 if (logger_) {
1681 logger_->warn("PUPnP: Failed to send action {} for mapping from {}. {:d}: {}",
1682 ACTION_DELETE_PORT_MAPPING,
1683 mapping.toString(),
1684 upnp_err,
1685 UpnpGetErrorMessage(upnp_err));
1686 logger_->warn("PUPnP: IGD ctrlUrl {}", igd->getControlURL());
1687 logger_->warn("PUPnP: IGD service type {}", igd->getServiceType());
1688 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001689 success = false;
1690 }
1691
1692 if (not response) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001693 if (logger_) logger_->warn("PUPnP: Failed to get response for {}", ACTION_DELETE_PORT_MAPPING);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001694 success = false;
1695 }
1696
1697 // Check if there is an error code.
1698 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1699 if (not errorCode.empty()) {
1700 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001701 if (logger_) logger_->warn("PUPnP: {:s} returned with error: {:s}: {:s}",
1702 ACTION_DELETE_PORT_MAPPING,
1703 errorCode,
1704 errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001705 success = false;
1706 }
1707
1708 return success;
1709}
1710
1711} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -04001712} // namespace dhtnet