diff --git a/.gitmodules b/.gitmodules index 497981c4..153d2de8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "ViGEmClient"] path = third-party/ViGEmClient url = https://github.com/ViGEm/ViGEmClient +[submodule "third-party/miniupnp"] + path = third-party/miniupnp + url = https://github.com/miniupnp/miniupnp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7af013a9..1a80ab08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,13 @@ set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) add_subdirectory(third-party/Simple-Web-Server) +set(UPNPC_BUILD_SHARED OFF CACHE BOOL "no shared libraries") +set(UPNPC_BUILD_TESTS OFF CACHE BOOL "Don't build tests for miniupnpc") +set(UPNPC_BUILD_SAMPLE OFF CACHE BOOL "Don't build samples for miniupnpc") +set(UPNPC_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for miniupnpc") +add_subdirectory(third-party/miniupnp/miniupnpc) +include_directories(third-party/miniupnp) + if(WIN32) # Ugly hack to compile with #include add_compile_definitions( @@ -155,6 +162,8 @@ set(SUNSHINE_TARGET_FILES third-party/moonlight-common-c/src/Rtsp.h third-party/moonlight-common-c/src/RtspParser.c third-party/moonlight-common-c/src/Video.h + sunshine/upnp.cpp + sunshine/upnp.h sunshine/cbs.cpp sunshine/utility.h sunshine/uuid.h @@ -193,6 +202,8 @@ set(SUNSHINE_TARGET_FILES sunshine/round_robin.h ${PLATFORM_TARGET_FILES}) +set_source_files_properties(sunshine/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic) + include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/third-party @@ -222,6 +233,7 @@ list(APPEND CBS_EXTERNAL_LIBRARIES cbs) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + libminiupnpc-static ${CBS_EXTERNAL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} stdc++fs diff --git a/sunshine/main.cpp b/sunshine/main.cpp index 912da577..bfbcbf69 100644 --- a/sunshine/main.cpp +++ b/sunshine/main.cpp @@ -23,6 +23,7 @@ #include "nvhttp.h" #include "rtsp.h" #include "thread_pool.h" +#include "upnp.h" #include "video.h" #include "platform/common.h" @@ -208,9 +209,14 @@ int main(int argc, char *argv[]) { return 3; } - std::unique_ptr unregister; - auto sync = std::async(std::launch::async, [&unregister]() { - unregister = platf::publish::start(); + std::unique_ptr mDNS; + auto sync_mDNS = std::async(std::launch::async, [&mDNS]() { + mDNS = platf::publish::start(); + }); + + std::unique_ptr upnp_unmap; + auto sync_upnp = std::async(std::launch::async, [&upnp_unmap]() { + upnp_unmap = upnp::start(); }); //FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced diff --git a/sunshine/upnp.cpp b/sunshine/upnp.cpp new file mode 100644 index 00000000..58fd0102 --- /dev/null +++ b/sunshine/upnp.cpp @@ -0,0 +1,166 @@ +#include +#include + +#include "config.h" +#include "main.h" +#include "upnp.h" +#include "utility.h" + +using namespace std::literals; + +namespace upnp { +constexpr auto INET6_ADDRSTRLEN = 46; + +constexpr auto IPv4 = 0; +constexpr auto IPv6 = 1; + +using device_t = util::safe_ptr; + +KITTY_USING_MOVE_T(urls_t, UPNPUrls, , { + FreeUPNPUrls(&el); +}); + +struct mapping_t { + mapping_t(std::string &&wan, std::string &&lan, std::string &&description, bool tcp) + : port { std::move(wan), std::move(lan) }, description { std::move(description) }, tcp { tcp } {} + + struct { + std::string wan; + std::string lan; + } port; + + std::string description; + bool tcp; +}; + +static const std::vector mappings { + { "48010"s, "48010"s, "RTSP setup port"s, false }, + { "47998"s, "47998"s, "Video stream port"s, false }, + { "47999"s, "47998"s, "Control stream port"s, false }, + { "48000"s, "48000"s, "Audio stream port"s, false }, + { "47989"s, "47989"s, "Gamestream http port"s, true }, + { "47984"s, "47984"s, "Gamestream https port"s, true }, + { "47990"s, "47990"s, "Sunshine Web Manager port"s, true }, +}; + +void unmap( + const urls_t &urls, + const IGDdatas &data, + std::vector::const_reverse_iterator begin, + std::vector::const_reverse_iterator end) { + + BOOST_LOG(debug) << "Unmapping UPNP ports"sv; + + for(auto it = begin; it != end; ++it) { + auto status = UPNP_DeletePortMapping( + urls->controlURL, + data.first.servicetype, + it->port.wan.c_str(), + it->tcp ? "TCP" : "UDP", + nullptr); + + if(status) { + BOOST_LOG(warning) << "Failed to unmap port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']'; + break; + } + } +} + +class deinit_t : public platf::deinit_t { +public: + using iter_t = std::vector::const_reverse_iterator; + deinit_t(urls_t &&urls, IGDdatas data, iter_t begin, iter_t end) + : urls { std::move(urls) }, data { data }, begin { begin }, end { end } {} + + ~deinit_t() { + unmap(urls, data, begin, end); + } + + urls_t urls; + IGDdatas data; + + iter_t begin; + iter_t end; +}; + +static std::string_view status_string(int status) { + switch(status) { + case 0: + return "No IGD device found"sv; + case 1: + return "Valid IGD device found"sv; + case 2: + return "A UPnP device has been found, but it wasn't recognized as an IGD"sv; + } + + return "Unknown status"sv; +} + +std::unique_ptr start() { + int err {}; + + device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) }; + if(!device || err) { + BOOST_LOG(error) << "Couldn't discover any UPNP devices"sv; + + return nullptr; + } + + for(auto dev = device.get(); dev != nullptr; dev = dev->pNext) { + BOOST_LOG(debug) << "Found device: "sv << dev->descURL; + } + + std::array lan_addr; + std::array wan_addr; + + urls_t urls; + IGDdatas data; + + auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size()); + if(status != 1) { + BOOST_LOG(error) << status_string(status); + return nullptr; + } + + BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; + + if(UPNP_GetExternalIPAddress(urls->controlURL, data.first.servicetype, wan_addr.data())) { + BOOST_LOG(warning) << "Could not get external ip"sv; + } + else { + BOOST_LOG(debug) << "Found external ip: "sv << wan_addr.data(); + if(config::nvhttp.external_ip.empty()) { + config::nvhttp.external_ip = wan_addr.data(); + } + } + + auto it = std::begin(mappings); + + status = 0; + for(; it != std::end(mappings); ++it) { + status = UPNP_AddPortMapping( + urls->controlURL, + data.first.servicetype, + it->port.wan.c_str(), + it->port.lan.c_str(), + lan_addr.data(), + it->description.c_str(), + it->tcp ? "TCP" : "UDP", + nullptr, + "86400"); + + if(status) { + BOOST_LOG(error) << "Failed to map port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']'; + break; + } + } + + if(status) { + unmap(urls, data, std::make_reverse_iterator(it), std::rend(mappings)); + + return nullptr; + } + + return std::make_unique(std::move(urls), data, std::rbegin(mappings), std::rend(mappings)); +} +} // namespace upnp \ No newline at end of file diff --git a/sunshine/upnp.h b/sunshine/upnp.h new file mode 100644 index 00000000..478d69b1 --- /dev/null +++ b/sunshine/upnp.h @@ -0,0 +1,10 @@ +#ifndef SUNSHINE_UPNP_H +#define SUNSHINE_UPNP_H + +#include "platform/common.h" + +namespace upnp { +[[nodiscard]] std::unique_ptr start(); +} + +#endif \ No newline at end of file diff --git a/third-party/miniupnp b/third-party/miniupnp new file mode 160000 index 00000000..6f848ae0 --- /dev/null +++ b/third-party/miniupnp @@ -0,0 +1 @@ +Subproject commit 6f848ae0821f1dd1be393edb52115f36812d3c2c