diff --git a/daemon/configure.ac b/daemon/configure.ac index de3bf82e8670c12ec79f27c1dc04fdfa76ade7b3..85c8f27d3e30d909587ca5bfbdfa375b9c327bca 100644 --- a/daemon/configure.ac +++ b/daemon/configure.ac @@ -531,60 +531,17 @@ AC_ARG_ENABLE([ipv6], AS_HELP_STRING([--enable-ipv6], [Enable IPv6 support])) AC_DEFINE_UNQUOTED([HAVE_IPV6], `if test "x$enable_ipv6" = "xyes"; then echo 1; else echo 0; fi`, [Define if you have IPv6]) AM_CONDITIONAL(BUILD_IPV6, test "x$enable_ipv6" = "xyes" ) -# miniupnpc -# required dependency(ies): libminiupc -dnl check for libminiupnpc (doesn't use pkg-config) +# LIBUPNP +dnl check for libupnp AC_ARG_WITH([upnp], [AS_HELP_STRING([--without-upnp], [disable support for upnp])], [], [with_upnp=yes]) -LIBMINIUPNPC= -AS_IF([test "x$with_upnp" != xno], - [AC_CHECK_HEADER([miniupnpc/miniupnpc.h], , AC_MSG_FAILURE([Unable to find libminiupnpc headers (you may need to install the dev package). You may use --without-upnp to compile without upnp support.]))] - [AC_CHECK_LIB([miniupnpc], [simpleUPnPcommand], [ - UPNP_LIBS=-lminiupnpc - AC_SUBST(UPNP_LIBS) - miniupnp_version="unknown"], - [AC_MSG_FAILURE([libminiupnpc link test failed. You may use --without-upnp to compile without upnp support.]) - miniupnp_version="none" - AC_DEFINE([HAVE_UPNP], 0, [Define if you have miniupnpc])] - )], - [AC_DEFINE([HAVE_UPNP], 0, [Define if you have miniupnpc])]) - -dnl need to determine API version -dnl if libminiupnpc is v1.7 or higher, then the API version should be defined -if test "x$miniupnp_version" = "xunknown" ; then - AC_MSG_CHECKING("if libminiupnpc is v1.7 or higher to determine API version") - AC_RUN_IFELSE( - [AC_LANG_PROGRAM( - [#include <stdlib.h> - #include <miniupnpc/miniupnpc.h>], - [#ifdef MINIUPNPC_API_VERSION - return EXIT_SUCCESS; - #else - return EXIT_FAILURE; - #endif] - )], - [miniupnp_version=">= 1.7" - AC_MSG_RESULT("found v1.7 or higher") - AC_DEFINE([HAVE_UPNP], 1, [Define if you have miniupnpc])], - [AC_MSG_RESULT("found v1.6 or lower")]) -fi - -dnl otherwise it must be v1.6 or lower; we don't support lower than v1.6, which is API v8 -dnl v1.6 introduced IPv6 commands, so we check for one of them -if test "x$miniupnp_version" = "xunknown" ; then - AC_MSG_CHECKING("if libminiupnpc is at least v1.6") - AC_CHECK_LIB([miniupnpc], [UPNP_GetFirewallStatus],[ - miniupnp_version="1.6" - AC_DEFINE(MINIUPNPC_API_VERSION, 8, [libminiupnpc 1.6 has API version 8]) - AC_MSG_RESULT("detected libminiupnpc is v1.6") - AC_DEFINE([HAVE_UPNP], 1, [Define if you have miniupnpc])], - [AC_DEFINE([HAVE_UPNP], 0, [Define if you have miniupnpc]) - AC_MSG_RESULT("detected libminiupnpc is lower than v1.6") - AC_MSG_FAILURE([libminiupnpc less than v1.6 is not supported])]) -fi - -AM_CONDITIONAL(BUILD_UPNP, test "x$with_upnp" = "xyes" ) +AS_IF([test "x$with_upnp" = "xyes"], + [PKG_CHECK_MODULES(LIBUPNP, [libupnp], + [AC_DEFINE([HAVE_LIBUPNP], 1, [Define if you have libupnp])], + [AC_MSG_WARN([Missing libupnp development files]) + AC_DEFINE([HAVE_LIBUPNP], 0, [Define if you have libupnp])]) + ]) # DOXYGEN # required dependency(ies): doxygen diff --git a/daemon/contrib/src/miniupnpc/SHA512SUMS b/daemon/contrib/src/miniupnpc/SHA512SUMS deleted file mode 100644 index c3683153494bed968ba9e69decb579028c3c9966..0000000000000000000000000000000000000000 --- a/daemon/contrib/src/miniupnpc/SHA512SUMS +++ /dev/null @@ -1 +0,0 @@ -7994749fa92e6478285dada191f4ad0d15fdce6cf281ee4d1d3d5615548338d68be3b9ce96a04ef4b81c697589689398060cc6f610101fa1d0e6f9e24b7850a9 miniupnpc-1.9.tar.gz diff --git a/daemon/contrib/src/miniupnpc/makefile.patch b/daemon/contrib/src/miniupnpc/makefile.patch deleted file mode 100644 index acafe2d11a7917c7ac293625a9cb8d8c382453ce..0000000000000000000000000000000000000000 --- a/daemon/contrib/src/miniupnpc/makefile.patch +++ /dev/null @@ -1,16 +0,0 @@ ---- a/Makefile 2015-01-05 15:45:40.426809962 -0500 -+++ b/Makefile 2015-01-05 15:48:24.470817044 -0500 -@@ -212,6 +212,13 @@ - endif - endif - -+install-static: updateversion $(FILESTOINSTALL) -+ $(INSTALL) -d $(DESTDIR)$(INSTALLDIRINC) -+ $(INSTALL) -m 644 $(HEADERS) $(DESTDIR)$(INSTALLDIRINC) -+ $(INSTALL) -d $(DESTDIR)$(INSTALLDIRLIB) -+ $(INSTALL) -m 644 $(LIBRARY) $(DESTDIR)$(INSTALLDIRLIB) -+ $(INSTALL) -d $(DESTDIR)$(INSTALLDIRBIN) -+ $(INSTALL) -m 755 external-ip.sh $(DESTDIR)$(INSTALLDIRBIN)/external-ip - - cleaninstall: - $(RM) -r $(DESTDIR)$(INSTALLDIRINC) diff --git a/daemon/contrib/src/miniupnpc/rules.mak b/daemon/contrib/src/miniupnpc/rules.mak deleted file mode 100644 index d77aff5f7987e77995c1670e4ddfe6ec267ca9ea..0000000000000000000000000000000000000000 --- a/daemon/contrib/src/miniupnpc/rules.mak +++ /dev/null @@ -1,26 +0,0 @@ -# miniupnpc - -MINIUPNPC_VERSION := 1.9 -MINIUPNPC_URL := http://miniupnp.free.fr/files/download.php?file=miniupnpc-$(MINIUPNPC_VERSION).tar.gz - -PKGS += miniupnpc -ifeq ($(call need_pkg,"miniupnpc >= 1.6"),) -PKGS_FOUND += miniupnpc -endif - -$(TARBALLS)/miniupnpc-$(MINIUPNPC_VERSION).tar.gz: - $(call download,$(MINIUPNPC_URL)) - -.sum-miniupnpc: miniupnpc-$(MINIUPNPC_VERSION).tar.gz - $(warning $@ not implemented) - touch $@ - -miniupnpc: miniupnpc-$(MINIUPNPC_VERSION).tar.gz .sum-miniupnpc - $(UNPACK) - $(APPLY) $(SRC)/miniupnpc/makefile.patch - $(UPDATE_AUTOCONFIG) && cd $(UNPACK_DIR) - $(MOVE) - -.miniupnpc: miniupnpc - cd $< && INSTALLPREFIX=$(PREFIX) $(MAKE) install-static - touch $@ diff --git a/daemon/contrib/src/upnp/SHA512SUMS b/daemon/contrib/src/upnp/SHA512SUMS new file mode 100644 index 0000000000000000000000000000000000000000..f30c34e49bdcddbfb931a4a10580a611a8c7203c --- /dev/null +++ b/daemon/contrib/src/upnp/SHA512SUMS @@ -0,0 +1 @@ +97af62a7483cc19cfe80157cbc3383c1b4b7c9c39b848f4ed063784b74df0b9b0527f7b467e01451e0a44dbf9e8a9eab510619146a6ee1e3dce46f3e4af6e661 libupnp-1.6.19.tar.bz2 diff --git a/daemon/contrib/src/upnp/libupnp-configure.patch b/daemon/contrib/src/upnp/libupnp-configure.patch new file mode 100644 index 0000000000000000000000000000000000000000..dbc14cca5e50f88e22edf19da1cace37abfe3edf --- /dev/null +++ b/daemon/contrib/src/upnp/libupnp-configure.patch @@ -0,0 +1,82 @@ +--- libupnp/configure.ac.orig 2011-02-09 00:55:44.000000000 +0100 ++++ libupnp/configure.ac 2011-02-10 23:39:44.154929678 +0100 +@@ -397,7 +397,6 @@ + AC_PROG_MAKE_SET + AC_PROG_EGREP + +-# + # Default compilation flags + # + echo "--------------------- Default compilation flags -------------------------------" +@@ -531,39 +530,46 @@ + # Checks for POSIX Threads + # + echo "--------------------------- pthread stuff -------------------------------------" +-ACX_PTHREAD( +- [], +- [AC_MSG_ERROR([POSIX threads are required to build this program])]) ++#ACX_PTHREAD( ++# [], ++# [AC_MSG_ERROR([POSIX threads are required to build this program])]) + # ++PTHREAD_LIBS=" -lpthreadGC2 -lws2_32" ++PTHREAD_CFLAGS=" -DPTW32_STATIC_LIB -DUPNP_STATIC_LIB" + # Update environment variables for pthreads + # +-CC="$PTHREAD_CC" ++#CC="$PTHREAD_CC" + CFLAGS="$PTHREAD_CFLAGS $CFLAGS" + LIBS="$PTHREAD_LIBS $LIBS" ++ ++AC_SUBST(PTHREAD_LIBS) ++AC_SUBST(PTHREAD_CFLAGS) ++AC_SUBST(PTHREAD_CC) ++ + # + # Determine if pthread_rwlock_t is available + # +-echo "----------------------- pthread_rwlock_t stuff --------------------------------" +-AC_MSG_CHECKING([if pthread_rwlock_t is available]) +-AC_LANG([C]) +-AC_COMPILE_IFELSE( +- [AC_LANG_PROGRAM( +- [#include <pthread.h>], +- [pthread_rwlock_t *x;])], +- [AC_DEFINE([UPNP_USE_RWLOCK], [1], [Use pthread_rwlock_t]) +- AC_MSG_RESULT([yes, supported without any options])], +- [AC_COMPILE_IFELSE( +- [AC_LANG_PROGRAM( +- [#define _GNU_SOURCE +- #include <pthread.h>], +- [pthread_rwlock_t *x;])], +- [AC_DEFINE([UPNP_USE_RWLOCK], [1], [Use pthread_rwlock_t]) +- CPPFLAGS="$CPPFLAGS -D_GNU_SOURCE" +- AC_MSG_RESULT([yes, definition of _GNU_SOURCE required])], +- [AC_DEFINE([UPNP_USE_RWLOCK], [0], [Do not use pthread_rwlock_t]) +- AC_MSG_RESULT([no, needs to fallback to pthread_mutex]) +- AC_MSG_ERROR([pthread_rwlock_t not available])])]) +-echo "-------------------------------------------------------------------------------" ++#echo "----------------------- pthread_rwlock_t stuff --------------------------------" ++#AC_MSG_CHECKING([if pthread_rwlock_t is available]) ++#AC_LANG([C]) ++#AC_COMPILE_IFELSE( ++# [AC_LANG_PROGRAM( ++# [#include <pthread.h>], ++# [pthread_rwlock_t *x;])], ++# [AC_DEFINE([UPNP_USE_RWLOCK], [1], [Use pthread_rwlock_t]) ++# AC_MSG_RESULT([yes, supported without any options])], ++# [AC_COMPILE_IFELSE( ++# [AC_LANG_PROGRAM( ++# [#define _GNU_SOURCE ++# #include <pthread.h>], ++# [pthread_rwlock_t *x;])], ++# [AC_DEFINE([UPNP_USE_RWLOCK], [1], [Use pthread_rwlock_t]) ++# CPPFLAGS="$CPPFLAGS -D_GNU_SOURCE" ++# AC_MSG_RESULT([yes, definition of _GNU_SOURCE required])], ++# [AC_DEFINE([UPNP_USE_RWLOCK], [0], [Do not use pthread_rwlock_t]) ++# AC_MSG_RESULT([no, needs to fallback to pthread_mutex]) ++# AC_MSG_ERROR([pthread_rwlock_t not available])])]) ++#echo "-------------------------------------------------------------------------------" + + + AC_CONFIG_FILES([ diff --git a/daemon/contrib/src/upnp/libupnp-ipv6.patch b/daemon/contrib/src/upnp/libupnp-ipv6.patch new file mode 100644 index 0000000000000000000000000000000000000000..7b89bc26886b12733866faad69fda62e7bfe3c5f --- /dev/null +++ b/daemon/contrib/src/upnp/libupnp-ipv6.patch @@ -0,0 +1,49 @@ +From 438ace99538713fb1370411188e0f370069a1818 Mon Sep 17 00:00:00 2001 +From: Konstantin Pavlov <thresh@videolan.org> +Date: Tue, 29 May 2012 10:18:40 +0400 +Subject: [PATCH] Fix compile under mingw with IPv6 enabled. + +--- + upnp/src/genlib/miniserver/miniserver.c | 7 +++++++ + upnp/src/ssdp/ssdp_server.c | 7 +++++++ + 2 files changed, 14 insertions(+) + +diff --git a/upnp/src/genlib/miniserver/miniserver.c b/upnp/src/genlib/miniserver/miniserver.c +index af310ca..1ae422f 100644 +--- a/upnp/src/genlib/miniserver/miniserver.c ++++ b/upnp/src/genlib/miniserver/miniserver.c +@@ -68,6 +68,13 @@ + /*! . */ + #define APPLICATION_LISTENING_PORT 49152 + ++/* IPV6_V6ONLY is missing from MinGW, hack taken from ++ * http://svn.apache.org/repos/asf/apr/apr/trunk/network_io/win32/sockopt.c ++ */ ++#ifndef IPV6_V6ONLY ++#define IPV6_V6ONLY 27 ++#endif ++ + struct mserv_request_t { + /*! Connection handle. */ + SOCKET connfd; +diff --git a/upnp/src/ssdp/ssdp_server.c b/upnp/src/ssdp/ssdp_server.c +index 231c2c5..6a9c27f 100644 +--- a/upnp/src/ssdp/ssdp_server.c ++++ b/upnp/src/ssdp/ssdp_server.c +@@ -69,6 +69,13 @@ + #endif /* UPNP_ENABLE_IPV6 */ + #endif /* INCLUDE_CLIENT_APIS */ + ++/* IPV6_V6ONLY is missing from MinGW, hack taken from ++ * http://svn.apache.org/repos/asf/apr/apr/trunk/network_io/win32/sockopt.c ++ */ ++#ifndef IPV6_V6ONLY ++#define IPV6_V6ONLY 27 ++#endif ++ + void RequestHandler(); + + enum Listener { +-- +1.7.9.7 + diff --git a/daemon/contrib/src/upnp/libupnp-win32.patch b/daemon/contrib/src/upnp/libupnp-win32.patch new file mode 100644 index 0000000000000000000000000000000000000000..686e3bae19c1da6fd74fb0fef455a6f71b9ab8a9 --- /dev/null +++ b/daemon/contrib/src/upnp/libupnp-win32.patch @@ -0,0 +1,45 @@ +--- libupnp/configure.ac.orig 2011-02-10 23:53:25.000000000 +0100 ++++ libupnp/configure.ac 2011-02-10 23:54:23.574454501 +0100 +@@ -546,6 +546,7 @@ + AC_SUBST(PTHREAD_CFLAGS) + AC_SUBST(PTHREAD_CC) + ++AC_DEFINE([_WIN32_WINNT], 0x0501, [Define to '0x0500' for Windows 2000 APIs.]) + # + # Determine if pthread_rwlock_t is available + # +--- libupnp/libupnp.pc.in 2010-12-23 21:24:05.000000000 +0100 ++++ libupnp.new/libupnp.pc.in 2011-02-13 11:27:23.000000000 +0100 +@@ -6,6 +6,6 @@ + Name: libupnp + Description: Linux SDK for UPnP Devices + Version: @VERSION@ +-Libs: @PTHREAD_CFLAGS@ @PTHREAD_LIBS@ -L${libdir} -lupnp -lthreadutil -lixml ++Libs: @PTHREAD_CFLAGS@ -L${libdir} -lupnp -lthreadutil -lixml -liphlpapi @PTHREAD_LIBS@ + Cflags: @PTHREAD_CFLAGS@ -I${includedir}/upnp + +--- libupnp/upnp/src/inc/upnputil.h 2010-12-23 21:24:06.000000000 +0100 ++++ libupnp.new/upnp/src/inc/upnputil.h 2011-02-13 08:24:24.000000000 +0100 +@@ -125,7 +125,7 @@ + #define strncasecmp strnicmp + #define sleep(a) Sleep((a)*1000) + #define usleep(a) Sleep((a)/1000) +- #define strerror_r(a,b,c) (strerror_s((b),(c),(a))) ++ #define strerror_r(a,b,c) strncpy( b, strerror(a), c) + #else + #define max(a, b) (((a)>(b))? (a):(b)) + #define min(a, b) (((a)<(b))? (a):(b)) +--- upnp/upnp/inc/UpnpInet.h 2011-04-03 04:50:36.000000000 +0200 ++++ upnp.neww/upnp/inc/UpnpInet.h 2011-11-18 01:54:45.418529337 +0100 +@@ -15,11 +15,6 @@ + + #ifdef WIN32 + #include <stdarg.h> +- #ifndef UPNP_USE_MSVCPP +- /* Removed: not required (and cause compilation issues) */ +- #include <winbase.h> +- #include <windef.h> +- #endif + #include <winsock2.h> + #include <iphlpapi.h> + #include <ws2tcpip.h> diff --git a/daemon/contrib/src/upnp/libupnp-win64.patch b/daemon/contrib/src/upnp/libupnp-win64.patch new file mode 100644 index 0000000000000000000000000000000000000000..8064e6d214e3c382602272c44c26aff7611a9b1a --- /dev/null +++ b/daemon/contrib/src/upnp/libupnp-win64.patch @@ -0,0 +1,41 @@ +--- libupnp/threadutil/inc/ThreadPool.h 2011-01-20 07:46:57.000000000 +0100 ++++ libupnp.new/threadutil/inc/ThreadPool.h 2011-09-23 01:36:12.000000000 +0200 +@@ -45,6 +45,7 @@ + #include <errno.h> + + #ifdef WIN32 ++ #ifndef _TIMEZONE_DEFINED + #include <time.h> + struct timezone + { +@@ -52,6 +53,7 @@ + int tz_dsttime; /* type of dst correction */ + }; + int gettimeofday(struct timeval *tv, struct timezone *tz); ++ #endif + #else /* WIN32 */ + #include <sys/param.h> + #include <sys/time.h> /* for gettimeofday() */ +--- libupnp-1.6.16/upnp/inc/upnp.h.orig 2012-03-22 00:15:38.000000000 +0100 ++++ libupnp-1.6.16/upnp/inc/upnp.h 2012-03-28 18:58:55.043642000 +0200 +@@ -61,6 +61,20 @@ + /* Other systems ??? */ + #endif + ++# if defined( __MINGW32__ ) ++# if !defined( _OFF_T_ ) ++ typedef long long _off_t; ++ typedef _off_t off_t; ++# define _OFF_T_ ++# else ++# ifdef off_t ++# undef off_t ++# endif ++# define off_t long long ++# endif ++# endif ++ ++ + #define LINE_SIZE (size_t)180 + #define NAME_SIZE (size_t)256 + #define MNFT_NAME_SIZE 64 diff --git a/daemon/contrib/src/upnp/miniserver.patch b/daemon/contrib/src/upnp/miniserver.patch new file mode 100644 index 0000000000000000000000000000000000000000..45ff90c675953400c07c4834defdad80fcf3f4db --- /dev/null +++ b/daemon/contrib/src/upnp/miniserver.patch @@ -0,0 +1,17 @@ +--- upnp/upnp/src/api/upnpapi.c.orig 2013-04-08 00:23:46.000000000 +0200 ++++ upnp/upnp/src/api/upnpapi.c 2013-04-08 00:25:49.000000000 +0200 +@@ -358,13 +358,13 @@ + return retVal; + } + ++#ifdef INTERNAL_WEB_SERVER + #ifdef INCLUDE_DEVICE_APIS + #if EXCLUDE_SOAP == 0 + SetSoapCallback(soap_device_callback); + #endif + #endif /* INCLUDE_DEVICE_APIS */ + +-#ifdef INTERNAL_WEB_SERVER + #if EXCLUDE_GENA == 0 + SetGenaCallback(genaCallback); + #endif diff --git a/daemon/contrib/src/upnp/rules.mak b/daemon/contrib/src/upnp/rules.mak new file mode 100644 index 0000000000000000000000000000000000000000..717fdd9b1d3f35bba1bf7c7f5498718ef4ec07db --- /dev/null +++ b/daemon/contrib/src/upnp/rules.mak @@ -0,0 +1,38 @@ +# UPNP +UPNP_VERSION := 1.6.19 +UPNP_URL := http://sourceforge.net/projects/pupnp/files/pupnp/libUPnP%20$(UPNP_VERSION)/libupnp-$(UPNP_VERSION).tar.bz2/download + +PKGS += upnp +ifeq ($(call need_pkg,'libupnp'),) +PKGS_FOUND += upnp +endif + +$(TARBALLS)/libupnp-$(UPNP_VERSION).tar.bz2: + $(call download,$(UPNP_URL)) + +.sum-upnp: libupnp-$(UPNP_VERSION).tar.bz2 + +ifdef HAVE_WIN32 +DEPS_upnp += pthreads $(DEPS_pthreads) +LIBUPNP_ECFLAGS = -DPTW32_STATIC_LIB +endif + +upnp: libupnp-$(UPNP_VERSION).tar.bz2 .sum-upnp + $(UNPACK) +ifdef HAVE_WIN32 + $(APPLY) $(SRC)/upnp/libupnp-configure.patch + $(APPLY) $(SRC)/upnp/libupnp-win32.patch + $(APPLY) $(SRC)/upnp/libupnp-win64.patch +endif + $(APPLY) $(SRC)/upnp/libupnp-ipv6.patch + $(APPLY) $(SRC)/upnp/miniserver.patch + $(UPDATE_AUTOCONFIG) && cd $(UNPACK_DIR) && mv config.guess config.sub build-aux/ + $(MOVE) + +.upnp: upnp +ifdef HAVE_WIN32 + $(RECONF) +endif + cd $< && $(HOSTVARS) CFLAGS="$(CFLAGS) -DUPNP_STATIC_LIB $(LIBUPNP_ECFLAGS)" ./configure --disable-samples --without-documentation --disable-blocking_tcp_connections $(HOSTCONF) + cd $< && $(MAKE) install + touch $@ diff --git a/daemon/src/Makefile.am b/daemon/src/Makefile.am index 1a7b319c3e0842d8fbe7f16564fed7852cafb9ed..f32f2ad5c52be427175b243e2af4f5f8246d66b2 100644 --- a/daemon/src/Makefile.am +++ b/daemon/src/Makefile.am @@ -48,7 +48,7 @@ libring_la_LIBADD = \ ./config/libconfig.la \ ./hooks/libhooks.la \ ./history/libhistory.la \ - ./upnp/libupnp.la \ + ./upnp/libupnpcontrol.la \ $(RINGACC_LIBA) \ $(IAX_LIBA) \ $(IM_LIBA) \ diff --git a/daemon/src/account.cpp b/daemon/src/account.cpp index 1fc905fd3d86d9614586fda05c2efa930e43fb50..fa8ce0b2a7eef4f3ec0b5d9539842c5beff08eeb 100644 --- a/daemon/src/account.cpp +++ b/daemon/src/account.cpp @@ -54,7 +54,7 @@ #include <yaml-cpp/yaml.h> -#include "upnp/upnp.h" +#include "upnp/upnp_control.h" #include "ip_utils.h" namespace ring { @@ -107,7 +107,7 @@ Account::Account(const string &accountID) , userAgent_(DEFAULT_USER_AGENT) , hasCustomUserAgent_(false) , mailBox_() - , upnpEnabled_(false) + , upnp_(new upnp::Controller()) { std::random_device rdev; std::seed_seq seed {rdev(), rdev()}; @@ -222,7 +222,9 @@ void Account::unserialize(const YAML::Node &node) parseValue(node, RINGTONE_PATH_KEY, ringtonePath_); parseValue(node, RINGTONE_ENABLED_KEY, ringtoneEnabled_); - parseValue(node, UPNP_ENABLED_KEY, upnpEnabled_); + bool enabled; + parseValue(node, UPNP_ENABLED_KEY, enabled); + upnpEnabled_.store(enabled); } void Account::setAccountDetails(const std::map<std::string, std::string> &details) @@ -242,7 +244,9 @@ void Account::setAccountDetails(const std::map<std::string, std::string> &detail parseString(details, Conf::CONFIG_ACCOUNT_USERAGENT, userAgent_); else userAgent_ = DEFAULT_USER_AGENT; - parseBool(details, Conf::CONFIG_UPNP_ENABLED, upnpEnabled_); + bool enabled; + parseBool(details, Conf::CONFIG_UPNP_ENABLED, enabled); + upnpEnabled_.store(enabled); } std::map<std::string, std::string> Account::getAccountDetails() const @@ -467,40 +471,30 @@ Account::parseBool(const std::map<std::string, std::string> &details, const char #undef find_iter /** - * Checks whether the upnp settings is enabled in the account. - * If so, tries to get a controller with a valid IGD. - * If not, destroys the controller. - * - * Returns whether or not there is a controller with a valid IGD to use. + * Get the UPnP IP (external router) address. + * If use UPnP is set to false, the address will be empty. */ -bool -Account::checkUPnP() -{ - std::unique_lock<std::mutex> lk(upnp_mtx); - - if (upnpEnabled_ != (bool)upnp_) { - if (upnpEnabled_){ - upnp_.reset(new upnp::Controller()); - if (upnp_->hasValidIGD()) - upnpIp_ = upnp_->getExternalIP(); - } else { - upnp_.reset(); - upnpIp_ = IpAddr{}; - } - } +IpAddr +Account::getUPnPIpAddress() const { + std::lock_guard<std::mutex> lk(upnp_mtx); + if (not upnpEnabled_) + return {}; - return upnp_ and upnp_->hasValidIGD(); + return upnp_->getExternalIP(); } /** - * Returns whether or not the upnp controller has a valid IGD - * with which to make port mappings + * returns whether or not UPnP is enabled and active_ + * ie: if it is able to make port mappings */ bool -Account::getUseUPnP() const +Account::getUPnPActive() const { - std::unique_lock<std::mutex> lk(upnp_mtx); - return upnp_ and upnp_->hasValidIGD(); + std::lock_guard<std::mutex> lk(upnp_mtx); + if (not upnpEnabled_) + return false; + + return upnp_->hasValidIGD(); } } // namespace ring diff --git a/daemon/src/account.h b/daemon/src/account.h index 9f5075ff203caeba9f003169f0819e572a9ac4a3..9da2304fca4404428eed1fe78c532c6e546c1ede 100644 --- a/daemon/src/account.h +++ b/daemon/src/account.h @@ -255,18 +255,16 @@ class Account : public Serializable, public std::enable_shared_from_this<Account static const char * const VIDEO_CODEC_BITRATE; /** - * @return bool Flag which determines if UPnP is used or not. + * returns whether or not UPnP is enabled and active + * ie: if it is able to make port mappings */ - bool getUseUPnP() const; + bool getUPnPActive() const; /** * Get the UPnP IP (external router) address. * If use UPnP is set to false, the address will be empty. */ - IpAddr getUPnPIpAddress() const { - std::unique_lock<std::mutex> lk(upnp_mtx); - return upnpIp_; - } + IpAddr getUPnPIpAddress() const; private: NON_COPYABLE(Account); @@ -403,29 +401,15 @@ class Account : public Serializable, public std::enable_shared_from_this<Account std::mt19937_64 rand_ {}; /** - * UPnP IP address (external router address), - * used only if use UPnP is set to true + * UPnP IGD controller and the mutex to access it */ - IpAddr upnpIp_ {}; std::unique_ptr<ring::upnp::Controller> upnp_; mutable std::mutex upnp_mtx {}; /** - * Whether or not UPnP is enabled for this account - * Note that this is only used to store the account setting, - * getUseUPnP() should be used to check if UPnP is being use - * ie: if the UPnP controller has been initialized - */ - bool upnpEnabled_; - - /** - * Checks whether the upnp settings is enabled in the account. - * If so, tries to get a controller with a valid IGD. - * If not, destroys the controller. - * - * Returns whether or not there is a controller with a valid IGD to use. + * flag which determines if this account is set to use UPnP. */ - bool checkUPnP(); + std::atomic_bool upnpEnabled_ {false}; }; } // namespace ring diff --git a/daemon/src/call.cpp b/daemon/src/call.cpp index 2c4d4c838f79d706dacfc431e8924a9cbd722439..7e9bda7e0d8e31e556cf56c53d4f80561def1573 100644 --- a/daemon/src/call.cpp +++ b/daemon/src/call.cpp @@ -308,7 +308,7 @@ Call::initIceTransport(bool master, unsigned channel_num) { auto& iceTransportFactory = Manager::instance().getIceTransportFactory(); iceTransport_ = iceTransportFactory.createTransport(getCallId().c_str(), channel_num, - master, account_.getUseUPnP()); + master, account_.getUPnPActive()); } int diff --git a/daemon/src/ice_transport.cpp b/daemon/src/ice_transport.cpp index d894daad48c2081209a35c277dc911bbc1a90cb3..6a3671c376065010cf6ad26959731871122ec97c 100644 --- a/daemon/src/ice_transport.cpp +++ b/daemon/src/ice_transport.cpp @@ -32,7 +32,7 @@ #include "logger.h" #include "sip/sip_utils.h" #include "manager.h" -#include "upnp/upnp.h" +#include "upnp/upnp_control.h" #include <pjlib.h> #include <utility> @@ -477,7 +477,7 @@ IceTransport::selectUPnPIceCandidates() IpAddr publicIP = upnp_->getExternalIP(); if (publicIP) { for(unsigned comp_id = 0; comp_id < component_count_; comp_id++) { - RING_DBG("UPnP : Opening port(s) for Ice comp %d and adding candidate with public IP.", comp_id); + RING_DBG("UPnP: Opening port(s) for Ice comp %d and adding candidate with public IP.", comp_id); std::vector<IpAddr> candidates = getLocalCandidatesAddr(comp_id); for(IpAddr addr : candidates) { uint16_t port = addr.getPort(); @@ -486,11 +486,11 @@ IceTransport::selectUPnPIceCandidates() publicIP.setPort(port_used); addCandidate(comp_id, publicIP); } else - RING_WARN("UPnP : Could not create a port mapping for the ICE candidae."); + RING_WARN("UPnP: Could not create a port mapping for the ICE candidae."); } } } else { - RING_WARN("UPnP : Could not determine public IP for ICE candidates."); + RING_WARN("UPnP: Could not determine public IP for ICE candidates."); } } } diff --git a/daemon/src/ringdht/ringaccount.cpp b/daemon/src/ringdht/ringaccount.cpp index 783145fc98a922d29b84ba92b2d188646f3aba46..18a733098170f85ff692f791c150a9fdadec460a 100644 --- a/daemon/src/ringdht/ringaccount.cpp +++ b/daemon/src/ringdht/ringaccount.cpp @@ -61,7 +61,7 @@ #include "config/yamlparser.h" #include <yaml-cpp/yaml.h> -#include "upnp/upnp.h" +#include "upnp/upnp_control.h" #include <algorithm> #include <array> @@ -160,7 +160,7 @@ RingAccount::newOutgoingCall(const std::string& id, const std::string& toUrl) ("sip:"+call->getCallId()).c_str(), ICE_COMPONENTS, true, - getUseUPnP() + getUPnPActive() ); if (not ice or ice->waitForInitialization(ICE_INIT_TIMEOUT) <= 0) { call->setConnectionState(Call::DISCONNECTED); @@ -241,7 +241,7 @@ RingAccount::createOutgoingCall(const std::shared_ptr<SIPCall>& call, const std: call->setCallMediaLocal(call->getIceTransport()->getDefaultLocalAddress()); IpAddr addrSdp; - if (getUseUPnP()) { + if (getUPnPActive()) { /* use UPnP addr, or published addr if its set */ addrSdp = getPublishedSameasLocal() ? getUPnPIpAddress() : getPublishedIpAddress(); @@ -572,7 +572,7 @@ RingAccount::handleEvents() bool RingAccount::mapPortUPnP() { - if (getUseUPnP()) { + if (getUPnPActive()) { /* create port mapping from published port to local port to the local IP * note that since different RING accounts can use the same port, * it may already be open, thats OK @@ -581,6 +581,7 @@ bool RingAccount::mapPortUPnP() * a different port, if succesfull, then we have to use that port for DHT */ uint16_t port_used; + std::lock_guard<std::mutex> lock(upnp_mtx); if (upnp_->addAnyMapping(dhtPort_, ring::upnp::PortType::UDP, false, &port_used)) { if (port_used != dhtPort_) RING_DBG("UPnP could not map port %u for DHT, using %u instead", dhtPort_, port_used); @@ -603,11 +604,23 @@ void RingAccount::doRegister() return; } - /* check UPnP to see if the account setting is enabled and try to get a - * UPnP controller; if this succeeds then try to map the account port */ - if (not (checkUPnP() and mapPortUPnP()) ) - RING_WARN("Could not successfully map DHT port with UPnP, continuing with account registration anyways."); + /* if UPnP is enabled, then wait for IGD to complete registration */ + if ( upnpEnabled_ ) { + auto shared = shared_from_this(); + RING_DBG("UPnP: waiting for IGD to register RING account"); + setRegistrationState(RegistrationState::TRYING); + std::thread{ [shared] { + auto this_ = std::static_pointer_cast<RingAccount>(shared).get(); + if ( not this_->mapPortUPnP()) + RING_WARN("UPnP: Could not successfully map DHT port with UPnP, continuing with account registration anyways."); + this_->doRegister_(); + }}.detach(); + } else + doRegister_(); +} +void RingAccount::doRegister_() +{ try { loadTreatedCalls(); if (dht_.isRunning()) { @@ -724,7 +737,7 @@ void RingAccount::doRegister() ("sip:"+call->getCallId()).c_str(), ICE_COMPONENTS, false, - this_.getUseUPnP() + this_.getUPnPActive() ); if (ice->waitForInitialization(ICE_INIT_TIMEOUT) <= 0) throw std::runtime_error("Can't initialize ICE.."); @@ -787,10 +800,8 @@ void RingAccount::doUnregister(std::function<void(bool)> released_cb) pendingSipCalls_.clear(); } - if (getUseUPnP()) { - RING_DBG("UPnP : removing port mapping for DHT account."); - upnp_->removeMappings(); - } + /* RING_DBG("UPnP: removing port mapping for DHT account."); */ + upnp_->removeMappings(); Manager::instance().unregisterEventHandler((uintptr_t)this); saveNodes(dht_.exportNodes()); diff --git a/daemon/src/ringdht/ringaccount.h b/daemon/src/ringdht/ringaccount.h index 2f3b6b8bc24dbf80685a3723f5f1a15214b468e3..13c5716a4273018ad07af9335c99dcb8ac5b18b5 100644 --- a/daemon/src/ringdht/ringaccount.h +++ b/daemon/src/ringdht/ringaccount.h @@ -256,6 +256,8 @@ class RingAccount : public SIPAccountBase { private: + void doRegister_(); + const dht::ValueType USER_PROFILE_TYPE = {9, "User profile", std::chrono::hours(24 * 7)}; const dht::ValueType ICE_ANNOUCEMENT_TYPE = {10, "ICE descriptors", std::chrono::minutes(3)}; diff --git a/daemon/src/sip/sipaccount.cpp b/daemon/src/sip/sipaccount.cpp index 94406e7b4901800543d6a05186adcb582a870e27..95d815b8b475341ed2a4f8db9ed44c8e632cf205 100644 --- a/daemon/src/sip/sipaccount.cpp +++ b/daemon/src/sip/sipaccount.cpp @@ -70,7 +70,7 @@ #include <sstream> #include <cstdlib> -#include "upnp/upnp.h" +#include "upnp/upnp_control.h" #include "ip_utils.h" namespace ring { @@ -231,7 +231,7 @@ SIPAccount::newOutgoingCall(const std::string& id, const std::string& toUrl) call->setCallMediaLocal(localAddress); IpAddr addrSdp; - if (getUseUPnP()) { + if (getUPnPActive()) { /* use UPnP addr, or published addr if its set */ addrSdp = getPublishedSameasLocal() ? getUPnPIpAddress() : getPublishedIpAddress(); @@ -712,7 +712,7 @@ std::map<std::string, std::string> SIPAccount::getVolatileAccountDetails() const bool SIPAccount::mapPortUPnP() { - if (getUseUPnP()) { + if (getUPnPActive()) { /* create port mapping from published port to local port to the local IP * note that since different accounts can use the same port, * it may already be open, thats OK @@ -745,13 +745,39 @@ void SIPAccount::doRegister() RING_DBG("doRegister %s", hostname_.c_str()); - /* check UPnP to see if the account setting is enabled and try to get a - * UPnP controller; if this succeeds then try to map the account port */ - if (not (checkUPnP() and mapPortUPnP()) ) - RING_WARN("Could not successfully map SIP port with UPnP, continuing with account registration anyways."); + /* if UPnP is enabled, then wait for IGD to complete registration */ + if ( upnpEnabled_ ) { + RING_DBG("UPnP: waiting for IGD to register SIP account"); + setRegistrationState(RegistrationState::TRYING); + auto shared = shared_from_this(); + RING_DBG("UPnP: waiting for IGD to register RING account"); + std::thread{ [shared] { + /* We have to register the external thread so it could access the pjsip frameworks */ + if (!pj_thread_is_registered()) { +#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8) + static thread_local pj_thread_desc desc; + static thread_local pj_thread_t *this_thread; +#else + static __thread pj_thread_desc desc; + static __thread pj_thread_t *this_thread; +#endif + RING_DBG("Registering thread with pjlib"); + pj_thread_register(NULL, desc, &this_thread); + } + + auto this_ = std::static_pointer_cast<SIPAccount>(shared).get(); + if ( not this_->mapPortUPnP()) + RING_WARN("UPnP: Could not successfully map SIP port with UPnP, continuing with account registration anyways."); + this_->doRegister1_(); + }}.detach(); + } else + doRegister1_(); +} +void SIPAccount::doRegister1_() +{ if (hostname_.empty() || isIP2IP()) { - doRegister_(); + doRegister2_(); return; } @@ -767,12 +793,12 @@ void SIPAccount::doRegister() return; } this_->hostIp_ = host_ips[0]; - this_->doRegister_(); + this_->doRegister2_(); } ); } -void SIPAccount::doRegister_() +void SIPAccount::doRegister2_() { bool ipv6 = false; if (isIP2IP()) { @@ -888,10 +914,8 @@ void SIPAccount::doUnregister(std::function<void(bool)> released_cb) if (released_cb) released_cb(true); - if (getUseUPnP()) { - RING_DBG("UPnP : removing port mapping for SIP account."); - upnp_->removeMappings(); - } + /* RING_DBG("UPnP: removing port mapping for SIP account."); */ + upnp_->removeMappings(); } void SIPAccount::startKeepAliveTimer() @@ -969,7 +993,7 @@ SIPAccount::sendRegister() const pj_str_t pjContact(getContactHeader()); if (transport_) { - if (getUseUPnP() or not getPublishedSameasLocal() or (not received.empty() and received != getPublishedAddress())) { + if (getUPnPActive() or not getPublishedSameasLocal() or (not received.empty() and received != getPublishedAddress())) { pjsip_host_port *via = getViaAddr(); RING_DBG("Setting VIA sent-by to %.*s:%d", via->host.slen, via->host.ptr, via->port); @@ -1425,7 +1449,7 @@ SIPAccount::getContactHeader(pjsip_transport* t) hostname_, address, port); - if (getUseUPnP() and getUPnPIpAddress()) { + if (getUPnPActive() and getUPnPIpAddress()) { address = getUPnPIpAddress().toString(); port = publishedPortUsed_; useUPnPAddressPortInVIA(); diff --git a/daemon/src/sip/sipaccount.h b/daemon/src/sip/sipaccount.h index 9cb3a231ca21996ffde431effacbab602150619b..340c62cad328ad07a3f725470dcf4742af63939c 100644 --- a/daemon/src/sip/sipaccount.h +++ b/daemon/src/sip/sipaccount.h @@ -491,7 +491,8 @@ class SIPAccount : public SIPAccountBase { void onRegister(pjsip_regc_cbparam *param); private: - void doRegister_(); + void doRegister1_(); + void doRegister2_(); /** * Set the internal state for this account, mainly used to manage account details from the client application. diff --git a/daemon/src/sip/sipcall.cpp b/daemon/src/sip/sipcall.cpp index bc7744898768395906933ff26d0eb34186e1a8f0..11027bee8051d37714036283514dd6a2b8d5a04f 100644 --- a/daemon/src/sip/sipcall.cpp +++ b/daemon/src/sip/sipcall.cpp @@ -42,7 +42,7 @@ #include "sdp.h" #include "manager.h" #include "array_size.h" -#include "upnp/upnp.h" +#include "upnp/upnp_control.h" #include "audio/audiortp/avformat_rtp_session.h" #include "client/callmanager.h" @@ -127,7 +127,7 @@ SIPCall::SIPCall(SIPAccountBase& account, const std::string& id, Call::CallType #endif , sdp_(new Sdp(id)) { - if (account.getUseUPnP()) + if (account.getUPnPActive()) upnp_.reset(new upnp::Controller()); } @@ -866,7 +866,7 @@ SIPCall::openPortsUPnP() * the newly selected port should possibly be checked against the list of used ports and marked * as used, the old port should be "released" */ - RING_DBG("UPnP : openening ports via upnp for SDP session."); + RING_DBG("UPnP: openening ports via upnp for SDP session."); uint16_t audio_port_used; if (upnp_->addAnyMapping(sdp_->getLocalAudioPort(), upnp::PortType::UDP, true, &audio_port_used)) { uint16_t control_port_used; diff --git a/daemon/src/sip/sipvoiplink.cpp b/daemon/src/sip/sipvoiplink.cpp index 3f7c8e40bb6527ad623ae008dfffa1ee2ec083f3..b7b78398076e9dad7be0cd6dd6f873e51fbcb230 100644 --- a/daemon/src/sip/sipvoiplink.cpp +++ b/daemon/src/sip/sipvoiplink.cpp @@ -320,7 +320,7 @@ transaction_request_cb(pjsip_rx_data *rdata) IpAddr addrToUse = ip_utils::getInterfaceAddr(account->getLocalInterface(), family); IpAddr addrSdp; - if (account->getUseUPnP()) { + if (account->getUPnPActive()) { /* use UPnP addr, or published addr if its set */ addrSdp = account->getPublishedSameasLocal() ? account->getUPnPIpAddress() : account->getPublishedIpAddress(); @@ -899,7 +899,7 @@ sdp_create_offer_cb(pjsip_inv_session *inv, pjmedia_sdp_session **p_offer) IpAddr addrToUse = ip_utils::getInterfaceAddr(account.getLocalInterface(), family); IpAddr address; - if (account.getUseUPnP()) { + if (account.getUPnPActive()) { /* use UPnP addr, or published addr if its set */ address = account.getPublishedSameasLocal() ? account.getUPnPIpAddress() : account.getPublishedIpAddress(); diff --git a/daemon/src/upnp/Makefile.am b/daemon/src/upnp/Makefile.am index 1560c07e7ce77c70c88b74cfdf88fedec95369aa..e62c6dcfc5619236333eccb6de108da869118526 100644 --- a/daemon/src/upnp/Makefile.am +++ b/daemon/src/upnp/Makefile.am @@ -1,11 +1,17 @@ include $(top_srcdir)/globals.mak -noinst_LTLIBRARIES = libupnp.la +noinst_LTLIBRARIES = libupnpcontrol.la -libupnp_la_SOURCES = \ - upnp.cpp \ - upnp.h +libupnpcontrol_la_CXXFLAGS = \ + @CXXFLAGS@ \ + @LIBUPNP_CFLAGS@ -libupnp_la_CXXFLAGS = @CXXFLAGS@ +libupnpcontrol_la_LDFLAGS = @LIBUPNP_LIBS@ -libupnp_la_LDFLAGS = @UPNP_LIBS@ +libupnpcontrol_la_SOURCES = \ + upnp_control.cpp \ + upnp_control.h \ + upnp_context.cpp \ + upnp_context.h \ + upnp_igd.cpp \ + upnp_igd.h diff --git a/daemon/src/upnp/upnp.cpp b/daemon/src/upnp/upnp.cpp deleted file mode 100644 index c2938242d8c76d9e69817ff7482c668d67457ed2..0000000000000000000000000000000000000000 --- a/daemon/src/upnp/upnp.cpp +++ /dev/null @@ -1,631 +0,0 @@ -/* - * Copyright (C) 2004-2014 Savoir-Faire Linux Inc. - * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Additional permission under GNU GPL version 3 section 7: - * - * If you modify this program, or any covered work, by linking or - * combining it with the OpenSSL project's OpenSSL library (or a - * modified version of that library), containing parts covered by the - * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. - * grants you additional permission to convey the resulting work. - * Corresponding Source for a non-source form of such a combination - * shall include the source code for the parts of OpenSSL used as well - * as that of the covered work. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "upnp.h" - -#include <stdio.h> -#include <string.h> -#include <stdlib.h> -#include <map> -#include <random> -#include <memory> - -#if HAVE_UPNP -#include <miniupnpc/upnperrors.h> -#include <miniupnpc/miniwget.h> -#include <miniupnpc/miniupnpc.h> -#include <miniupnpc/upnpcommands.h> -#endif - -#include "logger.h" -#include "ip_utils.h" -#include "ring_types.h" - -namespace ring { namespace upnp { - -#if HAVE_UPNP - -static void -resetURLs(UPNPUrls& urls) -{ - urls.controlURL = nullptr; - urls.ipcondescURL = nullptr; - urls.controlURL_CIF = nullptr; - urls.controlURL_6FC = nullptr; -#ifdef MINIUPNPC_VERSION /* if not defined, its version 1.6 */ - urls.rootdescURL = nullptr; -#endif -} - -/* move constructor */ -IGD::IGD(IGD&& other) - : datas_(other.datas_) - , urls_(other.urls_) -{ - resetURLs(other.urls_); -} - -/* move operator */ -IGD& IGD::operator=(IGD&& other) -{ - datas_ = other.datas_; - urls_ = other.urls_; - resetURLs(other.urls_); - return *this; -} - -IGD::~IGD() -{ - /* free the URLs */ - FreeUPNPUrls(&urls_); -} - -#endif /* HAVE_UPNP */ - -/** - * removes all mappings with the local IP and the given description - */ -void -IGD::removeMappingsByLocalIPAndDescription(const std::string& description) -{ -#if HAVE_UPNP - if (isEmpty()) - return; - - /* need to get the local addr */ - IpAddr local_ip = ip_utils::getLocalAddr(pj_AF_INET()); - if (!local_ip) { - RING_DBG("UPnP : cannot determine local IP"); - return; - } - - int upnp_status; - int i = 0; - char index[6]; - char intClient[40]; - char intPort[6]; - char extPort[6]; - char protocol[4]; - char desc[80]; - char enabled[6]; - char rHost[64]; - char duration[16]; - - RING_DBG("UPnP : removing all port mappings with description: \"%s\" and local ip: %s", - description.c_str(), local_ip.toString().c_str()); - - do { - snprintf(index, 6, "%d", i); - rHost[0] = '\0'; - enabled[0] = '\0'; - duration[0] = '\0'; - desc[0] = '\0'; - extPort[0] = '\0'; - intPort[0] = '\0'; - intClient[0] = '\0'; - upnp_status = UPNP_GetGenericPortMappingEntry(getURLs().controlURL, - getDatas().first.servicetype, - index, - extPort, intClient, intPort, - protocol, desc, enabled, - rHost, duration); - if(upnp_status == UPNPCOMMAND_SUCCESS) { - /* remove if matches description and ip - * once the port mapping is deleted, there will be one less, and the rest will "move down" - * that is, we don't need to increment the mapping index in that case - */ - if( strcmp(description.c_str(), desc) == 0 and strcmp(local_ip.toString().c_str(), intClient) == 0) { - RING_DBG("UPnP : found mapping with matching description and ip:\n\t%s %5s->%s:%-5s '%s'", - protocol, extPort, intClient, intPort, desc); - int delete_err = 0; - delete_err = UPNP_DeletePortMapping(getURLs().controlURL, getDatas().first.servicetype, extPort, protocol, NULL); - if(delete_err != UPNPCOMMAND_SUCCESS) { - RING_DBG("UPnP : UPNP_DeletePortMapping() failed with error code %d : %s", delete_err, strupnperror(delete_err)); - } else { - RING_DBG("UPnP : deletion success"); - /* decrement the mapping index since it will be incremented */ - i--; - } - } - } else if (upnp_status == 713) { - /* 713 : SpecifiedArrayIndexInvalid - * this means there are no more mappings to check, and we're done - */ - } else { - RING_DBG("UPnP : GetGenericPortMappingEntry() failed with error code %d : %s", upnp_status, strupnperror(upnp_status)); - } - i++; - } while(upnp_status == UPNPCOMMAND_SUCCESS); -#endif -} - -/** - * checks if the instance of IGD is empty - * ie: not actually an IGD - */ -bool -IGD::isEmpty() const -{ -#if HAVE_UPNP - if (urls_.controlURL != nullptr) { - if (urls_.controlURL[0] == '\0') { - return true; - } else { - return false; - } - } else { - return true; - } -#else - return true; -#endif -} - -bool operator== (Mapping &cMap1, Mapping &cMap2) -{ - /* we don't compare the description because it doesn't change the function of the - * mapping; we don't compare the IGD because for now we assume that we always - * use the same one and that all mappings are active - */ - return (cMap1.local_ip == cMap2.local_ip && - cMap1.port_external == cMap2.port_external && - cMap1.port_internal == cMap2.port_internal && - cMap1.type == cMap2.type); -} - -bool operator!= (Mapping &cMap1, Mapping &cMap2) -{ - return !(cMap1 == cMap2); -} - -Controller::Controller() - : defaultIGD_(getIGD()) - , udpGlobalMappings_(getGlobalInstance<UDPMapGlobal>()) - , tcpGlobalMappings_(getGlobalInstance<TCPMapGlobal>()) -{} - -Controller::~Controller() -{ - /* remove all mappings */ - removeMappings(); -} - -/** - * Return whether or not this controller has a valid IGD, - * if 'flase' then all requests will fail - */ -bool -Controller::hasValidIGD() -{ -#if HAVE_UPNP - return not defaultIGD_->isEmpty(); -#endif - return false; -} - -/** - * tries to add mapping - */ -bool -Controller::addMapping(uint16_t port_external, uint16_t port_internal, PortType type, int *upnp_error) -{ - /* initialiaze upnp_error */ - if (upnp_error) - *upnp_error = -1; /* UPNPCOMMAND_UNKNOWN_ERROR */ -#if HAVE_UPNP - if (defaultIGD_->isEmpty()) { - RING_WARN("UPnP : cannot perform command as the IGD has either not been chosen or is not available."); - return false; - } - - int upnp_status; - - IpAddr local_ip = ip_utils::getLocalAddr(pj_AF_INET()); - if (!local_ip) { - RING_DBG("UPnP : cannot determine local IP"); - return false; - } - - Mapping mapping{defaultIGD_, local_ip, port_external, port_internal}; - - /* check if a mapping with the same external port already exists in this instance - * if one exists, check that it is the same, if so, nothing needs to be done - * if it is not the same, then we can't add it - */ - auto& instanceMappings = type == PortType::UDP ? (PortMapLocal&)udpInstanceMappings_ : (PortMapLocal&)tcpInstanceMappings_; - auto instanceIter = instanceMappings.find(port_external); - if (instanceIter != instanceMappings.end()) { - /* mapping exists with the same exteran port */ - Mapping *mapping_ptr = &instanceIter->second; - if (*mapping_ptr == mapping) { - /* the same mapping has already been mapped by this instance, nothing to do */ - RING_DBG("UPnP : mapping has already been added previously."); - if (upnp_error) - *upnp_error = UPNPCOMMAND_SUCCESS; - return true; - } else { - /* this port is already used by a different mapping */ - RING_WARN("UPnP : cannot add a mapping with an external port which is already used by another:\n\tcurrent: %s\n\ttrying to add: %s", - mapping_ptr->toString().c_str(), mapping.toString().c_str()); - if (upnp_error) - *upnp_error = 718; /* ConflictInMappingEntry */ - return false; - } - } - - /* mapping doesn't exist in this instance, check if it was added by another - * if the mapping is the same, then we just need to increment the number of users globally - * if the mapping is not the same, then we have to return fail, as the external port is used - * for something else - * if the mapping doesn't exist, then try to add it - */ - auto globalMappings = type == PortType::UDP ? (PortMapGlobal*)udpGlobalMappings_.get() : (PortMapGlobal*)tcpGlobalMappings_.get(); - auto iter = globalMappings->find(port_external); - if (iter != globalMappings->end()) { - /* mapping exists with same external port */ - GlobalMapping *mapping_ptr = &iter->second; - if (*mapping_ptr == mapping) { - /* the same mapping, so nothing needs to be done */ - RING_DBG("UPnP : mapping was already added."); - if (upnp_error) - *upnp_error = UPNPCOMMAND_SUCCESS; - /* increment users */ - mapping_ptr->users++; - RING_DBG("UPnp : number of users of mapping should now be incremented: %d", iter->second.users); - /* add it to map of instance mappings */ - instanceMappings.emplace(std::make_pair(port_external,std::move(mapping))); - return true; - } else { - /* this port is already used by a different mapping */ - RING_WARN("UPnP : cannot add a mapping with an external port which is already used by another:\n\tcurrent: %s\n\ttrying to add: %s", - mapping_ptr->toString().c_str(), mapping.toString().c_str()); - if (upnp_error) - *upnp_error = 718; /* ConflictInMappingEntry */ - return false; - } - } - - /* mapping doesn't exist, so try to add it */ - RING_DBG("UPnP : adding port mapping : %s", mapping.toString().c_str()); - - upnp_status = UPNP_AddPortMapping(mapping.igd->getURLs().controlURL, - mapping.igd->getDatas().first.servicetype, - mapping.getExternalPort().c_str(), - mapping.getInternalPort().c_str(), - mapping.local_ip.toString().c_str(), - mapping.description.c_str(), - mapping.getType().c_str(), - NULL, NULL); - - if (upnp_error) - *upnp_error = upnp_status; - - if(upnp_status!=UPNPCOMMAND_SUCCESS) { - RING_DBG("UPnP : AddPortMapping(%s) failed with error: %d: %s", - mapping.toString().c_str(), upnp_status, strupnperror(upnp_status)); - return false; - } else { - /* success; add it to global list and local list */ - globalMappings->emplace(std::make_pair(port_external, std::move(GlobalMapping{mapping}))); - instanceMappings.emplace(std::make_pair(port_external,std::move(mapping))); - return true; - } -#else - return false; -#endif -} - -/** - * tries to add mapping from and to the port_desired - * if unique == true, makes sure the client is not using this port already - * if the mapping fails, tries other available ports until success - *defaultIGD_ - * tries to use a random port between 1024 < > 65535 if desired port fails - * - * maps port_desired to port_local; if use_same_port == true, makes sure that - * that the extranl and internal ports are the same - */ -bool -Controller::addAnyMapping(uint16_t port_desired, uint16_t port_local, PortType type, bool use_same_port, bool unique, uint16_t *port_used) -{ -#if HAVE_UPNP - if (defaultIGD_->isEmpty()) { - RING_WARN("UPnP : cannot perform command as the IGD has either not been chosen or is not available."); - return false; - } - - auto globalMappings = type == PortType::UDP ? (PortMapGlobal*)udpGlobalMappings_.get() : (PortMapGlobal*)tcpGlobalMappings_.get(); - if (unique) { - /* check that port is not already used by the client */ - auto iter = globalMappings->find(port_desired); - if (iter != globalMappings->end()) { - /* port already used, we need a unique port */ - port_desired = chooseRandomPort(type); - } - } - - if (use_same_port) - port_local = port_desired; - - int upnp_error; - bool result = addMapping(port_desired, port_local, type, &upnp_error); - /* keep trying to add the mapping as long as the upnp error is 718 == conflicting mapping - * if adding the mapping fails for any other reason, give up - */ - while( result == false and (upnp_error == 718 or upnp_error == 402) ) { - /* acceptable errors to keep trying: - * 718 : conflictin mapping - * 402 : invalid args (due to router implementation) - */ - /* TODO: make sure we don't try sellecting the same random port twice if it fails ? */ - port_desired = chooseRandomPort(type); - if (use_same_port) - port_local = port_desired; - result = addMapping(port_desired, port_local, type, &upnp_error); - } - - *port_used = port_desired; - return result; -#else - return false; -#endif -} - -/** - * addAnyMapping with the local port being the same as the external port - */ -bool -Controller::addAnyMapping(uint16_t port_desired, PortType type, bool unique, uint16_t *port_used) { - addAnyMapping(port_desired, port_desired, type, true, unique, port_used); -} - -/** - * chooses a random port that is not yet used by the daemon for UPnP - */ -uint16_t -Controller::chooseRandomPort(PortType type) -{ - auto globalMappings = type == PortType::UDP ? (PortMapGlobal*)udpGlobalMappings_.get() : (PortMapGlobal*)tcpGlobalMappings_.get(); - - std::random_device rd; // obtain a random number from hardware - std::mt19937 gen(rd()); // seed the generator - std::uniform_int_distribution<uint16_t> dist(Mapping::UPNP_PORT_MIN, Mapping::UPNP_PORT_MAX); // define the range - - uint16_t port = dist(gen); - - /* keep generating random ports until we find one which is not used */ - while(globalMappings->find(port) != globalMappings->end()) { - port = dist(gen); - } - - RING_DBG("UPnP : chose random port %u", port); - - return port; -} - -/** - * removes mappings added by this instance of the specified port type - * if an mapping has more than one user in the global list, it is not deleted - * from the router, but the number of users is decremented - */ -void -Controller::removeMappings(PortType type) { -#if HAVE_UPNP - auto& instanceMappings = type == PortType::UDP ? (PortMapLocal&)udpInstanceMappings_ : (PortMapLocal&)tcpInstanceMappings_; - for (auto instanceIter = instanceMappings.begin(); instanceIter != instanceMappings.end(); ){ - /* first check if there is more than one global user of this mapping */ - int users = 1; - auto globalMappings = type == PortType::UDP ? (PortMapGlobal*)udpGlobalMappings_.get() : (PortMapGlobal*)tcpGlobalMappings_.get(); - auto iter = globalMappings->find(instanceIter->first); - if ( iter != globalMappings->end() ) { - GlobalMapping *mapping_ptr = &iter->second; - users = mapping_ptr->users; - - if ( users > 1 ) { - /* just decrement the users */ - mapping_ptr->users--; - } else { - /* remove the mapping from the global list */ - globalMappings->erase(iter); - } - } - - /* now remove the mapping from the instance list... - * even if the delete operation fails, we assume that the mapping was deleted - */ - auto& mapping = instanceIter->second; - instanceIter = instanceMappings.erase(instanceIter); - - /* delete the mapping from the router if this instance was the only user */ - if (users < 2) { - RING_DBG("UPnP : deleting mapping: %s", mapping.toString().c_str()); - int upnp_status = UPNP_DeletePortMapping(mapping.igd->getURLs().controlURL, - mapping.igd->getDatas().first.servicetype, - mapping.getExternalPort().c_str(), - mapping.getType().c_str(), - NULL); - if(upnp_status != UPNPCOMMAND_SUCCESS) { - RING_DBG("UPnP : DeletePortMapping(%s) failed with error: %d: %s", - mapping.toString().c_str(), upnp_status, strupnperror(upnp_status)); - } else { - /* RING_DBG("UPnP : deletion success"); */ - } - } else { - RING_DBG("UPnP : removing mapping: %s, but not deleting because there are %d other users", - mapping.toString().c_str(), users - 1); - } - } -#endif -} - -/** - * removes all mappings added by this instance - */ -void -Controller::removeMappings() -{ -#if HAVE_UPNP - removeMappings(PortType::UDP); - removeMappings(PortType::TCP); -#endif -} - -/** - * tries to get the external ip of the router - */ -IpAddr -Controller::getExternalIP() -{ -#if HAVE_UPNP - if (defaultIGD_->isEmpty()) { - RING_WARN("UPnP : cannot perform command as the IGD has either not been chosen or is not available."); - return {}; - } - - int upnp_status; - char externalIPAddress[40]; - - RING_DBG("UPnP : getting external IP"); - - upnp_status = UPNP_GetExternalIPAddress(defaultIGD_->getURLs().controlURL, - defaultIGD_->getDatas().first.servicetype, - externalIPAddress); - if(upnp_status != UPNPCOMMAND_SUCCESS) { - RING_DBG("UPnP : GetExternalIPAddress failed with error code %d : %s", upnp_status, strupnperror(upnp_status)); - return {}; - } else { - RING_DBG("UPnP : got external IP = %s", externalIPAddress); - return {std::string(externalIPAddress)}; - } -#else - /* return empty address */ - return {}; -#endif -} - -/** - * tries to find a valid IGD on the network - */ -static IGD -chooseIGD(void) -{ -#if HAVE_UPNP - std::unique_ptr<UPNPDev, decltype(freeUPNPDevlist)&> devlist(nullptr, freeUPNPDevlist); - int upnp_status = 0; - - RING_DBG("UPnP : finding default IGD"); - - /* look for UPnP devices on the network */ - devlist.reset(upnpDiscover(2000, NULL, NULL, 0, 0, &upnp_status)); - - if (devlist) { - UPNPDev * device = nullptr; - std::unique_ptr<UPNPUrls, decltype(FreeUPNPUrls)&> newIGDURLs(new UPNPUrls, FreeUPNPUrls); - std::unique_ptr<IGDdatas> newIGDDatas(new IGDdatas); - char lanaddr[64]; - - RING_DBG("UPnP devices found on the network"); - for(device = devlist.get(); device; device = device->pNext) - { - RING_DBG(" desc: %s\n st: %s", - device->descURL, device->st); - } - - upnp_status = UPNP_GetValidIGD(devlist.get(), newIGDURLs.get(), newIGDDatas.get(), lanaddr, sizeof(lanaddr)); - - switch(upnp_status) { - case -1: - RING_ERR("UPnP : internal error getting valid IGD"); - break; - case 0: - RING_WARN("UPnP : no valid IGD found"); - break; - case 1: - RING_DBG("UPnP : found valid IGD : %s", newIGDURLs->controlURL); - break; - case 2: - RING_WARN("UPnP : found an IGD, but it does not seem to be connected : %s", newIGDURLs->controlURL); - break; - case 3: - RING_WARN("UPnP : UPnP device found, but it does not seem to be an IGD : %s", newIGDURLs->controlURL); - break; - default: - RING_WARN("UPnP : device found, but cannot determine if it is a UPnP device : %s", newIGDURLs->controlURL); - } - - /* only accept IGD if it was determined to be valid */ - if (upnp_status == 1) { - RING_DBG("UPnP : local IP address reported as: %s", lanaddr); - return {*newIGDDatas.release(), *newIGDURLs.release()}; - } else { - return {}; - } - } else { - RING_WARN("UPnP : looking for IGD UPnP devices on the network failed with error: %d : %s", upnp_status, strupnperror(upnp_status)); - return {}; - } - -#else - return {}; -#endif -} - -static void -clean_igd(IGD* igd) -{ - if (igd and not igd->isEmpty()) { - RING_DBG("UPnP : removing all RING mappings before deleting shared IGD object"); - igd->removeMappingsByLocalIPAndDescription(Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION); - } - delete igd; -}; - -std::shared_ptr<IGD> -getIGD(void) -{ - static std::shared_ptr<IGD> igd(nullptr, clean_igd); - - if (not igd) { - igd = std::make_shared<IGD>(chooseIGD()); - if (not igd->isEmpty()) { - /* remove any old RING mappings the first time we find an IGD */ - RING_DBG("UPnP : removing any existing RING mappings for new IGD"); - igd->removeMappingsByLocalIPAndDescription(Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION); - } - } - - return igd; -} - -}} // namespace ring::upnp diff --git a/daemon/src/upnp/upnp.h b/daemon/src/upnp/upnp.h deleted file mode 100644 index db2616b88b814d9c83e741219977ee5aebd0a363..0000000000000000000000000000000000000000 --- a/daemon/src/upnp/upnp.h +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (C) 2004-2014 Savoir-Faire Linux Inc. - * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Additional permission under GNU GPL version 3 section 7: - * - * If you modify this program, or any covered work, by linking or - * combining it with the OpenSSL project's OpenSSL library (or a - * modified version of that library), containing parts covered by the - * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. - * grants you additional permission to convey the resulting work. - * Corresponding Source for a non-source form of such a combination - * shall include the source code for the parts of OpenSSL used as well - * as that of the covered work. - */ - -#ifndef UPNP_H_ -#define UPNP_H_ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include <string> -#include <map> -#include <memory> - -#include "noncopyable.h" -#include "logger.h" -#include "ip_utils.h" - -#if HAVE_UPNP -#include <miniupnpc/miniupnpc.h> -#endif - -namespace ring { namespace upnp { - -/* defines a UPnP capable Internet Gateway Device (a router) */ -class IGD { -public: - -#if HAVE_UPNP - - /* constructors */ - IGD() : datas_(), urls_() {}; - IGD(IGDdatas& d , UPNPUrls& u) : datas_(d), urls_(u) {}; - - /* move constructor and operator */ - IGD(IGD&& other); - IGD& operator=(IGD&& other); - - ~IGD(); - - const IGDdatas& getDatas() const {return datas_;}; - const UPNPUrls& getURLs() const {return urls_;}; -#else - /* use default constructor and destructor */ - IGD() = default; - ~IGD() = default;; - /* use default move constructor and operator */ - IGD(IGD&&) = default; - IGD& operator=(IGD&&) = default; -#endif - bool isEmpty() const; - - /** - * removes all mappings with the local IP and the given description - */ - void removeMappingsByLocalIPAndDescription(const std::string& description); - -private: - NON_COPYABLE(IGD); - -#if HAVE_UPNP - IGDdatas datas_; - UPNPUrls urls_; -#endif - -}; - -enum class PortType {UDP,TCP}; - -/* defines a UPnP port mapping */ -class Mapping { -public: - constexpr static char const * UPNP_DEFAULT_MAPPING_DESCRIPTION = "RING"; - /* TODO: what should the port range really be? - * Should it be the ephemeral ports as defined by the system? - */ - constexpr static uint16_t UPNP_PORT_MIN = 1024; - constexpr static uint16_t UPNP_PORT_MAX = 65535; - - std::shared_ptr<const IGD> igd; /* the IGD associated with this mapping */ - IpAddr local_ip; /* the destination of the mapping */ - uint16_t port_external; - uint16_t port_internal; - PortType type; /* UPD or TCP */ - std::string description; - - Mapping( - std::shared_ptr<const IGD> igd = std::make_shared<const IGD>(), - IpAddr local_ip = IpAddr(), - uint16_t port_external = 0, - uint16_t port_internal = 0, - PortType type = PortType::UDP, - std::string description = UPNP_DEFAULT_MAPPING_DESCRIPTION) - : igd(igd) - , local_ip(local_ip) - , port_external(port_external) - , port_internal(port_internal) - , type(type) - , description(description) - {}; - - /* move constructor and operator */ - Mapping(Mapping&&) = default; - Mapping& operator=(Mapping&&) = default; - - friend bool operator== (Mapping &cRedir1, Mapping &cRedir2); - friend bool operator!= (Mapping &cRedir1, Mapping &cRedir2); - - std::string getExternalPort() const {return std::to_string(port_external);}; - std::string getInternalPort() const {return std::to_string(port_internal);}; - std::string getType() const { return type == PortType::UDP ? "UDP" : "TCP";}; - std::string toString() const { - return local_ip.toString() + ", " + getExternalPort() + ":" + getInternalPort() + ", " + getType(); - }; - - bool isValid() const { - return igd->isEmpty() or port_external == 0 or port_internal == 0 ? false : true; - }; -private: - NON_COPYABLE(Mapping); -}; - -/** - * GlobalMapping is like a mapping, but it tracks the number of global users, - * ie: the number of upnp:Controller which are using this mapping - * this is usually only relevant for accounts (not calls) as multiple SIP accounts - * can use the same SIP port and we don't want to delete a mapping from the router - * if other accounts are using it - */ -class GlobalMapping : public Mapping { -public: - /* number of users of this mapping; - * this is only relevant when multiple accounts are using the same SIP port */ - unsigned users; - GlobalMapping(const Mapping& mapping, unsigned users = 1) - : Mapping(mapping.igd, mapping.local_ip, mapping.port_external, mapping.port_internal, mapping.type, mapping.description) - , users(users) - {}; -}; - -class Controller { -public: - /* constructor */ - Controller(); - /* destructor */ - ~Controller(); - - /** - * Return whether or not this controller has a valid IGD, - * if 'flase' then all requests will fail - */ - bool hasValidIGD(); - - /** - * tries to add mapping from and to the port_desired - * if unique == true, makes sure the client is not using this port already - * if the mapping fails, tries other available ports until success - * - * tries to use a random port between 1024 < > 65535 if desired port fails - * - * maps port_desired to port_local; if use_same_port == true, makes sure that - * that the extranl and internal ports are the same - */ - bool addAnyMapping(uint16_t port_desired, uint16_t port_local, PortType type, bool use_same_port, bool unique, uint16_t *port_used); - - /** - * addAnyMapping with the local port being the same as the external port - */ - bool addAnyMapping(uint16_t port_desired, PortType type, bool unique, uint16_t *port_used); - - /** - * removes all mappings added by this instance - */ - void removeMappings(); - - /** - * tries to get the external ip of the IGD (router) - */ - IpAddr getExternalIP(); - -private: - /* subclasses to make it easier to differentiate and cast maps of port mappings */ - class PortMapLocal : public std::map<uint16_t, Mapping> {}; - class UDPMapLocal : public PortMapLocal {}; - class TCPMapLocal : public PortMapLocal {}; - class PortMapGlobal : public std::map<uint16_t, GlobalMapping> {}; - class UDPMapGlobal: public PortMapGlobal {}; - class TCPMapGlobal : public PortMapGlobal {}; - - /** - * The IGD being used by this instance; - * TODO: Currently, we assume that the IGD will not change once it has been selected; - * however, the user could switch routers while the client is running - */ - std::shared_ptr<IGD> defaultIGD_; - - /** - * list of mappings created by this instance - * the key is the external port number, as there can only be one mapping - * at a time for each external port - */ - UDPMapLocal udpInstanceMappings_; - TCPMapLocal tcpInstanceMappings_; - - /** - * list of all mappings - */ - std::shared_ptr<UDPMapGlobal> udpGlobalMappings_; - std::shared_ptr<TCPMapGlobal> tcpGlobalMappings_; - - /** - * tries to add mapping - */ - bool addMapping(uint16_t port_external, uint16_t port_internal, PortType type = PortType::UDP, int *upnp_error = nullptr); - - /** - * Try to remove all mappings of the given type - */ - void removeMappings(PortType type); - - /** - * chooses a random port that is not yet used by the daemon for UPnP - */ - uint16_t chooseRandomPort(PortType type); -}; - -std::shared_ptr<IGD> getIGD(); - -}} // namespace ring::upnp - -#endif /* UPNP_H_ */ diff --git a/daemon/src/upnp/upnp_context.cpp b/daemon/src/upnp/upnp_context.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2cdc46d49b50dbba201907236cb02c79c7ffdcf9 --- /dev/null +++ b/daemon/src/upnp/upnp_context.cpp @@ -0,0 +1,1061 @@ +/* + * Copyright (C) 2004-2015 Savoir-Faire Linux Inc. + * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "upnp_context.h" + +#include <string> +#include <set> +#include <mutex> +#include <memory> +#include <condition_variable> +#include <random> + +#if HAVE_LIBUPNP +#include <upnp/upnp.h> +#include <upnp/upnptools.h> +#endif + +#include "logger.h" +#include "ip_utils.h" +#include "upnp_igd.h" + +namespace ring { namespace upnp { + +/** + * This should be used to get a UPnPContext. + * It only makes sense to have one unless you have separate + * contexts for multiple internet interfaces, which is not currently + * supported. + */ +std::shared_ptr<UPnPContext> +getUPnPContext() +{ + static auto context = std::make_shared<UPnPContext>(); + return context; +} + +#if HAVE_LIBUPNP + +/* UPnP IGD definitions */ +constexpr static const char * UPNP_ROOT_DEVICE = "upnp:rootdevice"; +constexpr static const char * UPNP_IGD_DEVICE = "urn:schemas-upnp-org:device:InternetGatewayDevice:1"; +constexpr static const char * UPNP_WAN_DEVICE = "urn:schemas-upnp-org:device:WANDevice:1"; +constexpr static const char * UPNP_WANCON_DEVICE = "urn:schemas-upnp-org:device:WANConnectionDevice:1"; +constexpr static const char * UPNP_WANIP_SERVICE = "urn:schemas-upnp-org:service:WANIPConnection:1"; +constexpr static const char * UPNP_WANPPP_SERVICE = "urn:schemas-upnp-org:service:WANPPPConnection:1"; + +/* UPnP error codes */ +constexpr static int INVALID_ARGS = 402; +constexpr static const char * INVALID_ARGS_STR = "402"; +constexpr static int ARRAY_IDX_INVALID = 713; +constexpr static const char * ARRAY_IDX_INVALID_STR = "713"; +constexpr static int CONFLICT_IN_MAPPING = 718; +constexpr static const char * CONFLICT_IN_MAPPING_STR = "718"; + +/* + * Local prototypes + */ +int cp_callback(Upnp_EventType, void*, void*); +static std::string get_element_text(IXML_Node*); +static std::string get_first_doc_item(IXML_Document*, const char*); +static std::string get_first_element_item(IXML_Element*, const char*); +static void checkResponseError(IXML_Document*); + +UPnPContext::UPnPContext() +{ + int upnp_err; + char* ip_address = nullptr; + unsigned short port = 0; + + /* TODO: allow user to specify interface to be used + * by selecting the IP + */ + +#ifdef UPNP_ENABLE_IPV6 + /* TODO: test if ipv6 support works properly, eg: what if router doesn't support ipv6? */ + RING_DBG("UPnP: using IPv6"); + upnp_err = UpnpInit2(0, 0); +#else + RING_DBG("UPnP: using IPv4"); + upnp_err = UpnpInit(0, 0); +#endif + if ( upnp_err != UPNP_E_SUCCESS ) { + RING_ERR("UPnP: error in UpnpInit(): %s", UpnpGetErrorMessage(upnp_err)); + UpnpFinish(); + } + initialized_ = true; + + ip_address = UpnpGetServerIpAddress(); /* do not free, it is freed by UpnpFinish() */ + port = UpnpGetServerPort(); + + RING_DBG("UPnP: initialiazed on %s:%u", ip_address, port); + + /* relax the parser to allow malformed XML text */ + ixmlRelaxParser( 1 ); + + /* Register a control point to start looking for devices right away */ + upnp_err = UpnpRegisterClient( cp_callback, nullptr, &ctrlptHandle_ ); + if ( upnp_err != UPNP_E_SUCCESS ) { + RING_ERR("UPnP: error registering control point: %s", + UpnpGetErrorMessage(upnp_err)); + UpnpFinish(); + initialized_ = false; + } + clientRegistered_ = true; + + /* send out async searches; + * even if no account is using UPnP currently we might as well start + * gathering a list of available devices; + * we will probably receive their advertisements either way + */ + searchForIGD(); +} + +UPnPContext::~UPnPContext() +{ + if (initialized_){ + /* make sure everything is unregistered, freed, and UpnpFinish() is called */ + + { + std::lock_guard<std::mutex> lock(validIGDMutex_); + for( auto const &it : validIGDs_) { + removeMappingsByLocalIPAndDescription(it.second.get(), Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION); + } + } + + if (clientRegistered_) + UpnpUnRegisterClient( ctrlptHandle_ ); + + if (deviceRegistered_) + UpnpUnRegisterRootDevice( deviceHandle_ ); + + UpnpFinish(); + } +} + +void +UPnPContext::searchForIGD() +{ + if (clientRegistered_) { + std::lock_guard<std::mutex> lock(igdSearchMutex_); + /* send out search for both types, as some routers may possibly only reply to one */ + UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT_SEC, UPNP_ROOT_DEVICE, nullptr); + UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT_SEC, UPNP_IGD_DEVICE, nullptr); + UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT_SEC, UPNP_WANIP_SERVICE, nullptr); + UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT_SEC, UPNP_WANPPP_SERVICE, nullptr); + + pendingIGDSearchRequests_ = 4; + } else + RING_WARN("UPnP: Control Point not registered."); +} + +/** + * returns 'true' if there is at least one valid (connected) IGD + * note: this function will until an IGD has been found or SEARCH_TIMEOUT_SEC + * have expired; the timeout starts when the context is created as we start + * searchign for IGDs immediately + */ +bool +UPnPContext::hasValidIGD() +{ + if (not clientRegistered_) { + RING_WARN("UPnP: Control Point not registered."); + return false; + } + + { + std::unique_lock<std::mutex> lock(igdSearchMutex_); + igdSearchCondition_.wait(lock, [this] { return pendingIGDSearchRequests_ <= 0;}); + } + + std::lock_guard<std::mutex> lock(validIGDMutex_); + return not validIGDs_.empty(); +} + +/** + * chooses the IGD to use, + * assumes you already have a lock on validIGDMutex_ + */ +IGD* +UPnPContext::chooseIGD_unlocked() +{ + if (validIGDs_.empty()) + return nullptr; + return validIGDs_.begin()->second.get(); +} + +/** + * tries to add mapping + */ +Mapping +UPnPContext::addMapping(IGD* igd, + uint16_t port_external, + uint16_t port_internal, + PortType type, + int *upnp_error) +{ + *upnp_error = -1; + + IpAddr local_ip = ip_utils::getLocalAddr(pj_AF_INET()); + if (!local_ip) { + RING_DBG("UPnP: cannot determine local IP"); + return {}; + } + + Mapping mapping{local_ip, port_external, port_internal}; + + /* check if this mapping already exists + * if the mapping is the same, then we just need to increment the number of users globally + * if the mapping is not the same, then we have to return fail, as the external port is used + * for something else + * if the mapping doesn't exist, then try to add it + */ + auto globalMappings = type == PortType::UDP ? &igd->udpMappings : &igd->tcpMappings; + auto iter = globalMappings->find(port_external); + if (iter != globalMappings->end()) { + /* mapping exists with same external port */ + GlobalMapping *mapping_ptr = &iter->second; + if (*mapping_ptr == mapping) { + /* the same mapping, so nothing needs to be done */ + *upnp_error = UPNP_E_SUCCESS; + ++(mapping_ptr->users); + RING_DBG("UPnp : mapping already exists, incrementing number of users: %d", + iter->second.users); + return mapping; + } else { + /* this port is already used by a different mapping */ + RING_WARN("UPnP: cannot add a mapping with an external port which is already used by another:\n\tcurrent: %s\n\ttrying to add: %s", + mapping_ptr->toString().c_str(), mapping.toString().c_str()); + *upnp_error = CONFLICT_IN_MAPPING; + return {}; + } + } + + /* mapping doesn't exist, so try to add it */ + RING_DBG("UPnP: adding port mapping : %s", mapping.toString().c_str()); + + if(addPortMapping(igd, mapping, upnp_error)) { + /* success; add it to global list */ + globalMappings->emplace(port_external, std::move(GlobalMapping{mapping})); + return mapping; + } + return {}; +} + +static uint16_t +generateRandomPort() +{ + /* obtain a random number from hardware */ + static std::random_device rd; + /* seed the generator */ + static std::mt19937 gen(rd()); + /* define the range */ + static std::uniform_int_distribution<uint16_t> dist(Mapping::UPNP_PORT_MIN, Mapping::UPNP_PORT_MAX); + + return dist(gen);; +} + +/** + * chooses a random port that is not yet used by the daemon for UPnP + */ +uint16_t +UPnPContext::chooseRandomPort(const IGD* igd, PortType type) +{ + auto globalMappings = type == PortType::UDP ? + &igd->udpMappings : &igd->tcpMappings; + + uint16_t port = generateRandomPort(); + + /* keep generating random ports until we find one which is not used */ + while(globalMappings->find(port) != globalMappings->end()) { + port = generateRandomPort(); + } + + RING_DBG("UPnP: chose random port %u", port); + + return port; +} + +/** + * tries to add mapping from and to the port_desired + * if unique == true, makes sure the client is not using this port already + * if the mapping fails, tries other available ports until success + * + * tries to use a random port between 1024 < > 65535 if desired port fails + * + * maps port_desired to port_local; if use_same_port == true, makes sure that + * that the external and internal ports are the same + * + * returns a valid mapping on success and an invalid mapping on failure + */ +Mapping +UPnPContext::addAnyMapping(uint16_t port_desired, + uint16_t port_local, + PortType type, + bool use_same_port, + bool unique) +{ + if (not hasValidIGD()) { + RING_WARN("UPnP: no valid IGD availabe"); + return {}; + } + + /* get a lock on the igd list because we don't want the igd to be modified + * or removed from the list while using it */ + std::lock_guard<std::mutex> lock(validIGDMutex_); + IGD* igd = chooseIGD_unlocked(); + if (not igd) { + RING_WARN("UPnP: no valid IGD availabe"); + return {}; + } + + auto globalMappings = type == PortType::UDP ? + &igd->udpMappings : &igd->tcpMappings; + if (unique) { + /* check that port is not already used by the client */ + auto iter = globalMappings->find(port_desired); + if (iter != globalMappings->end()) { + /* port already used, we need a unique port */ + port_desired = chooseRandomPort(igd, type); + } + } + + if (use_same_port) + port_local = port_desired; + + int upnp_error; + Mapping mapping = addMapping(igd, port_desired, port_local, type, &upnp_error); + /* keep trying to add the mapping as long as the upnp error is 718 == conflicting mapping + * if adding the mapping fails for any other reason, give up + */ + while ( not mapping and (upnp_error == CONFLICT_IN_MAPPING or upnp_error == INVALID_ARGS) ) { + /* acceptable errors to keep trying: + * 718 : conflictin mapping + * 402 : invalid args (due to router implementation) + */ + /* TODO: make sure we don't try sellecting the same random port twice if it fails ? */ + port_desired = chooseRandomPort(igd, type); + if (use_same_port) + port_local = port_desired; + mapping = addMapping(igd, port_desired, port_local, type, &upnp_error); + } + + return mapping; +} + +/** + * tries to remove the given mapping + */ +void +UPnPContext::removeMapping(const Mapping& mapping) +{ + if (not hasValidIGD()) { + RING_WARN("UPnP: no valid IGD availabe"); + return; + } + + /* get a lock on the igd list because we don't want the igd to be modified + * or removed from the list while using it */ + std::lock_guard<std::mutex> lock(validIGDMutex_); + IGD* igd = chooseIGD_unlocked(); + if (not igd) { + RING_WARN("UPnP: no valid IGD availabe"); + return;; + } + + /* first make sure the mapping exists in the global list of the igd */ + auto globalMappings = mapping.getType() == PortType::UDP ? + &igd->udpMappings : &igd->tcpMappings; + + auto iter = globalMappings->find(mapping.getPortExternal()); + if ( iter != globalMappings->end() ) { + /* make sure its the same mapping */ + GlobalMapping *global_mapping = &iter->second; + if (mapping == *global_mapping ) { + /* now check the users */ + if (global_mapping->users > 1) { + /* more than one user, simply decrement the number */ + --(global_mapping->users); + RING_DBG("UPnP: decrementing user of mapping: %s, %d users remaining", + mapping.toString().c_str(), global_mapping->users); + } else { + /* no other users, can delete */ + RING_DBG("UPnP: removing port mapping : %s", + mapping.toString().c_str()); + deletePortMapping(igd, + mapping.getPortExternalStr(), + mapping.getTypeStr()); + globalMappings->erase(iter); + } + } else { + RING_WARN("UPnP: cannot remove mapping which doesn't match the existing one in the IGD list"); + } + } else { + RING_WARN("UPnP: cannot remove mapping which is not in the list of existing mappings of the IGD"); + } +} + +IpAddr +UPnPContext::getExternalIP() +{ + if (not hasValidIGD()) { + RING_WARN("UPnP: no valid IGD availabe"); + return {}; + } + + /* get a lock on the igd list because we don't want the igd to be modified + * or removed from the list while using it */ + std::lock_guard<std::mutex> lock(validIGDMutex_); + IGD* igd = chooseIGD_unlocked(); + if (not igd) { + RING_WARN("UPnP: no valid IGD availabe"); + return {}; + } + + /* if its a valid igd, we must have already gotten the external ip */ + return igd->publicIp; +} + +/** + * Parses the device description and adds desired devices to + * relevant lists + */ +void +UPnPContext::parseDevice(IXML_Document* doc, const Upnp_Discovery* d_event) +{ + if (not doc or not d_event) + return; + + /* check to see the device type */ + std::string deviceType = get_first_doc_item(doc, "deviceType"); + if (deviceType.empty()) { + /* RING_DBG("UPnP: could not find deviceType in the description document of the device"); */ + return; + } + + if (deviceType.compare(UPNP_IGD_DEVICE) == 0) { + parseIGD(doc, d_event); + } + + /* TODO: check if its a ring device */ +} + +void +UPnPContext::parseIGD(IXML_Document* doc, const Upnp_Discovery* d_event) +{ + if (not doc or not d_event) + return; + + /* check the UDN to see if its already in our device list(s) + * if it is, then update the device advertisement timeout (expiration) + */ + std::string UDN = get_first_doc_item(doc, "UDN"); + if (UDN.empty()) { + RING_DBG("UPnP: could not find UDN in description document of device"); + return; + } + + { + std::lock_guard<std::mutex> lock(validIGDMutex_); + auto it = validIGDs_.find(UDN); + + if (it != validIGDs_.end()) { + /* we already have this device in our list */ + /* TODO: update expiration */ + return; + } + } + + std::unique_ptr<IGD> new_igd; + int upnp_err; + + std::string friendlyName = get_first_doc_item(doc, "friendlyName"); + if (not friendlyName.empty() ) + RING_DBG("UPnP: checking new device of type IGD: '%s'", + friendlyName.c_str()); + + /* determine baseURL */ + std::string baseURL = get_first_doc_item(doc, "URLBase"); + if (baseURL.empty()) { + /* get it from the discovery event location */ + baseURL = std::string(d_event->Location); + } + + /* check if its a valid IGD: + * 1. check for IGD device... already done if this function is called + * 2. check for WAN device... skip checking for this and check for the services directly + * 3. check for WANIPConnection service or WANPPPConnection service + * 4. check if connected to Internet (if not, no point in port forwarding) + * 5. check that we can get the external IP + */ + + /* get list of services defined by serviceType */ + std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> serviceList(nullptr, ixmlNodeList_free); + serviceList.reset(ixmlDocument_getElementsByTagName(doc, "serviceType")); + + /* get list of all 'serviceType' elements */ + bool found_connected_IGD = false; + unsigned long list_length = ixmlNodeList_length(serviceList.get()); + + /* go through the 'serviceType' nodes until we find the first service of type + * WANIPConnection or WANPPPConnection which is connected to an external network */ + for (unsigned long node_idx = 0; node_idx < list_length and not found_connected_IGD; node_idx++) { + IXML_Node* serviceType_node = ixmlNodeList_item(serviceList.get(), node_idx); + std::string serviceType = get_element_text(serviceType_node); + + /* only check serviceType of WANIPConnection or WANPPPConnection */ + if (serviceType.compare(UPNP_WANIP_SERVICE) == 0 + or serviceType.compare(UPNP_WANPPP_SERVICE) == 0) { + + /* we found a correct 'serviceType', now get the parent node because + * the rest of the service definitions are siblings of 'serviceType' */ + IXML_Node* service_node = ixmlNode_getParentNode(serviceType_node); + if (service_node) { + /* perform sanity check; the parent node should be called "service" */ + if( strcmp(ixmlNode_getNodeName(service_node), "service") == 0) { + /* get the rest of the service definitions */ + + /* serviceId */ + IXML_Element* service_element = (IXML_Element*)service_node; + std::string serviceId = get_first_element_item(service_element, "serviceId"); + + /* get the relative controlURL and turn it into absolute address using the URLBase */ + std::string controlURL = get_first_element_item(service_element, "controlURL"); + if (not controlURL.empty()) { + std::unique_ptr<char> absolute_url(new char[baseURL.size() + controlURL.size() + 1]); + upnp_err = UpnpResolveURL(baseURL.c_str(), controlURL.c_str(), absolute_url.get()); + if (upnp_err == UPNP_E_SUCCESS) { + controlURL = std::string(absolute_url.get()); + } else { + RING_WARN("UPnP: error resolving absolute controlURL: %s", UpnpGetErrorMessage(upnp_err)); + } + } + + /* get the relative eventSubURL and turn it into absolute address using the URLBase */ + std::string eventSubURL = get_first_element_item(service_element, "eventSubURL"); + if (not eventSubURL.empty()) { + std::unique_ptr<char> absolute_url(new char[baseURL.size() + controlURL.size() + 1]); + upnp_err = UpnpResolveURL(baseURL.c_str(), eventSubURL.c_str(), absolute_url.get()); + if (upnp_err == UPNP_E_SUCCESS) { + eventSubURL = std::string(absolute_url.get()); + } else { + RING_WARN("UPnP: error resolving absolute eventSubURL: %s", UpnpGetErrorMessage(upnp_err)); + } + } + + /* make sure all of the services are defined + * and check if the IGD is connected to an external network */ + if (not (serviceId.empty() and controlURL.empty() and eventSubURL.empty()) ) { + /* RING_DBG("UPnP: got service info from device:\n\tserviceType: %s\n\tserviceID: %s\n\tcontrolURL: %s\n\teventSubURL: %s", + serviceType.c_str(), serviceId.c_str(), controlURL.c_str(), eventSubURL.c_str()); */ + new_igd.reset(new IGD(UDN, baseURL, friendlyName, serviceType, serviceId, controlURL, eventSubURL)); + if (isIGDConnected(new_igd.get())) { + new_igd->publicIp = getExternalIP(new_igd.get()); + if (new_igd->publicIp) { + RING_DBG("UPnP: got external IP: %s", new_igd->publicIp.toString().c_str()); + found_connected_IGD = true; + } + } + } + /* TODO: subscribe to the service to get events, eg: when IP changes */ + } else + RING_WARN("UPnP: IGD \"serviceType\" parent node is not called \"service\"!"); + } else + RING_WARN("UPnP: IGD \"serviceType\" has no parent node!"); + } + } + + /* if its a valid IGD, add to list of IGDs (ideally there is only one at a time) + * subscribe to the WANIPConnection or WANPPPConnection service to receive + * updates about state changes, eg: new external IP + */ + if (found_connected_IGD) { + RING_DBG("UPnP: found a valid IGD!"); + + { + std::lock_guard<std::mutex> lock(validIGDMutex_); + /* delete all RING mappings first */ + removeMappingsByLocalIPAndDescription(new_igd.get(), Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION); + validIGDs_.emplace(UDN, std::move(new_igd)); + } + + /* check if we have pending search requests and notify anyone waiting */ + { + std::lock_guard<std::mutex> lock(igdSearchMutex_); + if (pendingIGDSearchRequests_ > 0) { + pendingIGDSearchRequests_ = 0; + } + } + igdSearchCondition_.notify_all(); + } +} + +static std::string +get_element_text(IXML_Node* node) +{ + std::string ret; + if (node) { + IXML_Node *textNode = ixmlNode_getFirstChild(node); + if (textNode) { + const char* value = ixmlNode_getNodeValue(textNode); + if (value) + ret = std::string(value); + } + } + return ret; +} + +static std::string +get_first_doc_item(IXML_Document* doc, const char* item) +{ + std::string ret; + + std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> + nodeList(ixmlDocument_getElementsByTagName(doc, item), ixmlNodeList_free); + if (nodeList) { + /* if there are several nodes which match the tag, we only want the first one */ + ret = get_element_text( ixmlNodeList_item(nodeList.get(), 0) ); + } + return ret; +} + +static std::string +get_first_element_item(IXML_Element* element, const char* item) +{ + std::string ret; + + std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> + nodeList(ixmlElement_getElementsByTagName(element, item), ixmlNodeList_free); + if (nodeList) { + /* if there are several nodes which match the tag, we only want the first one */ + ret = get_element_text( ixmlNodeList_item(nodeList.get(), 0) ); + } + return ret; +} + + +int +cp_callback(Upnp_EventType event_type, void* event, void* user_data) +{ + auto upnpContext = getUPnPContext(); + + switch( event_type ) + { + case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE: + /* RING_DBG("UPnP: CP received a discovery advertisement"); */ + case UPNP_DISCOVERY_SEARCH_RESULT: + { + struct Upnp_Discovery* d_event = ( struct Upnp_Discovery* )event; + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> desc_doc(nullptr, ixmlDocument_free); + int upnp_err; + + /* if (event_type != UPNP_DISCOVERY_ADVERTISEMENT_ALIVE) + RING_DBG("UPnP: CP received a discovery search result"); */ + + /* check if we are already in the process of checking this device */ + std::unique_lock<std::mutex> lock(upnpContext->cpDeviceMutex_); + auto it = upnpContext->cpDevices_.find(std::string(d_event->Location)); + + if (it == upnpContext->cpDevices_.end()) { + upnpContext->cpDevices_.emplace(std::string(d_event->Location)); + lock.unlock(); + + if (d_event->ErrCode != UPNP_E_SUCCESS) + RING_WARN("UPnP: Error in discovery event received by the CP: %s", + UpnpGetErrorMessage(d_event->ErrCode)); + + /* RING_DBG("UPnP: Control Point received discovery event from device:\n\tid: %s\n\ttype: %s\n\tservice: %s\n\tversion: %s\n\tlocation: %s\n\tOS: %s", + d_event->DeviceId, d_event->DeviceType, d_event->ServiceType, d_event->ServiceVer, d_event->Location, d_event->Os); + */ + + /* note: this thing will block until success for the system socket timeout + * unless libupnp is compile with '-disable-blocking-tcp-connections' + * in which case it will block for the libupnp specified timeout + */ + IXML_Document* desc_doc_ptr = nullptr; + upnp_err = UpnpDownloadXmlDoc( d_event->Location, &desc_doc_ptr); + desc_doc.reset(desc_doc_ptr); + if ( upnp_err != UPNP_E_SUCCESS ) { + RING_WARN("UPnP: Error downloading device description: %s", + UpnpGetErrorMessage(upnp_err)); + } else { + /* TODO: parse device description and add desired devices to relevant lists, etc */ + upnpContext->parseDevice(desc_doc.get(), d_event); + } + + /* finished parsing device; remove it from know devices list, + * since next time it could be a new device with same URL + * eg: if we switch routers or if a new device with the same IP appears + */ + lock.lock(); + upnpContext->cpDevices_.erase(d_event->Location); + lock.unlock(); + } else { + lock.unlock(); + /* RING_DBG("UPnP: Control Point is already checking this device"); */ + } + } + break; + + case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: + { + struct Upnp_Discovery *d_event = (struct Upnp_Discovery *)event; + + RING_DBG("UPnP: Control Point received ByeBye for device: %s", d_event->DeviceId); + + if (d_event->ErrCode != UPNP_E_SUCCESS) + RING_WARN("UPnP: Error in ByeBye received by the CP: %s", + UpnpGetErrorMessage(d_event->ErrCode)); + + /* TODO: check if its a device we care about and remove it from the relevant lists */ + } + break; + + case UPNP_EVENT_RECEIVED: + { + struct Upnp_Event *e_event = (struct Upnp_Event *)event; + + RING_DBG("UPnP: Control Point event received"); + + /* TODO: handle event by updating any changed state variables */ + + } + break; + + case UPNP_EVENT_AUTORENEWAL_FAILED: + { + RING_WARN("UPnP: Control Point subscription auto-renewal failed"); + } + break; + + case UPNP_EVENT_SUBSCRIPTION_EXPIRED: + { + RING_DBG("UPnP: Control Point subscription expired"); + } + break; + + case UPNP_EVENT_SUBSCRIBE_COMPLETE: + RING_DBG("UPnP: Control Point async subscription complete"); + + /* TODO: check if successfull */ + + break; + + case UPNP_DISCOVERY_SEARCH_TIMEOUT: + { + RING_DBG("UPnP: Control Point search timeout"); + { + std::lock_guard<std::mutex> lock(upnpContext->igdSearchMutex_); + if (upnpContext->pendingIGDSearchRequests_ > 0) + --(upnpContext->pendingIGDSearchRequests_); + } + upnpContext->igdSearchCondition_.notify_all(); + } + break; + + case UPNP_CONTROL_ACTION_COMPLETE: + { + struct Upnp_Action_Complete *a_event = (struct Upnp_Action_Complete *)event; + + RING_DBG("UPnP: Control Point async action complete"); + + if (a_event->ErrCode != UPNP_E_SUCCESS) + RING_WARN("UPnP: Error in action complete event: %s", + UpnpGetErrorMessage(a_event->ErrCode)); + + /* TODO: no need for any processing here, just print out results. + * Service state table updates are handled by events. */ + } + break; + + case UPNP_CONTROL_GET_VAR_COMPLETE: + { + struct Upnp_State_Var_Complete *sv_event = (struct Upnp_State_Var_Complete *)event; + + RING_DBG("UPnP: Control Point async get variable complete"); + + if (sv_event->ErrCode != UPNP_E_SUCCESS) + RING_WARN("UPnP: Error in get variable complete event: %s", + UpnpGetErrorMessage(sv_event->ErrCode)); + + /* TODO: update state variables */ + } + break; + + default: + RING_WARN("UPnP: unhandled Control Point event"); + break; + } + + return UPNP_E_SUCCESS; /* return value currently ignored by SDK */ +} + +static void +checkResponseError(IXML_Document* doc) +{ + if (not doc) + return; + + std::string errorCode = get_first_doc_item(doc, "errorCode"); + if (not errorCode.empty()) { + std::string errorDescription = get_first_doc_item(doc, "errorDescription"); + RING_WARN("UPnP: response contains error: %s : %s", + errorCode.c_str(), errorDescription.c_str()); + } +} + +bool +UPnPContext::isIGDConnected(const IGD* igd) +{ + bool connected = false; + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free); + action.reset(UpnpMakeAction("GetStatusInfo", igd->getServiceType().c_str(), 0, nullptr)); + + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free); + IXML_Document* response_ptr = nullptr; + int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(), + igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr); + response.reset(response_ptr); + checkResponseError(response.get()); + if( upnp_err != UPNP_E_SUCCESS) { + /* TODO: if failed, should we chck if the igd is disconnected? */ + RING_WARN("UPnP: Failed to get GetStatusInfo from: %s, %s", + igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err)); + + return false; + } + + /* parse response */ + std::string status = get_first_doc_item(response.get(), "NewConnectionStatus"); + if (status.compare("Connected") == 0) + connected = true; + + /* response should also contain the following elements, but we don't care for now: + * "NewLastConnectionError" + * "NewUptime" + */ + return connected; +} + +IpAddr +UPnPContext::getExternalIP(const IGD* igd) +{ + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free); + action.reset(UpnpMakeAction("GetExternalIPAddress", igd->getServiceType().c_str(), 0, nullptr)); + + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free); + IXML_Document* response_ptr = nullptr; + int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(), + igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr); + response.reset(response_ptr); + checkResponseError(response.get()); + if( upnp_err != UPNP_E_SUCCESS) { + /* TODO: if failed, should we chck if the igd is disconnected? */ + RING_WARN("UPnP: Failed to get GetExternalIPAddress from: %s, %s", + igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err)); + return {}; + } + + /* parse response */ + return {get_first_doc_item(response.get(), "NewExternalIPAddress")}; +} + +void +UPnPContext::removeMappingsByLocalIPAndDescription(const IGD* igd, const std::string& description) +{ + /* need to get the local addr */ + IpAddr local_ip = ip_utils::getLocalAddr(pj_AF_INET()); + if (!local_ip) { + RING_DBG("UPnP: cannot determine local IP in function removeMappingsByLocalIPAndDescription()"); + return; + } + + RING_DBG("UPnP: removing all port mappings with description: \"%s\" and local ip: %s", + description.c_str(), local_ip.toString().c_str()); + + int upnp_err; + int entry_idx = 0; + bool done = false; + + do { + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free); + IXML_Document* action_ptr = nullptr; + UpnpAddToAction(&action_ptr, "GetGenericPortMappingEntry", igd->getServiceType().c_str(), + "NewPortMappingIndex", std::to_string(entry_idx).c_str()); + action.reset(action_ptr); + + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free); + IXML_Document* response_ptr = nullptr; + int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(), + igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr); + response.reset(response_ptr); + if( not response and upnp_err != UPNP_E_SUCCESS) { + /* TODO: if failed, should we chck if the igd is disconnected? */ + RING_WARN("UPnP: Failed to get GetGenericPortMappingEntry from: %s, %s", + igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err)); + return; + } + + /* check if there is an error code */ + std::string errorCode = get_first_doc_item(response.get(), "errorCode"); + + if (errorCode.empty()) { + /* no error, prase the rest of the response */ + std::string desc_actual = get_first_doc_item(response.get(), "NewPortMappingDescription"); + std::string client_ip = get_first_doc_item(response.get(), "NewInternalClient"); + + /* check if same IP and description */ + if (IpAddr(client_ip) == local_ip and desc_actual.compare(description) == 0) { + /* get the rest of the needed parameters */ + std::string port_internal = get_first_doc_item(response.get(), "NewInternalPort"); + std::string port_external = get_first_doc_item(response.get(), "NewExternalPort"); + std::string protocol = get_first_doc_item(response.get(), "NewProtocol"); + + RING_DBG("UPnP: deleting entry with matching desciption and ip:\n\t%s %5s->%s:%-5s '%s'", + protocol.c_str(), port_external.c_str(), client_ip.c_str(), port_internal.c_str(), protocol.c_str()); + + /* delete entry */ + if (not deletePortMapping(igd, port_external, protocol)) { + /* failed to delete entry, skip it and try the next one */ + ++entry_idx; + } + /* note: in the case that the entry deletion is successfull, we do not increment the entry + * idx as the number of entries has decreased by one */ + } else + ++entry_idx; + + } else if (errorCode.compare(ARRAY_IDX_INVALID_STR) == 0 + or errorCode.compare(INVALID_ARGS_STR) == 0) { + /* 713 means there are no more entires, but some routers will return 402 instead */ + done = true; + } else { + std::string errorDescription = get_first_doc_item(response.get(), "errorDescription"); + RING_WARN("UPnP: GetGenericPortMappingEntry returned with error: %s: %s", + errorCode.c_str(), errorDescription.c_str()); + done = true; + } + } while(not done); +} + +bool +UPnPContext::deletePortMapping(const IGD* igd, const std::string& port_external, const std::string& protocol) +{ + std::string action_name{"DeletePortMapping"}; + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free); + IXML_Document* action_ptr = nullptr; + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewRemoteHost", ""); + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewExternalPort", port_external.c_str()); + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewProtocol", protocol.c_str()); + action.reset(action_ptr); + + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free); + IXML_Document* response_ptr = nullptr; + int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(), + igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr); + response.reset(response_ptr); + if( upnp_err != UPNP_E_SUCCESS) { + /* TODO: if failed, should we check if the igd is disconnected? */ + RING_WARN("UPnP: Failed to get %s from: %s, %s", + action_name.c_str(), igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err)); + return false; + } + /* check if there is an error code */ + std::string errorCode = get_first_doc_item(response.get(), "errorCode"); + if (not errorCode.empty()) { + std::string errorDescription = get_first_doc_item(response.get(), "errorDescription"); + RING_WARN("UPnP: %s returned with error: %s: %s", + action_name.c_str(), errorCode.c_str(), errorDescription.c_str()); + return false; + } + return true; +} + +bool +UPnPContext::addPortMapping(const IGD* igd, const Mapping& mapping, int* error_code) +{ + *error_code = UPNP_E_SUCCESS; + + std::string action_name{"AddPortMapping"}; + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free); + IXML_Document* action_ptr = nullptr; + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewRemoteHost", ""); + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewExternalPort", mapping.getPortExternalStr().c_str()); + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewProtocol", mapping.getTypeStr().c_str()); + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewInternalPort", mapping.getPortInternalStr().c_str()); + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewInternalClient", mapping.getLocalIp().toString().c_str()); + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewEnabled", "1"); + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewPortMappingDescription", mapping.getDescription().c_str()); + /* for now assume lease duration is always infinite */ + UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(), + "NewLeaseDuration", "0"); + action.reset(action_ptr); + + std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free); + IXML_Document* response_ptr = nullptr; + int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(), + igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr); + response.reset(response_ptr); + if( not response and upnp_err != UPNP_E_SUCCESS) { + /* TODO: if failed, should we chck if the igd is disconnected? */ + RING_WARN("UPnP: Failed to get GetGenericPortMappingEntry from: %s, %s", + igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err)); + *error_code = upnp_err; + return false; + } + + /* check if there is an error code */ + std::string errorCode = get_first_doc_item(response.get(), "errorCode"); + if (not errorCode.empty()) { + std::string errorDescription = get_first_doc_item(response.get(), "errorDescription"); + RING_WARN("UPnP: %s returned with error: %s: %s", + action_name.c_str(), errorCode.c_str(), errorDescription.c_str()); + *error_code = std::stoi(errorCode); + return false; + } + return true; +} + +#endif /* HAVE_LIBUPNP */ + +}} // namespace ring::upnp diff --git a/daemon/src/upnp/upnp_context.h b/daemon/src/upnp/upnp_context.h new file mode 100644 index 0000000000000000000000000000000000000000..cff06827cfa24c3431ea745273526a98d6ad76e8 --- /dev/null +++ b/daemon/src/upnp/upnp_context.h @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2004-2015 Savoir-Faire Linux Inc. + * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef UPNP_CONTEXT_H_ +#define UPNP_CONTEXT_H_ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <set> +#include <map> +#include <mutex> +#include <memory> +#include <condition_variable> + +#if HAVE_LIBUPNP +#include <upnp/upnp.h> +#include <upnp/upnptools.h> +#endif + +#include "noncopyable.h" +#include "upnp_igd.h" + +namespace ring { +class IpAddr; +} + +namespace ring { namespace upnp { + +class UPnPContext { + +public: + +#if HAVE_LIBUPNP + + /* search timeout in seconds */ + constexpr static int SEARCH_TIMEOUT_SEC = 30; + + UPnPContext(); + ~UPnPContext(); + + /** + * returns 'true' if there is at least one valid (connected) IGD + * note: this function will wait until an IGD has been found or SEARCH_TIMEOUT_SEC + * have expired; the timeout starts when the context is created as we start + * searching for IGDs immediately + */ + bool hasValidIGD(); + + /** + * tries to add mapping from and to the port_desired + * if unique == true, makes sure the client is not using this port already + * if the mapping fails, tries other available ports until success + * + * tries to use a random port between 1024 < > 65535 if desired port fails + * + * maps port_desired to port_local; if use_same_port == true, makes sure + * that the external and internal ports are the same + * + * returns a valid mapping on success and an invalid mapping on failure + * + * note: this function will call hasValidIGD and wait upto SEARCH_TIMEOUT_SEC + */ + Mapping addAnyMapping(uint16_t port_desired, + uint16_t port_local, + PortType type, + bool use_same_port, + bool unique); + + /** + * tries to remove the given mapping + * + * note: this function will call hasValidIGD and wait upto SEARCH_TIMEOUT_SEC + */ + void removeMapping(const Mapping& mapping); + + IpAddr getExternalIP(); + +#else + /* use default constructor and destructor */ + UPnPContext() = default; + ~UPnPContext() = default; +#endif + + bool isInitialized() const { return initialized_; }; + +private: + NON_COPYABLE(UPnPContext); + + bool initialized_ {false}; + +#if HAVE_LIBUPNP + + /** + * UPnP devices typically send out several discovery + * packets at the same time. libupnp creates a separate event + * for each discovery packet which is processed in the threadpool, + * even if the multiple discovery packets are received from the + * same IP at the same time. In order to prevent trying + * to download and parse the device description from the + * same location in multiple threads at the same time, we + * keep track from which URL(s) we are in the process of downloading + * and parsing the device description in this set. + * + * The main purspose of this is to prevent blocking multiple + * threads when trying to download the description from an + * unresponsive device (the timeout can be several seconds) + * + * The mutex is to access the set in a thread safe manner + */ + std::set<std::string> cpDevices_; + std::mutex cpDeviceMutex_; + + /** + * control and device handles; + * set by the SDK once each is registered + */ + UpnpClient_Handle ctrlptHandle_ {-1}; + UpnpDevice_Handle deviceHandle_ {-1}; + + /** + * keep track if we've successfully registered + * a client and/ore device + */ + bool clientRegistered_ {false}; + bool deviceRegistered_ {false}; + + /** + * map of valid IGDs - IGDs which have the correct services and are connected + * to some external network (have an external IP) + * + * the UDN string is used to uniquely identify the IGD + * + * the mutex is used to access these lists and IGDs in a thread-safe manner + */ + std::map<std::string, std::unique_ptr<IGD>> validIGDs_; + std::mutex validIGDMutex_; + + /** + * chooses the IGD to use (currently selects the first one in the map) + * assumes you already have a lock on igd_mutex_ + */ + IGD* chooseIGD_unlocked(); + + /* sends out async search for IGD */ + void searchForIGD(); + + /* vars to sync search timeout */ + unsigned pendingIGDSearchRequests_ {0}; + std::mutex igdSearchMutex_; + std::condition_variable igdSearchCondition_; + + /** + * callback function for the UPnP client (control point) + * all UPnP events received by the client are processed here + */ + friend int cp_callback(Upnp_EventType, void*, void*); + + /** + * Parses the device description and adds desired devices to + * relevant lists + */ + void parseDevice(IXML_Document* doc, const Upnp_Discovery* d_event); + + void parseIGD(IXML_Document* doc, const Upnp_Discovery* d_event); + + /* tries to add mapping, assumes you alreayd have lock on igd_mutex_ */ + Mapping addMapping(IGD* igd, + uint16_t port_external, + uint16_t port_internal, + PortType type, + int *upnp_error); + + uint16_t chooseRandomPort(const IGD* igd, PortType type); + + /* these functions directly create UPnP actions + * and make synchronous UPnP control point calls + * they assume you have a lock on the igd_mutex_ */ + bool isIGDConnected(const IGD* igd); + + IpAddr getExternalIP(const IGD* igd); + + void removeMappingsByLocalIPAndDescription(const IGD* igd, + const std::string& description); + + bool deletePortMapping(const IGD* igd, + const std::string& port_external, + const std::string& protocol); + + bool addPortMapping(const IGD* igd, + const Mapping& mapping, + int* error_code); + +#endif /* HAVE_LIBUPNP */ + +}; + +/** + * This should be used to get a UPnPContext. + * It only makes sense to have one unless you have separate + * contexts for multiple internet interfaces, which is not currently + * supported. + */ +std::shared_ptr<UPnPContext> getUPnPContext(); + +}} // namespace ring::upnp + +#endif /* UPNP_CONTEXT_H_ */ \ No newline at end of file diff --git a/daemon/src/upnp/upnp_control.cpp b/daemon/src/upnp/upnp_control.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e21d55c87603148c287fd3a9237b647ddf9b32a7 --- /dev/null +++ b/daemon/src/upnp/upnp_control.cpp @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2004-2015 Savoir-Faire Linux Inc. + * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "upnp_control.h" + +#include <memory> + +#include "logger.h" +#include "ip_utils.h" +#include "upnp_context.h" +#include "upnp_igd.h" + +namespace ring { namespace upnp { + +Controller::Controller() + : upnpContext_(getUPnPContext()) +{ + if (not upnpContext_->isInitialized()) + RING_WARN("UPnP: context is not initialized"); +} + +Controller::~Controller() +{ + /* remove all mappings */ + removeMappings(); +} + +/** + * Return whether or not this controller has a valid IGD, + * if 'flase' then all requests will fail + */ +bool +Controller::hasValidIGD() +{ +#if HAVE_LIBUPNP + return upnpContext_->hasValidIGD(); +#endif + return false; +} + +/** + * tries to add mapping from and to the port_desired + * if unique == true, makes sure the client is not using this port already + * if the mapping fails, tries other available ports until success + * + * tries to use a random port between 1024 < > 65535 if desired port fails + * + * maps port_desired to port_local; if use_same_port == true, makes sure that + * that the extranl and internal ports are the same + */ +bool +Controller::addAnyMapping(uint16_t port_desired, + uint16_t port_local, + PortType type, + bool use_same_port, + bool unique, + uint16_t *port_used) +{ +#if HAVE_LIBUPNP + Mapping mapping = upnpContext_->addAnyMapping(port_desired, port_local, type, + use_same_port, unique); + if (mapping) { + if (port_used) + *port_used = mapping.getPortExternal(); + + /* add to map */ + auto& instanceMappings = type == PortType::UDP ? udpMappings_ : tcpMappings_; + instanceMappings.emplace(*port_used, std::move(mapping)); + return true; + } else + return false; +#endif + return false; +} + +/** + * addAnyMapping with the local port being the same as the external port + */ +bool +Controller::addAnyMapping(uint16_t port_desired, + PortType type, + bool unique, + uint16_t *port_used) +{ + addAnyMapping(port_desired, port_desired, type, true, unique, port_used); +} + +/** + * removes mappings added by this instance of the specified port type + * if an mapping has more than one user in the global list, it is not deleted + * from the router, but the number of users is decremented + */ +void +Controller::removeMappings(PortType type) { +#if HAVE_LIBUPNP + auto& instanceMappings = type == PortType::UDP ? udpMappings_ : tcpMappings_; + for (auto iter = instanceMappings.begin(); iter != instanceMappings.end(); ){ + auto& mapping = iter->second; + upnpContext_->removeMapping(mapping); + iter = instanceMappings.erase(iter); + } +#endif +} + +/** + * removes all mappings added by this instance + */ +void +Controller::removeMappings() +{ +#if HAVE_LIBUPNP + removeMappings(PortType::UDP); + removeMappings(PortType::TCP); +#endif +} + +/** + * tries to get the external ip of the router + */ +IpAddr +Controller::getExternalIP() +{ +#if HAVE_LIBUPNP + return upnpContext_->getExternalIP(); +#else + /* return empty address */ + return {}; +#endif +} + +}} // namespace ring::upnp diff --git a/daemon/src/upnp/upnp_control.h b/daemon/src/upnp/upnp_control.h new file mode 100644 index 0000000000000000000000000000000000000000..e2c1770322d2dc7b560e3be863f2a02a670af81c --- /dev/null +++ b/daemon/src/upnp/upnp_control.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2004-2015 Savoir-Faire Linux Inc. + * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef UPNP_H_ +#define UPNP_H_ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <memory> + +#include "noncopyable.h" +#include "upnp_igd.h" + +namespace ring { +class IpAddr; +} + +namespace ring { namespace upnp { + +class UPnPContext; + +class Controller { +public: + typedef std::function<void(bool)> IGDFoundCallback; + + /* constructor */ + Controller(); + /* destructor */ + ~Controller(); + + /** + * Return whether or not this controller has a valid IGD, + * if 'flase' then all requests will fail + */ + bool hasValidIGD(); + + /** + * like hasValidIGD, but calls the given callback when the IGD is found + * or when the search times out without finding one + */ + // void waitForValildIGD(IGDFoundCallback cb); + + /** + * tries to add mapping from and to the port_desired + * if unique == true, makes sure the client is not using this port already + * if the mapping fails, tries other available ports until success + * + * tries to use a random port between 1024 < > 65535 if desired port fails + * + * maps port_desired to port_local; if use_same_port == true, makes sure that + * that the extranl and internal ports are the same + */ + bool addAnyMapping(uint16_t port_desired, + uint16_t port_local, + PortType type, + bool use_same_port, + bool unique, + uint16_t *port_used); + + /** + * addAnyMapping with the local port being the same as the external port + */ + bool addAnyMapping(uint16_t port_desired, + PortType type, + bool unique, + uint16_t *port_used); + + /** + * removes all mappings added by this instance + */ + void removeMappings(); + + /** + * tries to get the external ip of the IGD (router) + */ + IpAddr getExternalIP(); + +private: + + /** + * All UPnP commands require an initialized upnpContext + */ + std::shared_ptr<UPnPContext> upnpContext_; + + /** + * list of mappings created by this instance + * the key is the external port number, as there can only be one mapping + * at a time for each external port + */ + PortMapLocal udpMappings_; + PortMapLocal tcpMappings_; + + /** + * Try to remove all mappings of the given type + */ + void removeMappings(PortType type); +}; + +}} // namespace ring::upnp + +#endif /* UPNP_H_ */ diff --git a/daemon/src/upnp/upnp_igd.cpp b/daemon/src/upnp/upnp_igd.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8a9f482f5567da97bdada7d6bcaaa0f26c755087 --- /dev/null +++ b/daemon/src/upnp/upnp_igd.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2004-2015 Savoir-Faire Linux Inc. + * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include "upnp_igd.h" + +namespace ring { namespace upnp { + +/* move constructor and operator */ +Mapping::Mapping(Mapping&& other) + : local_ip_(std::move(other.local_ip_)) + , port_external_(other.port_external_) + , port_internal_(other.port_internal_) + , type_(other.type_) + , description_(std::move(other.description_)) +{ + other.port_external_ = 0; + other.port_internal_ = 0; +} + +Mapping& Mapping::operator=(Mapping&& other) +{ + if (this != &other) { + local_ip_ = std::move(other.local_ip_); + port_external_ = other.port_external_; + other.port_external_ = 0; + port_internal_ = other.port_internal_; + other.port_internal_ = 0; + type_ = other.type_; + description_ = std::move(other.description_); + } + return *this; +} + +bool operator== (Mapping &cMap1, Mapping &cMap2) +{ + /* we don't compare the description because it doesn't change the function of the + * mapping; we don't compare the IGD because for now we assume that we always + * use the same one and that all mappings are active + */ + return (cMap1.local_ip_ == cMap2.local_ip_ && + cMap1.port_external_ == cMap2.port_external_ && + cMap1.port_internal_ == cMap2.port_internal_ && + cMap1.type_ == cMap2.type_); +} + +bool operator!= (Mapping &cMap1, Mapping &cMap2) +{ + return !(cMap1 == cMap2); +} + +}} // namespace ring::upnp \ No newline at end of file diff --git a/daemon/src/upnp/upnp_igd.h b/daemon/src/upnp/upnp_igd.h new file mode 100644 index 0000000000000000000000000000000000000000..938b56ae30d7488cdf0212bdf28388871116c4cf --- /dev/null +++ b/daemon/src/upnp/upnp_igd.h @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2004-2015 Savoir-Faire Linux Inc. + * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef UPNP_IGD_H_ +#define UPNP_IGD_H_ + +#include <string> +#include <map> + +#include "noncopyable.h" +#include "ip_utils.h" + +namespace ring { namespace upnp { + +enum class PortType {UDP,TCP}; + +/* defines a UPnP port mapping */ +class Mapping { +public: + constexpr static const char * UPNP_DEFAULT_MAPPING_DESCRIPTION = "RING"; + /* TODO: what should the port range really be? + * Should it be the ephemeral ports as defined by the system? + */ + constexpr static uint16_t UPNP_PORT_MIN = 1024; + constexpr static uint16_t UPNP_PORT_MAX = 65535; + + Mapping( + IpAddr local_ip = IpAddr(), + uint16_t port_external = 0, + uint16_t port_internal = 0, + PortType type = PortType::UDP, + std::string description = UPNP_DEFAULT_MAPPING_DESCRIPTION) + : local_ip_(local_ip) + , port_external_(port_external) + , port_internal_(port_internal) + , type_(type) + , description_(description) + {}; + + /* move constructor and operator */ + Mapping(Mapping&&); + Mapping& operator=(Mapping&&); + + ~Mapping() = default; + + friend bool operator== (Mapping &cRedir1, Mapping &cRedir2); + friend bool operator!= (Mapping &cRedir1, Mapping &cRedir2); + + const IpAddr& getLocalIp() const { return local_ip_; }; + uint16_t getPortExternal() const { return port_external_; }; + std::string getPortExternalStr() const { return std::to_string(port_external_); }; + uint16_t getPortInternal() const { return port_internal_; }; + std::string getPortInternalStr() const { return std::to_string(port_internal_); }; + PortType getType() const { return type_; }; + std::string getTypeStr() const { return type_ == PortType::UDP ? "UDP" : "TCP"; } + std::string getDescription() const { return description_; }; + + std::string toString() const { + return local_ip_.toString() + ", " + getPortExternalStr() + ":" + getPortInternalStr() + ", " + getTypeStr(); + }; + + bool isValid() const { + return port_external_ == 0 or port_internal_ == 0 ? false : true; + }; + + inline operator bool() const { + return isValid(); + } + +private: + NON_COPYABLE(Mapping); + +protected: + IpAddr local_ip_; /* the destination of the mapping */ + uint16_t port_external_; + uint16_t port_internal_; + PortType type_; /* UPD or TCP */ + std::string description_; +}; + +/** + * GlobalMapping is like a mapping, but it tracks the number of global users, + * ie: the number of upnp:Controller which are using this mapping + * this is usually only relevant for accounts (not calls) as multiple SIP accounts + * can use the same SIP port and we don't want to delete a mapping from the router + * if other accounts are using it + */ +class GlobalMapping : public Mapping { +public: + /* number of users of this mapping; + * this is only relevant when multiple accounts are using the same SIP port */ + unsigned users; + GlobalMapping(const Mapping& mapping, unsigned users = 1) + : Mapping(mapping.getLocalIp() + , mapping.getPortExternal() + , mapping.getPortInternal() + , mapping.getType() + , mapping.getDescription()) + , users(users) + {}; +}; + +/* subclasses to make it easier to differentiate and cast maps of port mappings */ +class PortMapLocal : public std::map<uint16_t, Mapping> {}; +class PortMapGlobal : public std::map<uint16_t, GlobalMapping> {}; + +/* defines a UPnP capable Internet Gateway Device (a router) */ +class IGD { +public: + + /* external IP of IGD; can change */ + IpAddr publicIp; + + /* port mappings associated with this IGD */ + PortMapGlobal udpMappings; + PortMapGlobal tcpMappings; + + /* constructors */ + IGD() {}; + IGD(std::string UDN, + std::string baseURL, + std::string friendlyName, + std::string serviceType, + std::string serviceId, + std::string controlURL, + std::string eventSubURL) + : UDN_(UDN) + , baseURL_(baseURL) + , friendlyName_(friendlyName) + , serviceType_(serviceType) + , serviceId_(serviceId) + , controlURL_(controlURL) + , eventSubURL_(eventSubURL) + {}; + + /* move constructor and operator */ + IGD(IGD&&) = default; + IGD& operator=(IGD&&) = default; + + ~IGD() = default; + + const std::string& getUDN() const { return UDN_; }; + const std::string& getBaseURL() const { return baseURL_; }; + const std::string& getFriendlyName() const { return friendlyName_; }; + const std::string& getServiceType() const { return serviceType_; }; + const std::string& getServiceId() const { return serviceId_; }; + const std::string& getControlURL() const { return controlURL_; }; + const std::string& getEventSubURL() const { return eventSubURL_; }; + +private: + NON_COPYABLE(IGD); + + /* root device info */ + std::string UDN_ {}; /* used to uniquely identify this UPnP device */ + std::string baseURL_ {}; + std::string friendlyName_ {}; + + /* port forwarding service info */ + std::string serviceType_ {}; + std::string serviceId_ {}; + std::string controlURL_ {}; + std::string eventSubURL_ {}; + +}; + +}} // namespace ring::upnp + +#endif /* UPNP_IGD_H_ */ \ No newline at end of file