blob: ad6ef9e37feb7bd2f32e58cbc71ae23decea1a9a [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"
18
19#include <opendht/thread_pool.h>
20#include <opendht/http.h>
21
Adrien Béraud1ae60aa2023-07-07 09:55:09 -040022namespace dhtnet {
Adrien Béraud612b55b2023-05-29 10:42:04 -040023namespace upnp {
24
25// Action identifiers.
26constexpr static const char* ACTION_ADD_PORT_MAPPING {"AddPortMapping"};
27constexpr static const char* ACTION_DELETE_PORT_MAPPING {"DeletePortMapping"};
28constexpr static const char* ACTION_GET_GENERIC_PORT_MAPPING_ENTRY {"GetGenericPortMappingEntry"};
29constexpr static const char* ACTION_GET_STATUS_INFO {"GetStatusInfo"};
30constexpr static const char* ACTION_GET_EXTERNAL_IP_ADDRESS {"GetExternalIPAddress"};
31
32// Error codes returned by router when trying to remove ports.
33constexpr static int ARRAY_IDX_INVALID = 713;
34constexpr static int CONFLICT_IN_MAPPING = 718;
35
36// Max number of IGD search attempts before failure.
37constexpr static unsigned int PUPNP_MAX_RESTART_SEARCH_RETRIES {3};
38// IGD search timeout (in seconds).
39constexpr static unsigned int SEARCH_TIMEOUT {60};
40// Base unit for the timeout between two successive IGD search.
41constexpr static auto PUPNP_SEARCH_RETRY_UNIT {std::chrono::seconds(10)};
42
43// Helper functions for xml parsing.
44static std::string_view
45getElementText(IXML_Node* node)
46{
47 if (node) {
48 if (IXML_Node* textNode = ixmlNode_getFirstChild(node))
49 if (const char* value = ixmlNode_getNodeValue(textNode))
50 return std::string_view(value);
51 }
52 return {};
53}
54
55static std::string_view
56getFirstDocItem(IXML_Document* doc, const char* item)
57{
58 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
59 nodeList(ixmlDocument_getElementsByTagName(doc, item), ixmlNodeList_free);
60 if (nodeList) {
61 // If there are several nodes which match the tag, we only want the first one.
62 return getElementText(ixmlNodeList_item(nodeList.get(), 0));
63 }
64 return {};
65}
66
67static std::string_view
68getFirstElementItem(IXML_Element* element, const char* item)
69{
70 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
71 nodeList(ixmlElement_getElementsByTagName(element, item), ixmlNodeList_free);
72 if (nodeList) {
73 // If there are several nodes which match the tag, we only want the first one.
74 return getElementText(ixmlNodeList_item(nodeList.get(), 0));
75 }
76 return {};
77}
78
79static bool
80errorOnResponse(IXML_Document* doc)
81{
82 if (not doc)
83 return true;
84
85 auto errorCode = getFirstDocItem(doc, "errorCode");
86 if (not errorCode.empty()) {
87 auto errorDescription = getFirstDocItem(doc, "errorDescription");
Morteza Namvar5f639522023-07-04 17:08:58 -040088 // JAMI_WARNING("PUPnP: Response contains error: {:s}: {:s}",
89 // errorCode,
90 // errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -040091 return true;
92 }
93 return false;
94}
95
96// UPNP class implementation
97
98PUPnP::PUPnP()
99{
Morteza Namvar5f639522023-07-04 17:08:58 -0400100 // JAMI_DBG("PUPnP: Creating instance [%p] ...", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400101 runOnPUPnPQueue([this] {
102 threadId_ = getCurrentThread();
Morteza Namvar5f639522023-07-04 17:08:58 -0400103 // JAMI_DBG("PUPnP: Instance [%p] created", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400104 });
105}
106
107PUPnP::~PUPnP()
108{
Morteza Namvar5f639522023-07-04 17:08:58 -0400109 // JAMI_DBG("PUPnP: Instance [%p] destroyed", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400110}
111
112void
113PUPnP::initUpnpLib()
114{
115 assert(not initialized_);
116
117 int upnp_err = UpnpInit2(nullptr, 0);
118
119 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400120 // JAMI_ERR("PUPnP: Can't initialize libupnp: %s", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400121 UpnpFinish();
122 initialized_ = false;
123 return;
124 }
125
126 // Disable embedded WebServer if any.
127 if (UpnpIsWebserverEnabled() == 1) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400128 // JAMI_WARN("PUPnP: Web-server is enabled. Disabling");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400129 UpnpEnableWebserver(0);
130 if (UpnpIsWebserverEnabled() == 1) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400131 // JAMI_ERR("PUPnP: Could not disable Web-server!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400132 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400133 // JAMI_DBG("PUPnP: Web-server successfully disabled");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400134 }
135 }
136
137 char* ip_address = UpnpGetServerIpAddress();
138 char* ip_address6 = nullptr;
139 unsigned short port = UpnpGetServerPort();
140 unsigned short port6 = 0;
141#if UPNP_ENABLE_IPV6
142 ip_address6 = UpnpGetServerIp6Address();
143 port6 = UpnpGetServerPort6();
144#endif
145 if (ip_address6 and port6)
Morteza Namvar5f639522023-07-04 17:08:58 -0400146 // JAMI_DBG("PUPnP: Initialized on %s:%u | %s:%u", ip_address, port, ip_address6, port6);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400147 else
Morteza Namvar5f639522023-07-04 17:08:58 -0400148 // JAMI_DBG("PUPnP: Initialized on %s:%u", ip_address, port);
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
168 CHECK_VALID_THREAD();
169
170 // Register Upnp control point.
171 int upnp_err = UpnpRegisterClient(ctrlPtCallback, this, &ctrlptHandle_);
172 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400173 // JAMI_ERR("PUPnP: Can't register client: %s", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400174 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400175 // JAMI_DBG("PUPnP: Successfully registered client");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400176 clientRegistered_ = true;
177 }
178}
179
180void
181PUPnP::setObserver(UpnpMappingObserver* obs)
182{
183 if (not isValidThread()) {
184 runOnPUPnPQueue([w = weak(), obs] {
185 if (auto upnpThis = w.lock()) {
186 upnpThis->setObserver(obs);
187 }
188 });
189 return;
190 }
191
Morteza Namvar5f639522023-07-04 17:08:58 -0400192 // JAMI_DBG("PUPnP: Setting observer to %p", obs);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400193
194 observer_ = obs;
195}
196
197const IpAddr
198PUPnP::getHostAddress() const
199{
200 std::lock_guard<std::mutex> lock(pupnpMutex_);
201 return hostAddress_;
202}
203
204void
205PUPnP::terminate(std::condition_variable& cv)
206{
Morteza Namvar5f639522023-07-04 17:08:58 -0400207 // JAMI_DBG("PUPnP: Terminate instance %p", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400208
209 clientRegistered_ = false;
210 observer_ = nullptr;
211
212 UpnpUnRegisterClient(ctrlptHandle_);
213
214 if (initialized_) {
215 if (UpnpFinish() != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400216 // JAMI_ERR("PUPnP: Failed to properly close lib-upnp");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400217 }
218
219 initialized_ = false;
220 }
221
222 // Clear all the lists.
223 discoveredIgdList_.clear();
224
225 {
226 std::lock_guard<std::mutex> lock(pupnpMutex_);
227 validIgdList_.clear();
228 shutdownComplete_ = true;
229 cv.notify_one();
230 }
231}
232
233void
234PUPnP::terminate()
235{
236 std::unique_lock<std::mutex> lk(pupnpMutex_);
237 std::condition_variable cv {};
238
239 runOnPUPnPQueue([w = weak(), &cv = cv] {
240 if (auto upnpThis = w.lock()) {
241 upnpThis->terminate(cv);
242 }
243 });
244
245 if (cv.wait_for(lk, std::chrono::seconds(10), [this] { return shutdownComplete_; })) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400246 // JAMI_DBG("PUPnP: Shutdown completed");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400247 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400248 // JAMI_ERR("PUPnP: Shutdown timed-out");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400249 // Force stop if the shutdown take too much time.
250 shutdownComplete_ = true;
251 }
252}
253
254void
255PUPnP::searchForDevices()
256{
257 CHECK_VALID_THREAD();
258
Morteza Namvar5f639522023-07-04 17:08:58 -0400259 // JAMI_DBG("PUPnP: Send IGD search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400260
261 // Send out search for multiple types of devices, as some routers may possibly
262 // only reply to one.
263
264 auto err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_ROOT_DEVICE, this);
265 if (err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400266 // JAMI_WARN("PUPnP: Send search for UPNP_ROOT_DEVICE failed. Error %d: %s",
267 // err,
268 // UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400269 }
270
271 err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_IGD_DEVICE, this);
272 if (err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400273 // JAMI_WARN("PUPnP: Send search for UPNP_IGD_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_WANIP_SERVICE, this);
279 if (err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400280 // JAMI_WARN("PUPnP: Send search for UPNP_WANIP_SERVICE 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_WANPPP_SERVICE, this);
286 if (err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400287 // JAMI_WARN("PUPnP: Send search for UPNP_WANPPP_SERVICE failed. Error %d: %s",
288 // err,
289 // UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400290 }
291}
292
293void
294PUPnP::clearIgds()
295{
296 if (not isValidThread()) {
297 runOnPUPnPQueue([w = weak()] {
298 if (auto upnpThis = w.lock()) {
299 upnpThis->clearIgds();
300 }
301 });
302 return;
303 }
304
Morteza Namvar5f639522023-07-04 17:08:58 -0400305 // JAMI_DBG("PUPnP: clearing IGDs and devices lists");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400306
307 if (searchForIgdTimer_)
308 searchForIgdTimer_->cancel();
309
310 igdSearchCounter_ = 0;
311
312 {
313 std::lock_guard<std::mutex> lock(pupnpMutex_);
314 for (auto const& igd : validIgdList_) {
315 igd->setValid(false);
316 }
317 validIgdList_.clear();
318 hostAddress_ = {};
319 }
320
321 discoveredIgdList_.clear();
322}
323
324void
325PUPnP::searchForIgd()
326{
327 if (not isValidThread()) {
328 runOnPUPnPQueue([w = weak()] {
329 if (auto upnpThis = w.lock()) {
330 upnpThis->searchForIgd();
331 }
332 });
333 return;
334 }
335
336 // Update local address before searching.
337 updateHostAddress();
338
339 if (isReady()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400340 // JAMI_DBG("PUPnP: Already have a valid IGD. Skip the search request");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400341 return;
342 }
343
344 if (igdSearchCounter_++ >= PUPNP_MAX_RESTART_SEARCH_RETRIES) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400345 // JAMI_WARN("PUPnP: Setup failed after %u trials. PUPnP will be disabled!",
346 // PUPNP_MAX_RESTART_SEARCH_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400347 return;
348 }
349
Morteza Namvar5f639522023-07-04 17:08:58 -0400350 // JAMI_DBG("PUPnP: Start search for IGD: attempt %u", igdSearchCounter_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400351
352 // Do not init if the host is not valid. Otherwise, the init will fail
353 // anyway and may put libupnp in an unstable state (mainly deadlocks)
354 // even if the UpnpFinish() method is called.
355 if (not hasValidHostAddress()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400356 // JAMI_WARN("PUPnP: Host address is invalid. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400357 } else {
358 // Init and register if needed
359 if (not initialized_) {
360 initUpnpLib();
361 }
362 if (initialized_ and not clientRegistered_) {
363 registerClient();
364 }
365 // Start searching
366 if (clientRegistered_) {
367 assert(initialized_);
368 searchForDevices();
369 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400370 // JAMI_WARN("PUPnP: PUPNP not fully setup. Skipping the IGD search");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400371 }
372 }
373
374 // Cancel the current timer (if any) and re-schedule.
375 // The connectivity change may be received while the the local
376 // interface is not fully setup. The rescheduling typically
377 // usefull to mitigate this race.
378 if (searchForIgdTimer_)
379 searchForIgdTimer_->cancel();
380
381 searchForIgdTimer_ = getUpnContextScheduler()->scheduleIn(
382 [w = weak()] {
383 if (auto upnpThis = w.lock())
384 upnpThis->searchForIgd();
385 },
386 PUPNP_SEARCH_RETRY_UNIT * igdSearchCounter_);
387}
388
389std::list<std::shared_ptr<IGD>>
390PUPnP::getIgdList() const
391{
392 std::lock_guard<std::mutex> lock(pupnpMutex_);
393 std::list<std::shared_ptr<IGD>> igdList;
394 for (auto& it : validIgdList_) {
395 // Return only active IGDs.
396 if (it->isValid()) {
397 igdList.emplace_back(it);
398 }
399 }
400 return igdList;
401}
402
403bool
404PUPnP::isReady() const
405{
406 // Must at least have a valid local address.
407 if (not getHostAddress() or getHostAddress().isLoopback())
408 return false;
409
410 return hasValidIgd();
411}
412
413bool
414PUPnP::hasValidIgd() const
415{
416 std::lock_guard<std::mutex> lock(pupnpMutex_);
417 for (auto& it : validIgdList_) {
418 if (it->isValid()) {
419 return true;
420 }
421 }
422 return false;
423}
424
425void
426PUPnP::updateHostAddress()
427{
428 std::lock_guard<std::mutex> lock(pupnpMutex_);
429 hostAddress_ = ip_utils::getLocalAddr(AF_INET);
430}
431
432bool
433PUPnP::hasValidHostAddress()
434{
435 std::lock_guard<std::mutex> lock(pupnpMutex_);
436 return hostAddress_ and not hostAddress_.isLoopback();
437}
438
439void
440PUPnP::incrementErrorsCounter(const std::shared_ptr<IGD>& igd)
441{
442 if (not igd or not igd->isValid())
443 return;
444 if (not igd->incrementErrorsCounter()) {
445 // Disable this IGD.
446 igd->setValid(false);
447 // Notify the listener.
448 if (observer_)
449 observer_->onIgdUpdated(igd, UpnpIgdEvent::INVALID_STATE);
450 }
451}
452
453bool
454PUPnP::validateIgd(const std::string& location, IXML_Document* doc_container_ptr)
455{
456 CHECK_VALID_THREAD();
457
458 assert(doc_container_ptr != nullptr);
459
460 XMLDocument document(doc_container_ptr, ixmlDocument_free);
461 auto descDoc = document.get();
462 // Check device type.
463 auto deviceType = getFirstDocItem(descDoc, "deviceType");
464 if (deviceType != UPNP_IGD_DEVICE) {
465 // Device type not IGD.
466 return false;
467 }
468
469 std::shared_ptr<UPnPIGD> igd_candidate = parseIgd(descDoc, location);
470 if (not igd_candidate) {
471 // No valid IGD candidate.
472 return false;
473 }
474
Morteza Namvar5f639522023-07-04 17:08:58 -0400475 // JAMI_DBG("PUPnP: Validating the IGD candidate [UDN: %s]\n"
476 // " Name : %s\n"
477 // " Service Type : %s\n"
478 // " Service ID : %s\n"
479 // " Base URL : %s\n"
480 // " Location URL : %s\n"
481 // " control URL : %s\n"
482 // " Event URL : %s",
483 // igd_candidate->getUID().c_str(),
484 // igd_candidate->getFriendlyName().c_str(),
485 // igd_candidate->getServiceType().c_str(),
486 // igd_candidate->getServiceId().c_str(),
487 // igd_candidate->getBaseURL().c_str(),
488 // igd_candidate->getLocationURL().c_str(),
489 // igd_candidate->getControlURL().c_str(),
490 // igd_candidate->getEventSubURL().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400491
492 // Check if IGD is connected.
493 if (not actionIsIgdConnected(*igd_candidate)) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400494 // JAMI_WARN("PUPnP: IGD candidate %s is not connected", igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400495 return false;
496 }
497
498 // Validate external Ip.
499 igd_candidate->setPublicIp(actionGetExternalIP(*igd_candidate));
500 if (igd_candidate->getPublicIp().toString().empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400501 // JAMI_WARN("PUPnP: IGD candidate %s has no valid external Ip",
502 // igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400503 return false;
504 }
505
506 // Validate internal Ip.
507 if (igd_candidate->getBaseURL().empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400508 // JAMI_WARN("PUPnP: IGD candidate %s has no valid internal Ip",
509 // igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400510 return false;
511 }
512
513 // Typically the IGD local address should be extracted from the XML
514 // document (e.g. parsing the base URL). For simplicity, we assume
515 // that it matches the gateway as seen by the local interface.
516 if (const auto& localGw = ip_utils::getLocalGateway()) {
517 igd_candidate->setLocalIp(localGw);
518 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400519 // JAMI_WARN("PUPnP: Could not set internal address for IGD candidate %s",
520 // igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400521 return false;
522 }
523
524 // Store info for subscription.
525 std::string eventSub = igd_candidate->getEventSubURL();
526
527 {
528 // Add the IGD if not already present in the list.
529 std::lock_guard<std::mutex> lock(pupnpMutex_);
530 for (auto& igd : validIgdList_) {
531 // Must not be a null pointer
532 assert(igd.get() != nullptr);
533 if (*igd == *igd_candidate) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400534 // JAMI_DBG("PUPnP: Device [%s] with int/ext addresses [%s:%s] is already in the list "
535 // "of valid IGDs",
536 // igd_candidate->getUID().c_str(),
537 // igd_candidate->toString().c_str(),
538 // igd_candidate->getPublicIp().toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400539 return true;
540 }
541 }
542 }
543
544 // We have a valid IGD
545 igd_candidate->setValid(true);
546
Morteza Namvar5f639522023-07-04 17:08:58 -0400547 // JAMI_DBG("PUPnP: Added a new IGD [%s] to the list of valid IGDs",
548 // igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400549
Morteza Namvar5f639522023-07-04 17:08:58 -0400550 // JAMI_DBG("PUPnP: New IGD addresses [int: %s - ext: %s]",
551 // igd_candidate->toString().c_str(),
552 // igd_candidate->getPublicIp().toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400553
554 // Subscribe to IGD events.
555 int upnp_err = UpnpSubscribeAsync(ctrlptHandle_,
556 eventSub.c_str(),
557 UPNP_INFINITE,
558 subEventCallback,
559 this);
560 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400561 // JAMI_WARN("PUPnP: Failed to send subscribe request to %s: error %i - %s",
562 // igd_candidate->getUID().c_str(),
563 // upnp_err,
564 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400565 // return false;
566 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400567 // JAMI_DBG("PUPnP: Successfully subscribed to IGD %s", igd_candidate->getUID().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400568 }
569
570 {
571 // This is a new (and hopefully valid) IGD.
572 std::lock_guard<std::mutex> lock(pupnpMutex_);
573 validIgdList_.emplace_back(igd_candidate);
574 }
575
576 // Report to the listener.
577 runOnUpnpContextQueue([w = weak(), igd_candidate] {
578 if (auto upnpThis = w.lock()) {
579 if (upnpThis->observer_)
580 upnpThis->observer_->onIgdUpdated(igd_candidate, UpnpIgdEvent::ADDED);
581 }
582 });
583
584 return true;
585}
586
587void
588PUPnP::requestMappingAdd(const Mapping& mapping)
589{
590 runOnPUPnPQueue([w = weak(), mapping] {
591 if (auto upnpThis = w.lock()) {
592 if (not upnpThis->isRunning())
593 return;
594 Mapping mapRes(mapping);
595 if (upnpThis->actionAddPortMapping(mapRes)) {
596 mapRes.setState(MappingState::OPEN);
597 mapRes.setInternalAddress(upnpThis->getHostAddress().toString());
598 upnpThis->processAddMapAction(mapRes);
599 } else {
600 upnpThis->incrementErrorsCounter(mapRes.getIgd());
601 mapRes.setState(MappingState::FAILED);
602 upnpThis->processRequestMappingFailure(mapRes);
603 }
604 }
605 });
606}
607
608void
609PUPnP::requestMappingRemove(const Mapping& mapping)
610{
611 // Send remove request using the matching IGD
612 runOnPUPnPQueue([w = weak(), mapping] {
613 if (auto upnpThis = w.lock()) {
614 // Abort if we are shutting down.
615 if (not upnpThis->isRunning())
616 return;
617 if (upnpThis->actionDeletePortMapping(mapping)) {
618 upnpThis->processRemoveMapAction(mapping);
619 } else {
620 assert(mapping.getIgd());
621 // Dont need to report in case of failure.
622 upnpThis->incrementErrorsCounter(mapping.getIgd());
623 }
624 }
625 });
626}
627
628std::shared_ptr<UPnPIGD>
629PUPnP::findMatchingIgd(const std::string& ctrlURL) const
630{
631 std::lock_guard<std::mutex> lock(pupnpMutex_);
632
633 auto iter = std::find_if(validIgdList_.begin(),
634 validIgdList_.end(),
635 [&ctrlURL](const std::shared_ptr<IGD>& igd) {
636 if (auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd)) {
637 return upnpIgd->getControlURL() == ctrlURL;
638 }
639 return false;
640 });
641
642 if (iter == validIgdList_.end()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400643 // JAMI_WARN("PUPnP: Did not find the IGD matching ctrl URL [%s]", ctrlURL.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400644 return {};
645 }
646
647 return std::dynamic_pointer_cast<UPnPIGD>(*iter);
648}
649
650void
651PUPnP::processAddMapAction(const Mapping& map)
652{
653 CHECK_VALID_THREAD();
654
655 if (observer_ == nullptr)
656 return;
657
658 runOnUpnpContextQueue([w = weak(), map] {
659 if (auto upnpThis = w.lock()) {
660 if (upnpThis->observer_)
661 upnpThis->observer_->onMappingAdded(map.getIgd(), std::move(map));
662 }
663 });
664}
665
666void
667PUPnP::processRequestMappingFailure(const Mapping& map)
668{
669 CHECK_VALID_THREAD();
670
671 if (observer_ == nullptr)
672 return;
673
674 runOnUpnpContextQueue([w = weak(), map] {
675 if (auto upnpThis = w.lock()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400676 // JAMI_DBG("PUPnP: Failed to request mapping %s", map.toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400677 if (upnpThis->observer_)
678 upnpThis->observer_->onMappingRequestFailed(map);
679 }
680 });
681}
682
683void
684PUPnP::processRemoveMapAction(const Mapping& map)
685{
686 CHECK_VALID_THREAD();
687
688 if (observer_ == nullptr)
689 return;
690
691 runOnUpnpContextQueue([map, obs = observer_] {
Morteza Namvar5f639522023-07-04 17:08:58 -0400692 // JAMI_DBG("PUPnP: Closed mapping %s", map.toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400693 obs->onMappingRemoved(map.getIgd(), std::move(map));
694 });
695}
696
697const char*
698PUPnP::eventTypeToString(Upnp_EventType eventType)
699{
700 switch (eventType) {
701 case UPNP_CONTROL_ACTION_REQUEST:
702 return "UPNP_CONTROL_ACTION_REQUEST";
703 case UPNP_CONTROL_ACTION_COMPLETE:
704 return "UPNP_CONTROL_ACTION_COMPLETE";
705 case UPNP_CONTROL_GET_VAR_REQUEST:
706 return "UPNP_CONTROL_GET_VAR_REQUEST";
707 case UPNP_CONTROL_GET_VAR_COMPLETE:
708 return "UPNP_CONTROL_GET_VAR_COMPLETE";
709 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
710 return "UPNP_DISCOVERY_ADVERTISEMENT_ALIVE";
711 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
712 return "UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE";
713 case UPNP_DISCOVERY_SEARCH_RESULT:
714 return "UPNP_DISCOVERY_SEARCH_RESULT";
715 case UPNP_DISCOVERY_SEARCH_TIMEOUT:
716 return "UPNP_DISCOVERY_SEARCH_TIMEOUT";
717 case UPNP_EVENT_SUBSCRIPTION_REQUEST:
718 return "UPNP_EVENT_SUBSCRIPTION_REQUEST";
719 case UPNP_EVENT_RECEIVED:
720 return "UPNP_EVENT_RECEIVED";
721 case UPNP_EVENT_RENEWAL_COMPLETE:
722 return "UPNP_EVENT_RENEWAL_COMPLETE";
723 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
724 return "UPNP_EVENT_SUBSCRIBE_COMPLETE";
725 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE:
726 return "UPNP_EVENT_UNSUBSCRIBE_COMPLETE";
727 case UPNP_EVENT_AUTORENEWAL_FAILED:
728 return "UPNP_EVENT_AUTORENEWAL_FAILED";
729 case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
730 return "UPNP_EVENT_SUBSCRIPTION_EXPIRED";
731 default:
732 return "Unknown UPNP Event";
733 }
734}
735
736int
737PUPnP::ctrlPtCallback(Upnp_EventType event_type, const void* event, void* user_data)
738{
739 auto pupnp = static_cast<PUPnP*>(user_data);
740
741 if (pupnp == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400742 // JAMI_WARN("PUPnP: Control point callback without PUPnP");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400743 return UPNP_E_SUCCESS;
744 }
745
746 auto upnpThis = pupnp->weak().lock();
747
748 if (not upnpThis)
749 return UPNP_E_SUCCESS;
750
751 // Ignore if already unregistered.
752 if (not upnpThis->clientRegistered_)
753 return UPNP_E_SUCCESS;
754
755 // Process the callback.
756 return upnpThis->handleCtrlPtUPnPEvents(event_type, event);
757}
758
759PUPnP::CtrlAction
760PUPnP::getAction(const char* xmlNode)
761{
762 if (strstr(xmlNode, ACTION_ADD_PORT_MAPPING)) {
763 return CtrlAction::ADD_PORT_MAPPING;
764 } else if (strstr(xmlNode, ACTION_DELETE_PORT_MAPPING)) {
765 return CtrlAction::DELETE_PORT_MAPPING;
766 } else if (strstr(xmlNode, ACTION_GET_GENERIC_PORT_MAPPING_ENTRY)) {
767 return CtrlAction::GET_GENERIC_PORT_MAPPING_ENTRY;
768 } else if (strstr(xmlNode, ACTION_GET_STATUS_INFO)) {
769 return CtrlAction::GET_STATUS_INFO;
770 } else if (strstr(xmlNode, ACTION_GET_EXTERNAL_IP_ADDRESS)) {
771 return CtrlAction::GET_EXTERNAL_IP_ADDRESS;
772 } else {
773 return CtrlAction::UNKNOWN;
774 }
775}
776
777void
778PUPnP::processDiscoverySearchResult(const std::string& cpDeviceId,
779 const std::string& igdLocationUrl,
780 const IpAddr& dstAddr)
781{
782 CHECK_VALID_THREAD();
783
784 // Update host address if needed.
785 if (not hasValidHostAddress())
786 updateHostAddress();
787
788 // The host address must be valid to proceed.
789 if (not hasValidHostAddress()) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400790 // JAMI_WARN("PUPnP: Local address is invalid. Ignore search result for now!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400791 return;
792 }
793
794 // Use the device ID and the URL as ID. This is necessary as some
795 // IGDs may have the same device ID but different URLs.
796
797 auto igdId = cpDeviceId + " url: " + igdLocationUrl;
798
799 if (not discoveredIgdList_.emplace(igdId).second) {
800 // JAMI_WARN("PUPnP: IGD [%s] already in the list", igdId.c_str());
801 return;
802 }
803
Morteza Namvar5f639522023-07-04 17:08:58 -0400804 // JAMI_DBG("PUPnP: Discovered a new IGD [%s]", igdId.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400805
806 // NOTE: here, we check if the location given is related to the source address.
807 // If it's not the case, it's certainly a router plugged in the network, but not
808 // related to this network. So the given location will be unreachable and this
809 // will cause some timeout.
810
811 // Only check the IP address (ignore the port number).
812 dht::http::Url url(igdLocationUrl);
813 if (IpAddr(url.host).toString(false) != dstAddr.toString(false)) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400814 // JAMI_DBG("PUPnP: Returned location %s does not match the source address %s",
815 // IpAddr(url.host).toString(true, true).c_str(),
816 // dstAddr.toString(true, true).c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400817 return;
818 }
819
820 // Run a separate thread to prevent blocking this thread
821 // if the IGD HTTP server is not responsive.
822 dht::ThreadPool::io().run([w = weak(), igdLocationUrl] {
823 if (auto upnpThis = w.lock()) {
824 upnpThis->downLoadIgdDescription(igdLocationUrl);
825 }
826 });
827}
828
829void
830PUPnP::downLoadIgdDescription(const std::string& locationUrl)
831{
832 IXML_Document* doc_container_ptr = nullptr;
833 int upnp_err = UpnpDownloadXmlDoc(locationUrl.c_str(), &doc_container_ptr);
834
835 if (upnp_err != UPNP_E_SUCCESS or not doc_container_ptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400836 // JAMI_WARN("PUPnP: Error downloading device XML document from %s -> %s",
837 // locationUrl.c_str(),
838 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400839 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -0400840 // JAMI_DBG("PUPnP: Succeeded to download device XML document from %s", locationUrl.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400841 runOnPUPnPQueue([w = weak(), url = locationUrl, doc_container_ptr] {
842 if (auto upnpThis = w.lock()) {
843 upnpThis->validateIgd(url, doc_container_ptr);
844 }
845 });
846 }
847}
848
849void
850PUPnP::processDiscoveryAdvertisementByebye(const std::string& cpDeviceId)
851{
852 CHECK_VALID_THREAD();
853
854 discoveredIgdList_.erase(cpDeviceId);
855
856 std::shared_ptr<IGD> igd;
857 {
858 std::lock_guard<std::mutex> lk(pupnpMutex_);
859 for (auto it = validIgdList_.begin(); it != validIgdList_.end();) {
860 if ((*it)->getUID() == cpDeviceId) {
861 igd = *it;
Morteza Namvar5f639522023-07-04 17:08:58 -0400862 // JAMI_DBG("PUPnP: Received [%s] for IGD [%s] %s. Will be removed.",
863 // PUPnP::eventTypeToString(UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE),
864 // igd->getUID().c_str(),
865 // igd->toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400866 igd->setValid(false);
867 // Remove the IGD.
868 it = validIgdList_.erase(it);
869 break;
870 } else {
871 it++;
872 }
873 }
874 }
875
876 // Notify the listener.
877 if (observer_ and igd) {
878 observer_->onIgdUpdated(igd, UpnpIgdEvent::REMOVED);
879 }
880}
881
882void
883PUPnP::processDiscoverySubscriptionExpired(Upnp_EventType event_type, const std::string& eventSubUrl)
884{
885 CHECK_VALID_THREAD();
886
887 std::lock_guard<std::mutex> lk(pupnpMutex_);
888 for (auto& it : validIgdList_) {
889 if (auto igd = std::dynamic_pointer_cast<UPnPIGD>(it)) {
890 if (igd->getEventSubURL() == eventSubUrl) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400891 // JAMI_DBG("PUPnP: Received [%s] event for IGD [%s] %s. Request a new subscribe.",
892 // PUPnP::eventTypeToString(event_type),
893 // igd->getUID().c_str(),
894 // igd->toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400895 UpnpSubscribeAsync(ctrlptHandle_,
896 eventSubUrl.c_str(),
897 UPNP_INFINITE,
898 subEventCallback,
899 this);
900 break;
901 }
902 }
903 }
904}
905
906int
907PUPnP::handleCtrlPtUPnPEvents(Upnp_EventType event_type, const void* event)
908{
909 switch (event_type) {
910 // "ALIVE" events are processed as "SEARCH RESULT". It might be usefull
911 // if "SEARCH RESULT" was missed.
912 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
913 case UPNP_DISCOVERY_SEARCH_RESULT: {
914 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
915
916 // First check the error code.
917 auto upnp_status = UpnpDiscovery_get_ErrCode(d_event);
918 if (upnp_status != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400919 // JAMI_ERR("PUPnP: UPNP discovery is in erroneous state: %s",
920 // UpnpGetErrorMessage(upnp_status));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400921 break;
922 }
923
924 // Parse the event's data.
925 std::string deviceId {UpnpDiscovery_get_DeviceID_cstr(d_event)};
926 std::string location {UpnpDiscovery_get_Location_cstr(d_event)};
927 IpAddr dstAddr(*(const pj_sockaddr*) (UpnpDiscovery_get_DestAddr(d_event)));
928 runOnPUPnPQueue([w = weak(),
929 deviceId = std::move(deviceId),
930 location = std::move(location),
931 dstAddr = std::move(dstAddr)] {
932 if (auto upnpThis = w.lock()) {
933 upnpThis->processDiscoverySearchResult(deviceId, location, dstAddr);
934 }
935 });
936 break;
937 }
938 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: {
939 const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
940
941 std::string deviceId(UpnpDiscovery_get_DeviceID_cstr(d_event));
942
943 // Process the response on the main thread.
944 runOnPUPnPQueue([w = weak(), deviceId = std::move(deviceId)] {
945 if (auto upnpThis = w.lock()) {
946 upnpThis->processDiscoveryAdvertisementByebye(deviceId);
947 }
948 });
949 break;
950 }
951 case UPNP_DISCOVERY_SEARCH_TIMEOUT: {
952 // Even if the discovery search is successful, it's normal to receive
953 // time-out events. This because we send search requests using various
954 // device types, which some of them may not return a response.
955 break;
956 }
957 case UPNP_EVENT_RECEIVED: {
958 // Nothing to do.
959 break;
960 }
961 // Treat failed autorenewal like an expired subscription.
962 case UPNP_EVENT_AUTORENEWAL_FAILED:
963 case UPNP_EVENT_SUBSCRIPTION_EXPIRED: // This event will occur only if autorenewal is disabled.
964 {
Morteza Namvar5f639522023-07-04 17:08:58 -0400965 // JAMI_WARN("PUPnP: Received Subscription Event %s", eventTypeToString(event_type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400966 const UpnpEventSubscribe* es_event = (const UpnpEventSubscribe*) event;
967 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400968 // JAMI_WARN("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400969 break;
970 }
971 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
972
973 // Process the response on the main thread.
974 runOnPUPnPQueue([w = weak(), event_type, publisherUrl = std::move(publisherUrl)] {
975 if (auto upnpThis = w.lock()) {
976 upnpThis->processDiscoverySubscriptionExpired(event_type, publisherUrl);
977 }
978 });
979 break;
980 }
981 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
982 case UPNP_EVENT_UNSUBSCRIBE_COMPLETE: {
983 UpnpEventSubscribe* es_event = (UpnpEventSubscribe*) event;
984 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400985 // JAMI_WARN("PUPnP: Received Subscription Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400986 } else {
987 UpnpEventSubscribe_delete(es_event);
988 }
989 break;
990 }
991 case UPNP_CONTROL_ACTION_COMPLETE: {
992 const UpnpActionComplete* a_event = (const UpnpActionComplete*) event;
993 if (a_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -0400994 // JAMI_WARN("PUPnP: Received Action Complete Event with null pointer");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400995 break;
996 }
997 auto res = UpnpActionComplete_get_ErrCode(a_event);
998 if (res != UPNP_E_SUCCESS and res != UPNP_E_TIMEDOUT) {
999 auto err = UpnpActionComplete_get_ErrCode(a_event);
Morteza Namvar5f639522023-07-04 17:08:58 -04001000 // JAMI_WARN("PUPnP: Received Action Complete error %i %s", err, UpnpGetErrorMessage(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001001 } else {
1002 auto actionRequest = UpnpActionComplete_get_ActionRequest(a_event);
1003 // Abort if there is no action to process.
1004 if (actionRequest == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001005 // JAMI_WARN("PUPnP: Can't get the Action Request data from the event");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001006 break;
1007 }
1008
1009 auto actionResult = UpnpActionComplete_get_ActionResult(a_event);
1010 if (actionResult != nullptr) {
1011 ixmlDocument_free(actionResult);
1012 } else {
Morteza Namvar5f639522023-07-04 17:08:58 -04001013 // JAMI_WARN("PUPnP: Action Result document not found");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001014 }
1015 }
1016 break;
1017 }
1018 default: {
Morteza Namvar5f639522023-07-04 17:08:58 -04001019 // JAMI_WARN("PUPnP: Unhandled Control Point event");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001020 break;
1021 }
1022 }
1023
1024 return UPNP_E_SUCCESS;
1025}
1026
1027int
1028PUPnP::subEventCallback(Upnp_EventType event_type, const void* event, void* user_data)
1029{
1030 if (auto pupnp = static_cast<PUPnP*>(user_data))
1031 return pupnp->handleSubscriptionUPnPEvent(event_type, event);
Morteza Namvar5f639522023-07-04 17:08:58 -04001032 // JAMI_WARN("PUPnP: Subscription callback without service Id string");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001033 return 0;
1034}
1035
1036int
1037PUPnP::handleSubscriptionUPnPEvent(Upnp_EventType, const void* event)
1038{
1039 UpnpEventSubscribe* es_event = static_cast<UpnpEventSubscribe*>(const_cast<void*>(event));
1040
1041 if (es_event == nullptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001042 // JAMI_ERR("PUPnP: Unexpected null pointer!");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001043 return UPNP_E_INVALID_ARGUMENT;
1044 }
1045 std::string publisherUrl(UpnpEventSubscribe_get_PublisherUrl_cstr(es_event));
1046 int upnp_err = UpnpEventSubscribe_get_ErrCode(es_event);
1047 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001048 // JAMI_WARN("PUPnP: Subscription error %s from %s",
1049 // UpnpGetErrorMessage(upnp_err),
1050 // publisherUrl.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001051 return upnp_err;
1052 }
1053
1054 return UPNP_E_SUCCESS;
1055}
1056
1057std::unique_ptr<UPnPIGD>
1058PUPnP::parseIgd(IXML_Document* doc, std::string locationUrl)
1059{
1060 if (not(doc and locationUrl.c_str()))
1061 return nullptr;
1062
1063 // Check the UDN to see if its already in our device list.
1064 std::string UDN(getFirstDocItem(doc, "UDN"));
1065 if (UDN.empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001066 // JAMI_WARN("PUPnP: could not find UDN in description document of device");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001067 return nullptr;
1068 } else {
1069 std::lock_guard<std::mutex> lk(pupnpMutex_);
1070 for (auto& it : validIgdList_) {
1071 if (it->getUID() == UDN) {
1072 // We already have this device in our list.
1073 return nullptr;
1074 }
1075 }
1076 }
1077
Morteza Namvar5f639522023-07-04 17:08:58 -04001078 // JAMI_DBG("PUPnP: Found new device [%s]", UDN.c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001079
1080 std::unique_ptr<UPnPIGD> new_igd;
1081 int upnp_err;
1082
1083 // Get friendly name.
1084 std::string friendlyName(getFirstDocItem(doc, "friendlyName"));
1085
1086 // Get base URL.
1087 std::string baseURL(getFirstDocItem(doc, "URLBase"));
1088 if (baseURL.empty())
1089 baseURL = locationUrl;
1090
1091 // Get list of services defined by serviceType.
1092 std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> serviceList(nullptr,
1093 ixmlNodeList_free);
1094 serviceList.reset(ixmlDocument_getElementsByTagName(doc, "serviceType"));
1095 unsigned long list_length = ixmlNodeList_length(serviceList.get());
1096
1097 // Go through the "serviceType" nodes until we find the the correct service type.
1098 for (unsigned long node_idx = 0; node_idx < list_length; node_idx++) {
1099 IXML_Node* serviceType_node = ixmlNodeList_item(serviceList.get(), node_idx);
1100 std::string serviceType(getElementText(serviceType_node));
1101
1102 // Only check serviceType of WANIPConnection or WANPPPConnection.
1103 if (serviceType != UPNP_WANIP_SERVICE
1104 && serviceType != UPNP_WANPPP_SERVICE) {
1105 // IGD is not WANIP or WANPPP service. Going to next node.
1106 continue;
1107 }
1108
1109 // Get parent node.
1110 IXML_Node* service_node = ixmlNode_getParentNode(serviceType_node);
1111 if (not service_node) {
1112 // IGD serviceType has no parent node. Going to next node.
1113 continue;
1114 }
1115
1116 // Perform sanity check. The parent node should be called "service".
1117 if (strcmp(ixmlNode_getNodeName(service_node), "service") != 0) {
1118 // IGD "serviceType" parent node is not called "service". Going to next node.
1119 continue;
1120 }
1121
1122 // Get serviceId.
1123 IXML_Element* service_element = (IXML_Element*) service_node;
1124 std::string serviceId(getFirstElementItem(service_element, "serviceId"));
1125 if (serviceId.empty()) {
1126 // IGD "serviceId" is empty. Going to next node.
1127 continue;
1128 }
1129
1130 // Get the relative controlURL and turn it into absolute address using the URLBase.
1131 std::string controlURL(getFirstElementItem(service_element, "controlURL"));
1132 if (controlURL.empty()) {
1133 // IGD control URL is empty. Going to next node.
1134 continue;
1135 }
1136
1137 char* absolute_control_url = nullptr;
1138 upnp_err = UpnpResolveURL2(baseURL.c_str(), controlURL.c_str(), &absolute_control_url);
1139 if (upnp_err == UPNP_E_SUCCESS)
1140 controlURL = absolute_control_url;
1141 else
Morteza Namvar5f639522023-07-04 17:08:58 -04001142 // JAMI_WARN("PUPnP: Error resolving absolute controlURL -> %s",
1143 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001144
1145 std::free(absolute_control_url);
1146
1147 // Get the relative eventSubURL and turn it into absolute address using the URLBase.
1148 std::string eventSubURL(getFirstElementItem(service_element, "eventSubURL"));
1149 if (eventSubURL.empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001150 // JAMI_WARN("PUPnP: IGD event sub URL is empty. Going to next node");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001151 continue;
1152 }
1153
1154 char* absolute_event_sub_url = nullptr;
1155 upnp_err = UpnpResolveURL2(baseURL.c_str(), eventSubURL.c_str(), &absolute_event_sub_url);
1156 if (upnp_err == UPNP_E_SUCCESS)
1157 eventSubURL = absolute_event_sub_url;
1158 else
Morteza Namvar5f639522023-07-04 17:08:58 -04001159 // JAMI_WARN("PUPnP: Error resolving absolute eventSubURL -> %s",
1160 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001161
1162 std::free(absolute_event_sub_url);
1163
1164 new_igd.reset(new UPnPIGD(std::move(UDN),
1165 std::move(baseURL),
1166 std::move(friendlyName),
1167 std::move(serviceType),
1168 std::move(serviceId),
1169 std::move(locationUrl),
1170 std::move(controlURL),
1171 std::move(eventSubURL)));
1172
1173 return new_igd;
1174 }
1175
1176 return nullptr;
1177}
1178
1179bool
1180PUPnP::actionIsIgdConnected(const UPnPIGD& igd)
1181{
1182 if (not clientRegistered_)
1183 return false;
1184
1185 // Set action name.
1186 IXML_Document* action_container_ptr = UpnpMakeAction("GetStatusInfo",
1187 igd.getServiceType().c_str(),
1188 0,
1189 nullptr);
1190 if (not action_container_ptr) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001191 // JAMI_WARN("PUPnP: Failed to make GetStatusInfo action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001192 return false;
1193 }
1194 XMLDocument action(action_container_ptr, ixmlDocument_free); // Action pointer.
1195
1196 IXML_Document* response_container_ptr = nullptr;
1197 int upnp_err = UpnpSendAction(ctrlptHandle_,
1198 igd.getControlURL().c_str(),
1199 igd.getServiceType().c_str(),
1200 nullptr,
1201 action.get(),
1202 &response_container_ptr);
1203 if (not response_container_ptr or upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001204 // JAMI_WARN("PUPnP: Failed to send GetStatusInfo action -> %s", UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001205 return false;
1206 }
1207 XMLDocument response(response_container_ptr, ixmlDocument_free);
1208
1209 if (errorOnResponse(response.get())) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001210 // JAMI_WARN("PUPnP: Failed to get GetStatusInfo from %s -> %d: %s",
1211 // igd.getServiceType().c_str(),
1212 // upnp_err,
1213 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001214 return false;
1215 }
1216
1217 // Parse response.
1218 auto status = getFirstDocItem(response.get(), "NewConnectionStatus");
1219 return status == "Connected";
1220}
1221
1222IpAddr
1223PUPnP::actionGetExternalIP(const UPnPIGD& igd)
1224{
1225 if (not clientRegistered_)
1226 return {};
1227
1228 // Action and response pointers.
1229 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1230 action(nullptr, ixmlDocument_free); // Action pointer.
1231 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1232 response(nullptr, ixmlDocument_free); // Response pointer.
1233
1234 // Set action name.
1235 static constexpr const char* action_name {"GetExternalIPAddress"};
1236
1237 IXML_Document* action_container_ptr = nullptr;
1238 action_container_ptr = UpnpMakeAction(action_name, igd.getServiceType().c_str(), 0, nullptr);
1239 action.reset(action_container_ptr);
1240
1241 if (not action) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001242 // JAMI_WARN("PUPnP: Failed to make GetExternalIPAddress action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001243 return {};
1244 }
1245
1246 IXML_Document* response_container_ptr = nullptr;
1247 int upnp_err = UpnpSendAction(ctrlptHandle_,
1248 igd.getControlURL().c_str(),
1249 igd.getServiceType().c_str(),
1250 nullptr,
1251 action.get(),
1252 &response_container_ptr);
1253 response.reset(response_container_ptr);
1254
1255 if (not response or upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001256 // JAMI_WARN("PUPnP: Failed to send GetExternalIPAddress action -> %s",
1257 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001258 return {};
1259 }
1260
1261 if (errorOnResponse(response.get())) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001262 // JAMI_WARN("PUPnP: Failed to get GetExternalIPAddress from %s -> %d: %s",
1263 // igd.getServiceType().c_str(),
1264 // upnp_err,
1265 // UpnpGetErrorMessage(upnp_err));
Adrien Béraud612b55b2023-05-29 10:42:04 -04001266 return {};
1267 }
1268
1269 return {getFirstDocItem(response.get(), "NewExternalIPAddress")};
1270}
1271
1272std::map<Mapping::key_t, Mapping>
1273PUPnP::getMappingsListByDescr(const std::shared_ptr<IGD>& igd, const std::string& description) const
1274{
1275 auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd);
1276 assert(upnpIgd);
1277
1278 std::map<Mapping::key_t, Mapping> mapList;
1279
1280 if (not clientRegistered_ or not upnpIgd->isValid() or not upnpIgd->getLocalIp())
1281 return mapList;
1282
1283 // Set action name.
1284 static constexpr const char* action_name {"GetGenericPortMappingEntry"};
1285
1286 for (int entry_idx = 0;; entry_idx++) {
1287 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1288 action(nullptr, ixmlDocument_free); // Action pointer.
1289 IXML_Document* action_container_ptr = nullptr;
1290
1291 std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
1292 response(nullptr, ixmlDocument_free); // Response pointer.
1293 IXML_Document* response_container_ptr = nullptr;
1294
1295 UpnpAddToAction(&action_container_ptr,
1296 action_name,
1297 upnpIgd->getServiceType().c_str(),
1298 "NewPortMappingIndex",
1299 std::to_string(entry_idx).c_str());
1300 action.reset(action_container_ptr);
1301
1302 if (not action) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001303 // JAMI_WARN("PUPnP: Failed to add NewPortMappingIndex action");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001304 break;
1305 }
1306
1307 int upnp_err = UpnpSendAction(ctrlptHandle_,
1308 upnpIgd->getControlURL().c_str(),
1309 upnpIgd->getServiceType().c_str(),
1310 nullptr,
1311 action.get(),
1312 &response_container_ptr);
1313 response.reset(response_container_ptr);
1314
1315 if (not response) {
1316 // No existing mapping. Abort silently.
1317 break;
1318 }
1319
1320 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001321 // JAMI_ERR("PUPnP: GetGenericPortMappingEntry returned with error: %i", upnp_err);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001322 break;
1323 }
1324
1325 // Check error code.
1326 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1327 if (not errorCode.empty()) {
1328 auto error = to_int<int>(errorCode);
1329 if (error == ARRAY_IDX_INVALID or error == CONFLICT_IN_MAPPING) {
1330 // No more port mapping entries in the response.
Morteza Namvar5f639522023-07-04 17:08:58 -04001331 // JAMI_DBG("PUPnP: No more mappings (found a total of %i mappings", entry_idx);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001332 break;
1333 } else {
1334 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
1335 JAMI_ERROR("PUPnP: GetGenericPortMappingEntry returned with error: {:s}: {:s}",
1336 errorCode,
1337 errorDescription);
1338 break;
1339 }
1340 }
1341
1342 // Parse the response.
1343 auto desc_actual = getFirstDocItem(response.get(), "NewPortMappingDescription");
1344 auto client_ip = getFirstDocItem(response.get(), "NewInternalClient");
1345
1346 if (client_ip != getHostAddress().toString()) {
1347 // Silently ignore un-matching addresses.
1348 continue;
1349 }
1350
1351 if (desc_actual.find(description) == std::string::npos)
1352 continue;
1353
1354 auto port_internal = getFirstDocItem(response.get(), "NewInternalPort");
1355 auto port_external = getFirstDocItem(response.get(), "NewExternalPort");
1356 std::string transport(getFirstDocItem(response.get(), "NewProtocol"));
1357
1358 if (port_internal.empty() || port_external.empty() || transport.empty()) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001359 // JAMI_ERR("PUPnP: GetGenericPortMappingEntry returned an invalid entry at index %i",
1360 // entry_idx);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001361 continue;
1362 }
1363
1364 std::transform(transport.begin(), transport.end(), transport.begin(), ::toupper);
1365 PortType type = transport.find("TCP") != std::string::npos ? PortType::TCP : PortType::UDP;
1366 auto ePort = to_int<uint16_t>(port_external);
1367 auto iPort = to_int<uint16_t>(port_internal);
1368
1369 Mapping map(type, ePort, iPort);
1370 map.setIgd(igd);
1371
1372 mapList.emplace(map.getMapKey(), std::move(map));
1373 }
1374
1375 JAMI_DEBUG("PUPnP: Found {:d} allocated mappings on IGD {:s}",
1376 mapList.size(),
1377 upnpIgd->toString());
1378
1379 return mapList;
1380}
1381
1382void
1383PUPnP::deleteMappingsByDescription(const std::shared_ptr<IGD>& igd, const std::string& description)
1384{
1385 if (not(clientRegistered_ and igd->getLocalIp()))
1386 return;
1387
Morteza Namvar5f639522023-07-04 17:08:58 -04001388 // JAMI_DBG("PUPnP: Remove all mappings (if any) on IGD %s matching descr prefix %s",
1389 // igd->toString().c_str(),
1390 // Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001391
1392 auto mapList = getMappingsListByDescr(igd, description);
1393
1394 for (auto const& [_, map] : mapList) {
1395 requestMappingRemove(map);
1396 }
1397}
1398
1399bool
1400PUPnP::actionAddPortMapping(const Mapping& mapping)
1401{
1402 CHECK_VALID_THREAD();
1403
1404 if (not clientRegistered_)
1405 return false;
1406
1407 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1408 if (not igdIn)
1409 return false;
1410
1411 // The requested IGD must be present in the list of local valid IGDs.
1412 auto igd = findMatchingIgd(igdIn->getControlURL());
1413
1414 if (not igd or not igd->isValid())
1415 return false;
1416
1417 // Action and response pointers.
1418 XMLDocument action(nullptr, ixmlDocument_free);
1419 IXML_Document* action_container_ptr = nullptr;
1420 XMLDocument response(nullptr, ixmlDocument_free);
1421 IXML_Document* response_container_ptr = nullptr;
1422
1423 // Set action sequence.
1424 UpnpAddToAction(&action_container_ptr,
1425 ACTION_ADD_PORT_MAPPING,
1426 igd->getServiceType().c_str(),
1427 "NewRemoteHost",
1428 "");
1429 UpnpAddToAction(&action_container_ptr,
1430 ACTION_ADD_PORT_MAPPING,
1431 igd->getServiceType().c_str(),
1432 "NewExternalPort",
1433 mapping.getExternalPortStr().c_str());
1434 UpnpAddToAction(&action_container_ptr,
1435 ACTION_ADD_PORT_MAPPING,
1436 igd->getServiceType().c_str(),
1437 "NewProtocol",
1438 mapping.getTypeStr());
1439 UpnpAddToAction(&action_container_ptr,
1440 ACTION_ADD_PORT_MAPPING,
1441 igd->getServiceType().c_str(),
1442 "NewInternalPort",
1443 mapping.getInternalPortStr().c_str());
1444 UpnpAddToAction(&action_container_ptr,
1445 ACTION_ADD_PORT_MAPPING,
1446 igd->getServiceType().c_str(),
1447 "NewInternalClient",
1448 getHostAddress().toString().c_str());
1449 UpnpAddToAction(&action_container_ptr,
1450 ACTION_ADD_PORT_MAPPING,
1451 igd->getServiceType().c_str(),
1452 "NewEnabled",
1453 "1");
1454 UpnpAddToAction(&action_container_ptr,
1455 ACTION_ADD_PORT_MAPPING,
1456 igd->getServiceType().c_str(),
1457 "NewPortMappingDescription",
1458 mapping.toString().c_str());
1459 UpnpAddToAction(&action_container_ptr,
1460 ACTION_ADD_PORT_MAPPING,
1461 igd->getServiceType().c_str(),
1462 "NewLeaseDuration",
1463 "0");
1464
1465 action.reset(action_container_ptr);
1466
1467 int upnp_err = UpnpSendAction(ctrlptHandle_,
1468 igd->getControlURL().c_str(),
1469 igd->getServiceType().c_str(),
1470 nullptr,
1471 action.get(),
1472 &response_container_ptr);
1473 response.reset(response_container_ptr);
1474
1475 bool success = true;
1476
1477 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001478 // JAMI_WARN("PUPnP: Failed to send action %s for mapping %s. %d: %s",
1479 // ACTION_ADD_PORT_MAPPING,
1480 // mapping.toString().c_str(),
1481 // upnp_err,
1482 // UpnpGetErrorMessage(upnp_err));
1483 // JAMI_WARN("PUPnP: IGD ctrlUrl %s", igd->getControlURL().c_str());
1484 // JAMI_WARN("PUPnP: IGD service type %s", igd->getServiceType().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001485
1486 success = false;
1487 }
1488
1489 // Check if an error has occurred.
1490 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1491 if (not errorCode.empty()) {
1492 success = false;
1493 // Try to get the error description.
1494 std::string errorDescription;
1495 if (response) {
1496 errorDescription = getFirstDocItem(response.get(), "errorDescription");
1497 }
1498
Morteza Namvar5f639522023-07-04 17:08:58 -04001499 // JAMI_WARNING("PUPnP: {:s} returned with error: {:s} {:s}",
1500 // ACTION_ADD_PORT_MAPPING,
1501 // errorCode,
1502 // errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001503 }
1504 return success;
1505}
1506
1507bool
1508PUPnP::actionDeletePortMapping(const Mapping& mapping)
1509{
1510 CHECK_VALID_THREAD();
1511
1512 if (not clientRegistered_)
1513 return false;
1514
1515 auto igdIn = std::dynamic_pointer_cast<UPnPIGD>(mapping.getIgd());
1516 if (not igdIn)
1517 return false;
1518
1519 // The requested IGD must be present in the list of local valid IGDs.
1520 auto igd = findMatchingIgd(igdIn->getControlURL());
1521
1522 if (not igd or not igd->isValid())
1523 return false;
1524
1525 // Action and response pointers.
1526 XMLDocument action(nullptr, ixmlDocument_free);
1527 IXML_Document* action_container_ptr = nullptr;
1528 XMLDocument response(nullptr, ixmlDocument_free);
1529 IXML_Document* response_container_ptr = nullptr;
1530
1531 // Set action sequence.
1532 UpnpAddToAction(&action_container_ptr,
1533 ACTION_DELETE_PORT_MAPPING,
1534 igd->getServiceType().c_str(),
1535 "NewRemoteHost",
1536 "");
1537 UpnpAddToAction(&action_container_ptr,
1538 ACTION_DELETE_PORT_MAPPING,
1539 igd->getServiceType().c_str(),
1540 "NewExternalPort",
1541 mapping.getExternalPortStr().c_str());
1542 UpnpAddToAction(&action_container_ptr,
1543 ACTION_DELETE_PORT_MAPPING,
1544 igd->getServiceType().c_str(),
1545 "NewProtocol",
1546 mapping.getTypeStr());
1547
1548 action.reset(action_container_ptr);
1549
1550 int upnp_err = UpnpSendAction(ctrlptHandle_,
1551 igd->getControlURL().c_str(),
1552 igd->getServiceType().c_str(),
1553 nullptr,
1554 action.get(),
1555 &response_container_ptr);
1556 response.reset(response_container_ptr);
1557
1558 bool success = true;
1559
1560 if (upnp_err != UPNP_E_SUCCESS) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001561 // JAMI_WARN("PUPnP: Failed to send action %s for mapping from %s. %d: %s",
1562 // ACTION_DELETE_PORT_MAPPING,
1563 // mapping.toString().c_str(),
1564 // upnp_err,
1565 // UpnpGetErrorMessage(upnp_err));
1566 // JAMI_WARN("PUPnP: IGD ctrlUrl %s", igd->getControlURL().c_str());
1567 // JAMI_WARN("PUPnP: IGD service type %s", igd->getServiceType().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001568
1569 success = false;
1570 }
1571
1572 if (not response) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001573 // JAMI_WARN("PUPnP: Failed to get response for %s", ACTION_DELETE_PORT_MAPPING);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001574 success = false;
1575 }
1576
1577 // Check if there is an error code.
1578 auto errorCode = getFirstDocItem(response.get(), "errorCode");
1579 if (not errorCode.empty()) {
1580 auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
Morteza Namvar5f639522023-07-04 17:08:58 -04001581 // JAMI_WARNING("PUPnP: {:s} returned with error: {:s}: {:s}",
1582 // ACTION_DELETE_PORT_MAPPING,
1583 // errorCode,
1584 // errorDescription);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001585 success = false;
1586 }
1587
1588 return success;
1589}
1590
1591} // namespace upnp
1592} // namespace jami