Implement UPnP for Linux using libupnp

Armin Burgmeier 2012-01-21 02:04:14 +01:00
parent a059f0f64a
commit 23d54ea2c9
5 changed files with 429 additions and 6 deletions

View File

@ -3,7 +3,7 @@
# Copyright (c) 2009-2011 Günther Brammer
# Copyright (c) 2009-2012 Nicolas Hake
# Copyright (c) 2009 David Dormagen
# Copyright (c) 2009-2011 Armin Burgmeier
# Copyright (c) 2009-2012 Armin Burgmeier
# Copyright (c) 2009-2010 Sven Eberhardt
# Copyright (c) 2009 Tobias Zwick
# Copyright (c) 2009 Richard Gerum
@ -747,9 +747,17 @@ if(HAVE_NATUPNP_H)
src/network/C4Network2UPnPWin32.cpp
)
else()
list(APPEND OC_SYSTEM_SOURCES
src/network/C4Network2UPnPDummy.cpp
)
FIND_PACKAGE(Upnp)
SET(HAVE_UPNP ${UPNP_FOUND})
if(UPNP_FOUND)
list(APPEND OC_SYSTEM_SOURCES
src/network/C4Network2UPnPLinux.cpp
)
else()
list(APPEND OC_SYSTEM_SOURCES
src/network/C4Network2UPnPDummy.cpp
)
endif()
endif()
############################################################################
@ -1220,6 +1228,11 @@ if(NOT HAVE_FMOD AND NOT USE_OPEN_AL OR USE_SDL_MAINLOOP AND NOT USE_OPEN_AL)
endif()
endif()
if(HAVE_UPNP)
include_directories(${UPNP_INCLUDE_DIR})
target_link_libraries(clonk ${UPNP_LIBRARIES})
endif()
# GTest
include(FindGTest)
if(GTEST_FOUND)

View File

@ -0,0 +1,36 @@
# - Find libupnp
# Find the libupnp library
# This module defines
# UPNP_INCLUDE_DIR, where to find upnp.h, etc.
# UPNP_LIBRARIES, the libraries needed to use libupnp.
# UPNP_FOUND, If false, do not try to use libupnp.
#=============================================================================
# OpenClonk, http://www.openclonk.org
#
# Copyright (c) 2012 Armin Burgmeier
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# See isc_license.txt for full license and disclaimer.
#
# "Clonk" is a registered trademark of Matthes Bender.
# See clonk_trademark_license.txt for full license.
#=============================================================================
# TODO: Use pkg-config if available
find_path(UPNP_INCLUDE_DIR NAMES upnp.h PATH_SUFFIXES upnp)
set(UPNP_NAMES ${UPNP_NAMES} upnp)
find_library(UPNP_LIBRARY NAMES ${UPNP_NAMES})
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(UPNP DEFAULT_MSG UPNP_LIBRARY UPNP_INCLUDE_DIR)
if(UPNP_FOUND)
set(UPNP_LIBRARIES ${UPNP_LIBRARY})
set(UPNP_INCLUDE_DIR ${UPNP_INCLUDE_DIR})
endif()
mark_as_advanced(UPNP_LIBRARY UPNP_INCLUDE_DIR)

View File

