blob: 1df82191294120864813e75f846c7764635196e0 [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) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400117 if (logger_) logger_->error("PUPnP: Can't initialize libupnp: {}", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400118 UpnpFinish();
119 initialized_ = false;
120 return;
121 }
122
123 // Disable embedded WebServer if any.
124 if (UpnpIsWebserverEnabled() == 1) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400125 if (logger_) logger_->warn("PUPnP: Web-server is enabled. Disabling");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400126 UpnpEnableWebserver(0);
127 if (UpnpIsWebserverEnabled() == 1) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400128 if (logger_) logger_->error("PUPnP: Could not disable Web-server!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400129 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400130 if (logger_) logger_->debug("PUPnP: Web-server successfully disabled");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400131 }
132 }
133
134 char* ip_address = UpnpGetServerIpAddress();
135 char* ip_address6 = nullptr;
136 unsigned short port = UpnpGetServerPort();
137 unsigned short port6 = 0;
138#if UPNP_ENABLE_IPV6
139 ip_address6 = UpnpGetServerIp6Address();
140 port6 = UpnpGetServerPort6();
141#endif
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400142 if (logger_) {
143 if (ip_address6 and port6)
144 logger_->debug("PUPnP: Initialized on {}:{:d} | {}:{:d}", ip_address, port, ip_address6, port6);
145 else
146 logger_->debug("PUPnP: Initialized on {}:{:d}", ip_address, port);
147 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400148
149 // Relax the parser to allow malformed XML text.
150 ixmlRelaxParser(1);
151
152 initialized_ = true;
153}
154
155bool
156PUPnP::isRunning() const
157{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500158 std::unique_lock lk(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400159 return not shutdownComplete_;
160}
161
162void
163PUPnP::registerClient()
164{
165 assert(not clientRegistered_);
166
Adrien Béraud612b55b2023-05-29 10:42:04 -0400167 // Register Upnp control point.
168 int upnp_err = UpnpRegisterClient(ctrlPtCallback, this, &ctrlptHandle_);
169 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400170 if (logger_) logger_->error("PUPnP: Can't register client: {}", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400171 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400172 if (logger_) logger_->debug("PUPnP: Successfully registered client");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400173 clientRegistered_ = true;
174 }
175}
176
177void
François-Simon Fauteux-Chapleaud7976982024-04-26 16:06:23 -0400178PUPnP::unregisterClient()
179{
180 int upnp_err = UpnpUnRegisterClient(ctrlptHandle_);
181 if (upnp_err != UPNP_E_SUCCESS) {
182 if (logger_) logger_->error("PUPnP: Failed to unregister client: {}", UpnpGetErrorMessage(upnp_err));
183 } else {
184 if (logger_) logger_->debug("PUPnP: Successfully unregistered client");
185 clientRegistered_ = false;
186 }
187}
188
189void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400190PUPnP::setObserver(UpnpMappingObserver* obs)
191{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400192 observer_ = obs;
193}
194
195const IpAddr
196PUPnP::getHostAddress() const
197{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500198 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400199 return hostAddress_;
200}
201
202void
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400203PUPnP::terminate()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400204{
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400205 if (logger_) logger_->debug("PUPnP: Terminate instance {}", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400206
207 clientRegistered_ = false;
208 observer_ = nullptr;
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400209 {
210 std::lock_guard lk(ongoingOpsMtx_);
211 destroying_ = true;
212 if (ongoingOps_ > 0) {
213 if (logger_) logger_->debug("PUPnP: {} ongoing operations, detaching corresponding threads", ongoingOps_);
214 ongoingOpsThreadPool_.detach();
215 }
216 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400217
218 UpnpUnRegisterClient(ctrlptHandle_);
219
220 if (initialized_) {
221 if (UpnpFinish() != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400222 if (logger_) logger_->error("PUPnP: Failed to properly close lib-upnp");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400223 }
224
225 initialized_ = false;
226 }
227
228 // Clear all the lists.
229 discoveredIgdList_.clear();
230
Adrien Béraud024c46f2024-03-02 23:53:18 -0500231 std::lock_guard lock(pupnpMutex_);
Adrien Béraud7a82bee2023-08-30 10:26:45 -0400232 validIgdList_.clear();
233 shutdownComplete_ = true;
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400234 if (logger_) logger_->debug("PUPnP: Instance {} terminated", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400235}
236
237void
238PUPnP::searchForDevices()
239{
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400240 if (logger_) logger_->debug("PUPnP: Send IGD search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400241
242 // Send out search for multiple types of devices, as some routers may possibly
243 // only reply to one.
244
245 auto err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_ROOT_DEVICE, this);
246 if (err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400247 if (logger_) logger_->warn("PUPnP: Send search for UPNP_ROOT_DEVICE failed. Error {:d}: {}",
248 err,
249 UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400250 }
251
252 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_IGD_DEVICE, this);
253 if (err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400254 if (logger_) logger_->warn("PUPnP: Send search for UPNP_IGD_DEVICE failed. Error {:d}: {}",
255 err,
256 UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400257 }
258
259 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANIP_SERVICE, this);
260 if (err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400261 if (logger_) logger_->warn("PUPnP: Send search for UPNP_WANIP_SERVICE failed. Error {:d}: {}",
262 err,
263 UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400264 }
265
266 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANPPP_SERVICE, this);
267 if (err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400268 if (logger_) logger_->warn("PUPnP: Send search for UPNP_WANPPP_SERVICE failed. Error {:d}: {}",
269 err,
270 UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400271 }
272}
273
274void
275PUPnP::clearIgds()
276{
Morteza Namvar5f639522023-07-04 17:08:58 -0400277 // JAMI_DBG("PUPnP: clearing IGDs and devices lists");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400278
François-Simon Fauteux-Chapleaud7976982024-04-26 16:06:23 -0400279 // We need to unregister the client to make sure that we don't keep receiving and
280 // processing IGD-related events unnecessarily, see:
281 // https://git.jami.net/savoirfairelinux/dhtnet/-/issues/29
282 if (clientRegistered_)
283 unregisterClient();
284
Adrien Béraud370257c2023-08-15 20:53:09 -0400285 searchForIgdTimer_.cancel();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400286
287 igdSearchCounter_ = 0;
288
289 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500290 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400291 for (auto const& igd : validIgdList_) {
292 igd->setValid(false);
293 }
294 validIgdList_.clear();
295 hostAddress_ = {};
296 }
297
298 discoveredIgdList_.clear();
299}
300
301void
302PUPnP::searchForIgd()
303{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400304 // Update local address before searching.
305 updateHostAddress();
306
307 if (isReady()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400308 if (logger_) logger_->debug("PUPnP: Already have a valid IGD. Skip the search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400309 return;
310 }
311
312 if (igdSearchCounter_++ >= PUPNP_MAX_RESTART_SEARCH_RETRIES) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400313 if (logger_) logger_->warn("PUPnP: Setup failed after {:d} trials. PUPnP will be disabled!",
314 PUPNP_MAX_RESTART_SEARCH_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400315 return;
316 }
317
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400318 if (logger_) logger_->debug("PUPnP: Start search for IGD: attempt {:d}", igdSearchCounter_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400319
320 // Do not init if the host is not valid. Otherwise, the init will fail
321 // anyway and may put libupnp in an unstable state (mainly deadlocks)
322 // even if the UpnpFinish() method is called.
323 if (not hasValidHostAddress()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400324 if (logger_) logger_->warn("PUPnP: Host address is invalid. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400325 } else {
326 // Init and register if needed
327 if (not initialized_) {
328 initUpnpLib();
329 }
330 if (initialized_ and not clientRegistered_) {
331 registerClient();
332 }
333 // Start searching
334 if (clientRegistered_) {
335 assert(initialized_);
336 searchForDevices();
337 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400338 if (logger_) logger_->warn("PUPnP: PUPNP not fully setup. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400339 }
340 }
341
342 // Cancel the current timer (if any) and re-schedule.
343 // The connectivity change may be received while the the local
344 // interface is not fully setup. The rescheduling typically
345 // usefull to mitigate this race.
Adrien Béraud370257c2023-08-15 20:53:09 -0400346 searchForIgdTimer_.expires_after(PUPNP_SEARCH_RETRY_UNIT * igdSearchCounter_);
347 searchForIgdTimer_.async_wait([w = weak()] (const asio::error_code& ec) {
348 if (not ec) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400349 if (auto upnpThis = w.lock())
350 upnpThis->searchForIgd();
Adrien Béraud370257c2023-08-15 20:53:09 -0400351 }
352 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400353}
354
355std::list<std::shared_ptr<IGD>>
356PUPnP::getIgdList() const
357{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500358 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400359 std::list<std::shared_ptr<IGD>> igdList;
360 for (auto& it : validIgdList_) {
361 // Return only active IGDs.
362 if (it->isValid()) {
363 igdList.emplace_back(it);
364 }
365 }
366 return igdList;
367}
368
369bool
370PUPnP::isReady() const
371{
372 // Must at least have a valid local address.
373 if (not getHostAddress() or getHostAddress().isLoopback())
374 return false;
375
376 return hasValidIgd();
377}
378
379bool
380PUPnP::hasValidIgd() const
381{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500382 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400383 for (auto& it : validIgdList_) {
384 if (it->isValid()) {
385 return true;
386 }
387 }
388 return false;
389}
390
391void
392PUPnP::updateHostAddress()
393{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500394 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400395 hostAddress_ = ip_utils::getLocalAddr(AF_INET);
396}
397
398bool
399PUPnP::hasValidHostAddress()
400{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500401 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400402 return hostAddress_ and not hostAddress_.isLoopback();
403}
404
405void
406PUPnP::incrementErrorsCounter(const std::shared_ptr<IGD>& igd)
407{
408 if (not igd or not igd->isValid())
409 return;
410 if (not igd->incrementErrorsCounter()) {
411 // Disable this IGD.
412 igd->setValid(false);
413 // Notify the listener.
414 if (observer_)
415 observer_->onIgdUpdated(igd, UpnpIgdEvent::INVALID_STATE);
416 }
417}
418
419bool
420PUPnP::validateIgd(const std::string& location, IXML_Document* doc_container_ptr)
421{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400422 assert(doc_container_ptr != nullptr);
423
424 XMLDocument document(doc_container_ptr, ixmlDocument_free);
425 auto descDoc = document.get();
426 // Check device type.
427 auto deviceType = getFirstDocItem(descDoc, "deviceType");
428 if (deviceType != UPNP_IGD_DEVICE) {
429 // Device type not IGD.
430 return false;
431 }
432
433 std::shared_ptr<UPnPIGD> igd_candidate = parseIgd(descDoc, location);
434 if (not igd_candidate) {
435 // No valid IGD candidate.
436 return false;
437 }
438
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400439 if (logger_) logger_->debug("PUPnP: Validating the IGD candidate [UDN: {}]\n"
440 " Name : {}\n"
441 " Service Type : {}\n"
442 " Service ID : {}\n"
443 " Base URL : {}\n"
444 " Location URL : {}\n"
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400445 " Control URL : {}\n"
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400446 " Event URL : {}",
447 igd_candidate->getUID(),
448 igd_candidate->getFriendlyName(),
449 igd_candidate->getServiceType(),
450 igd_candidate->getServiceId(),
451 igd_candidate->getBaseURL(),
452 igd_candidate->getLocationURL(),
453 igd_candidate->getControlURL(),
454 igd_candidate->getEventSubURL());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400455
456 // Check if IGD is connected.
457 if (not actionIsIgdConnected(*igd_candidate)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400458 if (logger_) logger_->warn("PUPnP: IGD candidate {} is not connected", igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400459 return false;
460 }
461
462 // Validate external Ip.
463 igd_candidate->setPublicIp(actionGetExternalIP(*igd_candidate));
464 if (igd_candidate->getPublicIp().toString().empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400465 if (logger_) logger_->warn("PUPnP: IGD candidate {} has no valid external Ip",
466 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400467 return false;
468 }
469
470 // Validate internal Ip.
471 if (igd_candidate->getBaseURL().empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400472 if (logger_) logger_->warn("PUPnP: IGD candidate {} has no valid internal Ip",
473 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400474 return false;
475 }
476
477 // Typically the IGD local address should be extracted from the XML
478 // document (e.g. parsing the base URL). For simplicity, we assume
479 // that it matches the gateway as seen by the local interface.
480 if (const auto& localGw = ip_utils::getLocalGateway()) {
481 igd_candidate->setLocalIp(localGw);
482 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400483 if (logger_) logger_->warn("PUPnP: Could not set internal address for IGD candidate {}",
484 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400485 return false;
486 }
487
488 // Store info for subscription.
489 std::string eventSub = igd_candidate->getEventSubURL();
490
491 {
492 // Add the IGD if not already present in the list.
Adrien Béraud024c46f2024-03-02 23:53:18 -0500493 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400494 for (auto& igd : validIgdList_) {
495 // Must not be a null pointer
496 assert(igd.get() != nullptr);
497 if (*igd == *igd_candidate) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400498 if (logger_) logger_->debug("PUPnP: Device [{}] with int/ext addresses [{}:{}] is already in the list of valid IGDs",
499 igd_candidate->getUID(),
500 igd_candidate->toString(),
501 igd_candidate->getPublicIp().toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400502 return true;
503 }
504 }
505 }
506
507 // We have a valid IGD
508 igd_candidate->setValid(true);
509
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400510 if (logger_) logger_->debug("PUPnP: Added a new IGD [{}] to the list of valid IGDs",
511 igd_candidate->getUID());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400512
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400513 if (logger_) logger_->debug("PUPnP: New IGD addresses [int: {} - ext: {}]",
514 igd_candidate->toString(),
515 igd_candidate->getPublicIp().toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400516
517 // Subscribe to IGD events.
518 int upnp_err = UpnpSubscribeAsync(ctrlptHandle_,
519 eventSub.c_str(),
520 UPNP_INFINITE,
521 subEventCallback,
522 this);
523 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400524 if (logger_) logger_->warn("PUPnP: Failed to send subscribe request to {}: error %i - {}",
525 igd_candidate->getUID(),
526 upnp_err,
527 UpnpGetErrorMessage(upnp_err));
528 return false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400529 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400530 if (logger_) logger_->debug("PUPnP: Successfully subscribed to IGD {}", igd_candidate->getUID());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400531 }
532
533 {
534 // This is a new (and hopefully valid) IGD.
Adrien Béraud024c46f2024-03-02 23:53:18 -0500535 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400536 validIgdList_.emplace_back(igd_candidate);
537 }
538
539 // Report to the listener.
Adrien Béraud370257c2023-08-15 20:53:09 -0400540 ioContext->post([w = weak(), igd_candidate] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400541 if (auto upnpThis = w.lock()) {
542 if (upnpThis->observer_)
543 upnpThis->observer_->onIgdUpdated(igd_candidate, UpnpIgdEvent::ADDED);
544 }
545 });
546
547 return true;
548}
549
550void
551PUPnP::requestMappingAdd(const Mapping& mapping)
552{
Adrien Béraud370257c2023-08-15 20:53:09 -0400553 ioContext->post([w = weak(), mapping] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400554 if (auto upnpThis = w.lock()) {
555 if (not upnpThis->isRunning())
556 return;
557 Mapping mapRes(mapping);
558 if (upnpThis->actionAddPortMapping(mapRes)) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400559 auto now = sys_clock::now();
560 mapRes.setRenewalTime(now + std::chrono::seconds(MAPPING_LEASE_DURATION / 2));
561 mapRes.setExpiryTime(now + std::chrono::seconds(MAPPING_LEASE_DURATION));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400562 mapRes.setState(MappingState::OPEN);
563 mapRes.setInternalAddress(upnpThis->getHostAddress().toString());
564 upnpThis->processAddMapAction(mapRes);
565 } else {
566 upnpThis->incrementErrorsCounter(mapRes.getIgd());
567 mapRes.setState(MappingState::FAILED);
568 upnpThis->processRequestMappingFailure(mapRes);
569 }
570 }
571 });
572}
573
574void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400575PUPnP::requestMappingRenew(const Mapping& mapping)
576{
577 ioContext->post([w = weak(), mapping] {
578 if (auto upnpThis = w.lock()) {
579 if (not upnpThis->isRunning())
580 return;
581 Mapping mapRes(mapping);
582 if (upnpThis->actionAddPortMapping(mapRes)) {
583 if (upnpThis->logger_)
584 upnpThis->logger_->debug("PUPnP: Renewal request for mapping {} on {} succeeded",
585 mapRes.toString(),
586 mapRes.getIgd()->toString());
587 auto now = sys_clock::now();
588 mapRes.setRenewalTime(now + std::chrono::seconds(MAPPING_LEASE_DURATION / 2));
589 mapRes.setExpiryTime(now + std::chrono::seconds(MAPPING_LEASE_DURATION));
590 mapRes.setState(MappingState::OPEN);
591 mapRes.setInternalAddress(upnpThis->getHostAddress().toString());
592 upnpThis->processMappingRenewed(mapRes);
593 } else {
594 if (upnpThis->logger_)
595 upnpThis->logger_->debug("PUPnP: Renewal request for mapping {} on {} failed",
596 mapRes.toString(),
597 mapRes.getIgd()->toString());
598 upnpThis->incrementErrorsCounter(mapRes.getIgd());
599 mapRes.setState(MappingState::FAILED);
600 upnpThis->processRequestMappingFailure(mapRes);
601 }
602 }
603 });
604}
605
606void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400607PUPnP::requestMappingRemove(const Mapping& mapping)
608{
609 // Send remove request using the matching IGD
Adrien Béraud370257c2023-08-15 20:53:09 -0400610 ioContext->dispatch([w = weak(), mapping] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400611 if (auto upnpThis = w.lock()) {
612 // Abort if we are shutting down.
613 if (not upnpThis->isRunning())
614 return;
615 if (upnpThis->actionDeletePortMapping(mapping)) {
616 upnpThis->processRemoveMapAction(mapping);
617 } else {
618 assert(mapping.getIgd());
619 // Dont need to report in case of failure.
620 upnpThis->incrementErrorsCounter(mapping.getIgd());
621 }
622 }
623 });
624}
625
626std::shared_ptr<UPnPIGD>
627PUPnP::findMatchingIgd(const std::string& ctrlURL) const
628{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500629 std::lock_guard lock(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400630
631 auto iter = std::find_if(validIgdList_.begin(),
632 validIgdList_.end(),
633 [&ctrlURL](const std::shared_ptr<IGD>& igd) {
634 if (auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd)) {
635 return upnpIgd->getControlURL() == ctrlURL;
636 }
637 return false;
638 });
639
640 if (iter == validIgdList_.end()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400641 if (logger_) logger_->warn("PUPnP: Did not find the IGD matching ctrl URL [{}]", ctrlURL);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400642 return {};
643 }
644
645 return std::dynamic_pointer_cast<UPnPIGD>(*iter);
646}
647
648void
649PUPnP::processAddMapAction(const Mapping& map)
650{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400651 if (observer_ == nullptr)
652 return;
653
Adrien Béraud370257c2023-08-15 20:53:09 -0400654 ioContext->post([w = weak(), map] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400655 if (auto upnpThis = w.lock()) {
656 if (upnpThis->observer_)
657 upnpThis->observer_->onMappingAdded(map.getIgd(), std::move(map));
658 }
659 });
660}
661
662void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400663PUPnP::processMappingRenewed(const Mapping& map)
664{
665 if (observer_ == nullptr)
666 return;
667
668 ioContext->post([w = weak(), map] {
669 if (auto upnpThis = w.lock()) {
670 if (upnpThis->observer_)
671 upnpThis->observer_->onMappingRenewed(map.getIgd(), std::move(map));
672 }
673 });
674}
675
676void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400677PUPnP::processRequestMappingFailure(const Mapping& map)
678{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400679 if (observer_ == nullptr)
680 return;
681
Adrien Béraud370257c2023-08-15 20:53:09 -0400682 ioContext->post([w = weak(), map] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400683 if (auto upnpThis = w.lock()) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400684 if (upnpThis->observer_)
685 upnpThis->observer_->onMappingRequestFailed(map);
686 }
687 });
688}
689
690void
691PUPnP::processRemoveMapAction(const Mapping& map)
692{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400693 if (observer_ == nullptr)
694 return;
695
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400696 if (logger_) logger_->warn("PUPnP: Closed mapping {}", map.toString());
Adrien Béraud370257c2023-08-15 20:53:09 -0400697 ioContext->post([map, obs = observer_] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400698 obs->onMappingRemoved(map.getIgd(), std::move(map));
699 });
700}
701
702const char*
703PUPnP::eventTypeToString(Upnp_EventType eventType)
704{
705 switch (eventType) {
706 case UPNP_CONTROL_ACTION_REQUEST:
707 return "UPNP_CONTROL_ACTION_REQUEST";
708 case UPNP_CONTROL_ACTION_COMPLETE:
709 return "UPNP_CONTROL_ACTION_COMPLETE";
710 case UPNP_CONTROL_GET_VAR_REQUEST:
711 return "UPNP_CONTROL_GET_VAR_REQUEST";
712 case UPNP_CONTROL_GET_VAR_COMPLETE:
713 return "UPNP_CONTROL_GET_VAR_COMPLETE";
714 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
715 return "UPNP_DISCOVERY_ADVERTISEMENT_ALIVE";
716 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
717 return "UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE";
718 case UPNP_DISCOVERY_SEARCH_RESULT:
719 return "UPNP_DISCOVERY_SEARCH_RESULT";
720 case UPNP_DISCOVERY_SEARCH_TIMEOUT:
721 return "UPNP_DISCOVERY_SEARCH_TIMEOUT";
722 case UPNP_EVENT_SUBSCRIPTION_REQUEST:
723 return "UPNP_EVENT_SUBSCRIPTION_REQUEST";
724 case UPNP_EVENT_RECEIVED:
725 return "UPNP_EVENT_RECEIVED";
726 case UPNP_EVENT_RENEWAL_COMPLETE:
727 return "UPNP_EVENT_RENEWAL_COMPLETE";
728 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
729 return "UPNP_EVENT_SUBSCRIBE_COMPLETE";
730 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE:
731 return "UPNP_EVENT_UNSUBSCRIBE_COMPLETE";
732 case UPNP_EVENT_AUTORENEWAL_FAILED:
733 return "UPNP_EVENT_AUTORENEWAL_FAILED";
734 case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
735 return "UPNP_EVENT_SUBSCRIPTION_EXPIRED";
736 default:
737 return "Unknown UPNP Event";
738 }
739}
740
741int
742PUPnP::ctrlPtCallback(Upnp_EventType event_type, const void* event, void* user_data)
743{
744 auto pupnp = static_cast<PUPnP*>(user_data);
745
746 if (pupnp == nullptr) {
Adrien Bérauda61adb52023-08-23 09:31:02 -0400747 fmt::print(stderr, "PUPnP: Control point callback without PUPnP");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400748 return UPNP_E_SUCCESS;
749 }
750
751 auto upnpThis = pupnp->weak().lock();
Adrien Bérauda61adb52023-08-23 09:31:02 -0400752 if (not upnpThis) {
753 fmt::print(stderr, "PUPnP: Control point callback without PUPnP");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400754 return UPNP_E_SUCCESS;
Adrien Bérauda61adb52023-08-23 09:31:02 -0400755 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400756
757 // Ignore if already unregistered.
758 if (not upnpThis->clientRegistered_)
759 return UPNP_E_SUCCESS;
760
761 // Process the callback.
762 return upnpThis->handleCtrlPtUPnPEvents(event_type, event);
763}
764
765PUPnP::CtrlAction
766PUPnP::getAction(const char* xmlNode)
767{
768 if (strstr(xmlNode, ACTION_ADD_PORT_MAPPING)) {
769 return CtrlAction::ADD_PORT_MAPPING;
770 } else if (strstr(xmlNode, ACTION_DELETE_PORT_MAPPING)) {
771 return CtrlAction::DELETE_PORT_MAPPING;
772 } else if (strstr(xmlNode, ACTION_GET_GENERIC_PORT_MAPPING_ENTRY)) {
773 return CtrlAction::GET_GENERIC_PORT_MAPPING_ENTRY;
774 } else if (strstr(xmlNode, ACTION_GET_STATUS_INFO)) {
775 return CtrlAction::GET_STATUS_INFO;
776 } else if (strstr(xmlNode, ACTION_GET_EXTERNAL_IP_ADDRESS)) {
777 return CtrlAction::GET_EXTERNAL_IP_ADDRESS;
778 } else {
779 return CtrlAction::UNKNOWN;
780 }
781}
782
783void
784PUPnP::processDiscoverySearchResult(const std::string& cpDeviceId,
785 const std::string& igdLocationUrl,
786 const IpAddr& dstAddr)
787{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400788 // Update host address if needed.
789 if (not hasValidHostAddress())
790 updateHostAddress();
791
792 // The host address must be valid to proceed.
793 if (not hasValidHostAddress()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400794 if (logger_) logger_->warn("PUPnP: Local address is invalid. Ignore search result for now!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400795 return;
796 }
797
798 // Use the device ID and the URL as ID. This is necessary as some
799 // IGDs may have the same device ID but different URLs.
800
801 auto igdId = cpDeviceId + " url: " + igdLocationUrl;
802
803 if (not discoveredIgdList_.emplace(igdId).second) {
Adrien Beraud64bb00f2023-08-23 19:06:46 -0400804 //if (logger_) logger_->debug("PUPnP: IGD [{}] already in the list", igdId);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400805 return;
806 }
807
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400808 if (logger_) logger_->debug("PUPnP: Discovered a new IGD [{}]", igdId);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400809
810 // NOTE: here, we check if the location given is related to the source address.
811 // If it's not the case, it's certainly a router plugged in the network, but not
812 // related to this network. So the given location will be unreachable and this
813 // will cause some timeout.
814
815 // Only check the IP address (ignore the port number).
816 dht::http::Url url(igdLocationUrl);
817 if (IpAddr(url.host).toString(false) != dstAddr.toString(false)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400818 if (logger_) logger_->debug("PUPnP: Returned location {} does not match the source address {}",
819 IpAddr(url.host).toString(true, true),
820 dstAddr.toString(true, true));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400821 return;
822 }
823
824 // Run a separate thread to prevent blocking this thread
825 // if the IGD HTTP server is not responsive.
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400826 ongoingOpsThreadPool_.run([w = weak(), url=igdLocationUrl] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400827 if (auto upnpThis = w.lock()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400828 upnpThis->downLoadIgdDescription(url);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400829 }
830 });
831}
832
833void
834PUPnP::downLoadIgdDescription(const std::string& locationUrl)
835{
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400836 if(logger_) logger_->debug("PUPnP: downLoadIgdDescription {}", locationUrl);
Sébastien Blind14fc352023-10-06 15:21:53 -0400837 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500838 std::lock_guard lk(ongoingOpsMtx_);
Sébastien Blind14fc352023-10-06 15:21:53 -0400839 if (destroying_)
840 return;
841 ongoingOps_++;
842 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400843 IXML_Document* doc_container_ptr = nullptr;
844 int upnp_err = UpnpDownloadXmlDoc(locationUrl.c_str(), &doc_container_ptr);
845
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400846 std::lock_guard lk(ongoingOpsMtx_);
847 // Trying to use libupnp functions after UpnpFinish has been called (which may
848 // be the case if destroying_ is true) can cause errors. It's probably not a
849 // problem here, but return early just in case.
850 if (destroying_)
851 return;
852
Adrien Béraud612b55b2023-05-29 10:42:04 -0400853 if (upnp_err != UPNP_E_SUCCESS or not doc_container_ptr) {
Adrien Béraud370257c2023-08-15 20:53:09 -0400854 if(logger_) logger_->warn("PUPnP: Error downloading device XML document from {} -> {}",
855 locationUrl,
856 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400857 } else {
Adrien Béraud370257c2023-08-15 20:53:09 -0400858 if(logger_) logger_->debug("PUPnP: Succeeded to download device XML document from {}", locationUrl);
859 ioContext->post([w = weak(), url = locationUrl, doc_container_ptr] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400860 if (auto upnpThis = w.lock()) {
861 upnpThis->validateIgd(url, doc_container_ptr);
862 }
863 });
864 }
Sébastien Blind14fc352023-10-06 15:21:53 -0400865 ongoingOps_--;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400866}
867
868void
869PUPnP::processDiscoveryAdvertisementByebye(const std::string& cpDeviceId)
870{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400871 discoveredIgdList_.erase(cpDeviceId);
872
873 std::shared_ptr<IGD> igd;
874 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500875 std::lock_guard lk(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400876 for (auto it = validIgdList_.begin(); it != validIgdList_.end();) {
877 if ((*it)->getUID() == cpDeviceId) {
878 igd = *it;
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400879 if (logger_) logger_->debug("PUPnP: Received [{}] for IGD [{}] {}. Will be removed.",
880 PUPnP::eventTypeToString(UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE),
881 igd->getUID(),
882 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400883 igd->setValid(false);
884 // Remove the IGD.
885 it = validIgdList_.erase(it);
886 break;
887 } else {
888 it++;
889 }
890 }
891 }
892
893 // Notify the listener.
894 if (observer_ and igd) {
895 observer_->onIgdUpdated(igd, UpnpIgdEvent::REMOVED);
896 }
897}
898
899void
900PUPnP::processDiscoverySubscriptionExpired(Upnp_EventType event_type, const std::string& eventSubUrl)
901{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500902 std::lock_guard lk(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400903 for (auto& it : validIgdList_) {
904 if (auto igd = std::dynamic_pointer_cast<UPnPIGD>(it)) {
905 if (igd->getEventSubURL() == eventSubUrl) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400906 if (logger_) logger_->debug("PUPnP: Received [{}] event for IGD [{}] {}. Request a new subscribe.",
907 PUPnP::eventTypeToString(event_type),
908 igd->getUID(),
909 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400910 UpnpSubscribeAsync(ctrlptHandle_,
911 eventSubUrl.c_str(),
912 UPNP_INFINITE,
913 subEventCallback,
914 this);
915 break;
916 }
917 }
918 }
919}
920
921int
922PUPnP::handleCtrlPtUPnPEvents(Upnp_EventType event_type, const void* event)
923{
924 switch (event_type) {
925 // "ALIVE" events are processed as "SEARCH RESULT". It might be usefull
926 // if "SEARCH RESULT" was missed.
927 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
928 case UPNP_DISCOVERY_SEARCH_RESULT: {
929 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
930
931 // First check the error code.
932 auto upnp_status = UpnpDiscovery_get_ErrCode(d_event);
933 if (upnp_status != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400934 if (logger_) logger_->error("PUPnP: UPNP discovery is in erroneous state: %s",
935 UpnpGetErrorMessage(upnp_status));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400936 break;
937 }
938
939 // Parse the event's data.
940 std::string deviceId {UpnpDiscovery_get_DeviceID_cstr(d_event)};
941 std::string location {UpnpDiscovery_get_Location_cstr(d_event)};
942 IpAddr dstAddr(*(const pj_sockaddr*) (UpnpDiscovery_get_DestAddr(d_event)));
Adrien Béraud370257c2023-08-15 20:53:09 -0400943 ioContext->post([w = weak(),
Adrien Béraud612b55b2023-05-29 10:42:04 -0400944 deviceId = std::move(deviceId),
945 location = std::move(location),
946 dstAddr = std::move(dstAddr)] {
947 if (auto upnpThis = w.lock()) {
948 upnpThis->processDiscoverySearchResult(deviceId, location, dstAddr);
949 }
950 });
951 break;
952 }
953 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: {
954 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
955
956 std::string deviceId(UpnpDiscovery_get_DeviceID_cstr(d_event));
957
958 // Process the response on the main thread.
Adrien Béraud370257c2023-08-15 20:53:09 -0400959 ioContext->post([w = weak(), deviceId = std::move(deviceId)] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400960 if (auto upnpThis = w.lock()) {
961 upnpThis->processDiscoveryAdvertisementByebye(deviceId);
962 }
963 });
964 break;
965 }
966 case UPNP_DISCOVERY_SEARCH_TIMEOUT: {
967 // Even if the discovery search is successful, it's normal to receive
968 // time-out events. This because we send search requests using various
969 // device types, which some of them may not return a response.
970 break;
971 }
972 case UPNP_EVENT_RECEIVED: {
973 // Nothing to do.
974 break;
975 }
976 // Treat failed autorenewal like an expired subscription.
977 case UPNP_EVENT_AUTORENEWAL_FAILED:
978 case UPNP_EVENT_SUBSCRIPTION_EXPIRED: // This event will occur only if autorenewal is disabled.
979 {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400980 if (logger_) logger_->warn("PUPnP: Received Subscription Event {}", eventTypeToString(event_type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400981 const UpnpEventSubscribe* es_event = (const UpnpEventSubscribe*) event;
982 if (es_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400983 if (logger_) logger_->warn("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400984 break;
985 }
986 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
987
988 // Process the response on the main thread.
Adrien Béraud370257c2023-08-15 20:53:09 -0400989 ioContext->post([w = weak(), event_type, publisherUrl = std::move(publisherUrl)] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400990 if (auto upnpThis = w.lock()) {
991 upnpThis->processDiscoverySubscriptionExpired(event_type, publisherUrl);
992 }
993 });
994 break;
995 }
996 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
997 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE: {
998 UpnpEventSubscribe* es_event = (UpnpEventSubscribe*) event;
999 if (es_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001000 if (logger_) logger_->warn("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001001 } else {
1002 UpnpEventSubscribe_delete(es_event);
1003 }
1004 break;
1005 }
1006 case UPNP_CONTROL_ACTION_COMPLETE: {
1007 const UpnpActionComplete* a_event = (const UpnpActionComplete*) event;
1008 if (a_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001009 if (logger_) logger_->warn("PUPnP: Received Action Complete Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001010 break;
1011 }
1012 auto res = UpnpActionComplete_get_ErrCode(a_event);
1013 if (res != UPNP_E_SUCCESS and res != UPNP_E_TIMEDOUT) {
1014 auto err = UpnpActionComplete_get_ErrCode(a_event);
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001015 if (logger_) logger_->warn("PUPnP: Received Action Complete error %i %s", err, UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001016 } else {
1017 auto actionRequest = UpnpActionComplete_get_ActionRequest(a_event);
1018 // Abort if there is no action to process.
1019 if (actionRequest == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001020 if (logger_) logger_->warn("PUPnP: Can't get the Action Request data from the event");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001021 break;
1022 }
1023
1024 auto actionResult = UpnpActionComplete_get_ActionResult(a_event);
1025 if (actionResult != nullptr) {
1026 ixmlDocument_free(actionResult);
1027 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001028 if (logger_) logger_->warn("PUPnP: Action Result document not found");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001029 }
1030 }
1031 break;
1032 }
1033 default: {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001034 if (logger_) logger_->warn("PUPnP: Unhandled Control Point event");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001035 break;
1036 }
1037 }
1038
1039 return UPNP_E_SUCCESS;
1040}
1041
1042int
1043PUPnP::subEventCallback(Upnp_EventType event_type, const void* event, void* user_data)
1044{
1045 if (auto pupnp = static_cast<PUPnP*>(user_data))
1046 return pupnp->handleSubscriptionUPnPEvent(event_type, event);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001047 return 0;
1048}
1049
1050int
1051PUPnP::handleSubscriptionUPnPEvent(Upnp_EventType, const void* event)
1052{
1053 UpnpEventSubscribe* es_event = static_cast<UpnpEventSubscribe*>(const_cast<void*>(event));
1054
1055 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001056 // JAMI_ERR("PUPnP: Unexpected null pointer!");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001057 return UPNP_E_INVALID_ARGUMENT;
1058 }
1059 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
1060 int upnp_err = UpnpEventSubscribe_get_ErrCode(es_event);
1061 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001062 if (logger_) logger_->warn("PUPnP: Subscription error {} from {}",
1063 UpnpGetErrorMessage(upnp_err),
1064 publisherUrl);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001065 return upnp_err;
1066 }
1067
1068 return UPNP_E_SUCCESS;
1069}
1070
1071std::unique_ptr<UPnPIGD>
1072PUPnP::parseIgd(IXML_Document* doc, std::string locationUrl)
1073{
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001074 if (not(doc and !locationUrl.empty()))
Adrien Béraud612b55b2023-05-29 10:42:04 -04001075 return nullptr;
1076
1077 // Check the UDN to see if its already in our device list.
1078 std::string UDN(getFirstDocItem(doc, "UDN"));
1079 if (UDN.empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001080 if (logger_) logger_->warn("PUPnP: could not find UDN in description document of device");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001081 return nullptr;
1082 } else {
Adrien Béraud024c46f2024-03-02 23:53:18 -05001083 std::lock_guard lk(pupnpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001084 for (auto& it : validIgdList_) {
1085 if (it->getUID() == UDN) {
1086 // We already have this device in our list.
1087 return nullptr;
1088 }
1089 }
1090 }
1091
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001092 if (logger_) logger_->debug("PUPnP: Found new device [{}]", UDN);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001093
1094 std::unique_ptr<UPnPIGD> new_igd;
1095 int upnp_err;
1096
1097 // Get friendly name.
1098 std::string friendlyName(getFirstDocItem(doc, "friendlyName"));
1099
1100 // Get base URL.
1101 std::string baseURL(getFirstDocItem(doc, "URLBase"));
1102 if (baseURL.empty())
1103 baseURL = locationUrl;
1104
1105 // Get list of services defined by serviceType.
1106 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> serviceList(nullptr,
1107 ixmlNodeList_free);
1108 serviceList.reset(ixmlDocument_getElementsByTagName(doc, "serviceType"));
1109 unsigned long list_length = ixmlNodeList_length(serviceList.get());
1110
1111 // Go through the "serviceType" nodes until we find the the correct service type.
1112 for (unsigned long node_idx = 0; node_idx < list_length; node_idx++) {
1113 IXML_Node* serviceType_node = ixmlNodeList_item(serviceList.get(), node_idx);
1114 std::string serviceType(getElementText(serviceType_node));
1115
1116 // Only check serviceType of WANIPConnection or WANPPPConnection.
1117 if (serviceType != UPNP_WANIP_SERVICE
1118 && serviceType != UPNP_WANPPP_SERVICE) {
1119 // IGD is not WANIP or WANPPP service. Going to next node.
1120 continue;
1121 }
1122
1123 // Get parent node.
1124 IXML_Node* service_node = ixmlNode_getParentNode(serviceType_node);
1125 if (not service_node) {
1126 // IGD serviceType has no parent node. Going to next node.
1127 continue;
1128 }
1129
1130 // Perform sanity check. The parent node should be called "service".
1131 if (strcmp(ixmlNode_getNodeName(service_node), "service") != 0) {
1132 // IGD "serviceType" parent node is not called "service". Going to next node.
1133 continue;
1134 }
1135
1136 // Get serviceId.
1137 IXML_Element* service_element = (IXML_Element*) service_node;
1138 std::string serviceId(getFirstElementItem(service_element, "serviceId"));
1139 if (serviceId.empty()) {
1140 // IGD "serviceId" is empty. Going to next node.
1141 continue;
1142 }
1143
1144 // Get the relative controlURL and turn it into absolute address using the URLBase.
1145 std::string controlURL(getFirstElementItem(service_element, "controlURL"));
1146 if (controlURL.empty()) {
1147 // IGD control URL is empty. Going to next node.
1148 continue;
1149 }
1150
1151 char* absolute_control_url = nullptr;
1152 upnp_err = UpnpResolveURL2(baseURL.c_str(), controlURL.c_str(), &absolute_control_url);
1153 if (upnp_err == UPNP_E_SUCCESS)
1154 controlURL = absolute_control_url;
1155 else
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001156 if (logger_) logger_->warn("PUPnP: Error resolving absolute controlURL -> {}",
1157 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001158
1159 std::free(absolute_control_url);
1160
1161 // Get the relative eventSubURL and turn it into absolute address using the URLBase.
1162 std::string eventSubURL(getFirstElementItem(service_element, "eventSubURL"));
1163 if (eventSubURL.empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001164 if (logger_) logger_->warn("PUPnP: IGD event sub URL is empty. Going to next node");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001165 continue;
1166 }
1167
1168 char* absolute_event_sub_url = nullptr;
1169 upnp_err = UpnpResolveURL2(baseURL.c_str(), eventSubURL.c_str(), &absolute_event_sub_url);
1170 if (upnp_err == UPNP_E_SUCCESS)
1171 eventSubURL = absolute_event_sub_url;
1172 else
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001173 if (logger_) logger_->warn("PUPnP: Error resolving absolute eventSubURL -> {}",
1174 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001175
1176 std::free(absolute_event_sub_url);
1177
1178 new_igd.reset(new UPnPIGD(std::move(UDN),
1179 std::move(baseURL),
1180 std::move(friendlyName),
1181 std::move(serviceType),
1182 std::move(serviceId),
1183 std::move(locationUrl),
1184 std::move(controlURL),
1185 std::move(eventSubURL)));
1186
1187 return new_igd;
1188 }
1189
1190 return nullptr;
1191}
1192
1193bool
1194PUPnP::actionIsIgdConnected(const UPnPIGD& igd)
1195{
1196 if (not clientRegistered_)
1197 return false;
1198
1199 // Set action name.
1200 IXML_Document* action_container_ptr = UpnpMakeAction("GetStatusInfo",
1201 igd.getServiceType().c_str(),
1202 0,
1203 nullptr);
1204 if (not action_container_ptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001205 if (logger_) logger_->warn("PUPnP: Failed to make GetStatusInfo action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001206 return false;
1207 }
1208 XMLDocument action(action_container_ptr, ixmlDocument_free); // Action pointer.
1209
1210 IXML_Document* response_container_ptr = nullptr;
1211 int upnp_err = UpnpSendAction(ctrlptHandle_,
1212 igd.getControlURL().c_str(),
1213 igd.getServiceType().c_str(),
1214 nullptr,
1215 action.get(),
1216 &response_container_ptr);
Sébastien Blin45b50692024-03-06 11:18:50 -05001217 if (upnp_err == 401) {
1218 // YET ANOTHER UPNP HACK: MiniUpnp on some routers seems to not recognize this action, sending a 401: Invalid Action.
1219 // So even if mapping succeeds, the router was considered as not connected.
1220 // Returning true here works around this issue.
1221 // E.g. https://community.tp-link.com/us/home/forum/topic/577840
1222 return true;
1223 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001224 if (not response_container_ptr or upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001225 if (logger_) logger_->warn("PUPnP: Failed to send GetStatusInfo action -> {}", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001226 return false;
1227 }
1228 XMLDocument response(response_container_ptr, ixmlDocument_free);
1229
Adrien Béraudd78d1ac2023-08-25 10:43:33 -04001230 if (errorOnResponse(response.get(), logger_)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001231 if (logger_) logger_->warn("PUPnP: Failed to get GetStatusInfo from {} -> {:d}: {}",
1232 igd.getServiceType().c_str(),
1233 upnp_err,
1234 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001235 return false;
1236 }
1237
1238 // Parse response.
1239 auto status = getFirstDocItem(response.get(), "NewConnectionStatus");
1240 return status == "Connected";
1241}
1242
1243IpAddr
1244PUPnP::actionGetExternalIP(const UPnPIGD& igd)
1245{
1246 if (not clientRegistered_)
1247 return {};
1248
1249 // Action and response pointers.
1250 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1251 action(nullptr, ixmlDocument_free); // Action pointer.
1252 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1253 response(nullptr, ixmlDocument_free); // Response pointer.
1254
1255 // Set action name.
1256 static constexpr const char* action_name {"GetExternalIPAddress"};
1257
1258 IXML_Document* action_container_ptr = nullptr;
1259 action_container_ptr = UpnpMakeAction(action_name, igd.getServiceType().c_str(), 0, nullptr);
1260 action.reset(action_container_ptr);
1261
1262 if (not action) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001263 if (logger_) logger_->warn("PUPnP: Failed to make GetExternalIPAddress action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001264 return {};
1265 }
1266
1267 IXML_Document* response_container_ptr = nullptr;
1268 int upnp_err = UpnpSendAction(ctrlptHandle_,
1269 igd.getControlURL().c_str(),
1270 igd.getServiceType().c_str(),
1271 nullptr,
1272 action.get(),
1273 &response_container_ptr);
1274 response.reset(response_container_ptr);
1275
1276 if (not response or upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001277 if (logger_) logger_->warn("PUPnP: Failed to send GetExternalIPAddress action -> {}",
1278 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001279 return {};
1280 }
1281
Adrien Béraudd78d1ac2023-08-25 10:43:33 -04001282 if (errorOnResponse(response.get(), logger_)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001283 if (logger_) logger_->warn("PUPnP: Failed to get GetExternalIPAddress from {} -> {:d}: {}",
1284 igd.getServiceType(),
1285 upnp_err,
1286 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001287 return {};
1288 }
1289
1290 return {getFirstDocItem(response.get(), "NewExternalIPAddress")};
1291}
1292
1293std::map<Mapping::key_t, Mapping>
1294PUPnP::getMappingsListByDescr(const std::shared_ptr<IGD>& igd, const std::string& description) const
1295{
1296 auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd);
1297 assert(upnpIgd);
1298
1299 std::map<Mapping::key_t, Mapping> mapList;
1300
1301 if (not clientRegistered_ or not upnpIgd->isValid() or not upnpIgd->getLocalIp())
1302 return mapList;
1303
1304 // Set action name.
1305 static constexpr const char* action_name {"GetGenericPortMappingEntry"};
1306
1307 for (int entry_idx = 0;; entry_idx++) {
1308 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1309 action(nullptr, ixmlDocument_free); // Action pointer.
1310 IXML_Document* action_container_ptr = nullptr;
1311
1312 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1313 response(nullptr, ixmlDocument_free); // Response pointer.
1314 IXML_Document* response_container_ptr = nullptr;
1315
1316 UpnpAddToAction(&action_container_ptr,
1317 action_name,
1318 upnpIgd->getServiceType().c_str(),
1319 "NewPortMappingIndex",
1320 std::to_string(entry_idx).c_str());
1321 action.reset(action_container_ptr);
1322
1323 if (not action) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001324 // JAMI_WARN("PUPnP: Failed to add NewPortMappingIndex action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001325 break;
1326 }
1327
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001328 auto timeIgdRequestSent = sys_clock::now();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001329 int upnp_err = UpnpSendAction(ctrlptHandle_,
1330 upnpIgd->getControlURL().c_str(),
1331 upnpIgd->getServiceType().c_str(),
1332 nullptr,
1333 action.get(),
1334 &response_container_ptr);
1335 response.reset(response_container_ptr);
1336
1337 if (not response) {
1338 // No existing mapping. Abort silently.
1339 break;
1340 }
1341
1342 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001343 // JAMI_ERR("PUPnP: GetGenericPortMappingEntry returned with error: %i", upnp_err);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001344 break;
1345 }
1346
1347 // Check error code.
1348 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1349 if (not errorCode.empty()) {
1350 auto error = to_int<int>(errorCode);
1351 if (error == ARRAY_IDX_INVALID or error == CONFLICT_IN_MAPPING) {
1352 // No more port mapping entries in the response.
Morteza Namvar5f639522023-07-04 17:08:58 -04001353 // JAMI_DBG("PUPnP: No more mappings (found a total of %i mappings", entry_idx);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001354 break;
1355 } else {
1356 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
Adrien Béraud370257c2023-08-15 20:53:09 -04001357 if (logger_) logger_->error("PUPnP: GetGenericPortMappingEntry returned with error: {:s}: {:s}",
Adrien Béraud612b55b2023-05-29 10:42:04 -04001358 errorCode,
1359 errorDescription);
1360 break;
1361 }
1362 }
1363
1364 // Parse the response.
1365 auto desc_actual = getFirstDocItem(response.get(), "NewPortMappingDescription");
1366 auto client_ip = getFirstDocItem(response.get(), "NewInternalClient");
1367
1368 if (client_ip != getHostAddress().toString()) {
1369 // Silently ignore un-matching addresses.
1370 continue;
1371 }
1372
1373 if (desc_actual.find(description) == std::string::npos)
1374 continue;
1375
1376 auto port_internal = getFirstDocItem(response.get(), "NewInternalPort");
1377 auto port_external = getFirstDocItem(response.get(), "NewExternalPort");
1378 std::string transport(getFirstDocItem(response.get(), "NewProtocol"));
1379
1380 if (port_internal.empty() || port_external.empty() || transport.empty()) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001381 // Invalid entry, ignore
Adrien Béraud612b55b2023-05-29 10:42:04 -04001382 continue;
1383 }
1384
1385 std::transform(transport.begin(), transport.end(), transport.begin(), ::toupper);
1386 PortType type = transport.find("TCP") != std::string::npos ? PortType::TCP : PortType::UDP;
1387 auto ePort = to_int<uint16_t>(port_external);
1388 auto iPort = to_int<uint16_t>(port_internal);
1389
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001390 auto leaseDurationStr = getFirstDocItem(response.get(), "NewLeaseDuration");
1391 auto leaseDuration = to_int<uint32_t>(leaseDurationStr);
1392 auto expiryTime = (leaseDuration == 0) ? sys_clock::time_point::max()
1393 : timeIgdRequestSent + std::chrono::seconds(leaseDuration);
1394
Adrien Béraud612b55b2023-05-29 10:42:04 -04001395 Mapping map(type, ePort, iPort);
1396 map.setIgd(igd);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001397 map.setExpiryTime(expiryTime);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001398
1399 mapList.emplace(map.getMapKey(), std::move(map));
1400 }
1401
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001402 if (logger_) logger_->debug("PUPnP: Found {:d} allocated mappings on IGD {:s}",
1403 mapList.size(),
1404 upnpIgd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001405
1406 return mapList;
1407}
1408
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -04001409std::vector<MappingInfo>
1410PUPnP::getMappingsInfo(const std::shared_ptr<IGD>& igd) const
1411{
1412 auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd);
1413 assert(upnpIgd);
1414
1415 std::vector<MappingInfo> mappingInfoList;
1416
1417 if (not clientRegistered_ or not upnpIgd->isValid() or not upnpIgd->getLocalIp())
1418 return mappingInfoList;
1419
1420 static constexpr const char* action_name {"GetGenericPortMappingEntry"};
1421
1422 for (int entry_idx = 0;; entry_idx++) {
1423 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1424 action(nullptr, ixmlDocument_free); // Action pointer.
1425 IXML_Document* action_container_ptr = nullptr;
1426
1427 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1428 response(nullptr, ixmlDocument_free); // Response pointer.
1429 IXML_Document* response_container_ptr = nullptr;
1430
1431 UpnpAddToAction(&action_container_ptr,
1432 action_name,
1433 upnpIgd->getServiceType().c_str(),
1434 "NewPortMappingIndex",
1435 std::to_string(entry_idx).c_str());
1436 action.reset(action_container_ptr);
1437
1438 int upnp_err = UpnpSendAction(ctrlptHandle_,
1439 upnpIgd->getControlURL().c_str(),
1440 upnpIgd->getServiceType().c_str(),
1441 nullptr,
1442 action.get(),
1443 &response_container_ptr);
1444 response.reset(response_container_ptr);
1445
1446 if (!response || upnp_err != UPNP_E_SUCCESS) {
1447 break;
1448 }
1449
1450 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1451 if (not errorCode.empty()) {
1452 auto error = to_int<int>(errorCode);
1453 if (error == ARRAY_IDX_INVALID or error == CONFLICT_IN_MAPPING) {
1454 // No more port mapping entries in the response.
1455 break;
1456 } else {
1457 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
1458 if (logger_) logger_->error("PUPnP: GetGenericPortMappingEntry returned with error: {:s}: {:s}",
1459 errorCode,
1460 errorDescription);
1461 break;
1462 }
1463 }
1464
1465 // Parse the response.
1466 MappingInfo info;
1467 info.remoteHost = getFirstDocItem(response.get(), "NewRemoteHost");
1468 info.protocol = getFirstDocItem(response.get(), "NewProtocol");
1469 info.internalClient = getFirstDocItem(response.get(), "NewInternalClient");
1470 info.enabled = getFirstDocItem(response.get(), "NewEnabled");
1471 info.description = getFirstDocItem(response.get(), "NewPortMappingDescription");
1472
1473 auto externalPort = getFirstDocItem(response.get(), "NewExternalPort");
1474 info.externalPort = to_int<uint16_t>(externalPort);
1475
1476 auto internalPort = getFirstDocItem(response.get(), "NewInternalPort");
1477 info.internalPort = to_int<uint16_t>(internalPort);
1478
1479 auto leaseDuration = getFirstDocItem(response.get(), "NewLeaseDuration");
1480 info.leaseDuration = to_int<uint32_t>(leaseDuration);
1481
1482 mappingInfoList.push_back(std::move(info));
1483 }
1484
1485 return mappingInfoList;
1486}
1487
Adrien Béraud612b55b2023-05-29 10:42:04 -04001488void
1489PUPnP::deleteMappingsByDescription(const std::shared_ptr<IGD>& igd, const std::string& description)
1490{
1491 if (not(clientRegistered_ and igd->getLocalIp()))
1492 return;
1493
François-Simon Fauteux-Chapleau1b3aba22024-05-23 11:51:48 -04001494 if (logger_) logger_->debug("PUPnP: Remove all mappings (if any) on IGD {} matching description prefix {}",
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001495 igd->toString(),
François-Simon Fauteux-Chapleau1b3aba22024-05-23 11:51:48 -04001496 description);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001497
Adrien Béraud370257c2023-08-15 20:53:09 -04001498 ioContext->post([w=weak(), igd, description]{
1499 if (auto sthis = w.lock()) {
1500 auto mapList = sthis->getMappingsListByDescr(igd, description);
1501 for (auto const& [_, map] : mapList) {
1502 sthis->requestMappingRemove(map);
1503 }
1504 }
1505 });
Adrien Béraud612b55b2023-05-29 10:42:04 -04001506}
1507
1508bool
1509PUPnP::actionAddPortMapping(const Mapping& mapping)
1510{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001511 if (not clientRegistered_)
1512 return false;
1513
1514 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1515 if (not igdIn)
1516 return false;
1517
1518 // The requested IGD must be present in the list of local valid IGDs.
1519 auto igd = findMatchingIgd(igdIn->getControlURL());
1520
1521 if (not igd or not igd->isValid())
1522 return false;
1523
1524 // Action and response pointers.
1525 XMLDocument action(nullptr, ixmlDocument_free);
1526 IXML_Document* action_container_ptr = nullptr;
1527 XMLDocument response(nullptr, ixmlDocument_free);
1528 IXML_Document* response_container_ptr = nullptr;
1529
1530 // Set action sequence.
1531 UpnpAddToAction(&action_container_ptr,
1532 ACTION_ADD_PORT_MAPPING,
1533 igd->getServiceType().c_str(),
1534 "NewRemoteHost",
1535 "");
1536 UpnpAddToAction(&action_container_ptr,
1537 ACTION_ADD_PORT_MAPPING,
1538 igd->getServiceType().c_str(),
1539 "NewExternalPort",
1540 mapping.getExternalPortStr().c_str());
1541 UpnpAddToAction(&action_container_ptr,
1542 ACTION_ADD_PORT_MAPPING,
1543 igd->getServiceType().c_str(),
1544 "NewProtocol",
1545 mapping.getTypeStr());
1546 UpnpAddToAction(&action_container_ptr,
1547 ACTION_ADD_PORT_MAPPING,
1548 igd->getServiceType().c_str(),
1549 "NewInternalPort",
1550 mapping.getInternalPortStr().c_str());
1551 UpnpAddToAction(&action_container_ptr,
1552 ACTION_ADD_PORT_MAPPING,
1553 igd->getServiceType().c_str(),
1554 "NewInternalClient",
1555 getHostAddress().toString().c_str());
1556 UpnpAddToAction(&action_container_ptr,
1557 ACTION_ADD_PORT_MAPPING,
1558 igd->getServiceType().c_str(),
1559 "NewEnabled",
1560 "1");
1561 UpnpAddToAction(&action_container_ptr,
1562 ACTION_ADD_PORT_MAPPING,
1563 igd->getServiceType().c_str(),
1564 "NewPortMappingDescription",
1565 mapping.toString().c_str());
1566 UpnpAddToAction(&action_container_ptr,
1567 ACTION_ADD_PORT_MAPPING,
1568 igd->getServiceType().c_str(),
1569 "NewLeaseDuration",
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001570 std::to_string(MAPPING_LEASE_DURATION).c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001571
1572 action.reset(action_container_ptr);
1573
1574 int upnp_err = UpnpSendAction(ctrlptHandle_,
1575 igd->getControlURL().c_str(),
1576 igd->getServiceType().c_str(),
1577 nullptr,
1578 action.get(),
1579 &response_container_ptr);
1580 response.reset(response_container_ptr);
1581
1582 bool success = true;
1583
1584 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001585 if (logger_) {
1586 logger_->warn("PUPnP: Failed to send action {} for mapping {}. {:d}: {}",
1587 ACTION_ADD_PORT_MAPPING,
1588 mapping.toString(),
1589 upnp_err,
1590 UpnpGetErrorMessage(upnp_err));
1591 logger_->warn("PUPnP: IGD ctrlUrl {}", igd->getControlURL());
1592 logger_->warn("PUPnP: IGD service type {}", igd->getServiceType());
1593 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001594
1595 success = false;
1596 }
1597
1598 // Check if an error has occurred.
1599 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1600 if (not errorCode.empty()) {
1601 success = false;
1602 // Try to get the error description.
1603 std::string errorDescription;
1604 if (response) {
1605 errorDescription = getFirstDocItem(response.get(), "errorDescription");
1606 }
1607
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001608 if (logger_) logger_->warn("PUPnP: {:s} returned with error: {:s} {:s}",
1609 ACTION_ADD_PORT_MAPPING,
1610 errorCode,
1611 errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001612 }
1613 return success;
1614}
1615
1616bool
1617PUPnP::actionDeletePortMapping(const Mapping& mapping)
1618{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001619 if (not clientRegistered_)
1620 return false;
1621
1622 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1623 if (not igdIn)
1624 return false;
1625
1626 // The requested IGD must be present in the list of local valid IGDs.
1627 auto igd = findMatchingIgd(igdIn->getControlURL());
1628
1629 if (not igd or not igd->isValid())
1630 return false;
1631
1632 // Action and response pointers.
1633 XMLDocument action(nullptr, ixmlDocument_free);
1634 IXML_Document* action_container_ptr = nullptr;
1635 XMLDocument response(nullptr, ixmlDocument_free);
1636 IXML_Document* response_container_ptr = nullptr;
1637
1638 // Set action sequence.
1639 UpnpAddToAction(&action_container_ptr,
1640 ACTION_DELETE_PORT_MAPPING,
1641 igd->getServiceType().c_str(),
1642 "NewRemoteHost",
1643 "");
1644 UpnpAddToAction(&action_container_ptr,
1645 ACTION_DELETE_PORT_MAPPING,
1646 igd->getServiceType().c_str(),
1647 "NewExternalPort",
1648 mapping.getExternalPortStr().c_str());
1649 UpnpAddToAction(&action_container_ptr,
1650 ACTION_DELETE_PORT_MAPPING,
1651 igd->getServiceType().c_str(),
1652 "NewProtocol",
1653 mapping.getTypeStr());
1654
1655 action.reset(action_container_ptr);
1656
1657 int upnp_err = UpnpSendAction(ctrlptHandle_,
1658 igd->getControlURL().c_str(),
1659 igd->getServiceType().c_str(),
1660 nullptr,
1661 action.get(),
1662 &response_container_ptr);
1663 response.reset(response_container_ptr);
1664
1665 bool success = true;
1666
1667 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001668 if (logger_) {
1669 logger_->warn("PUPnP: Failed to send action {} for mapping from {}. {:d}: {}",
1670 ACTION_DELETE_PORT_MAPPING,
1671 mapping.toString(),
1672 upnp_err,
1673 UpnpGetErrorMessage(upnp_err));
1674 logger_->warn("PUPnP: IGD ctrlUrl {}", igd->getControlURL());
1675 logger_->warn("PUPnP: IGD service type {}", igd->getServiceType());
1676 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001677 success = false;
1678 }
1679
1680 if (not response) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001681 if (logger_) logger_->warn("PUPnP: Failed to get response for {}", ACTION_DELETE_PORT_MAPPING);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001682 success = false;
1683 }
1684
1685 // Check if there is an error code.
1686 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1687 if (not errorCode.empty()) {
1688 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001689 if (logger_) logger_->warn("PUPnP: {:s} returned with error: {:s}: {:s}",
1690 ACTION_DELETE_PORT_MAPPING,
1691 errorCode,
1692 errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001693 success = false;
1694 }
1695
1696 return success;
1697}
1698
1699} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -04001700} // namespace dhtnet