ppsspp/Core/Util/PortManager.cpp
2023-03-25 10:32:09 +01:00

544 lines
18 KiB
C++

// Copyright (c) 2013- PPSSPP Project.
// 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, version 2.0 or later versions.
// Copyright (c) 2012- PPSSPP Project.
// 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, version 2.0 or later versions.
// 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 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
// Most of the code are based on https://github.com/RJ/libportfwd and updated to the latest miniupnp library
// All credit goes to him and the official miniupnp project! http://miniupnp.free.fr/
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
#include "Common/TimeUtil.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Net/Resolve.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/System/System.h"
#include "Common/Log.h"
#include "Core/Config.h"
#include "Core/System.h"
#include "Core/ELF/ParamSFO.h"
#include "Core/Util/PortManager.h"
PortManager g_PortManager;
bool upnpServiceRunning = false;
std::thread upnpServiceThread;
std::recursive_mutex upnpLock;
std::deque<UPnPArgs> upnpReqs;
PortManager::PortManager():
urls(0),
datas(0),
m_InitState(UPNP_INITSTATE_NONE),
m_LocalPort(UPNP_LOCAL_PORT_ANY),
m_leaseDuration("43200") {
// Since WSAStartup can be used multiple times it should be safe to do this right?
net::Init();
}
PortManager::~PortManager() {
// FIXME: On Windows it seems using any UPnP functions in this destructor that gets triggered when exiting PPSSPP will resulting to UPNPCOMMAND_HTTP_ERROR due to early WSACleanup (miniupnpc was getting WSANOTINITIALISED internally)
Clear();
Restore();
Terminate();
net::Shutdown();
}
void PortManager::Terminate() {
VERBOSE_LOG(SCENET, "PortManager::Terminate()");
if (urls) {
#ifdef WITH_UPNP
FreeUPNPUrls(urls);
#endif
free(urls);
urls = NULL;
}
if (datas) {
free(datas);
datas = NULL;
}
m_otherPortList.clear(); m_otherPortList.shrink_to_fit();
m_portList.clear(); m_portList.shrink_to_fit();
m_lanip.clear();
m_defaultDesc.clear();
m_leaseDuration.clear();
m_LocalPort = UPNP_LOCAL_PORT_ANY;
m_InitState = UPNP_INITSTATE_NONE;
}
bool PortManager::Initialize(const unsigned int timeout) {
#ifdef WITH_UPNP
// Windows: Assuming WSAStartup already called beforehand
struct UPNPDev* devlist;
struct UPNPDev* dev;
char* descXML;
int descXMLsize = 0;
int descXMLstatus = 0;
int localport = m_LocalPort; // UPNP_LOCAL_PORT_ANY (0), or UPNP_LOCAL_PORT_SAME (1) as an alias for 1900 (for backwards compatability?)
int ipv6 = 0; // 0 = IPv4, 1 = IPv6
unsigned char ttl = 2; // defaulting to 2
int error = 0;
VERBOSE_LOG(SCENET, "PortManager::Initialize(%d)", timeout);
if (!g_Config.bEnableUPnP) {
ERROR_LOG(SCENET, "PortManager::Initialize - UPnP is Disabled on Networking Settings");
return false;
}
if (m_InitState != UPNP_INITSTATE_NONE) {
switch (m_InitState)
{
case UPNP_INITSTATE_BUSY: {
WARN_LOG(SCENET, "PortManager - Initialization already in progress");
return false;
}
// Should we redetect UPnP? just in case the player switched to a different network in the middle
case UPNP_INITSTATE_DONE: {
WARN_LOG(SCENET, "PortManager - Already Initialized");
return true;
}
default:
break;
}
}
m_leaseDuration = "43200"; // 12 hours
m_InitState = UPNP_INITSTATE_BUSY;
urls = (UPNPUrls*)malloc(sizeof(struct UPNPUrls));
if (!urls)
return false;
datas = (IGDdatas*)malloc(sizeof(struct IGDdatas));
if (!datas) {
free(urls);
return false;
}
memset(urls, 0, sizeof(struct UPNPUrls));
memset(datas, 0, sizeof(struct IGDdatas));
devlist = upnpDiscover(timeout, NULL, NULL, localport, ipv6, ttl, &error);
if (devlist)
{
dev = devlist;
while (dev)
{
if (strstr(dev->st, "InternetGatewayDevice"))
break;
dev = dev->pNext;
}
if (!dev)
dev = devlist; // defaulting to first device
INFO_LOG(SCENET, "PortManager - UPnP device: [desc: %s] [st: %s]", dev->descURL, dev->st);
descXML = (char*)miniwget(dev->descURL, &descXMLsize, dev->scope_id, &descXMLstatus);
if (descXML)
{
parserootdesc(descXML, descXMLsize, datas);
free(descXML); descXML = 0;
GetUPNPUrls(urls, datas, dev->descURL, dev->scope_id);
}
// Get LAN IP address that connects to the router
char lanaddr[64] = "unset";
int status = UPNP_GetValidIGD(devlist, urls, datas, lanaddr, sizeof(lanaddr)); //possible "status" values, 0 = NO IGD found, 1 = A valid connected IGD has been found, 2 = A valid IGD has been found but it reported as not connected, 3 = an UPnP device has been found but was not recognized as an IGD
m_lanip = std::string(lanaddr);
INFO_LOG(SCENET, "PortManager - Detected LAN IP: %s", m_lanip.c_str());
// Additional Info
char connectionType[64] = "";
if (UPNP_GetConnectionTypeInfo(urls->controlURL, datas->first.servicetype, connectionType) != UPNPCOMMAND_SUCCESS) {
WARN_LOG(SCENET, "PortManager - GetConnectionTypeInfo failed");
}
else {
INFO_LOG(SCENET, "PortManager - Connection Type: %s", connectionType);
}
// Using Game ID & Player Name as default description for mapping
std::string gameID = g_paramSFO.GetDiscID();
m_defaultDesc = "PPSSPP:" + gameID + ":" + g_Config.sNickName; // Some routers may automatically prefixed it with "UPnP:"
freeUPNPDevlist(devlist);
//m_LocalPort = localport; // We shouldn't keep the right port for the next game reset if we wanted to redetect UPnP
m_InitState = UPNP_INITSTATE_DONE;
RefreshPortList();
return true;
}
ERROR_LOG(SCENET, "PortManager - upnpDiscover failed (error: %i) or No UPnP device detected", error);
if (g_Config.bEnableUPnP) {
auto n = GetI18NCategory("Networking");
System_NotifyUserMessage(n->T("Unable to find UPnP device"), 2.0f, 0x0000ff);
}
m_InitState = UPNP_INITSTATE_NONE;
#endif // WITH_UPNP
return false;
}
int PortManager::GetInitState() {
return m_InitState;
}
bool PortManager::Add(const char* protocol, unsigned short port, unsigned short intport) {
#ifdef WITH_UPNP
char port_str[16];
char intport_str[16];
int r;
auto n = GetI18NCategory("Networking");
if (intport == 0)
intport = port;
INFO_LOG(SCENET, "PortManager::Add(%s, %d, %d)", protocol, port, intport);
if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0')
{
if (g_Config.bEnableUPnP) {
WARN_LOG(SCENET, "PortManager::Add - the init was not done !");
System_NotifyUserMessage(n->T("UPnP need to be reinitialized"), 2.0f, 0x0000ff);
}
Terminate();
return false;
}
sprintf(port_str, "%d", port);
sprintf(intport_str, "%d", intport);
// Only add new port map if it's not previously created by PPSSPP for current IP
auto el_it = std::find_if(m_portList.begin(), m_portList.end(),
[port_str, protocol](const std::pair<std::string, std::string> &el) { return el.first == port_str && el.second == protocol; });
if (el_it == m_portList.end()) {
auto el_it = std::find_if(m_otherPortList.begin(), m_otherPortList.end(),
[port_str, protocol](const PortMap& el) { return el.extPort_str == port_str && el.protocol == protocol; });
if (el_it != m_otherPortList.end()) {
// Try to delete the port mapping before we create it, just in case we have dangling port mapping from the daemon not being shut down correctly or the port was taken by other
r = UPNP_DeletePortMapping(urls->controlURL, datas->first.servicetype, port_str, protocol, NULL);
}
r = UPNP_AddPortMapping(urls->controlURL, datas->first.servicetype,
port_str, intport_str, m_lanip.c_str(), m_defaultDesc.c_str(), protocol, NULL, m_leaseDuration.c_str());
if (r == 725 && m_leaseDuration != "0") {
m_leaseDuration = "0";
r = UPNP_AddPortMapping(urls->controlURL, datas->first.servicetype,
port_str, intport_str, m_lanip.c_str(), m_defaultDesc.c_str(), protocol, NULL, m_leaseDuration.c_str());
}
if (r != 0)
{
ERROR_LOG(SCENET, "PortManager - AddPortMapping failed (error: %i)", r);
if (r == UPNPCOMMAND_HTTP_ERROR) {
if (g_Config.bEnableUPnP) {
System_NotifyUserMessage(n->T("UPnP need to be reinitialized"), 2.0f, 0x0000ff);
}
Terminate(); // Most of the time errors occurred because the router is no longer reachable (ie. changed networks) so we should invalidate the state to prevent further lags due to timeouts
return false;
}
}
m_portList.push_front({ port_str, protocol });
// Keep tracks of it to be restored later if it belongs to others
if (el_it != m_otherPortList.end()) el_it->taken = true;
}
return true;
#else
return false;
#endif // WITH_UPNP
}
bool PortManager::Remove(const char* protocol, unsigned short port) {
#ifdef WITH_UPNP
char port_str[16];
auto n = GetI18NCategory("Networking");
INFO_LOG(SCENET, "PortManager::Remove(%s, %d)", protocol, port);
if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0')
{
if (g_Config.bEnableUPnP) {
WARN_LOG(SCENET, "PortManager::Remove - the init was not done !");
System_NotifyUserMessage(n->T("UPnP need to be reinitialized"), 2.0f, 0x0000ff);
}
Terminate();
return false;
}
sprintf(port_str, "%d", port);
int r = UPNP_DeletePortMapping(urls->controlURL, datas->first.servicetype, port_str, protocol, NULL);
if (r != 0)
{
ERROR_LOG(SCENET, "PortManager - DeletePortMapping failed (error: %i)", r);
if (r == UPNPCOMMAND_HTTP_ERROR) {
if (g_Config.bEnableUPnP) {
System_NotifyUserMessage(n->T("UPnP need to be reinitialized"), 2.0f, 0x0000ff);
}
Terminate(); // Most of the time errors occurred because the router is no longer reachable (ie. changed networks) so we should invalidate the state to prevent further lags due to timeouts
return false;
}
}
for (auto it = m_portList.begin(); it != m_portList.end(); ) {
(it->first == port_str && it->second == protocol) ? it = m_portList.erase(it) : ++it;
}
return true;
#else
return false;
#endif // WITH_UPNP
}
bool PortManager::Restore() {
#ifdef WITH_UPNP
int r;
VERBOSE_LOG(SCENET, "PortManager::Restore()");
if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0')
{
if (g_Config.bEnableUPnP) WARN_LOG(SCENET, "PortManager::Remove - the init was not done !");
return false;
}
for (auto it = m_otherPortList.begin(); it != m_otherPortList.end(); ++it) {
if (it->taken) {
auto port_str = it->extPort_str;
auto protocol = it->protocol;
// Remove it first if it's still being taken by PPSSPP
auto el_it = std::find_if(m_portList.begin(), m_portList.end(),
[port_str, protocol](const std::pair<std::string, std::string>& el) { return el.first == port_str && el.second == protocol; });
if (el_it != m_portList.end()) {
r = UPNP_DeletePortMapping(urls->controlURL, datas->first.servicetype, port_str.c_str(), protocol.c_str(), NULL);
if (r == 0) {
m_portList.erase(el_it);
}
else {
ERROR_LOG(SCENET, "PortManager::Restore - DeletePortMapping failed (error: %i)", r);
if (r == UPNPCOMMAND_HTTP_ERROR)
return false; // Might be better not to exit here, but exiting a loop will avoid long timeouts in the case the router is no longer reachable
}
}
// Add the original owner back
r = UPNP_AddPortMapping(urls->controlURL, datas->first.servicetype,
it->extPort_str.c_str(), it->intPort_str.c_str(), it->lanip.c_str(), it->desc.c_str(), it->protocol.c_str(), it->remoteHost.c_str(), it->duration.c_str());
if (r == 0) {
it->taken = false;
}
else {
ERROR_LOG(SCENET, "PortManager::Restore - AddPortMapping failed (error: %i)", r);
if (r == UPNPCOMMAND_HTTP_ERROR)
return false; // Might be better not to exit here, but exiting a loop will avoid long timeouts in the case the router is no longer reachable
}
}
}
return true;
#else
return false;
#endif // WITH_UPNP
}
bool PortManager::Clear() {
#ifdef WITH_UPNP
int r;
int i = 0;
char index[16];
char intAddr[40];
char intPort[6];
char extPort[6];
char protocol[4];
char desc[80];
char enabled[6];
char rHost[64];
char duration[16];
VERBOSE_LOG(SCENET, "PortManager::Clear()");
if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0')
{
if (g_Config.bEnableUPnP) WARN_LOG(SCENET, "PortManager::Clear - the init was not done !");
return false;
}
//unsigned int num = 0;
//UPNP_GetPortMappingNumberOfEntries(urls->controlURL, datas->first.servicetype, &num); // Not supported by many routers
do {
snprintf(index, sizeof(index), "%d", i);
rHost[0] = '\0'; enabled[0] = '\0';
duration[0] = '\0'; desc[0] = '\0'; protocol[0] = '\0';
extPort[0] = '\0'; intPort[0] = '\0'; intAddr[0] = '\0';
// May gets UPNPCOMMAND_HTTP_ERROR when called while exiting PPSSPP (ie. used in destructor)
r = UPNP_GetGenericPortMappingEntry(urls->controlURL,
datas->first.servicetype,
index,
extPort, intAddr, intPort,
protocol, desc, enabled,
rHost, duration);
// Only removes port mappings created by PPSSPP for current LAN IP
if (r == 0 && intAddr == m_lanip && std::string(desc).find("PPSSPP:") != std::string::npos) {
int r2 = UPNP_DeletePortMapping(urls->controlURL, datas->first.servicetype, extPort, protocol, rHost);
if (r2 != 0)
{
ERROR_LOG(SCENET, "PortManager::Clear - DeletePortMapping(%s, %s) failed (error: %i)", extPort, protocol, r2);
if (r2 == UPNPCOMMAND_HTTP_ERROR)
return false;
}
else {
i--;
for (auto it = m_portList.begin(); it != m_portList.end(); ) {
(it->first == extPort && it->second == protocol) ? it = m_portList.erase(it) : ++it;
}
}
}
i++;
} while (r == 0 && i < 65536);
return true;
#else
return false;
#endif // WITH_UPNP
}
bool PortManager::RefreshPortList() {
#ifdef WITH_UPNP
int r;
int i = 0;
char index[16];
char intAddr[40];
char intPort[6];
char extPort[6];
char protocol[4];
char desc[80];
char enabled[6];
char rHost[64];
char duration[16];
INFO_LOG(SCENET, "PortManager::RefreshPortList()");
if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0')
{
if (g_Config.bEnableUPnP) WARN_LOG(SCENET, "PortManager::RefreshPortList - the init was not done !");
return false;
}
m_portList.clear();
m_otherPortList.clear();
//unsigned int num = 0;
//UPNP_GetPortMappingNumberOfEntries(urls->controlURL, datas->first.servicetype, &num); // Not supported by many routers
do {
snprintf(index, sizeof(index), "%d", i);
rHost[0] = '\0'; enabled[0] = '\0';
duration[0] = '\0'; desc[0] = '\0'; protocol[0] = '\0';
extPort[0] = '\0'; intPort[0] = '\0'; intAddr[0] = '\0';
r = UPNP_GetGenericPortMappingEntry(urls->controlURL,
datas->first.servicetype,
index,
extPort, intAddr, intPort,
protocol, desc, enabled,
rHost, duration);
if (r == 0) {
std::string desc_str = std::string(desc);
// Some router might prefix the description with "UPnP:" so we may need to truncate it to prevent it from getting multiple prefix when restored later
if (desc_str.find("UPnP:") == 0)
desc_str = desc_str.substr(5);
// Only include port mappings created by PPSSPP for current LAN IP
if (intAddr == m_lanip && desc_str.find("PPSSPP:") != std::string::npos) {
m_portList.push_back({ extPort, protocol });
}
// Port mappings belong to others that might be taken by PPSSPP later
else {
m_otherPortList.push_back({ false, protocol, extPort, intPort, intAddr, rHost, desc_str, duration, enabled });
}
}
i++;
} while (r == 0 && i < 65536);
return true;
#else
return false;
#endif // WITH_UPNP
}
int upnpService(const unsigned int timeout)
{
SetCurrentThreadName("UPnPService");
INFO_LOG(SCENET, "UPnPService: Begin of UPnPService Thread");
// Service Loop
while (upnpServiceRunning && coreState != CORE_POWERDOWN) {
// Attempts to reconnect if not connected yet or got disconnected
if (g_Config.bEnableUPnP && g_PortManager.GetInitState() == UPNP_INITSTATE_NONE) {
g_PortManager.Initialize(timeout);
}
if (g_Config.bEnableUPnP && g_PortManager.GetInitState() == UPNP_INITSTATE_DONE && !upnpReqs.empty()) {
upnpLock.lock();
UPnPArgs arg = upnpReqs.front();
upnpLock.unlock();
bool ok = true;
switch (arg.cmd) {
case UPNP_CMD_ADD:
ok = g_PortManager.Add(arg.protocol.c_str(), arg.port, arg.intport);
break;
case UPNP_CMD_REMOVE:
ok = g_PortManager.Remove(arg.protocol.c_str(), arg.port);
break;
default:
break;
}
// It's only considered failed when disconnected (should be retried when reconnected)
if (ok) {
upnpLock.lock();
upnpReqs.pop_front();
upnpLock.unlock();
}
}
// Sleep for 1ms for faster response
sleep_ms(1);
}
// Cleaning up regardless of g_Config.bEnableUPnP to prevent lingering open ports on the router
if (g_PortManager.GetInitState() == UPNP_INITSTATE_DONE) {
g_PortManager.Clear();
g_PortManager.Restore();
g_PortManager.Terminate();
}
// Should we ingore any leftover UPnP requests? instead of processing it on the next game start
upnpLock.lock();
upnpReqs.clear();
upnpLock.unlock();
INFO_LOG(SCENET, "UPnPService: End of UPnPService Thread");
return 0;
}
void __UPnPInit(const unsigned int timeout) {
if (!upnpServiceRunning) {
upnpServiceRunning = true;
upnpServiceThread = std::thread(upnpService, timeout);
}
}
void __UPnPShutdown() {
if (upnpServiceRunning) {
upnpServiceRunning = false;
if (upnpServiceThread.joinable()) {
upnpServiceThread.join();
}
}
}
void UPnP_Add(const char* protocol, unsigned short port, unsigned short intport) {
std::lock_guard<std::recursive_mutex> upnpGuard(upnpLock);
upnpReqs.push_back({ UPNP_CMD_ADD, protocol, port, intport });
}
void UPnP_Remove(const char* protocol, unsigned short port) {
std::lock_guard<std::recursive_mutex> upnpGuard(upnpLock);
upnpReqs.push_back({ UPNP_CMD_REMOVE, protocol, port, port });
}