@ -134,7 +134,7 @@ void C4InteractiveThread::ProcessEvents() // by main thread
switch (eEventType)
{
// Logging
case Ev_Log: case Ev_LogSilent: case Ev_LogFatal:
case Ev_Log: case Ev_LogSilent: case Ev_LogFatal: case Ev_LogDebug:
{
// Reconstruct the StdStrBuf which allocated the data.
StdStrBuf pLog;
@ -147,7 +147,9 @@ void C4InteractiveThread::ProcessEvents() // by main thread
LogSilent(pLog.getData()); break;
case Ev_LogFatal:
LogFatal(pLog.getData()); break;
default: assert(eEventType == Ev_Log || eEventType == Ev_LogSilent || eEventType == Ev_LogFatal); // obviously will not happen, but someone tell gcc
case Ev_LogDebug:
DebugLog(pLog.getData()); break;
default: assert(eEventType == Ev_Log || eEventType == Ev_LogSilent || eEventType == Ev_LogFatal || eEventType == Ev_LogDebug); // obviously will not happen, but someone tell gcc
}
}
@ -196,6 +198,15 @@ bool C4InteractiveThread::ThreadLogS(const char *szMessage, ...)
return PushEvent(Ev_LogSilent, Msg.GrabPointer());
}
bool C4InteractiveThread::ThreadLogDebug(const char *szMessage, ...)
{
// format message
va_list lst; va_start(lst, szMessage);
StdStrBuf Msg = FormatStringV(szMessage, lst);
// send to main thread
return PushEvent(Ev_LogDebug, Msg.GrabPointer());
}
bool C4InteractiveThreadNotifyProc::Execute(int, pollfd*)
{
if (CheckAndReset())

View File

@ -33,10 +33,12 @@ enum C4InteractiveEventType
Ev_Log,
Ev_LogSilent,
Ev_LogFatal,
Ev_LogDebug,
Ev_FileChange,
Ev_HTTP_Response,
Ev_UPNP_Response,
Ev_IRC_Message,
@ -110,6 +112,7 @@ public:
bool ThreadLog(const char *szMessage, ...) GNUC_FORMAT_ATTRIBUTE_O;
bool ThreadLogFatal(const char *szMessage, ...) GNUC_FORMAT_ATTRIBUTE_O;
bool ThreadLogS(const char *szMessage, ...) GNUC_FORMAT_ATTRIBUTE_O;
bool ThreadLogDebug(const char *szMessage, ...) GNUC_FORMAT_ATTRIBUTE_O;
template<typename Functor>
bool ThreadPostAsync(Functor function)

View File

@ -0,0 +1,360 @@
/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 2012 Armin Burgmeier
*
* Portions might be copyrighted by other authors who have contributed
* to OpenClonk.
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
* See isc_license.txt for full license and disclaimer.
*
* "Clonk" is a registered trademark of Matthes Bender.
* See clonk_trademark_license.txt for full license.
*/
/* Linux implementation of a UPnP port mapper (using libupnp) */
#include <C4Include.h>
#include <C4Application.h>
#include <C4Version.h>
#include <netdb.h>
#include <upnp.h>
#include <upnptools.h>
#include <C4Network2UPnP.h> // must come after upnp.h
namespace
{
// This attempts to return the local IP address which is used for
// internet connections. It does so by associating a UDP socket to talk to
// 8.8.8.8 (A Google nameserver) and then reading its local address. There
// might be cleverer ways to do this, such as reading the routing table.
std::string GetOutgoingAddress()
{
struct socket_wrapper
{
const int sock;
operator int() const { return sock; }
socket_wrapper(int domain, int type, int protocol):
sock(socket(domain, type, protocol))
{
if(sock == -1)
throw std::runtime_error(std::string("Failed to create a socket: ") + strerror(errno));
}
~socket_wrapper()
{
if(close(sock) != 0)
DebugLogF("Failed to close socket: %s\n", strerror(errno));
}
};
const char* const REMOTE_ADDRESS = "8.8.8.8";
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(53); // DNS port
if(inet_pton(AF_INET, REMOTE_ADDRESS, &addr.sin_addr) != 1)
throw std::runtime_error(std::string("Failed to convert address text to binary: ") + strerror(errno));
socket_wrapper sock(AF_INET, SOCK_DGRAM, 0);
if(connect(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) != 0)
throw std::runtime_error(std::string("Failed to set target address on UDP socket: ") + strerror(errno));
struct sockaddr_in local_addr;
socklen_t local_addr_len = sizeof(local_addr);
if(getsockname(sock, reinterpret_cast<struct sockaddr*>(&local_addr), &local_addr_len) != 0 || local_addr_len > sizeof(local_addr))
throw std::runtime_error(std::string("Failed to query peer name of UDP socket: ") + strerror(errno));
char text_address[INET_ADDRSTRLEN];
if(inet_ntop(AF_INET, &local_addr.sin_addr, text_address, INET_ADDRSTRLEN) == NULL)
throw std::runtime_error(std::string("Failed to convert binary address to text: ") + strerror(errno));
//DebugLogF("Outgoing address: %s", text_address);
return text_address;
}
}
class C4Network2UPnPP: public C4InteractiveThread::Callback
{
public:
C4Network2UPnPP();
virtual ~C4Network2UPnPP();
void AddMapping(C4Network2IOProtocol protocol, uint16_t intport, uint16_t extport);
void ClearMappings();
private:
struct IGD {
//std::string DeviceID;
std::string Location;
std::string ServiceType;
};
struct PortMapping {
std::string external_hostname;
uint16_t external_port;
std::string internal_hostname;
uint16_t internal_port;
std::string protocol;
};
struct ActionData {
ActionData(const IGD& igd_, const PortMapping& mapping_):
igd(igd_), mapping(mapping_) {}
IGD igd;
PortMapping mapping;
};
// Main thread notification:
struct Notify {
virtual ~Notify() {}
};
struct NotifySearchResult: Notify {
NotifySearchResult(const std::string& device_id, const IGD& igd_):
DeviceId(device_id), igd(igd_) {}
std::string DeviceId;
IGD igd;
};
struct NotifyActionComplete: Notify {
NotifyActionComplete(const ActionData& data, const std::string& action, int err_code):
igd(data.igd), Mapping(data.mapping), Action(action), ErrCode(err_code) {}
// Mapping than was added or removed
IGD igd;
PortMapping Mapping;
std::string Action;
int ErrCode;
};
virtual void OnThreadEvent(C4InteractiveEventType eEvent, void *pEventData);
static int Callback_Static(Upnp_EventType EventType, void* Event, void* Cookie);
void OnSearchResult(const std::string& DeviceID, const IGD& igd);
void OnActionComplete(const IGD& igd, const PortMapping& mapping, const std::string& Action, int ErrCode);
void AddPortMapping(const IGD& igd, const PortMapping& mapping);
void RemovePortMapping(const IGD& igd, const PortMapping& mapping);
std::string outgoing_address;
std::map<std::string, IGD> igds;
std::vector<PortMapping> added_mappings;
UpnpClient_Handle upnp_handle;
};
C4Network2UPnPP::C4Network2UPnPP()
{
// Query outgoing network address only once at the beginning. We talk to
// the IGD (if any) via this address.
try
{
outgoing_address = GetOutgoingAddress();
int res = UpnpInit(outgoing_address.c_str(), 0);
if(res != UPNP_E_SUCCESS)
throw std::runtime_error(std::string("Failed to initialize UPnP: ") + UpnpGetErrorMessage(res));
C4InteractiveThread &Thread = Application.InteractiveThread;
Thread.SetCallback(Ev_UPNP_Response, this);
res = UpnpRegisterClient(C4Network2UPnPP::Callback_Static, this, &upnp_handle);
if(res != UPNP_E_SUCCESS)
throw std::runtime_error(std::string("Failed to register UPnP client: ") + UpnpGetErrorMessage(res));
res = UpnpSearchAsync(upnp_handle, 5, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", this);
if(res != UPNP_E_SUCCESS)
throw std::runtime_error(std::string("Failed to search for IGDs: ") + UpnpGetErrorMessage(res));
}
catch(const std::runtime_error& error)
{
LogF("Failed to initialize UPnP: %s", error.what());
outgoing_address.clear();
}
}
C4Network2UPnPP::~C4Network2UPnPP()
{
ClearMappings();
UpnpUnRegisterClient(upnp_handle);
C4InteractiveThread &Thread = Application.InteractiveThread;
Thread.ClearCallback(Ev_UPNP_Response, this);
UpnpFinish();
}
void C4Network2UPnPP::AddMapping(C4Network2IOProtocol protocol, uint16_t intport, uint16_t extport)
{
PortMapping mapping;
mapping.external_hostname = "";
mapping.external_port = extport;
mapping.internal_hostname = outgoing_address;
mapping.internal_port = intport;
mapping.protocol = (protocol == P_TCP ? "TCP" : "UDP");
added_mappings.push_back(mapping);
for(std::map<std::string, IGD>::const_iterator iter = igds.begin(); iter != igds.end(); ++iter)
AddPortMapping(iter->second, mapping);
}
void C4Network2UPnPP::ClearMappings()
{
for(std::map<std::string, IGD>::const_iterator igd_iter = igds.begin(); igd_iter != igds.end(); ++igd_iter)
for(std::vector<PortMapping>::const_iterator mapping_iter = added_mappings.begin(); mapping_iter != added_mappings.end(); ++mapping_iter)
RemovePortMapping(igd_iter->second, *mapping_iter);
added_mappings.clear();
}
// This function is called asynchronously from a libupnp thread.
// It is not allowed to call back into the library so we queue a function
// to be called by the main thread. The event data is not guaranteed to
// stay alive past this function call so we need to inspect the event type
// and preserve the event data we want to process later.
int C4Network2UPnPP::Callback_Static(Upnp_EventType EventType, void* Event, void* Cookie)
{
//C4Network2UPnPP* upnp = static_cast<C4Network2UPnPP*>(Cookie);
switch(EventType)
{
case UPNP_DISCOVERY_SEARCH_RESULT:
{
Upnp_Discovery* discovery = static_cast<Upnp_Discovery*>(Event);
IGD igd;
igd.Location = discovery->Location;
igd.ServiceType = discovery->ServiceType;
Application.InteractiveThread.PushEvent(Ev_UPNP_Response, new NotifySearchResult(discovery->DeviceId, igd));
}
break;
case UPNP_CONTROL_ACTION_COMPLETE:
{
std::auto_ptr<ActionData> data(static_cast<ActionData*>(Cookie));
Upnp_Action_Complete* complete = static_cast<Upnp_Action_Complete*>(Event);
std::string action = ixmlNode_getNodeName(ixmlNode_getFirstChild(&complete->ActionRequest->n));
Application.InteractiveThread.PushEvent(Ev_UPNP_Response, new NotifyActionComplete(*data, action, complete->ErrCode));
}
break;
default:
Application.InteractiveThread.ThreadLogDebug("Unhandled UPNP event: %d", static_cast<int>(EventType));
break;
}
return 0;
}
void C4Network2UPnPP::OnThreadEvent(C4InteractiveEventType eEvent, void *pEventData)
{
std::auto_ptr<Notify> notify(static_cast<Notify*>(pEventData));
// TODO: Should call a virtual method instead of dynamic_casting
NotifySearchResult* notify_search_result = dynamic_cast<NotifySearchResult*>(notify.get());
NotifyActionComplete* notify_action_complete = dynamic_cast<NotifyActionComplete*>(notify.get());
if(notify_search_result)
OnSearchResult(notify_search_result->DeviceId, notify_search_result->igd);
if(notify_action_complete)
OnActionComplete(notify_action_complete->igd, notify_action_complete->Mapping, notify_action_complete->Action, notify_action_complete->ErrCode);
}
void C4Network2UPnPP::OnSearchResult(const std::string& DeviceID, const IGD& igd)
{
// Make sure we don't find the same device twice
std::map<std::string, IGD>::const_iterator iter = igds.find(DeviceID);
if(iter != igds.end()) return;
// Add device
igds[DeviceID] = igd;
// Add all port mappings with this device
for(std::vector<PortMapping>::const_iterator mapping_iter = added_mappings.begin(); mapping_iter != added_mappings.end(); ++mapping_iter)
AddPortMapping(igd, *mapping_iter);
}
void C4Network2UPnPP::OnActionComplete(const IGD& igd, const PortMapping& mapping, const std::string& Action, int ErrCode)
{
// If adding failed with an error of 718 this means that this external port
// already exists in the port mapping table. Probably this was from some
// previous OpenClonk game. We remove it, and then, after it has been
// successfully removed, we try to add it again.
if(Action == "u:AddPortMapping" && ErrCode == 718)
RemovePortMapping(igd, mapping);
else if(Action == "u:DeletePortMapping" && ErrCode == 0)
AddPortMapping(igd, mapping);
else if(ErrCode != 0)
LogF("UPnP operation %s failed: %s", Action.c_str(), UpnpGetErrorMessage(ErrCode));
}
void C4Network2UPnPP::AddPortMapping(const IGD& igd, const PortMapping& mapping)
{
if(igds.empty()) return; // Catches the case that UPnP initialization failed
StdStrBuf external_port_buf, internal_port_buf;
external_port_buf.Format("%d", static_cast<int>(mapping.external_port));
internal_port_buf.Format("%d", static_cast<int>(mapping.internal_port));
IXML_Document* action = UpnpMakeAction("AddPortMapping",
igd.ServiceType.c_str(), 8,
"NewRemoteHost", mapping.external_hostname.c_str(),
"NewExternalPort", external_port_buf.getData(),
"NewProtocol", mapping.protocol.c_str(),
"NewInternalPort", internal_port_buf.getData(),
"NewInternalClient", mapping.internal_hostname.c_str(),
"NewEnabled", "1",
"NewPortMappingDescription", C4ENGINECAPTION,
"NewLeaseDuration", "0");
UpnpSendActionAsync(upnp_handle, igd.Location.c_str(), igd.ServiceType.c_str(), NULL, action, Callback_Static, new ActionData(igd, mapping));
ixmlDocument_free(action);
}
void C4Network2UPnPP::RemovePortMapping(const IGD& igd, const PortMapping& mapping)
{
if(igds.empty()) return; // Catches the case that UPnP initialization failed
StdStrBuf external_port_buf;
external_port_buf.Format("%d", static_cast<int>(mapping.external_port));
IXML_Document* action = UpnpMakeAction("DeletePortMapping",
igd.ServiceType.c_str(), 3,
"NewRemoteHost", mapping.external_hostname.c_str(),
"NewExternalPort", external_port_buf.getData(),
"NewProtocol", mapping.protocol.c_str());
UpnpSendActionAsync(upnp_handle, igd.Location.c_str(), igd.ServiceType.c_str(), NULL, action, Callback_Static, new ActionData(igd, mapping));
ixmlDocument_free(action);
}
C4Network2UPnP::C4Network2UPnP():
p(new C4Network2UPnPP)
{
}
C4Network2UPnP::~C4Network2UPnP()
{
delete p;
}
void C4Network2UPnP::AddMapping(C4Network2IOProtocol protocol, uint16_t intport, uint16_t extport)
{
p->AddMapping(protocol, intport, extport);
}
void C4Network2UPnP::ClearMappings()
{
p->ClearMappings();
}