blob: 6ea556332f30a90706427254b6f16caaeb1dd5d7 [file] [log] [blame]
Adrien Béraud612b55b2023-05-29 10:42:04 -04001/*
2 * Copyright (C) 2004-2023 Savoir-faire Linux Inc.
3 *
4 * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
5 * Author: Eden Abitbol <eden.abitbol@savoirfairelinux.com>
6 * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
7 * Author: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com>
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 */
23
24#include "pupnp.h"
25
26#include <opendht/thread_pool.h>
27#include <opendht/http.h>
28
29namespace jami {
30namespace upnp {
31
32// Action identifiers.
33constexpr static const char* ACTION_ADD_PORT_MAPPING {"AddPortMapping"};
34constexpr static const char* ACTION_DELETE_PORT_MAPPING {"DeletePortMapping"};
35constexpr static const char* ACTION_GET_GENERIC_PORT_MAPPING_ENTRY {"GetGenericPortMappingEntry"};
36constexpr static const char* ACTION_GET_STATUS_INFO {"GetStatusInfo"};
37constexpr static const char* ACTION_GET_EXTERNAL_IP_ADDRESS {"GetExternalIPAddress"};
38
39// Error codes returned by router when trying to remove ports.
40constexpr static int ARRAY_IDX_INVALID = 713;
41constexpr static int CONFLICT_IN_MAPPING = 718;
42
43// Max number of IGD search attempts before failure.
44constexpr static unsigned int PUPNP_MAX_RESTART_SEARCH_RETRIES {3};
45// IGD search timeout (in seconds).
46constexpr static unsigned int SEARCH_TIMEOUT {60};
47// Base unit for the timeout between two successive IGD search.
48constexpr static auto PUPNP_SEARCH_RETRY_UNIT {std::chrono::seconds(10)};
49
50// Helper functions for xml parsing.
51static std::string_view
52getElementText(IXML_Node* node)
53{
54 if (node) {
55 if (IXML_Node* textNode = ixmlNode_getFirstChild(node))
56 if (const char* value = ixmlNode_getNodeValue(textNode))
57 return std::string_view(value);
58 }
59 return {};
60}
61
62static std::string_view
63getFirstDocItem(IXML_Document* doc, const char* item)
64{
65 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
66 nodeList(ixmlDocument_getElementsByTagName(doc, item), ixmlNodeList_free);
67 if (nodeList) {
68 // If there are several nodes which match the tag, we only want the first one.
69 return getElementText(ixmlNodeList_item(nodeList.get(), 0));
70 }
71 return {};
72}
73
74static std::string_view
75getFirstElementItem(IXML_Element* element, const char* item)
76{
77 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
78 nodeList(ixmlElement_getElementsByTagName(element, item), ixmlNodeList_free);
79 if (nodeList) {
80 // If there are several nodes which match the tag, we only want the first one.
81 return getElementText(ixmlNodeList_item(nodeList.get(), 0));
82 }
83 return {};
84}
85
86static bool
87errorOnResponse(IXML_Document* doc)
88{
89 if (not doc)
90 return true;
91
92 auto errorCode = getFirstDocItem(doc, "errorCode");
93 if (not errorCode.empty()) {
94 auto errorDescription = getFirstDocItem(doc, "errorDescription");
Morteza Namvar5f639522023-07-04 17:08:58 -040095 // JAMI_WARNING("PUPnP: Response contains error: {:s}: {:s}",
96 // errorCode,
97 // errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -040098 return true;
99 }
100 return false;
101}
102
103// UPNP class implementation
104
105PUPnP::PUPnP()
106{
Morteza Namvar5f639522023-07-04 17:08:58 -0400107 // JAMI_DBG("PUPnP: Creating instance [%p] ...", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400108 runOnPUPnPQueue([this] {
109 threadId_ = getCurrentThread();
Morteza Namvar5f639522023-07-04 17:08:58 -0400110 // JAMI_DBG("PUPnP: Instance [%p] created", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400111 });
112}
113
114PUPnP::~PUPnP()
115{
Morteza Namvar5f639522023-07-04 17:08:58 -0400116 // JAMI_DBG("PUPnP: Instance [%p] destroyed", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400117}
118
119void
120PUPnP::initUpnpLib()
121{
122 assert(not initialized_);
123
124 int upnp_err = UpnpInit2(nullptr, 0);
125
126 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400127 // JAMI_ERR("PUPnP: Can't initialize libupnp: %s", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400128 UpnpFinish();
129 initialized_ = false;
130 return;
131 }
132
133 // Disable embedded WebServer if any.
134 if (UpnpIsWebserverEnabled() == 1) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400135 // JAMI_WARN("PUPnP: Web-server is enabled. Disabling");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400136 UpnpEnableWebserver(0);
137 if (UpnpIsWebserverEnabled() == 1) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400138 // JAMI_ERR("PUPnP: Could not disable Web-server!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400139 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400140 // JAMI_DBG("PUPnP: Web-server successfully disabled");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400141 }
142 }
143
144 char* ip_address = UpnpGetServerIpAddress();
145 char* ip_address6 = nullptr;
146 unsigned short port = UpnpGetServerPort();
147 unsigned short port6 = 0;
148#if UPNP_ENABLE_IPV6
149 ip_address6 = UpnpGetServerIp6Address();
150 port6 = UpnpGetServerPort6();
151#endif
152 if (ip_address6 and port6)
Morteza Namvar5f639522023-07-04 17:08:58 -0400153 // JAMI_DBG("PUPnP: Initialized on %s:%u | %s:%u", ip_address, port, ip_address6, port6);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400154 else
Morteza Namvar5f639522023-07-04 17:08:58 -0400155 // JAMI_DBG("PUPnP: Initialized on %s:%u", ip_address, port);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400156
157 // Relax the parser to allow malformed XML text.
158 ixmlRelaxParser(1);
159
160 initialized_ = true;
161}
162
163bool
164PUPnP::isRunning() const
165{
166 std::unique_lock<std::mutex> lk(pupnpMutex_);
167 return not shutdownComplete_;
168}
169
170void
171PUPnP::registerClient()
172{
173 assert(not clientRegistered_);
174
175 CHECK_VALID_THREAD();
176
177 // Register Upnp control point.
178 int upnp_err = UpnpRegisterClient(ctrlPtCallback, this, &ctrlptHandle_);
179 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400180 // JAMI_ERR("PUPnP: Can't register client: %s", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400181 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400182 // JAMI_DBG("PUPnP: Successfully registered client");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400183 clientRegistered_ = true;
184 }
185}
186
187void
188PUPnP::setObserver(UpnpMappingObserver* obs)
189{
190 if (not isValidThread()) {
191 runOnPUPnPQueue([w = weak(), obs] {
192 if (auto upnpThis = w.lock()) {
193 upnpThis->setObserver(obs);
194 }
195 });
196 return;
197 }
198
Morteza Namvar5f639522023-07-04 17:08:58 -0400199 // JAMI_DBG("PUPnP: Setting observer to %p", obs);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400200
201 observer_ = obs;
202}
203
204const IpAddr
205PUPnP::getHostAddress() const
206{
207 std::lock_guard<std::mutex> lock(pupnpMutex_);
208 return hostAddress_;
209}
210
211void
212PUPnP::terminate(std::condition_variable& cv)
213{
Morteza Namvar5f639522023-07-04 17:08:58 -0400214 // JAMI_DBG("PUPnP: Terminate instance %p", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400215
216 clientRegistered_ = false;
217 observer_ = nullptr;
218
219 UpnpUnRegisterClient(ctrlptHandle_);
220
221 if (initialized_) {
222 if (UpnpFinish() != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400223 // JAMI_ERR("PUPnP: Failed to properly close lib-upnp");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400224 }
225
226 initialized_ = false;
227 }
228
229 // Clear all the lists.
230 discoveredIgdList_.clear();
231
232 {
233 std::lock_guard<std::mutex> lock(pupnpMutex_);
234 validIgdList_.clear();
235 shutdownComplete_ = true;
236 cv.notify_one();
237 }
238}
239
240void
241PUPnP::terminate()
242{
243 std::unique_lock<std::mutex> lk(pupnpMutex_);
244 std::condition_variable cv {};
245
246 runOnPUPnPQueue([w = weak(), &cv = cv] {
247 if (auto upnpThis = w.lock()) {
248 upnpThis->terminate(cv);
249 }
250 });
251
252 if (cv.wait_for(lk, std::chrono::seconds(10), [this] { return shutdownComplete_; })) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400253 // JAMI_DBG("PUPnP: Shutdown completed");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400254 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400255 // JAMI_ERR("PUPnP: Shutdown timed-out");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400256 // Force stop if the shutdown take too much time.
257 shutdownComplete_ = true;
258 }
259}
260
261void
262PUPnP::searchForDevices()
263{
264 CHECK_VALID_THREAD();
265
Morteza Namvar5f639522023-07-04 17:08:58 -0400266 // JAMI_DBG("PUPnP: Send IGD search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400267
268 // Send out search for multiple types of devices, as some routers may possibly
269 // only reply to one.
270
271 auto err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_ROOT_DEVICE, this);
272 if (err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400273 // JAMI_WARN("PUPnP: Send search for UPNP_ROOT_DEVICE failed. Error %d: %s",
274 // err,
275 // UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400276 }
277
278 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_IGD_DEVICE, this);
279 if (err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400280 // JAMI_WARN("PUPnP: Send search for UPNP_IGD_DEVICE failed. Error %d: %s",
281 // err,
282 // UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400283 }
284
285 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANIP_SERVICE, this);
286 if (err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400287 // JAMI_WARN("PUPnP: Send search for UPNP_WANIP_SERVICE failed. Error %d: %s",
288 // err,
289 // UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400290 }
291
292 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANPPP_SERVICE, this);
293 if (err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400294 // JAMI_WARN("PUPnP: Send search for UPNP_WANPPP_SERVICE failed. Error %d: %s",
295 // err,
296 // UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400297 }
298}
299
300void
301PUPnP::clearIgds()
302{
303 if (not isValidThread()) {
304 runOnPUPnPQueue([w = weak()] {
305 if (auto upnpThis = w.lock()) {
306 upnpThis->clearIgds();
307 }
308 });
309 return;
310 }
311
Morteza Namvar5f639522023-07-04 17:08:58 -0400312 // JAMI_DBG("PUPnP: clearing IGDs and devices lists");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400313
314 if (searchForIgdTimer_)
315 searchForIgdTimer_->cancel();
316
317 igdSearchCounter_ = 0;
318
319 {
320 std::lock_guard<std::mutex> lock(pupnpMutex_);
321 for (auto const& igd : validIgdList_) {
322 igd->setValid(false);
323 }
324 validIgdList_.clear();
325 hostAddress_ = {};
326 }
327
328 discoveredIgdList_.clear();
329}
330
331void
332PUPnP::searchForIgd()
333{
334 if (not isValidThread()) {
335 runOnPUPnPQueue([w = weak()] {
336 if (auto upnpThis = w.lock()) {
337 upnpThis->searchForIgd();
338 }
339 });
340 return;
341 }
342
343 // Update local address before searching.
344 updateHostAddress();
345
346 if (isReady()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400347 // JAMI_DBG("PUPnP: Already have a valid IGD. Skip the search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400348 return;
349 }
350
351 if (igdSearchCounter_++ >= PUPNP_MAX_RESTART_SEARCH_RETRIES) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400352 // JAMI_WARN("PUPnP: Setup failed after %u trials. PUPnP will be disabled!",
353 // PUPNP_MAX_RESTART_SEARCH_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400354 return;
355 }
356
Morteza Namvar5f639522023-07-04 17:08:58 -0400357 // JAMI_DBG("PUPnP: Start search for IGD: attempt %u", igdSearchCounter_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400358
359 // Do not init if the host is not valid. Otherwise, the init will fail
360 // anyway and may put libupnp in an unstable state (mainly deadlocks)
361 // even if the UpnpFinish() method is called.
362 if (not hasValidHostAddress()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400363 // JAMI_WARN("PUPnP: Host address is invalid. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400364 } else {
365 // Init and register if needed
366 if (not initialized_) {
367 initUpnpLib();
368 }
369 if (initialized_ and not clientRegistered_) {
370 registerClient();
371 }
372 // Start searching
373 if (clientRegistered_) {
374 assert(initialized_);
375 searchForDevices();
376 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400377 // JAMI_WARN("PUPnP: PUPNP not fully setup. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400378 }
379 }
380
381 // Cancel the current timer (if any) and re-schedule.
382 // The connectivity change may be received while the the local
383 // interface is not fully setup. The rescheduling typically
384 // usefull to mitigate this race.
385 if (searchForIgdTimer_)
386 searchForIgdTimer_->cancel();
387
388 searchForIgdTimer_ = getUpnContextScheduler()->scheduleIn(
389 [w = weak()] {
390 if (auto upnpThis = w.lock())
391 upnpThis->searchForIgd();
392 },
393 PUPNP_SEARCH_RETRY_UNIT * igdSearchCounter_);
394}
395
396std::list<std::shared_ptr<IGD>>
397PUPnP::getIgdList() const
398{
399 std::lock_guard<std::mutex> lock(pupnpMutex_);
400 std::list<std::shared_ptr<IGD>> igdList;
401 for (auto& it : validIgdList_) {
402 // Return only active IGDs.
403 if (it->isValid()) {
404 igdList.emplace_back(it);
405 }
406 }
407 return igdList;
408}
409
410bool
411PUPnP::isReady() const
412{
413 // Must at least have a valid local address.
414 if (not getHostAddress() or getHostAddress().isLoopback())
415 return false;
416
417 return hasValidIgd();
418}
419
420bool
421PUPnP::hasValidIgd() const
422{
423 std::lock_guard<std::mutex> lock(pupnpMutex_);
424 for (auto& it : validIgdList_) {
425 if (it->isValid()) {
426 return true;
427 }
428 }
429 return false;
430}
431
432void
433PUPnP::updateHostAddress()
434{
435 std::lock_guard<std::mutex> lock(pupnpMutex_);
436 hostAddress_ = ip_utils::getLocalAddr(AF_INET);
437}
438
439bool
440PUPnP::hasValidHostAddress()
441{
442 std::lock_guard<std::mutex> lock(pupnpMutex_);
443 return hostAddress_ and not hostAddress_.isLoopback();
444}
445
446void
447PUPnP::incrementErrorsCounter(const std::shared_ptr<IGD>& igd)
448{
449 if (not igd or not igd->isValid())
450 return;
451 if (not igd->incrementErrorsCounter()) {
452 // Disable this IGD.
453 igd->setValid(false);
454 // Notify the listener.
455 if (observer_)
456 observer_->onIgdUpdated(igd, UpnpIgdEvent::INVALID_STATE);
457 }
458}
459
460bool
461PUPnP::validateIgd(const std::string& location, IXML_Document* doc_container_ptr)
462{
463 CHECK_VALID_THREAD();
464
465 assert(doc_container_ptr != nullptr);
466
467 XMLDocument document(doc_container_ptr, ixmlDocument_free);
468 auto descDoc = document.get();
469 // Check device type.
470 auto deviceType = getFirstDocItem(descDoc, "deviceType");
471 if (deviceType != UPNP_IGD_DEVICE) {
472 // Device type not IGD.
473 return false;
474 }
475
476 std::shared_ptr<UPnPIGD> igd_candidate = parseIgd(descDoc, location);
477 if (not igd_candidate) {
478 // No valid IGD candidate.
479 return false;
480 }
481
Morteza Namvar5f639522023-07-04 17:08:58 -0400482 // JAMI_DBG("PUPnP: Validating the IGD candidate [UDN: %s]\n"
483 // " Name : %s\n"
484 // " Service Type : %s\n"
485 // " Service ID : %s\n"
486 // " Base URL : %s\n"
487 // " Location URL : %s\n"
488 // " control URL : %s\n"
489 // " Event URL : %s",
490 // igd_candidate->getUID().c_str(),
491 // igd_candidate->getFriendlyName().c_str(),
492 // igd_candidate->getServiceType().c_str(),
493 // igd_candidate->getServiceId().c_str(),
494 // igd_candidate->getBaseURL().c_str(),
495 // igd_candidate->getLocationURL().c_str(),
496 // igd_candidate->getControlURL().c_str(),
497 // igd_candidate->getEventSubURL().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400498
499 // Check if IGD is connected.
500 if (not actionIsIgdConnected(*igd_candidate)) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400501 // JAMI_WARN("PUPnP: IGD candidate %s is not connected", igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400502 return false;
503 }
504
505 // Validate external Ip.
506 igd_candidate->setPublicIp(actionGetExternalIP(*igd_candidate));
507 if (igd_candidate->getPublicIp().toString().empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400508 // JAMI_WARN("PUPnP: IGD candidate %s has no valid external Ip",
509 // igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400510 return false;
511 }
512
513 // Validate internal Ip.
514 if (igd_candidate->getBaseURL().empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400515 // JAMI_WARN("PUPnP: IGD candidate %s has no valid internal Ip",
516 // igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400517 return false;
518 }
519
520 // Typically the IGD local address should be extracted from the XML
521 // document (e.g. parsing the base URL). For simplicity, we assume
522 // that it matches the gateway as seen by the local interface.
523 if (const auto& localGw = ip_utils::getLocalGateway()) {
524 igd_candidate->setLocalIp(localGw);
525 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400526 // JAMI_WARN("PUPnP: Could not set internal address for IGD candidate %s",
527 // igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400528 return false;
529 }
530
531 // Store info for subscription.
532 std::string eventSub = igd_candidate->getEventSubURL();
533
534 {
535 // Add the IGD if not already present in the list.
536 std::lock_guard<std::mutex> lock(pupnpMutex_);
537 for (auto& igd : validIgdList_) {
538 // Must not be a null pointer
539 assert(igd.get() != nullptr);
540 if (*igd == *igd_candidate) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400541 // JAMI_DBG("PUPnP: Device [%s] with int/ext addresses [%s:%s] is already in the list "
542 // "of valid IGDs",
543 // igd_candidate->getUID().c_str(),
544 // igd_candidate->toString().c_str(),
545 // igd_candidate->getPublicIp().toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400546 return true;
547 }
548 }
549 }
550
551 // We have a valid IGD
552 igd_candidate->setValid(true);
553
Morteza Namvar5f639522023-07-04 17:08:58 -0400554 // JAMI_DBG("PUPnP: Added a new IGD [%s] to the list of valid IGDs",
555 // igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400556
Morteza Namvar5f639522023-07-04 17:08:58 -0400557 // JAMI_DBG("PUPnP: New IGD addresses [int: %s - ext: %s]",
558 // igd_candidate->toString().c_str(),
559 // igd_candidate->getPublicIp().toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400560
561 // Subscribe to IGD events.
562 int upnp_err = UpnpSubscribeAsync(ctrlptHandle_,
563 eventSub.c_str(),
564 UPNP_INFINITE,
565 subEventCallback,
566 this);
567 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400568 // JAMI_WARN("PUPnP: Failed to send subscribe request to %s: error %i - %s",
569 // igd_candidate->getUID().c_str(),
570 // upnp_err,
571 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400572 // return false;
573 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400574 // JAMI_DBG("PUPnP: Successfully subscribed to IGD %s", igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400575 }
576
577 {
578 // This is a new (and hopefully valid) IGD.
579 std::lock_guard<std::mutex> lock(pupnpMutex_);
580 validIgdList_.emplace_back(igd_candidate);
581 }
582
583 // Report to the listener.
584 runOnUpnpContextQueue([w = weak(), igd_candidate] {
585 if (auto upnpThis = w.lock()) {
586 if (upnpThis->observer_)
587 upnpThis->observer_->onIgdUpdated(igd_candidate, UpnpIgdEvent::ADDED);
588 }
589 });
590
591 return true;
592}
593
594void
595PUPnP::requestMappingAdd(const Mapping& mapping)
596{
597 runOnPUPnPQueue([w = weak(), mapping] {
598 if (auto upnpThis = w.lock()) {
599 if (not upnpThis->isRunning())
600 return;
601 Mapping mapRes(mapping);
602 if (upnpThis->actionAddPortMapping(mapRes)) {
603 mapRes.setState(MappingState::OPEN);
604 mapRes.setInternalAddress(upnpThis->getHostAddress().toString());
605 upnpThis->processAddMapAction(mapRes);
606 } else {
607 upnpThis->incrementErrorsCounter(mapRes.getIgd());
608 mapRes.setState(MappingState::FAILED);
609 upnpThis->processRequestMappingFailure(mapRes);
610 }
611 }
612 });
613}
614
615void
616PUPnP::requestMappingRemove(const Mapping& mapping)
617{
618 // Send remove request using the matching IGD
619 runOnPUPnPQueue([w = weak(), mapping] {
620 if (auto upnpThis = w.lock()) {
621 // Abort if we are shutting down.
622 if (not upnpThis->isRunning())
623 return;
624 if (upnpThis->actionDeletePortMapping(mapping)) {
625 upnpThis->processRemoveMapAction(mapping);
626 } else {
627 assert(mapping.getIgd());
628 // Dont need to report in case of failure.
629 upnpThis->incrementErrorsCounter(mapping.getIgd());
630 }
631 }
632 });
633}
634
635std::shared_ptr<UPnPIGD>
636PUPnP::findMatchingIgd(const std::string& ctrlURL) const
637{
638 std::lock_guard<std::mutex> lock(pupnpMutex_);
639
640 auto iter = std::find_if(validIgdList_.begin(),
641 validIgdList_.end(),
642 [&ctrlURL](const std::shared_ptr<IGD>& igd) {
643 if (auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd)) {
644 return upnpIgd->getControlURL() == ctrlURL;
645 }
646 return false;
647 });
648
649 if (iter == validIgdList_.end()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400650 // JAMI_WARN("PUPnP: Did not find the IGD matching ctrl URL [%s]", ctrlURL.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400651 return {};
652 }
653
654 return std::dynamic_pointer_cast<UPnPIGD>(*iter);
655}
656
657void
658PUPnP::processAddMapAction(const Mapping& map)
659{
660 CHECK_VALID_THREAD();
661
662 if (observer_ == nullptr)
663 return;
664
665 runOnUpnpContextQueue([w = weak(), map] {
666 if (auto upnpThis = w.lock()) {
667 if (upnpThis->observer_)
668 upnpThis->observer_->onMappingAdded(map.getIgd(), std::move(map));
669 }
670 });
671}
672
673void
674PUPnP::processRequestMappingFailure(const Mapping& map)
675{
676 CHECK_VALID_THREAD();
677
678 if (observer_ == nullptr)
679 return;
680
681 runOnUpnpContextQueue([w = weak(), map] {
682 if (auto upnpThis = w.lock()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400683 // JAMI_DBG("PUPnP: Failed to request mapping %s", map.toString().c_str());
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{
693 CHECK_VALID_THREAD();
694
695 if (observer_ == nullptr)
696 return;
697
698 runOnUpnpContextQueue([map, obs = observer_] {
Morteza Namvar5f639522023-07-04 17:08:58 -0400699 // JAMI_DBG("PUPnP: Closed mapping %s", map.toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400700 obs->onMappingRemoved(map.getIgd(), std::move(map));
701 });
702}
703
704const char*
705PUPnP::eventTypeToString(Upnp_EventType eventType)
706{
707 switch (eventType) {
708 case UPNP_CONTROL_ACTION_REQUEST:
709 return "UPNP_CONTROL_ACTION_REQUEST";
710 case UPNP_CONTROL_ACTION_COMPLETE:
711 return "UPNP_CONTROL_ACTION_COMPLETE";
712 case UPNP_CONTROL_GET_VAR_REQUEST:
713 return "UPNP_CONTROL_GET_VAR_REQUEST";
714 case UPNP_CONTROL_GET_VAR_COMPLETE:
715 return "UPNP_CONTROL_GET_VAR_COMPLETE";
716 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
717 return "UPNP_DISCOVERY_ADVERTISEMENT_ALIVE";
718 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
719 return "UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE";
720 case UPNP_DISCOVERY_SEARCH_RESULT:
721 return "UPNP_DISCOVERY_SEARCH_RESULT";
722 case UPNP_DISCOVERY_SEARCH_TIMEOUT:
723 return "UPNP_DISCOVERY_SEARCH_TIMEOUT";
724 case UPNP_EVENT_SUBSCRIPTION_REQUEST:
725 return "UPNP_EVENT_SUBSCRIPTION_REQUEST";
726 case UPNP_EVENT_RECEIVED:
727 return "UPNP_EVENT_RECEIVED";
728 case UPNP_EVENT_RENEWAL_COMPLETE:
729 return "UPNP_EVENT_RENEWAL_COMPLETE";
730 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
731 return "UPNP_EVENT_SUBSCRIBE_COMPLETE";
732 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE:
733 return "UPNP_EVENT_UNSUBSCRIBE_COMPLETE";
734 case UPNP_EVENT_AUTORENEWAL_FAILED:
735 return "UPNP_EVENT_AUTORENEWAL_FAILED";
736 case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
737 return "UPNP_EVENT_SUBSCRIPTION_EXPIRED";
738 default:
739 return "Unknown UPNP Event";
740 }
741}
742
743int
744PUPnP::ctrlPtCallback(Upnp_EventType event_type, const void* event, void* user_data)
745{
746 auto pupnp = static_cast<PUPnP*>(user_data);
747
748 if (pupnp == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400749 // JAMI_WARN("PUPnP: Control point callback without PUPnP");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400750 return UPNP_E_SUCCESS;
751 }
752
753 auto upnpThis = pupnp->weak().lock();
754
755 if (not upnpThis)
756 return UPNP_E_SUCCESS;
757
758 // Ignore if already unregistered.
759 if (not upnpThis->clientRegistered_)
760 return UPNP_E_SUCCESS;
761
762 // Process the callback.
763 return upnpThis->handleCtrlPtUPnPEvents(event_type, event);
764}
765
766PUPnP::CtrlAction
767PUPnP::getAction(const char* xmlNode)
768{
769 if (strstr(xmlNode, ACTION_ADD_PORT_MAPPING)) {
770 return CtrlAction::ADD_PORT_MAPPING;
771 } else if (strstr(xmlNode, ACTION_DELETE_PORT_MAPPING)) {
772 return CtrlAction::DELETE_PORT_MAPPING;
773 } else if (strstr(xmlNode, ACTION_GET_GENERIC_PORT_MAPPING_ENTRY)) {
774 return CtrlAction::GET_GENERIC_PORT_MAPPING_ENTRY;
775 } else if (strstr(xmlNode, ACTION_GET_STATUS_INFO)) {
776 return CtrlAction::GET_STATUS_INFO;
777 } else if (strstr(xmlNode, ACTION_GET_EXTERNAL_IP_ADDRESS)) {
778 return CtrlAction::GET_EXTERNAL_IP_ADDRESS;
779 } else {
780 return CtrlAction::UNKNOWN;
781 }
782}
783
784void
785PUPnP::processDiscoverySearchResult(const std::string& cpDeviceId,
786 const std::string& igdLocationUrl,
787 const IpAddr& dstAddr)
788{
789 CHECK_VALID_THREAD();
790
791 // Update host address if needed.
792 if (not hasValidHostAddress())
793 updateHostAddress();
794
795 // The host address must be valid to proceed.
796 if (not hasValidHostAddress()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400797 // JAMI_WARN("PUPnP: Local address is invalid. Ignore search result for now!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400798 return;
799 }
800
801 // Use the device ID and the URL as ID. This is necessary as some
802 // IGDs may have the same device ID but different URLs.
803
804 auto igdId = cpDeviceId + " url: " + igdLocationUrl;
805
806 if (not discoveredIgdList_.emplace(igdId).second) {
807 // JAMI_WARN("PUPnP: IGD [%s] already in the list", igdId.c_str());
808 return;
809 }
810
Morteza Namvar5f639522023-07-04 17:08:58 -0400811 // JAMI_DBG("PUPnP: Discovered a new IGD [%s]", igdId.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400812
813 // NOTE: here, we check if the location given is related to the source address.
814 // If it's not the case, it's certainly a router plugged in the network, but not
815 // related to this network. So the given location will be unreachable and this
816 // will cause some timeout.
817
818 // Only check the IP address (ignore the port number).
819 dht::http::Url url(igdLocationUrl);
820 if (IpAddr(url.host).toString(false) != dstAddr.toString(false)) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400821 // JAMI_DBG("PUPnP: Returned location %s does not match the source address %s",
822 // IpAddr(url.host).toString(true, true).c_str(),
823 // dstAddr.toString(true, true).c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400824 return;
825 }
826
827 // Run a separate thread to prevent blocking this thread
828 // if the IGD HTTP server is not responsive.
829 dht::ThreadPool::io().run([w = weak(), igdLocationUrl] {
830 if (auto upnpThis = w.lock()) {
831 upnpThis->downLoadIgdDescription(igdLocationUrl);
832 }
833 });
834}
835
836void
837PUPnP::downLoadIgdDescription(const std::string& locationUrl)
838{
839 IXML_Document* doc_container_ptr = nullptr;
840 int upnp_err = UpnpDownloadXmlDoc(locationUrl.c_str(), &doc_container_ptr);
841
842 if (upnp_err != UPNP_E_SUCCESS or not doc_container_ptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400843 // JAMI_WARN("PUPnP: Error downloading device XML document from %s -> %s",
844 // locationUrl.c_str(),
845 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400846 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400847 // JAMI_DBG("PUPnP: Succeeded to download device XML document from %s", locationUrl.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400848 runOnPUPnPQueue([w = weak(), url = locationUrl, doc_container_ptr] {
849 if (auto upnpThis = w.lock()) {
850 upnpThis->validateIgd(url, doc_container_ptr);
851 }
852 });
853 }
854}
855
856void
857PUPnP::processDiscoveryAdvertisementByebye(const std::string& cpDeviceId)
858{
859 CHECK_VALID_THREAD();
860
861 discoveredIgdList_.erase(cpDeviceId);
862
863 std::shared_ptr<IGD> igd;
864 {
865 std::lock_guard<std::mutex> lk(pupnpMutex_);
866 for (auto it = validIgdList_.begin(); it != validIgdList_.end();) {
867 if ((*it)->getUID() == cpDeviceId) {
868 igd = *it;
Morteza Namvar5f639522023-07-04 17:08:58 -0400869 // JAMI_DBG("PUPnP: Received [%s] for IGD [%s] %s. Will be removed.",
870 // PUPnP::eventTypeToString(UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE),
871 // igd->getUID().c_str(),
872 // igd->toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400873 igd->setValid(false);
874 // Remove the IGD.
875 it = validIgdList_.erase(it);
876 break;
877 } else {
878 it++;
879 }
880 }
881 }
882
883 // Notify the listener.
884 if (observer_ and igd) {
885 observer_->onIgdUpdated(igd, UpnpIgdEvent::REMOVED);
886 }
887}
888
889void
890PUPnP::processDiscoverySubscriptionExpired(Upnp_EventType event_type, const std::string& eventSubUrl)
891{
892 CHECK_VALID_THREAD();
893
894 std::lock_guard<std::mutex> lk(pupnpMutex_);
895 for (auto& it : validIgdList_) {
896 if (auto igd = std::dynamic_pointer_cast<UPnPIGD>(it)) {
897 if (igd->getEventSubURL() == eventSubUrl) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400898 // JAMI_DBG("PUPnP: Received [%s] event for IGD [%s] %s. Request a new subscribe.",
899 // PUPnP::eventTypeToString(event_type),
900 // igd->getUID().c_str(),
901 // igd->toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400902 UpnpSubscribeAsync(ctrlptHandle_,
903 eventSubUrl.c_str(),
904 UPNP_INFINITE,
905 subEventCallback,
906 this);
907 break;
908 }
909 }
910 }
911}
912
913int
914PUPnP::handleCtrlPtUPnPEvents(Upnp_EventType event_type, const void* event)
915{
916 switch (event_type) {
917 // "ALIVE" events are processed as "SEARCH RESULT". It might be usefull
918 // if "SEARCH RESULT" was missed.
919 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
920 case UPNP_DISCOVERY_SEARCH_RESULT: {
921 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
922
923 // First check the error code.
924 auto upnp_status = UpnpDiscovery_get_ErrCode(d_event);
925 if (upnp_status != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400926 // JAMI_ERR("PUPnP: UPNP discovery is in erroneous state: %s",
927 // UpnpGetErrorMessage(upnp_status));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400928 break;
929 }
930
931 // Parse the event's data.
932 std::string deviceId {UpnpDiscovery_get_DeviceID_cstr(d_event)};
933 std::string location {UpnpDiscovery_get_Location_cstr(d_event)};
934 IpAddr dstAddr(*(const pj_sockaddr*) (UpnpDiscovery_get_DestAddr(d_event)));
935 runOnPUPnPQueue([w = weak(),
936 deviceId = std::move(deviceId),
937 location = std::move(location),
938 dstAddr = std::move(dstAddr)] {
939 if (auto upnpThis = w.lock()) {
940 upnpThis->processDiscoverySearchResult(deviceId, location, dstAddr);
941 }
942 });
943 break;
944 }
945 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: {
946 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
947
948 std::string deviceId(UpnpDiscovery_get_DeviceID_cstr(d_event));
949
950 // Process the response on the main thread.
951 runOnPUPnPQueue([w = weak(), deviceId = std::move(deviceId)] {
952 if (auto upnpThis = w.lock()) {
953 upnpThis->processDiscoveryAdvertisementByebye(deviceId);
954 }
955 });
956 break;
957 }
958 case UPNP_DISCOVERY_SEARCH_TIMEOUT: {
959 // Even if the discovery search is successful, it's normal to receive
960 // time-out events. This because we send search requests using various
961 // device types, which some of them may not return a response.
962 break;
963 }
964 case UPNP_EVENT_RECEIVED: {
965 // Nothing to do.
966 break;
967 }
968 // Treat failed autorenewal like an expired subscription.
969 case UPNP_EVENT_AUTORENEWAL_FAILED:
970 case UPNP_EVENT_SUBSCRIPTION_EXPIRED: // This event will occur only if autorenewal is disabled.
971 {
Morteza Namvar5f639522023-07-04 17:08:58 -0400972 // JAMI_WARN("PUPnP: Received Subscription Event %s", eventTypeToString(event_type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400973 const UpnpEventSubscribe* es_event = (const UpnpEventSubscribe*) event;
974 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400975 // JAMI_WARN("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400976 break;
977 }
978 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
979
980 // Process the response on the main thread.
981 runOnPUPnPQueue([w = weak(), event_type, publisherUrl = std::move(publisherUrl)] {
982 if (auto upnpThis = w.lock()) {
983 upnpThis->processDiscoverySubscriptionExpired(event_type, publisherUrl);
984 }
985 });
986 break;
987 }
988 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
989 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE: {
990 UpnpEventSubscribe* es_event = (UpnpEventSubscribe*) event;
991 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400992 // JAMI_WARN("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400993 } else {
994 UpnpEventSubscribe_delete(es_event);
995 }
996 break;
997 }
998 case UPNP_CONTROL_ACTION_COMPLETE: {
999 const UpnpActionComplete* a_event = (const UpnpActionComplete*) event;
1000 if (a_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001001 // JAMI_WARN("PUPnP: Received Action Complete Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001002 break;
1003 }
1004 auto res = UpnpActionComplete_get_ErrCode(a_event);
1005 if (res != UPNP_E_SUCCESS and res != UPNP_E_TIMEDOUT) {
1006 auto err = UpnpActionComplete_get_ErrCode(a_event);
Morteza Namvar5f639522023-07-04 17:08:58 -04001007 // JAMI_WARN("PUPnP: Received Action Complete error %i %s", err, UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001008 } else {
1009 auto actionRequest = UpnpActionComplete_get_ActionRequest(a_event);
1010 // Abort if there is no action to process.
1011 if (actionRequest == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001012 // JAMI_WARN("PUPnP: Can't get the Action Request data from the event");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001013 break;
1014 }
1015
1016 auto actionResult = UpnpActionComplete_get_ActionResult(a_event);
1017 if (actionResult != nullptr) {
1018 ixmlDocument_free(actionResult);
1019 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -04001020 // JAMI_WARN("PUPnP: Action Result document not found");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001021 }
1022 }
1023 break;
1024 }
1025 default: {
Morteza Namvar5f639522023-07-04 17:08:58 -04001026 // JAMI_WARN("PUPnP: Unhandled Control Point event");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001027 break;
1028 }
1029 }
1030
1031 return UPNP_E_SUCCESS;
1032}
1033
1034int
1035PUPnP::subEventCallback(Upnp_EventType event_type, const void* event, void* user_data)
1036{
1037 if (auto pupnp = static_cast<PUPnP*>(user_data))
1038 return pupnp->handleSubscriptionUPnPEvent(event_type, event);
Morteza Namvar5f639522023-07-04 17:08:58 -04001039 // JAMI_WARN("PUPnP: Subscription callback without service Id string");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001040 return 0;
1041}
1042
1043int
1044PUPnP::handleSubscriptionUPnPEvent(Upnp_EventType, const void* event)
1045{
1046 UpnpEventSubscribe* es_event = static_cast<UpnpEventSubscribe*>(const_cast<void*>(event));
1047
1048 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001049 // JAMI_ERR("PUPnP: Unexpected null pointer!");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001050 return UPNP_E_INVALID_ARGUMENT;
1051 }
1052 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
1053 int upnp_err = UpnpEventSubscribe_get_ErrCode(es_event);
1054 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001055 // JAMI_WARN("PUPnP: Subscription error %s from %s",
1056 // UpnpGetErrorMessage(upnp_err),
1057 // publisherUrl.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001058 return upnp_err;
1059 }
1060
1061 return UPNP_E_SUCCESS;
1062}
1063
1064std::unique_ptr<UPnPIGD>
1065PUPnP::parseIgd(IXML_Document* doc, std::string locationUrl)
1066{
1067 if (not(doc and locationUrl.c_str()))
1068 return nullptr;
1069
1070 // Check the UDN to see if its already in our device list.
1071 std::string UDN(getFirstDocItem(doc, "UDN"));
1072 if (UDN.empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001073 // JAMI_WARN("PUPnP: could not find UDN in description document of device");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001074 return nullptr;
1075 } else {
1076 std::lock_guard<std::mutex> lk(pupnpMutex_);
1077 for (auto& it : validIgdList_) {
1078 if (it->getUID() == UDN) {
1079 // We already have this device in our list.
1080 return nullptr;
1081 }
1082 }
1083 }
1084
Morteza Namvar5f639522023-07-04 17:08:58 -04001085 // JAMI_DBG("PUPnP: Found new device [%s]", UDN.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001086
1087 std::unique_ptr<UPnPIGD> new_igd;
1088 int upnp_err;
1089
1090 // Get friendly name.
1091 std::string friendlyName(getFirstDocItem(doc, "friendlyName"));
1092
1093 // Get base URL.
1094 std::string baseURL(getFirstDocItem(doc, "URLBase"));
1095 if (baseURL.empty())
1096 baseURL = locationUrl;
1097
1098 // Get list of services defined by serviceType.
1099 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> serviceList(nullptr,
1100 ixmlNodeList_free);
1101 serviceList.reset(ixmlDocument_getElementsByTagName(doc, "serviceType"));
1102 unsigned long list_length = ixmlNodeList_length(serviceList.get());
1103
1104 // Go through the "serviceType" nodes until we find the the correct service type.
1105 for (unsigned long node_idx = 0; node_idx < list_length; node_idx++) {
1106 IXML_Node* serviceType_node = ixmlNodeList_item(serviceList.get(), node_idx);
1107 std::string serviceType(getElementText(serviceType_node));
1108
1109 // Only check serviceType of WANIPConnection or WANPPPConnection.
1110 if (serviceType != UPNP_WANIP_SERVICE
1111 && serviceType != UPNP_WANPPP_SERVICE) {
1112 // IGD is not WANIP or WANPPP service. Going to next node.
1113 continue;
1114 }
1115
1116 // Get parent node.
1117 IXML_Node* service_node = ixmlNode_getParentNode(serviceType_node);
1118 if (not service_node) {
1119 // IGD serviceType has no parent node. Going to next node.
1120 continue;
1121 }
1122
1123 // Perform sanity check. The parent node should be called "service".
1124 if (strcmp(ixmlNode_getNodeName(service_node), "service") != 0) {
1125 // IGD "serviceType" parent node is not called "service". Going to next node.
1126 continue;
1127 }
1128
1129 // Get serviceId.
1130 IXML_Element* service_element = (IXML_Element*) service_node;
1131 std::string serviceId(getFirstElementItem(service_element, "serviceId"));
1132 if (serviceId.empty()) {
1133 // IGD "serviceId" is empty. Going to next node.
1134 continue;
1135 }
1136
1137 // Get the relative controlURL and turn it into absolute address using the URLBase.
1138 std::string controlURL(getFirstElementItem(service_element, "controlURL"));
1139 if (controlURL.empty()) {
1140 // IGD control URL is empty. Going to next node.
1141 continue;
1142 }
1143
1144 char* absolute_control_url = nullptr;
1145 upnp_err = UpnpResolveURL2(baseURL.c_str(), controlURL.c_str(), &absolute_control_url);
1146 if (upnp_err == UPNP_E_SUCCESS)
1147 controlURL = absolute_control_url;
1148 else
Morteza Namvar5f639522023-07-04 17:08:58 -04001149 // JAMI_WARN("PUPnP: Error resolving absolute controlURL -> %s",
1150 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001151
1152 std::free(absolute_control_url);
1153
1154 // Get the relative eventSubURL and turn it into absolute address using the URLBase.
1155 std::string eventSubURL(getFirstElementItem(service_element, "eventSubURL"));
1156 if (eventSubURL.empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001157 // JAMI_WARN("PUPnP: IGD event sub URL is empty. Going to next node");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001158 continue;
1159 }
1160
1161 char* absolute_event_sub_url = nullptr;
1162 upnp_err = UpnpResolveURL2(baseURL.c_str(), eventSubURL.c_str(), &absolute_event_sub_url);
1163 if (upnp_err == UPNP_E_SUCCESS)
1164 eventSubURL = absolute_event_sub_url;
1165 else
Morteza Namvar5f639522023-07-04 17:08:58 -04001166 // JAMI_WARN("PUPnP: Error resolving absolute eventSubURL -> %s",
1167 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001168
1169 std::free(absolute_event_sub_url);
1170
1171 new_igd.reset(new UPnPIGD(std::move(UDN),
1172 std::move(baseURL),
1173 std::move(friendlyName),
1174 std::move(serviceType),
1175 std::move(serviceId),
1176 std::move(locationUrl),
1177 std::move(controlURL),
1178 std::move(eventSubURL)));
1179
1180 return new_igd;
1181 }
1182
1183 return nullptr;
1184}
1185
1186bool
1187PUPnP::actionIsIgdConnected(const UPnPIGD& igd)
1188{
1189 if (not clientRegistered_)
1190 return false;
1191
1192 // Set action name.
1193 IXML_Document* action_container_ptr = UpnpMakeAction("GetStatusInfo",
1194 igd.getServiceType().c_str(),
1195 0,
1196 nullptr);
1197 if (not action_container_ptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001198 // JAMI_WARN("PUPnP: Failed to make GetStatusInfo action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001199 return false;
1200 }
1201 XMLDocument action(action_container_ptr, ixmlDocument_free); // Action pointer.
1202
1203 IXML_Document* response_container_ptr = nullptr;
1204 int upnp_err = UpnpSendAction(ctrlptHandle_,
1205 igd.getControlURL().c_str(),
1206 igd.getServiceType().c_str(),
1207 nullptr,
1208 action.get(),
1209 &response_container_ptr);
1210 if (not response_container_ptr or upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001211 // JAMI_WARN("PUPnP: Failed to send GetStatusInfo action -> %s", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001212 return false;
1213 }
1214 XMLDocument response(response_container_ptr, ixmlDocument_free);
1215
1216 if (errorOnResponse(response.get())) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001217 // JAMI_WARN("PUPnP: Failed to get GetStatusInfo from %s -> %d: %s",
1218 // igd.getServiceType().c_str(),
1219 // upnp_err,
1220 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001221 return false;
1222 }
1223
1224 // Parse response.
1225 auto status = getFirstDocItem(response.get(), "NewConnectionStatus");
1226 return status == "Connected";
1227}
1228
1229IpAddr
1230PUPnP::actionGetExternalIP(const UPnPIGD& igd)
1231{
1232 if (not clientRegistered_)
1233 return {};
1234
1235 // Action and response pointers.
1236 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1237 action(nullptr, ixmlDocument_free); // Action pointer.
1238 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1239 response(nullptr, ixmlDocument_free); // Response pointer.
1240
1241 // Set action name.
1242 static constexpr const char* action_name {"GetExternalIPAddress"};
1243
1244 IXML_Document* action_container_ptr = nullptr;
1245 action_container_ptr = UpnpMakeAction(action_name, igd.getServiceType().c_str(), 0, nullptr);
1246 action.reset(action_container_ptr);
1247
1248 if (not action) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001249 // JAMI_WARN("PUPnP: Failed to make GetExternalIPAddress action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001250 return {};
1251 }
1252
1253 IXML_Document* response_container_ptr = nullptr;
1254 int upnp_err = UpnpSendAction(ctrlptHandle_,
1255 igd.getControlURL().c_str(),
1256 igd.getServiceType().c_str(),
1257 nullptr,
1258 action.get(),
1259 &response_container_ptr);
1260 response.reset(response_container_ptr);
1261
1262 if (not response or upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001263 // JAMI_WARN("PUPnP: Failed to send GetExternalIPAddress action -> %s",
1264 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001265 return {};
1266 }
1267
1268 if (errorOnResponse(response.get())) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001269 // JAMI_WARN("PUPnP: Failed to get GetExternalIPAddress from %s -> %d: %s",
1270 // igd.getServiceType().c_str(),
1271 // upnp_err,
1272 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001273 return {};
1274 }
1275
1276 return {getFirstDocItem(response.get(), "NewExternalIPAddress")};
1277}
1278
1279std::map<Mapping::key_t, Mapping>
1280PUPnP::getMappingsListByDescr(const std::shared_ptr<IGD>& igd, const std::string& description) const
1281{
1282 auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd);
1283 assert(upnpIgd);
1284
1285 std::map<Mapping::key_t, Mapping> mapList;
1286
1287 if (not clientRegistered_ or not upnpIgd->isValid() or not upnpIgd->getLocalIp())
1288 return mapList;
1289
1290 // Set action name.
1291 static constexpr const char* action_name {"GetGenericPortMappingEntry"};
1292
1293 for (int entry_idx = 0;; entry_idx++) {
1294 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1295 action(nullptr, ixmlDocument_free); // Action pointer.
1296 IXML_Document* action_container_ptr = nullptr;
1297
1298 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1299 response(nullptr, ixmlDocument_free); // Response pointer.
1300 IXML_Document* response_container_ptr = nullptr;
1301
1302 UpnpAddToAction(&action_container_ptr,
1303 action_name,
1304 upnpIgd->getServiceType().c_str(),
1305 "NewPortMappingIndex",
1306 std::to_string(entry_idx).c_str());
1307 action.reset(action_container_ptr);
1308
1309 if (not action) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001310 // JAMI_WARN("PUPnP: Failed to add NewPortMappingIndex action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001311 break;
1312 }
1313
1314 int upnp_err = UpnpSendAction(ctrlptHandle_,
1315 upnpIgd->getControlURL().c_str(),
1316 upnpIgd->getServiceType().c_str(),
1317 nullptr,
1318 action.get(),
1319 &response_container_ptr);
1320 response.reset(response_container_ptr);
1321
1322 if (not response) {
1323 // No existing mapping. Abort silently.
1324 break;
1325 }
1326
1327 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001328 // JAMI_ERR("PUPnP: GetGenericPortMappingEntry returned with error: %i", upnp_err);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001329 break;
1330 }
1331
1332 // Check error code.
1333 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1334 if (not errorCode.empty()) {
1335 auto error = to_int<int>(errorCode);
1336 if (error == ARRAY_IDX_INVALID or error == CONFLICT_IN_MAPPING) {
1337 // No more port mapping entries in the response.
Morteza Namvar5f639522023-07-04 17:08:58 -04001338 // JAMI_DBG("PUPnP: No more mappings (found a total of %i mappings", entry_idx);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001339 break;
1340 } else {
1341 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
1342 JAMI_ERROR("PUPnP: GetGenericPortMappingEntry returned with error: {:s}: {:s}",
1343 errorCode,
1344 errorDescription);
1345 break;
1346 }
1347 }
1348
1349 // Parse the response.
1350 auto desc_actual = getFirstDocItem(response.get(), "NewPortMappingDescription");
1351 auto client_ip = getFirstDocItem(response.get(), "NewInternalClient");
1352
1353 if (client_ip != getHostAddress().toString()) {
1354 // Silently ignore un-matching addresses.
1355 continue;
1356 }
1357
1358 if (desc_actual.find(description) == std::string::npos)
1359 continue;
1360
1361 auto port_internal = getFirstDocItem(response.get(), "NewInternalPort");
1362 auto port_external = getFirstDocItem(response.get(), "NewExternalPort");
1363 std::string transport(getFirstDocItem(response.get(), "NewProtocol"));
1364
1365 if (port_internal.empty() || port_external.empty() || transport.empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001366 // JAMI_ERR("PUPnP: GetGenericPortMappingEntry returned an invalid entry at index %i",
1367 // entry_idx);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001368 continue;
1369 }
1370
1371 std::transform(transport.begin(), transport.end(), transport.begin(), ::toupper);
1372 PortType type = transport.find("TCP") != std::string::npos ? PortType::TCP : PortType::UDP;
1373 auto ePort = to_int<uint16_t>(port_external);
1374 auto iPort = to_int<uint16_t>(port_internal);
1375
1376 Mapping map(type, ePort, iPort);
1377 map.setIgd(igd);
1378
1379 mapList.emplace(map.getMapKey(), std::move(map));
1380 }
1381
1382 JAMI_DEBUG("PUPnP: Found {:d} allocated mappings on IGD {:s}",
1383 mapList.size(),
1384 upnpIgd->toString());
1385
1386 return mapList;
1387}
1388
1389void
1390PUPnP::deleteMappingsByDescription(const std::shared_ptr<IGD>& igd, const std::string& description)
1391{
1392 if (not(clientRegistered_ and igd->getLocalIp()))
1393 return;
1394
Morteza Namvar5f639522023-07-04 17:08:58 -04001395 // JAMI_DBG("PUPnP: Remove all mappings (if any) on IGD %s matching descr prefix %s",
1396 // igd->toString().c_str(),
1397 // Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001398
1399 auto mapList = getMappingsListByDescr(igd, description);
1400
1401 for (auto const& [_, map] : mapList) {
1402 requestMappingRemove(map);
1403 }
1404}
1405
1406bool
1407PUPnP::actionAddPortMapping(const Mapping& mapping)
1408{
1409 CHECK_VALID_THREAD();
1410
1411 if (not clientRegistered_)
1412 return false;
1413
1414 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1415 if (not igdIn)
1416 return false;
1417
1418 // The requested IGD must be present in the list of local valid IGDs.
1419 auto igd = findMatchingIgd(igdIn->getControlURL());
1420
1421 if (not igd or not igd->isValid())
1422 return false;
1423
1424 // Action and response pointers.
1425 XMLDocument action(nullptr, ixmlDocument_free);
1426 IXML_Document* action_container_ptr = nullptr;
1427 XMLDocument response(nullptr, ixmlDocument_free);
1428 IXML_Document* response_container_ptr = nullptr;
1429
1430 // Set action sequence.
1431 UpnpAddToAction(&action_container_ptr,
1432 ACTION_ADD_PORT_MAPPING,
1433 igd->getServiceType().c_str(),
1434 "NewRemoteHost",
1435 "");
1436 UpnpAddToAction(&action_container_ptr,
1437 ACTION_ADD_PORT_MAPPING,
1438 igd->getServiceType().c_str(),
1439 "NewExternalPort",
1440 mapping.getExternalPortStr().c_str());
1441 UpnpAddToAction(&action_container_ptr,
1442 ACTION_ADD_PORT_MAPPING,
1443 igd->getServiceType().c_str(),
1444 "NewProtocol",
1445 mapping.getTypeStr());
1446 UpnpAddToAction(&action_container_ptr,
1447 ACTION_ADD_PORT_MAPPING,
1448 igd->getServiceType().c_str(),
1449 "NewInternalPort",
1450 mapping.getInternalPortStr().c_str());
1451 UpnpAddToAction(&action_container_ptr,
1452 ACTION_ADD_PORT_MAPPING,
1453 igd->getServiceType().c_str(),
1454 "NewInternalClient",
1455 getHostAddress().toString().c_str());
1456 UpnpAddToAction(&action_container_ptr,
1457 ACTION_ADD_PORT_MAPPING,
1458 igd->getServiceType().c_str(),
1459 "NewEnabled",
1460 "1");
1461 UpnpAddToAction(&action_container_ptr,
1462 ACTION_ADD_PORT_MAPPING,
1463 igd->getServiceType().c_str(),
1464 "NewPortMappingDescription",
1465 mapping.toString().c_str());
1466 UpnpAddToAction(&action_container_ptr,
1467 ACTION_ADD_PORT_MAPPING,
1468 igd->getServiceType().c_str(),
1469 "NewLeaseDuration",
1470 "0");
1471
1472 action.reset(action_container_ptr);
1473
1474 int upnp_err = UpnpSendAction(ctrlptHandle_,
1475 igd->getControlURL().c_str(),
1476 igd->getServiceType().c_str(),
1477 nullptr,
1478 action.get(),
1479 &response_container_ptr);
1480 response.reset(response_container_ptr);
1481
1482 bool success = true;
1483
1484 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001485 // JAMI_WARN("PUPnP: Failed to send action %s for mapping %s. %d: %s",
1486 // ACTION_ADD_PORT_MAPPING,
1487 // mapping.toString().c_str(),
1488 // upnp_err,
1489 // UpnpGetErrorMessage(upnp_err));
1490 // JAMI_WARN("PUPnP: IGD ctrlUrl %s", igd->getControlURL().c_str());
1491 // JAMI_WARN("PUPnP: IGD service type %s", igd->getServiceType().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001492
1493 success = false;
1494 }
1495
1496 // Check if an error has occurred.
1497 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1498 if (not errorCode.empty()) {
1499 success = false;
1500 // Try to get the error description.
1501 std::string errorDescription;
1502 if (response) {
1503 errorDescription = getFirstDocItem(response.get(), "errorDescription");
1504 }
1505
Morteza Namvar5f639522023-07-04 17:08:58 -04001506 // JAMI_WARNING("PUPnP: {:s} returned with error: {:s} {:s}",
1507 // ACTION_ADD_PORT_MAPPING,
1508 // errorCode,
1509 // errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001510 }
1511 return success;
1512}
1513
1514bool
1515PUPnP::actionDeletePortMapping(const Mapping& mapping)
1516{
1517 CHECK_VALID_THREAD();
1518
1519 if (not clientRegistered_)
1520 return false;
1521
1522 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1523 if (not igdIn)
1524 return false;
1525
1526 // The requested IGD must be present in the list of local valid IGDs.
1527 auto igd = findMatchingIgd(igdIn->getControlURL());
1528
1529 if (not igd or not igd->isValid())
1530 return false;
1531
1532 // Action and response pointers.
1533 XMLDocument action(nullptr, ixmlDocument_free);
1534 IXML_Document* action_container_ptr = nullptr;
1535 XMLDocument response(nullptr, ixmlDocument_free);
1536 IXML_Document* response_container_ptr = nullptr;
1537
1538 // Set action sequence.
1539 UpnpAddToAction(&action_container_ptr,
1540 ACTION_DELETE_PORT_MAPPING,
1541 igd->getServiceType().c_str(),
1542 "NewRemoteHost",
1543 "");
1544 UpnpAddToAction(&action_container_ptr,
1545 ACTION_DELETE_PORT_MAPPING,
1546 igd->getServiceType().c_str(),
1547 "NewExternalPort",
1548 mapping.getExternalPortStr().c_str());
1549 UpnpAddToAction(&action_container_ptr,
1550 ACTION_DELETE_PORT_MAPPING,
1551 igd->getServiceType().c_str(),
1552 "NewProtocol",
1553 mapping.getTypeStr());
1554
1555 action.reset(action_container_ptr);
1556
1557 int upnp_err = UpnpSendAction(ctrlptHandle_,
1558 igd->getControlURL().c_str(),
1559 igd->getServiceType().c_str(),
1560 nullptr,
1561 action.get(),
1562 &response_container_ptr);
1563 response.reset(response_container_ptr);
1564
1565 bool success = true;
1566
1567 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001568 // JAMI_WARN("PUPnP: Failed to send action %s for mapping from %s. %d: %s",
1569 // ACTION_DELETE_PORT_MAPPING,
1570 // mapping.toString().c_str(),
1571 // upnp_err,
1572 // UpnpGetErrorMessage(upnp_err));
1573 // JAMI_WARN("PUPnP: IGD ctrlUrl %s", igd->getControlURL().c_str());
1574 // JAMI_WARN("PUPnP: IGD service type %s", igd->getServiceType().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001575
1576 success = false;
1577 }
1578
1579 if (not response) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001580 // JAMI_WARN("PUPnP: Failed to get response for %s", ACTION_DELETE_PORT_MAPPING);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001581 success = false;
1582 }
1583
1584 // Check if there is an error code.
1585 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1586 if (not errorCode.empty()) {
1587 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
Morteza Namvar5f639522023-07-04 17:08:58 -04001588 // JAMI_WARNING("PUPnP: {:s} returned with error: {:s}: {:s}",
1589 // ACTION_DELETE_PORT_MAPPING,
1590 // errorCode,
1591 // errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001592 success = false;
1593 }
1594
1595 return success;
1596}
1597
1598} // namespace upnp
1599} // namespace jami