blob: 205b19f3a03e21c48f963bf9511b36bed5547844 [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
20#include <opendht/thread_pool.h>
21#include <opendht/http.h>
22
Adrien Béraud1ae60aa2023-07-07 09:55:09 -040023namespace dhtnet {
Adrien Béraud612b55b2023-05-29 10:42:04 -040024namespace upnp {
25
26// Action identifiers.
27constexpr static const char* ACTION_ADD_PORT_MAPPING {"AddPortMapping"};
28constexpr static const char* ACTION_DELETE_PORT_MAPPING {"DeletePortMapping"};
29constexpr static const char* ACTION_GET_GENERIC_PORT_MAPPING_ENTRY {"GetGenericPortMappingEntry"};
30constexpr static const char* ACTION_GET_STATUS_INFO {"GetStatusInfo"};
31constexpr static const char* ACTION_GET_EXTERNAL_IP_ADDRESS {"GetExternalIPAddress"};
32
33// Error codes returned by router when trying to remove ports.
34constexpr static int ARRAY_IDX_INVALID = 713;
35constexpr static int CONFLICT_IN_MAPPING = 718;
36
37// Max number of IGD search attempts before failure.
38constexpr static unsigned int PUPNP_MAX_RESTART_SEARCH_RETRIES {3};
39// IGD search timeout (in seconds).
40constexpr static unsigned int SEARCH_TIMEOUT {60};
41// Base unit for the timeout between two successive IGD search.
42constexpr static auto PUPNP_SEARCH_RETRY_UNIT {std::chrono::seconds(10)};
43
44// Helper functions for xml parsing.
45static std::string_view
46getElementText(IXML_Node* node)
47{
48 if (node) {
49 if (IXML_Node* textNode = ixmlNode_getFirstChild(node))
50 if (const char* value = ixmlNode_getNodeValue(textNode))
51 return std::string_view(value);
52 }
53 return {};
54}
55
56static std::string_view
57getFirstDocItem(IXML_Document* doc, const char* item)
58{
59 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
60 nodeList(ixmlDocument_getElementsByTagName(doc, item), ixmlNodeList_free);
61 if (nodeList) {
62 // If there are several nodes which match the tag, we only want the first one.
63 return getElementText(ixmlNodeList_item(nodeList.get(), 0));
64 }
65 return {};
66}
67
68static std::string_view
69getFirstElementItem(IXML_Element* element, const char* item)
70{
71 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
72 nodeList(ixmlElement_getElementsByTagName(element, item), ixmlNodeList_free);
73 if (nodeList) {
74 // If there are several nodes which match the tag, we only want the first one.
75 return getElementText(ixmlNodeList_item(nodeList.get(), 0));
76 }
77 return {};
78}
79
80static bool
81errorOnResponse(IXML_Document* doc)
82{
83 if (not doc)
84 return true;
85
86 auto errorCode = getFirstDocItem(doc, "errorCode");
87 if (not errorCode.empty()) {
88 auto errorDescription = getFirstDocItem(doc, "errorDescription");
Adrien Béraud4f7e8012023-08-16 15:28:18 -040089 // if (logger_) logger_->warn("PUPnP: Response contains error: {:s}: {:s}",
Morteza Namvar5f639522023-07-04 17:08:58 -040090 // errorCode,
91 // errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -040092 return true;
93 }
94 return false;
95}
96
97// UPNP class implementation
98
Adrien Béraud370257c2023-08-15 20:53:09 -040099PUPnP::PUPnP(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger)
100 : UPnPProtocol(logger), ioContext(ctx), searchForIgdTimer_(*ctx)
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_);
114
115 int upnp_err = UpnpInit2(nullptr, 0);
116
117 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400118 if (logger_) logger_->error("PUPnP: Can't initialize libupnp: {}", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400119 UpnpFinish();
120 initialized_ = false;
121 return;
122 }
123
124 // Disable embedded WebServer if any.
125 if (UpnpIsWebserverEnabled() == 1) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400126 if (logger_) logger_->warn("PUPnP: Web-server is enabled. Disabling");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400127 UpnpEnableWebserver(0);
128 if (UpnpIsWebserverEnabled() == 1) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400129 if (logger_) logger_->error("PUPnP: Could not disable Web-server!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400130 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400131 if (logger_) logger_->debug("PUPnP: Web-server successfully disabled");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400132 }
133 }
134
135 char* ip_address = UpnpGetServerIpAddress();
136 char* ip_address6 = nullptr;
137 unsigned short port = UpnpGetServerPort();
138 unsigned short port6 = 0;
139#if UPNP_ENABLE_IPV6
140 ip_address6 = UpnpGetServerIp6Address();
141 port6 = UpnpGetServerPort6();
142#endif
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400143 if (logger_) {
144 if (ip_address6 and port6)
145 logger_->debug("PUPnP: Initialized on {}:{:d} | {}:{:d}", ip_address, port, ip_address6, port6);
146 else
147 logger_->debug("PUPnP: Initialized on {}:{:d}", ip_address, port);
148 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400149
150 // Relax the parser to allow malformed XML text.
151 ixmlRelaxParser(1);
152
153 initialized_ = true;
154}
155
156bool
157PUPnP::isRunning() const
158{
159 std::unique_lock<std::mutex> lk(pupnpMutex_);
160 return not shutdownComplete_;
161}
162
163void
164PUPnP::registerClient()
165{
166 assert(not clientRegistered_);
167
Adrien Béraud612b55b2023-05-29 10:42:04 -0400168 // Register Upnp control point.
169 int upnp_err = UpnpRegisterClient(ctrlPtCallback, this, &ctrlptHandle_);
170 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400171 if (logger_) logger_->error("PUPnP: Can't register client: {}", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400172 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400173 if (logger_) logger_->debug("PUPnP: Successfully registered client");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400174 clientRegistered_ = true;
175 }
176}
177
178void
179PUPnP::setObserver(UpnpMappingObserver* obs)
180{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400181 observer_ = obs;
182}
183
184const IpAddr
185PUPnP::getHostAddress() const
186{
187 std::lock_guard<std::mutex> lock(pupnpMutex_);
188 return hostAddress_;
189}
190
191void
192PUPnP::terminate(std::condition_variable& cv)
193{
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400194 if (logger_) logger_->debug("PUPnP: Terminate instance {}", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400195
196 clientRegistered_ = false;
197 observer_ = nullptr;
198
199 UpnpUnRegisterClient(ctrlptHandle_);
200
201 if (initialized_) {
202 if (UpnpFinish() != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400203 if (logger_) logger_->error("PUPnP: Failed to properly close lib-upnp");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400204 }
205
206 initialized_ = false;
207 }
208
209 // Clear all the lists.
210 discoveredIgdList_.clear();
211
212 {
213 std::lock_guard<std::mutex> lock(pupnpMutex_);
214 validIgdList_.clear();
215 shutdownComplete_ = true;
216 cv.notify_one();
217 }
218}
219
220void
221PUPnP::terminate()
222{
223 std::unique_lock<std::mutex> lk(pupnpMutex_);
224 std::condition_variable cv {};
225
Adrien Béraud370257c2023-08-15 20:53:09 -0400226 ioContext->dispatch([&] {
227 terminate(cv);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400228 });
229
230 if (cv.wait_for(lk, std::chrono::seconds(10), [this] { return shutdownComplete_; })) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400231 if (logger_) logger_->debug("PUPnP: Shutdown completed");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400232 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400233 if (logger_) logger_->error("PUPnP: Shutdown timed-out");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400234 // Force stop if the shutdown take too much time.
235 shutdownComplete_ = true;
236 }
237}
238
239void
240PUPnP::searchForDevices()
241{
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400242 if (logger_) logger_->debug("PUPnP: Send IGD search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400243
244 // Send out search for multiple types of devices, as some routers may possibly
245 // only reply to one.
246
247 auto err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_ROOT_DEVICE, this);
248 if (err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400249 if (logger_) logger_->warn("PUPnP: Send search for UPNP_ROOT_DEVICE failed. Error {:d}: {}",
250 err,
251 UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400252 }
253
254 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_IGD_DEVICE, this);
255 if (err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400256 if (logger_) logger_->warn("PUPnP: Send search for UPNP_IGD_DEVICE failed. Error {:d}: {}",
257 err,
258 UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400259 }
260
261 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANIP_SERVICE, this);
262 if (err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400263 if (logger_) logger_->warn("PUPnP: Send search for UPNP_WANIP_SERVICE failed. Error {:d}: {}",
264 err,
265 UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400266 }
267
268 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANPPP_SERVICE, this);
269 if (err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400270 if (logger_) logger_->warn("PUPnP: Send search for UPNP_WANPPP_SERVICE failed. Error {:d}: {}",
271 err,
272 UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400273 }
274}
275
276void
277PUPnP::clearIgds()
278{
Morteza Namvar5f639522023-07-04 17:08:58 -0400279 // JAMI_DBG("PUPnP: clearing IGDs and devices lists");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400280
Adrien Béraud370257c2023-08-15 20:53:09 -0400281 searchForIgdTimer_.cancel();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400282
283 igdSearchCounter_ = 0;
284
285 {
286 std::lock_guard<std::mutex> lock(pupnpMutex_);
287 for (auto const& igd : validIgdList_) {
288 igd->setValid(false);
289 }
290 validIgdList_.clear();
291 hostAddress_ = {};
292 }
293
294 discoveredIgdList_.clear();
295}
296
297void
298PUPnP::searchForIgd()
299{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400300 // Update local address before searching.
301 updateHostAddress();
302
303 if (isReady()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400304 if (logger_) logger_->debug("PUPnP: Already have a valid IGD. Skip the search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400305 return;
306 }
307
308 if (igdSearchCounter_++ >= PUPNP_MAX_RESTART_SEARCH_RETRIES) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400309 if (logger_) logger_->warn("PUPnP: Setup failed after {:d} trials. PUPnP will be disabled!",
310 PUPNP_MAX_RESTART_SEARCH_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400311 return;
312 }
313
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400314 if (logger_) logger_->debug("PUPnP: Start search for IGD: attempt {:d}", igdSearchCounter_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400315
316 // Do not init if the host is not valid. Otherwise, the init will fail
317 // anyway and may put libupnp in an unstable state (mainly deadlocks)
318 // even if the UpnpFinish() method is called.
319 if (not hasValidHostAddress()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400320 if (logger_) logger_->warn("PUPnP: Host address is invalid. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400321 } else {
322 // Init and register if needed
323 if (not initialized_) {
324 initUpnpLib();
325 }
326 if (initialized_ and not clientRegistered_) {
327 registerClient();
328 }
329 // Start searching
330 if (clientRegistered_) {
331 assert(initialized_);
332 searchForDevices();
333 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400334 if (logger_) logger_->warn("PUPnP: PUPNP not fully setup. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400335 }
336 }
337
338 // Cancel the current timer (if any) and re-schedule.
339 // The connectivity change may be received while the the local
340 // interface is not fully setup. The rescheduling typically
341 // usefull to mitigate this race.
Adrien Béraud370257c2023-08-15 20:53:09 -0400342 searchForIgdTimer_.expires_after(PUPNP_SEARCH_RETRY_UNIT * igdSearchCounter_);
343 searchForIgdTimer_.async_wait([w = weak()] (const asio::error_code& ec) {
344 if (not ec) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400345 if (auto upnpThis = w.lock())
346 upnpThis->searchForIgd();
Adrien Béraud370257c2023-08-15 20:53:09 -0400347 }
348 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400349}
350
351std::list<std::shared_ptr<IGD>>
352PUPnP::getIgdList() const
353{
354 std::lock_guard<std::mutex> lock(pupnpMutex_);
355 std::list<std::shared_ptr<IGD>> igdList;
356 for (auto& it : validIgdList_) {
357 // Return only active IGDs.
358 if (it->isValid()) {
359 igdList.emplace_back(it);
360 }
361 }
362 return igdList;
363}
364
365bool
366PUPnP::isReady() const
367{
368 // Must at least have a valid local address.
369 if (not getHostAddress() or getHostAddress().isLoopback())
370 return false;
371
372 return hasValidIgd();
373}
374
375bool
376PUPnP::hasValidIgd() const
377{
378 std::lock_guard<std::mutex> lock(pupnpMutex_);
379 for (auto& it : validIgdList_) {
380 if (it->isValid()) {
381 return true;
382 }
383 }
384 return false;
385}
386
387void
388PUPnP::updateHostAddress()
389{
390 std::lock_guard<std::mutex> lock(pupnpMutex_);
391 hostAddress_ = ip_utils::getLocalAddr(AF_INET);
392}
393
394bool
395PUPnP::hasValidHostAddress()
396{
397 std::lock_guard<std::mutex> lock(pupnpMutex_);
398 return hostAddress_ and not hostAddress_.isLoopback();
399}
400
401void
402PUPnP::incrementErrorsCounter(const std::shared_ptr<IGD>& igd)
403{
404 if (not igd or not igd->isValid())
405 return;
406 if (not igd->incrementErrorsCounter()) {
407 // Disable this IGD.
408 igd->setValid(false);
409 // Notify the listener.
410 if (observer_)
411 observer_->onIgdUpdated(igd, UpnpIgdEvent::INVALID_STATE);
412 }
413}
414
415bool
416PUPnP::validateIgd(const std::string& location, IXML_Document* doc_container_ptr)
417{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400418 assert(doc_container_ptr != nullptr);
419
420 XMLDocument document(doc_container_ptr, ixmlDocument_free);
421 auto descDoc = document.get();
422 // Check device type.
423 auto deviceType = getFirstDocItem(descDoc, "deviceType");
424 if (deviceType != UPNP_IGD_DEVICE) {
425 // Device type not IGD.
426 return false;
427 }
428
429 std::shared_ptr<UPnPIGD> igd_candidate = parseIgd(descDoc, location);
430 if (not igd_candidate) {
431 // No valid IGD candidate.
432 return false;
433 }
434
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400435 if (logger_) logger_->debug("PUPnP: Validating the IGD candidate [UDN: {}]\n"
436 " Name : {}\n"
437 " Service Type : {}\n"
438 " Service ID : {}\n"
439 " Base URL : {}\n"
440 " Location URL : {}\n"
441 " control URL : {}\n"
442 " Event URL : {}",
443 igd_candidate->getUID(),
444 igd_candidate->getFriendlyName(),
445 igd_candidate->getServiceType(),
446 igd_candidate->getServiceId(),
447 igd_candidate->getBaseURL(),
448 igd_candidate->getLocationURL(),
449 igd_candidate->getControlURL(),
450 igd_candidate->getEventSubURL());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400451
452 // Check if IGD is connected.
453 if (not actionIsIgdConnected(*igd_candidate)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400454 if (logger_) logger_->warn("PUPnP: IGD candidate {} is not connected", igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400455 return false;
456 }
457
458 // Validate external Ip.
459 igd_candidate->setPublicIp(actionGetExternalIP(*igd_candidate));
460 if (igd_candidate->getPublicIp().toString().empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400461 if (logger_) logger_->warn("PUPnP: IGD candidate {} has no valid external Ip",
462 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400463 return false;
464 }
465
466 // Validate internal Ip.
467 if (igd_candidate->getBaseURL().empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400468 if (logger_) logger_->warn("PUPnP: IGD candidate {} has no valid internal Ip",
469 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400470 return false;
471 }
472
473 // Typically the IGD local address should be extracted from the XML
474 // document (e.g. parsing the base URL). For simplicity, we assume
475 // that it matches the gateway as seen by the local interface.
476 if (const auto& localGw = ip_utils::getLocalGateway()) {
477 igd_candidate->setLocalIp(localGw);
478 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400479 if (logger_) logger_->warn("PUPnP: Could not set internal address for IGD candidate {}",
480 igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400481 return false;
482 }
483
484 // Store info for subscription.
485 std::string eventSub = igd_candidate->getEventSubURL();
486
487 {
488 // Add the IGD if not already present in the list.
489 std::lock_guard<std::mutex> lock(pupnpMutex_);
490 for (auto& igd : validIgdList_) {
491 // Must not be a null pointer
492 assert(igd.get() != nullptr);
493 if (*igd == *igd_candidate) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400494 if (logger_) logger_->debug("PUPnP: Device [{}] with int/ext addresses [{}:{}] is already in the list of valid IGDs",
495 igd_candidate->getUID(),
496 igd_candidate->toString(),
497 igd_candidate->getPublicIp().toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400498 return true;
499 }
500 }
501 }
502
503 // We have a valid IGD
504 igd_candidate->setValid(true);
505
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400506 if (logger_) logger_->debug("PUPnP: Added a new IGD [{}] to the list of valid IGDs",
507 igd_candidate->getUID());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400508
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400509 if (logger_) logger_->debug("PUPnP: New IGD addresses [int: {} - ext: {}]",
510 igd_candidate->toString(),
511 igd_candidate->getPublicIp().toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400512
513 // Subscribe to IGD events.
514 int upnp_err = UpnpSubscribeAsync(ctrlptHandle_,
515 eventSub.c_str(),
516 UPNP_INFINITE,
517 subEventCallback,
518 this);
519 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400520 if (logger_) logger_->warn("PUPnP: Failed to send subscribe request to {}: error %i - {}",
521 igd_candidate->getUID(),
522 upnp_err,
523 UpnpGetErrorMessage(upnp_err));
524 return false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400525 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400526 if (logger_) logger_->debug("PUPnP: Successfully subscribed to IGD {}", igd_candidate->getUID());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400527 }
528
529 {
530 // This is a new (and hopefully valid) IGD.
531 std::lock_guard<std::mutex> lock(pupnpMutex_);
532 validIgdList_.emplace_back(igd_candidate);
533 }
534
535 // Report to the listener.
Adrien Béraud370257c2023-08-15 20:53:09 -0400536 ioContext->post([w = weak(), igd_candidate] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400537 if (auto upnpThis = w.lock()) {
538 if (upnpThis->observer_)
539 upnpThis->observer_->onIgdUpdated(igd_candidate, UpnpIgdEvent::ADDED);
540 }
541 });
542
543 return true;
544}
545
546void
547PUPnP::requestMappingAdd(const Mapping& mapping)
548{
Adrien Béraud370257c2023-08-15 20:53:09 -0400549 ioContext->post([w = weak(), mapping] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400550 if (auto upnpThis = w.lock()) {
551 if (not upnpThis->isRunning())
552 return;
553 Mapping mapRes(mapping);
554 if (upnpThis->actionAddPortMapping(mapRes)) {
555 mapRes.setState(MappingState::OPEN);
556 mapRes.setInternalAddress(upnpThis->getHostAddress().toString());
557 upnpThis->processAddMapAction(mapRes);
558 } else {
559 upnpThis->incrementErrorsCounter(mapRes.getIgd());
560 mapRes.setState(MappingState::FAILED);
561 upnpThis->processRequestMappingFailure(mapRes);
562 }
563 }
564 });
565}
566
567void
568PUPnP::requestMappingRemove(const Mapping& mapping)
569{
570 // Send remove request using the matching IGD
Adrien Béraud370257c2023-08-15 20:53:09 -0400571 ioContext->dispatch([w = weak(), mapping] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400572 if (auto upnpThis = w.lock()) {
573 // Abort if we are shutting down.
574 if (not upnpThis->isRunning())
575 return;
576 if (upnpThis->actionDeletePortMapping(mapping)) {
577 upnpThis->processRemoveMapAction(mapping);
578 } else {
579 assert(mapping.getIgd());
580 // Dont need to report in case of failure.
581 upnpThis->incrementErrorsCounter(mapping.getIgd());
582 }
583 }
584 });
585}
586
587std::shared_ptr<UPnPIGD>
588PUPnP::findMatchingIgd(const std::string& ctrlURL) const
589{
590 std::lock_guard<std::mutex> lock(pupnpMutex_);
591
592 auto iter = std::find_if(validIgdList_.begin(),
593 validIgdList_.end(),
594 [&ctrlURL](const std::shared_ptr<IGD>& igd) {
595 if (auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd)) {
596 return upnpIgd->getControlURL() == ctrlURL;
597 }
598 return false;
599 });
600
601 if (iter == validIgdList_.end()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400602 if (logger_) logger_->warn("PUPnP: Did not find the IGD matching ctrl URL [{}]", ctrlURL);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400603 return {};
604 }
605
606 return std::dynamic_pointer_cast<UPnPIGD>(*iter);
607}
608
609void
610PUPnP::processAddMapAction(const Mapping& map)
611{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400612 if (observer_ == nullptr)
613 return;
614
Adrien Béraud370257c2023-08-15 20:53:09 -0400615 ioContext->post([w = weak(), map] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400616 if (auto upnpThis = w.lock()) {
617 if (upnpThis->observer_)
618 upnpThis->observer_->onMappingAdded(map.getIgd(), std::move(map));
619 }
620 });
621}
622
623void
624PUPnP::processRequestMappingFailure(const Mapping& map)
625{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400626 if (observer_ == nullptr)
627 return;
628
Adrien Béraud370257c2023-08-15 20:53:09 -0400629 ioContext->post([w = weak(), map] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400630 if (auto upnpThis = w.lock()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400631 if (upnpThis->logger_) upnpThis->logger_->warn("PUPnP: Closed mapping {}", map.toString());
Morteza Namvar5f639522023-07-04 17:08:58 -0400632 // JAMI_DBG("PUPnP: Failed to request mapping %s", map.toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400633 if (upnpThis->observer_)
634 upnpThis->observer_->onMappingRequestFailed(map);
635 }
636 });
637}
638
639void
640PUPnP::processRemoveMapAction(const Mapping& map)
641{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400642 if (observer_ == nullptr)
643 return;
644
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400645 if (logger_) logger_->warn("PUPnP: Closed mapping {}", map.toString());
Adrien Béraud370257c2023-08-15 20:53:09 -0400646 ioContext->post([map, obs = observer_] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400647 obs->onMappingRemoved(map.getIgd(), std::move(map));
648 });
649}
650
651const char*
652PUPnP::eventTypeToString(Upnp_EventType eventType)
653{
654 switch (eventType) {
655 case UPNP_CONTROL_ACTION_REQUEST:
656 return "UPNP_CONTROL_ACTION_REQUEST";
657 case UPNP_CONTROL_ACTION_COMPLETE:
658 return "UPNP_CONTROL_ACTION_COMPLETE";
659 case UPNP_CONTROL_GET_VAR_REQUEST:
660 return "UPNP_CONTROL_GET_VAR_REQUEST";
661 case UPNP_CONTROL_GET_VAR_COMPLETE:
662 return "UPNP_CONTROL_GET_VAR_COMPLETE";
663 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
664 return "UPNP_DISCOVERY_ADVERTISEMENT_ALIVE";
665 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
666 return "UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE";
667 case UPNP_DISCOVERY_SEARCH_RESULT:
668 return "UPNP_DISCOVERY_SEARCH_RESULT";
669 case UPNP_DISCOVERY_SEARCH_TIMEOUT:
670 return "UPNP_DISCOVERY_SEARCH_TIMEOUT";
671 case UPNP_EVENT_SUBSCRIPTION_REQUEST:
672 return "UPNP_EVENT_SUBSCRIPTION_REQUEST";
673 case UPNP_EVENT_RECEIVED:
674 return "UPNP_EVENT_RECEIVED";
675 case UPNP_EVENT_RENEWAL_COMPLETE:
676 return "UPNP_EVENT_RENEWAL_COMPLETE";
677 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
678 return "UPNP_EVENT_SUBSCRIBE_COMPLETE";
679 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE:
680 return "UPNP_EVENT_UNSUBSCRIBE_COMPLETE";
681 case UPNP_EVENT_AUTORENEWAL_FAILED:
682 return "UPNP_EVENT_AUTORENEWAL_FAILED";
683 case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
684 return "UPNP_EVENT_SUBSCRIPTION_EXPIRED";
685 default:
686 return "Unknown UPNP Event";
687 }
688}
689
690int
691PUPnP::ctrlPtCallback(Upnp_EventType event_type, const void* event, void* user_data)
692{
693 auto pupnp = static_cast<PUPnP*>(user_data);
694
695 if (pupnp == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400696 // JAMI_WARN("PUPnP: Control point callback without PUPnP");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400697 return UPNP_E_SUCCESS;
698 }
699
700 auto upnpThis = pupnp->weak().lock();
701
702 if (not upnpThis)
703 return UPNP_E_SUCCESS;
704
705 // Ignore if already unregistered.
706 if (not upnpThis->clientRegistered_)
707 return UPNP_E_SUCCESS;
708
709 // Process the callback.
710 return upnpThis->handleCtrlPtUPnPEvents(event_type, event);
711}
712
713PUPnP::CtrlAction
714PUPnP::getAction(const char* xmlNode)
715{
716 if (strstr(xmlNode, ACTION_ADD_PORT_MAPPING)) {
717 return CtrlAction::ADD_PORT_MAPPING;
718 } else if (strstr(xmlNode, ACTION_DELETE_PORT_MAPPING)) {
719 return CtrlAction::DELETE_PORT_MAPPING;
720 } else if (strstr(xmlNode, ACTION_GET_GENERIC_PORT_MAPPING_ENTRY)) {
721 return CtrlAction::GET_GENERIC_PORT_MAPPING_ENTRY;
722 } else if (strstr(xmlNode, ACTION_GET_STATUS_INFO)) {
723 return CtrlAction::GET_STATUS_INFO;
724 } else if (strstr(xmlNode, ACTION_GET_EXTERNAL_IP_ADDRESS)) {
725 return CtrlAction::GET_EXTERNAL_IP_ADDRESS;
726 } else {
727 return CtrlAction::UNKNOWN;
728 }
729}
730
731void
732PUPnP::processDiscoverySearchResult(const std::string& cpDeviceId,
733 const std::string& igdLocationUrl,
734 const IpAddr& dstAddr)
735{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400736 // Update host address if needed.
737 if (not hasValidHostAddress())
738 updateHostAddress();
739
740 // The host address must be valid to proceed.
741 if (not hasValidHostAddress()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400742 if (logger_) logger_->warn("PUPnP: Local address is invalid. Ignore search result for now!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400743 return;
744 }
745
746 // Use the device ID and the URL as ID. This is necessary as some
747 // IGDs may have the same device ID but different URLs.
748
749 auto igdId = cpDeviceId + " url: " + igdLocationUrl;
750
751 if (not discoveredIgdList_.emplace(igdId).second) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400752 if (logger_) logger_->warn("PUPnP: IGD [{}] already in the list", igdId);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400753 return;
754 }
755
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400756 if (logger_) logger_->debug("PUPnP: Discovered a new IGD [{}]", igdId);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400757
758 // NOTE: here, we check if the location given is related to the source address.
759 // If it's not the case, it's certainly a router plugged in the network, but not
760 // related to this network. So the given location will be unreachable and this
761 // will cause some timeout.
762
763 // Only check the IP address (ignore the port number).
764 dht::http::Url url(igdLocationUrl);
765 if (IpAddr(url.host).toString(false) != dstAddr.toString(false)) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400766 if (logger_) logger_->debug("PUPnP: Returned location {} does not match the source address {}",
767 IpAddr(url.host).toString(true, true),
768 dstAddr.toString(true, true));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400769 return;
770 }
771
772 // Run a separate thread to prevent blocking this thread
773 // if the IGD HTTP server is not responsive.
774 dht::ThreadPool::io().run([w = weak(), igdLocationUrl] {
775 if (auto upnpThis = w.lock()) {
776 upnpThis->downLoadIgdDescription(igdLocationUrl);
777 }
778 });
779}
780
781void
782PUPnP::downLoadIgdDescription(const std::string& locationUrl)
783{
784 IXML_Document* doc_container_ptr = nullptr;
785 int upnp_err = UpnpDownloadXmlDoc(locationUrl.c_str(), &doc_container_ptr);
786
787 if (upnp_err != UPNP_E_SUCCESS or not doc_container_ptr) {
Adrien Béraud370257c2023-08-15 20:53:09 -0400788 if(logger_) logger_->warn("PUPnP: Error downloading device XML document from {} -> {}",
789 locationUrl,
790 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400791 } else {
Adrien Béraud370257c2023-08-15 20:53:09 -0400792 if(logger_) logger_->debug("PUPnP: Succeeded to download device XML document from {}", locationUrl);
793 ioContext->post([w = weak(), url = locationUrl, doc_container_ptr] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400794 if (auto upnpThis = w.lock()) {
795 upnpThis->validateIgd(url, doc_container_ptr);
796 }
797 });
798 }
799}
800
801void
802PUPnP::processDiscoveryAdvertisementByebye(const std::string& cpDeviceId)
803{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400804 discoveredIgdList_.erase(cpDeviceId);
805
806 std::shared_ptr<IGD> igd;
807 {
808 std::lock_guard<std::mutex> lk(pupnpMutex_);
809 for (auto it = validIgdList_.begin(); it != validIgdList_.end();) {
810 if ((*it)->getUID() == cpDeviceId) {
811 igd = *it;
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400812 if (logger_) logger_->debug("PUPnP: Received [{}] for IGD [{}] {}. Will be removed.",
813 PUPnP::eventTypeToString(UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE),
814 igd->getUID(),
815 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400816 igd->setValid(false);
817 // Remove the IGD.
818 it = validIgdList_.erase(it);
819 break;
820 } else {
821 it++;
822 }
823 }
824 }
825
826 // Notify the listener.
827 if (observer_ and igd) {
828 observer_->onIgdUpdated(igd, UpnpIgdEvent::REMOVED);
829 }
830}
831
832void
833PUPnP::processDiscoverySubscriptionExpired(Upnp_EventType event_type, const std::string& eventSubUrl)
834{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400835 std::lock_guard<std::mutex> lk(pupnpMutex_);
836 for (auto& it : validIgdList_) {
837 if (auto igd = std::dynamic_pointer_cast<UPnPIGD>(it)) {
838 if (igd->getEventSubURL() == eventSubUrl) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400839 if (logger_) logger_->debug("PUPnP: Received [{}] event for IGD [{}] {}. Request a new subscribe.",
840 PUPnP::eventTypeToString(event_type),
841 igd->getUID(),
842 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400843 UpnpSubscribeAsync(ctrlptHandle_,
844 eventSubUrl.c_str(),
845 UPNP_INFINITE,
846 subEventCallback,
847 this);
848 break;
849 }
850 }
851 }
852}
853
854int
855PUPnP::handleCtrlPtUPnPEvents(Upnp_EventType event_type, const void* event)
856{
857 switch (event_type) {
858 // "ALIVE" events are processed as "SEARCH RESULT". It might be usefull
859 // if "SEARCH RESULT" was missed.
860 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
861 case UPNP_DISCOVERY_SEARCH_RESULT: {
862 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
863
864 // First check the error code.
865 auto upnp_status = UpnpDiscovery_get_ErrCode(d_event);
866 if (upnp_status != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400867 if (logger_) logger_->error("PUPnP: UPNP discovery is in erroneous state: %s",
868 UpnpGetErrorMessage(upnp_status));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400869 break;
870 }
871
872 // Parse the event's data.
873 std::string deviceId {UpnpDiscovery_get_DeviceID_cstr(d_event)};
874 std::string location {UpnpDiscovery_get_Location_cstr(d_event)};
875 IpAddr dstAddr(*(const pj_sockaddr*) (UpnpDiscovery_get_DestAddr(d_event)));
Adrien Béraud370257c2023-08-15 20:53:09 -0400876 ioContext->post([w = weak(),
Adrien Béraud612b55b2023-05-29 10:42:04 -0400877 deviceId = std::move(deviceId),
878 location = std::move(location),
879 dstAddr = std::move(dstAddr)] {
880 if (auto upnpThis = w.lock()) {
881 upnpThis->processDiscoverySearchResult(deviceId, location, dstAddr);
882 }
883 });
884 break;
885 }
886 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: {
887 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
888
889 std::string deviceId(UpnpDiscovery_get_DeviceID_cstr(d_event));
890
891 // Process the response on the main thread.
Adrien Béraud370257c2023-08-15 20:53:09 -0400892 ioContext->post([w = weak(), deviceId = std::move(deviceId)] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400893 if (auto upnpThis = w.lock()) {
894 upnpThis->processDiscoveryAdvertisementByebye(deviceId);
895 }
896 });
897 break;
898 }
899 case UPNP_DISCOVERY_SEARCH_TIMEOUT: {
900 // Even if the discovery search is successful, it's normal to receive
901 // time-out events. This because we send search requests using various
902 // device types, which some of them may not return a response.
903 break;
904 }
905 case UPNP_EVENT_RECEIVED: {
906 // Nothing to do.
907 break;
908 }
909 // Treat failed autorenewal like an expired subscription.
910 case UPNP_EVENT_AUTORENEWAL_FAILED:
911 case UPNP_EVENT_SUBSCRIPTION_EXPIRED: // This event will occur only if autorenewal is disabled.
912 {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400913 if (logger_) logger_->warn("PUPnP: Received Subscription Event {}", eventTypeToString(event_type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400914 const UpnpEventSubscribe* es_event = (const UpnpEventSubscribe*) event;
915 if (es_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400916 if (logger_) logger_->warn("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400917 break;
918 }
919 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
920
921 // Process the response on the main thread.
Adrien Béraud370257c2023-08-15 20:53:09 -0400922 ioContext->post([w = weak(), event_type, publisherUrl = std::move(publisherUrl)] {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400923 if (auto upnpThis = w.lock()) {
924 upnpThis->processDiscoverySubscriptionExpired(event_type, publisherUrl);
925 }
926 });
927 break;
928 }
929 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
930 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE: {
931 UpnpEventSubscribe* es_event = (UpnpEventSubscribe*) event;
932 if (es_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400933 if (logger_) logger_->warn("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400934 } else {
935 UpnpEventSubscribe_delete(es_event);
936 }
937 break;
938 }
939 case UPNP_CONTROL_ACTION_COMPLETE: {
940 const UpnpActionComplete* a_event = (const UpnpActionComplete*) event;
941 if (a_event == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400942 if (logger_) logger_->warn("PUPnP: Received Action Complete Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400943 break;
944 }
945 auto res = UpnpActionComplete_get_ErrCode(a_event);
946 if (res != UPNP_E_SUCCESS and res != UPNP_E_TIMEDOUT) {
947 auto err = UpnpActionComplete_get_ErrCode(a_event);
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400948 if (logger_) logger_->warn("PUPnP: Received Action Complete error %i %s", err, UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400949 } else {
950 auto actionRequest = UpnpActionComplete_get_ActionRequest(a_event);
951 // Abort if there is no action to process.
952 if (actionRequest == nullptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400953 if (logger_) logger_->warn("PUPnP: Can't get the Action Request data from the event");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400954 break;
955 }
956
957 auto actionResult = UpnpActionComplete_get_ActionResult(a_event);
958 if (actionResult != nullptr) {
959 ixmlDocument_free(actionResult);
960 } else {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400961 if (logger_) logger_->warn("PUPnP: Action Result document not found");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400962 }
963 }
964 break;
965 }
966 default: {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400967 if (logger_) logger_->warn("PUPnP: Unhandled Control Point event");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400968 break;
969 }
970 }
971
972 return UPNP_E_SUCCESS;
973}
974
975int
976PUPnP::subEventCallback(Upnp_EventType event_type, const void* event, void* user_data)
977{
978 if (auto pupnp = static_cast<PUPnP*>(user_data))
979 return pupnp->handleSubscriptionUPnPEvent(event_type, event);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400980 return 0;
981}
982
983int
984PUPnP::handleSubscriptionUPnPEvent(Upnp_EventType, const void* event)
985{
986 UpnpEventSubscribe* es_event = static_cast<UpnpEventSubscribe*>(const_cast<void*>(event));
987
988 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400989 // JAMI_ERR("PUPnP: Unexpected null pointer!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400990 return UPNP_E_INVALID_ARGUMENT;
991 }
992 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
993 int upnp_err = UpnpEventSubscribe_get_ErrCode(es_event);
994 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -0400995 if (logger_) logger_->warn("PUPnP: Subscription error {} from {}",
996 UpnpGetErrorMessage(upnp_err),
997 publisherUrl);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400998 return upnp_err;
999 }
1000
1001 return UPNP_E_SUCCESS;
1002}
1003
1004std::unique_ptr<UPnPIGD>
1005PUPnP::parseIgd(IXML_Document* doc, std::string locationUrl)
1006{
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001007 if (not(doc and !locationUrl.empty()))
Adrien Béraud612b55b2023-05-29 10:42:04 -04001008 return nullptr;
1009
1010 // Check the UDN to see if its already in our device list.
1011 std::string UDN(getFirstDocItem(doc, "UDN"));
1012 if (UDN.empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001013 if (logger_) logger_->warn("PUPnP: could not find UDN in description document of device");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001014 return nullptr;
1015 } else {
1016 std::lock_guard<std::mutex> lk(pupnpMutex_);
1017 for (auto& it : validIgdList_) {
1018 if (it->getUID() == UDN) {
1019 // We already have this device in our list.
1020 return nullptr;
1021 }
1022 }
1023 }
1024
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001025 if (logger_) logger_->debug("PUPnP: Found new device [{}]", UDN);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001026
1027 std::unique_ptr<UPnPIGD> new_igd;
1028 int upnp_err;
1029
1030 // Get friendly name.
1031 std::string friendlyName(getFirstDocItem(doc, "friendlyName"));
1032
1033 // Get base URL.
1034 std::string baseURL(getFirstDocItem(doc, "URLBase"));
1035 if (baseURL.empty())
1036 baseURL = locationUrl;
1037
1038 // Get list of services defined by serviceType.
1039 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> serviceList(nullptr,
1040 ixmlNodeList_free);
1041 serviceList.reset(ixmlDocument_getElementsByTagName(doc, "serviceType"));
1042 unsigned long list_length = ixmlNodeList_length(serviceList.get());
1043
1044 // Go through the "serviceType" nodes until we find the the correct service type.
1045 for (unsigned long node_idx = 0; node_idx < list_length; node_idx++) {
1046 IXML_Node* serviceType_node = ixmlNodeList_item(serviceList.get(), node_idx);
1047 std::string serviceType(getElementText(serviceType_node));
1048
1049 // Only check serviceType of WANIPConnection or WANPPPConnection.
1050 if (serviceType != UPNP_WANIP_SERVICE
1051 && serviceType != UPNP_WANPPP_SERVICE) {
1052 // IGD is not WANIP or WANPPP service. Going to next node.
1053 continue;
1054 }
1055
1056 // Get parent node.
1057 IXML_Node* service_node = ixmlNode_getParentNode(serviceType_node);
1058 if (not service_node) {
1059 // IGD serviceType has no parent node. Going to next node.
1060 continue;
1061 }
1062
1063 // Perform sanity check. The parent node should be called "service".
1064 if (strcmp(ixmlNode_getNodeName(service_node), "service") != 0) {
1065 // IGD "serviceType" parent node is not called "service". Going to next node.
1066 continue;
1067 }
1068
1069 // Get serviceId.
1070 IXML_Element* service_element = (IXML_Element*) service_node;
1071 std::string serviceId(getFirstElementItem(service_element, "serviceId"));
1072 if (serviceId.empty()) {
1073 // IGD "serviceId" is empty. Going to next node.
1074 continue;
1075 }
1076
1077 // Get the relative controlURL and turn it into absolute address using the URLBase.
1078 std::string controlURL(getFirstElementItem(service_element, "controlURL"));
1079 if (controlURL.empty()) {
1080 // IGD control URL is empty. Going to next node.
1081 continue;
1082 }
1083
1084 char* absolute_control_url = nullptr;
1085 upnp_err = UpnpResolveURL2(baseURL.c_str(), controlURL.c_str(), &absolute_control_url);
1086 if (upnp_err == UPNP_E_SUCCESS)
1087 controlURL = absolute_control_url;
1088 else
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001089 if (logger_) logger_->warn("PUPnP: Error resolving absolute controlURL -> {}",
1090 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001091
1092 std::free(absolute_control_url);
1093
1094 // Get the relative eventSubURL and turn it into absolute address using the URLBase.
1095 std::string eventSubURL(getFirstElementItem(service_element, "eventSubURL"));
1096 if (eventSubURL.empty()) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001097 if (logger_) logger_->warn("PUPnP: IGD event sub URL is empty. Going to next node");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001098 continue;
1099 }
1100
1101 char* absolute_event_sub_url = nullptr;
1102 upnp_err = UpnpResolveURL2(baseURL.c_str(), eventSubURL.c_str(), &absolute_event_sub_url);
1103 if (upnp_err == UPNP_E_SUCCESS)
1104 eventSubURL = absolute_event_sub_url;
1105 else
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001106 if (logger_) logger_->warn("PUPnP: Error resolving absolute eventSubURL -> {}",
1107 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001108
1109 std::free(absolute_event_sub_url);
1110
1111 new_igd.reset(new UPnPIGD(std::move(UDN),
1112 std::move(baseURL),
1113 std::move(friendlyName),
1114 std::move(serviceType),
1115 std::move(serviceId),
1116 std::move(locationUrl),
1117 std::move(controlURL),
1118 std::move(eventSubURL)));
1119
1120 return new_igd;
1121 }
1122
1123 return nullptr;
1124}
1125
1126bool
1127PUPnP::actionIsIgdConnected(const UPnPIGD& igd)
1128{
1129 if (not clientRegistered_)
1130 return false;
1131
1132 // Set action name.
1133 IXML_Document* action_container_ptr = UpnpMakeAction("GetStatusInfo",
1134 igd.getServiceType().c_str(),
1135 0,
1136 nullptr);
1137 if (not action_container_ptr) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001138 if (logger_) logger_->warn("PUPnP: Failed to make GetStatusInfo action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001139 return false;
1140 }
1141 XMLDocument action(action_container_ptr, ixmlDocument_free); // Action pointer.
1142
1143 IXML_Document* response_container_ptr = nullptr;
1144 int upnp_err = UpnpSendAction(ctrlptHandle_,
1145 igd.getControlURL().c_str(),
1146 igd.getServiceType().c_str(),
1147 nullptr,
1148 action.get(),
1149 &response_container_ptr);
1150 if (not response_container_ptr or upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001151 if (logger_) logger_->warn("PUPnP: Failed to send GetStatusInfo action -> {}", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001152 return false;
1153 }
1154 XMLDocument response(response_container_ptr, ixmlDocument_free);
1155
1156 if (errorOnResponse(response.get())) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001157 if (logger_) logger_->warn("PUPnP: Failed to get GetStatusInfo from {} -> {:d}: {}",
1158 igd.getServiceType().c_str(),
1159 upnp_err,
1160 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001161 return false;
1162 }
1163
1164 // Parse response.
1165 auto status = getFirstDocItem(response.get(), "NewConnectionStatus");
1166 return status == "Connected";
1167}
1168
1169IpAddr
1170PUPnP::actionGetExternalIP(const UPnPIGD& igd)
1171{
1172 if (not clientRegistered_)
1173 return {};
1174
1175 // Action and response pointers.
1176 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1177 action(nullptr, ixmlDocument_free); // Action pointer.
1178 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1179 response(nullptr, ixmlDocument_free); // Response pointer.
1180
1181 // Set action name.
1182 static constexpr const char* action_name {"GetExternalIPAddress"};
1183
1184 IXML_Document* action_container_ptr = nullptr;
1185 action_container_ptr = UpnpMakeAction(action_name, igd.getServiceType().c_str(), 0, nullptr);
1186 action.reset(action_container_ptr);
1187
1188 if (not action) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001189 if (logger_) logger_->warn("PUPnP: Failed to make GetExternalIPAddress action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001190 return {};
1191 }
1192
1193 IXML_Document* response_container_ptr = nullptr;
1194 int upnp_err = UpnpSendAction(ctrlptHandle_,
1195 igd.getControlURL().c_str(),
1196 igd.getServiceType().c_str(),
1197 nullptr,
1198 action.get(),
1199 &response_container_ptr);
1200 response.reset(response_container_ptr);
1201
1202 if (not response or upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001203 if (logger_) logger_->warn("PUPnP: Failed to send GetExternalIPAddress action -> {}",
1204 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001205 return {};
1206 }
1207
1208 if (errorOnResponse(response.get())) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001209 if (logger_) logger_->warn("PUPnP: Failed to get GetExternalIPAddress from {} -> {:d}: {}",
1210 igd.getServiceType(),
1211 upnp_err,
1212 UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001213 return {};
1214 }
1215
1216 return {getFirstDocItem(response.get(), "NewExternalIPAddress")};
1217}
1218
1219std::map<Mapping::key_t, Mapping>
1220PUPnP::getMappingsListByDescr(const std::shared_ptr<IGD>& igd, const std::string& description) const
1221{
1222 auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd);
1223 assert(upnpIgd);
1224
1225 std::map<Mapping::key_t, Mapping> mapList;
1226
1227 if (not clientRegistered_ or not upnpIgd->isValid() or not upnpIgd->getLocalIp())
1228 return mapList;
1229
1230 // Set action name.
1231 static constexpr const char* action_name {"GetGenericPortMappingEntry"};
1232
1233 for (int entry_idx = 0;; entry_idx++) {
1234 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1235 action(nullptr, ixmlDocument_free); // Action pointer.
1236 IXML_Document* action_container_ptr = nullptr;
1237
1238 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1239 response(nullptr, ixmlDocument_free); // Response pointer.
1240 IXML_Document* response_container_ptr = nullptr;
1241
1242 UpnpAddToAction(&action_container_ptr,
1243 action_name,
1244 upnpIgd->getServiceType().c_str(),
1245 "NewPortMappingIndex",
1246 std::to_string(entry_idx).c_str());
1247 action.reset(action_container_ptr);
1248
1249 if (not action) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001250 // JAMI_WARN("PUPnP: Failed to add NewPortMappingIndex action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001251 break;
1252 }
1253
1254 int upnp_err = UpnpSendAction(ctrlptHandle_,
1255 upnpIgd->getControlURL().c_str(),
1256 upnpIgd->getServiceType().c_str(),
1257 nullptr,
1258 action.get(),
1259 &response_container_ptr);
1260 response.reset(response_container_ptr);
1261
1262 if (not response) {
1263 // No existing mapping. Abort silently.
1264 break;
1265 }
1266
1267 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001268 // JAMI_ERR("PUPnP: GetGenericPortMappingEntry returned with error: %i", upnp_err);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001269 break;
1270 }
1271
1272 // Check error code.
1273 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1274 if (not errorCode.empty()) {
1275 auto error = to_int<int>(errorCode);
1276 if (error == ARRAY_IDX_INVALID or error == CONFLICT_IN_MAPPING) {
1277 // No more port mapping entries in the response.
Morteza Namvar5f639522023-07-04 17:08:58 -04001278 // JAMI_DBG("PUPnP: No more mappings (found a total of %i mappings", entry_idx);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001279 break;
1280 } else {
1281 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
Adrien Béraud370257c2023-08-15 20:53:09 -04001282 if (logger_) logger_->error("PUPnP: GetGenericPortMappingEntry returned with error: {:s}: {:s}",
Adrien Béraud612b55b2023-05-29 10:42:04 -04001283 errorCode,
1284 errorDescription);
1285 break;
1286 }
1287 }
1288
1289 // Parse the response.
1290 auto desc_actual = getFirstDocItem(response.get(), "NewPortMappingDescription");
1291 auto client_ip = getFirstDocItem(response.get(), "NewInternalClient");
1292
1293 if (client_ip != getHostAddress().toString()) {
1294 // Silently ignore un-matching addresses.
1295 continue;
1296 }
1297
1298 if (desc_actual.find(description) == std::string::npos)
1299 continue;
1300
1301 auto port_internal = getFirstDocItem(response.get(), "NewInternalPort");
1302 auto port_external = getFirstDocItem(response.get(), "NewExternalPort");
1303 std::string transport(getFirstDocItem(response.get(), "NewProtocol"));
1304
1305 if (port_internal.empty() || port_external.empty() || transport.empty()) {
Adrien Béraud370257c2023-08-15 20:53:09 -04001306 // if (logger_) logger_->e("PUPnP: GetGenericPortMappingEntry returned an invalid entry at index %i",
Morteza Namvar5f639522023-07-04 17:08:58 -04001307 // entry_idx);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001308 continue;
1309 }
1310
1311 std::transform(transport.begin(), transport.end(), transport.begin(), ::toupper);
1312 PortType type = transport.find("TCP") != std::string::npos ? PortType::TCP : PortType::UDP;
1313 auto ePort = to_int<uint16_t>(port_external);
1314 auto iPort = to_int<uint16_t>(port_internal);
1315
1316 Mapping map(type, ePort, iPort);
1317 map.setIgd(igd);
1318
1319 mapList.emplace(map.getMapKey(), std::move(map));
1320 }
1321
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001322 if (logger_) logger_->debug("PUPnP: Found {:d} allocated mappings on IGD {:s}",
1323 mapList.size(),
1324 upnpIgd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001325
1326 return mapList;
1327}
1328
1329void
1330PUPnP::deleteMappingsByDescription(const std::shared_ptr<IGD>& igd, const std::string& description)
1331{
1332 if (not(clientRegistered_ and igd->getLocalIp()))
1333 return;
1334
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001335 if (logger_) logger_->debug("PUPnP: Remove all mappings (if any) on IGD {} matching descr prefix {}",
1336 igd->toString(),
1337 Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001338
Adrien Béraud370257c2023-08-15 20:53:09 -04001339 ioContext->post([w=weak(), igd, description]{
1340 if (auto sthis = w.lock()) {
1341 auto mapList = sthis->getMappingsListByDescr(igd, description);
1342 for (auto const& [_, map] : mapList) {
1343 sthis->requestMappingRemove(map);
1344 }
1345 }
1346 });
Adrien Béraud612b55b2023-05-29 10:42:04 -04001347}
1348
1349bool
1350PUPnP::actionAddPortMapping(const Mapping& mapping)
1351{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001352 if (not clientRegistered_)
1353 return false;
1354
1355 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1356 if (not igdIn)
1357 return false;
1358
1359 // The requested IGD must be present in the list of local valid IGDs.
1360 auto igd = findMatchingIgd(igdIn->getControlURL());
1361
1362 if (not igd or not igd->isValid())
1363 return false;
1364
1365 // Action and response pointers.
1366 XMLDocument action(nullptr, ixmlDocument_free);
1367 IXML_Document* action_container_ptr = nullptr;
1368 XMLDocument response(nullptr, ixmlDocument_free);
1369 IXML_Document* response_container_ptr = nullptr;
1370
1371 // Set action sequence.
1372 UpnpAddToAction(&action_container_ptr,
1373 ACTION_ADD_PORT_MAPPING,
1374 igd->getServiceType().c_str(),
1375 "NewRemoteHost",
1376 "");
1377 UpnpAddToAction(&action_container_ptr,
1378 ACTION_ADD_PORT_MAPPING,
1379 igd->getServiceType().c_str(),
1380 "NewExternalPort",
1381 mapping.getExternalPortStr().c_str());
1382 UpnpAddToAction(&action_container_ptr,
1383 ACTION_ADD_PORT_MAPPING,
1384 igd->getServiceType().c_str(),
1385 "NewProtocol",
1386 mapping.getTypeStr());
1387 UpnpAddToAction(&action_container_ptr,
1388 ACTION_ADD_PORT_MAPPING,
1389 igd->getServiceType().c_str(),
1390 "NewInternalPort",
1391 mapping.getInternalPortStr().c_str());
1392 UpnpAddToAction(&action_container_ptr,
1393 ACTION_ADD_PORT_MAPPING,
1394 igd->getServiceType().c_str(),
1395 "NewInternalClient",
1396 getHostAddress().toString().c_str());
1397 UpnpAddToAction(&action_container_ptr,
1398 ACTION_ADD_PORT_MAPPING,
1399 igd->getServiceType().c_str(),
1400 "NewEnabled",
1401 "1");
1402 UpnpAddToAction(&action_container_ptr,
1403 ACTION_ADD_PORT_MAPPING,
1404 igd->getServiceType().c_str(),
1405 "NewPortMappingDescription",
1406 mapping.toString().c_str());
1407 UpnpAddToAction(&action_container_ptr,
1408 ACTION_ADD_PORT_MAPPING,
1409 igd->getServiceType().c_str(),
1410 "NewLeaseDuration",
1411 "0");
1412
1413 action.reset(action_container_ptr);
1414
1415 int upnp_err = UpnpSendAction(ctrlptHandle_,
1416 igd->getControlURL().c_str(),
1417 igd->getServiceType().c_str(),
1418 nullptr,
1419 action.get(),
1420 &response_container_ptr);
1421 response.reset(response_container_ptr);
1422
1423 bool success = true;
1424
1425 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001426 if (logger_) {
1427 logger_->warn("PUPnP: Failed to send action {} for mapping {}. {:d}: {}",
1428 ACTION_ADD_PORT_MAPPING,
1429 mapping.toString(),
1430 upnp_err,
1431 UpnpGetErrorMessage(upnp_err));
1432 logger_->warn("PUPnP: IGD ctrlUrl {}", igd->getControlURL());
1433 logger_->warn("PUPnP: IGD service type {}", igd->getServiceType());
1434 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001435
1436 success = false;
1437 }
1438
1439 // Check if an error has occurred.
1440 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1441 if (not errorCode.empty()) {
1442 success = false;
1443 // Try to get the error description.
1444 std::string errorDescription;
1445 if (response) {
1446 errorDescription = getFirstDocItem(response.get(), "errorDescription");
1447 }
1448
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001449 if (logger_) logger_->warn("PUPnP: {:s} returned with error: {:s} {:s}",
1450 ACTION_ADD_PORT_MAPPING,
1451 errorCode,
1452 errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001453 }
1454 return success;
1455}
1456
1457bool
1458PUPnP::actionDeletePortMapping(const Mapping& mapping)
1459{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001460 if (not clientRegistered_)
1461 return false;
1462
1463 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1464 if (not igdIn)
1465 return false;
1466
1467 // The requested IGD must be present in the list of local valid IGDs.
1468 auto igd = findMatchingIgd(igdIn->getControlURL());
1469
1470 if (not igd or not igd->isValid())
1471 return false;
1472
1473 // Action and response pointers.
1474 XMLDocument action(nullptr, ixmlDocument_free);
1475 IXML_Document* action_container_ptr = nullptr;
1476 XMLDocument response(nullptr, ixmlDocument_free);
1477 IXML_Document* response_container_ptr = nullptr;
1478
1479 // Set action sequence.
1480 UpnpAddToAction(&action_container_ptr,
1481 ACTION_DELETE_PORT_MAPPING,
1482 igd->getServiceType().c_str(),
1483 "NewRemoteHost",
1484 "");
1485 UpnpAddToAction(&action_container_ptr,
1486 ACTION_DELETE_PORT_MAPPING,
1487 igd->getServiceType().c_str(),
1488 "NewExternalPort",
1489 mapping.getExternalPortStr().c_str());
1490 UpnpAddToAction(&action_container_ptr,
1491 ACTION_DELETE_PORT_MAPPING,
1492 igd->getServiceType().c_str(),
1493 "NewProtocol",
1494 mapping.getTypeStr());
1495
1496 action.reset(action_container_ptr);
1497
1498 int upnp_err = UpnpSendAction(ctrlptHandle_,
1499 igd->getControlURL().c_str(),
1500 igd->getServiceType().c_str(),
1501 nullptr,
1502 action.get(),
1503 &response_container_ptr);
1504 response.reset(response_container_ptr);
1505
1506 bool success = true;
1507
1508 if (upnp_err != UPNP_E_SUCCESS) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001509 if (logger_) {
1510 logger_->warn("PUPnP: Failed to send action {} for mapping from {}. {:d}: {}",
1511 ACTION_DELETE_PORT_MAPPING,
1512 mapping.toString(),
1513 upnp_err,
1514 UpnpGetErrorMessage(upnp_err));
1515 logger_->warn("PUPnP: IGD ctrlUrl {}", igd->getControlURL());
1516 logger_->warn("PUPnP: IGD service type {}", igd->getServiceType());
1517 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001518 success = false;
1519 }
1520
1521 if (not response) {
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001522 if (logger_) logger_->warn("PUPnP: Failed to get response for {}", ACTION_DELETE_PORT_MAPPING);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001523 success = false;
1524 }
1525
1526 // Check if there is an error code.
1527 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1528 if (not errorCode.empty()) {
1529 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
Adrien Béraud4f7e8012023-08-16 15:28:18 -04001530 if (logger_) logger_->warn("PUPnP: {:s} returned with error: {:s}: {:s}",
1531 ACTION_DELETE_PORT_MAPPING,
1532 errorCode,
1533 errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001534 success = false;
1535 }
1536
1537 return success;
1538}
1539
1540} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -04001541} // namespace dhtnet