openclonk/src/network/C4Network2Reference.cpp

699 lines
20 KiB
C++

/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
* Copyright (c) 2009-2016, The OpenClonk Team and contributors
*
* Distributed under the terms of the ISC license; see accompanying file
* "COPYING" for details.
*
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
* See accompanying file "TRADEMARK" for details.
*
* To redistribute this file separately, substitute the full license texts
* for the above references.
*/
#include "C4Include.h"
#include "network/C4Network2Reference.h"
#include "game/C4Game.h"
#include "control/C4RoundResults.h"
#include "C4Version.h"
#include "game/C4Application.h"
#include <utility>
#include <fcntl.h>
#include <zlib.h>
// *** C4Network2Reference
C4Network2Reference::C4Network2Reference()
: Icon(0), GameMode(), Time(0), Frame(0), StartTime(0), LeaguePerformance(0),
JoinAllowed(true), ObservingAllowed(true), PasswordNeeded(false), OfficialServer(false),
IsEditor(false), iAddrCnt(0), NetpuncherGameID(C4NetpuncherID())
{
}
C4Network2Reference::~C4Network2Reference()
{
}
void C4Network2Reference::SetSourceAddress(const C4NetIO::EndpointAddress &ip)
{
source = ip;
if (iAddrCnt < C4ClientMaxAddr)
Addrs[++iAddrCnt].SetAddr(ip);
}
void C4Network2Reference::InitLocal()
{
// Copy all game parameters
Parameters = ::Game.Parameters;
// Discard player resources (we don't want these infos in the reference)
// Add league performance (but only after game end)
C4ClientPlayerInfos *pClientInfos; C4PlayerInfo *pPlayerInfo;
int32_t i, j;
for (i = 0; (pClientInfos = Parameters.PlayerInfos.GetIndexedInfo(i)); i++)
for (j = 0; (pPlayerInfo = pClientInfos->GetPlayerInfo(j)); j++)
{
pPlayerInfo->DiscardResource();
if(::Game.GameOver)
pPlayerInfo->SetLeaguePerformance(::Game.RoundResults.GetLeaguePerformance(pPlayerInfo->GetID()));
}
// Special additional information in reference
Icon = ::Game.C4S.Head.Icon;
Title.CopyValidated(::Game.ScenarioTitle);
GameMode = ::Game.C4S.Game.Mode;
GameStatus = ::Network.Status;
Time = ::Game.Time;
Frame = ::Game.FrameCounter;
StartTime = ::Game.StartTime;
LeaguePerformance = ::Game.RoundResults.GetLeaguePerformance();
Comment = Config.Network.Comment;
JoinAllowed = ::Network.isJoinAllowed();
ObservingAllowed = ::Network.isObservingAllowed();
PasswordNeeded = ::Network.isPassworded();
IsEditor = !!::Application.isEditor;
NetpuncherGameID = ::Network.getNetpuncherGameID();
NetpuncherAddr = ::Network.getNetpuncherAddr();
Game.Set();
// Addresses
C4Network2Client *pLocalNetClient = ::Game.Clients.getLocal()->getNetClient();
iAddrCnt = pLocalNetClient->getAddrCnt();
for (i = 0; i < iAddrCnt; i++)
Addrs[i] = pLocalNetClient->getAddr(i);
}
void C4Network2Reference::SortNullIPsBack()
{
// Sort all addresses with zero IP to back of list
int iSortAddrCnt = iAddrCnt;
for (int i = 0; i < iSortAddrCnt; i++)
if (Addrs[i].isIPNull())
{
C4Network2Address Addr = Addrs[i];
for (int j = i + 1; j < iAddrCnt; j++)
Addrs[j - 1] = Addrs[j];
Addrs[iAddrCnt - 1] = Addr;
// Correct position
i--; iSortAddrCnt--;
}
}
void C4Network2Reference::CompileFunc(StdCompiler *pComp)
{
pComp->Value(mkNamingAdapt(Icon, "Icon", 0));
pComp->Value(mkNamingAdapt(Title, "Title", "No title"));
pComp->Value(mkNamingAdapt(mkParAdapt(GameMode, StdCompiler::RCT_IdtfAllowEmpty), "GameMode", ""));
pComp->Value(mkParAdapt(GameStatus, true));
pComp->Value(mkNamingAdapt(Time, "Time", 0));
pComp->Value(mkNamingAdapt(Frame, "Frame", 0));
pComp->Value(mkNamingAdapt(StartTime, "StartTime", 0));
pComp->Value(mkNamingAdapt(LeaguePerformance, "LeaguePerformance",0));
pComp->Value(mkNamingAdapt(Comment, "Comment", ""));
pComp->Value(mkNamingAdapt(JoinAllowed, "JoinAllowed", true));
pComp->Value(mkNamingAdapt(ObservingAllowed, "ObservingAllowed", true));
pComp->Value(mkNamingAdapt(PasswordNeeded, "PasswordNeeded", false));
pComp->Value(mkNamingAdapt(IsEditor, "IsEditor", false));
pComp->Value(mkNamingAdapt(mkIntPackAdapt(iAddrCnt), "AddressCount", 0));
iAddrCnt = std::min<uint8_t>(C4ClientMaxAddr, iAddrCnt);
pComp->Value(mkNamingAdapt(mkArrayAdapt(Addrs, iAddrCnt, C4Network2Address()), "Address"));
pComp->Value(mkNamingAdapt(Game.sEngineName, "Game", "None"));
pComp->Value(mkNamingAdapt(mkArrayAdaptDM(Game.iVer,0),"Version" ));
pComp->Value(mkNamingAdapt(OfficialServer, "OfficialServer", false));
pComp->Value(mkNamingAdapt(NetpuncherGameID, "NetpuncherID", C4NetpuncherID(), false, false));
pComp->Value(mkNamingAdapt(NetpuncherAddr, "NetpuncherAddr", "", false, false));
pComp->Value(Parameters);
}
int32_t C4Network2Reference::getSortOrder() const // Don't go over 100, because that's for the masterserver...
{
C4GameVersion verThis;
int iOrder = 0;
// Official server
if (isOfficialServer() && !Config.Network.UseAlternateServer) iOrder += 50;
// Joinable
if (isJoinAllowed() && (getGameVersion() == verThis)) iOrder += 25;
// League game
if (Parameters.isLeague()) iOrder += 5;
// In lobby
if (getGameStatus().isLobbyActive()) iOrder += 3;
// No password needed
if (!isPasswordNeeded()) iOrder += 1;
// Done
return iOrder;
}
StdStrBuf C4Network2Reference::getGameGoalString() const
{
if (GameMode.getLength() > 0)
{
// Prefer to derive string from game mode
return FormatString("%s: %s", LoadResStr("IDS_MENU_CPGOALS"), GameMode.getData());
}
else
{
// If not defined, fall back to goal string
return Parameters.GetGameGoalString();
}
}
// *** C4Network2RefServer
C4Network2RefServer::C4Network2RefServer()
: pReference(nullptr)
{
}
C4Network2RefServer::~C4Network2RefServer()
{
Clear();
}
void C4Network2RefServer::Clear()
{
C4NetIOTCP::Close();
delete pReference; pReference = nullptr;
}
void C4Network2RefServer::SetReference(C4Network2Reference *pNewReference)
{
CStdLock RefLock(&RefCSec);
delete pReference; pReference = pNewReference;
}
void C4Network2RefServer::PackPacket(const C4NetIOPacket &rPacket, StdBuf &rOutBuf)
{
// Just append the packet
rOutBuf.Append(rPacket);
}
size_t C4Network2RefServer::UnpackPacket(const StdBuf &rInBuf, const C4NetIO::addr_t &addr)
{
const char *pData = getBufPtr<char>(rInBuf);
// Check for complete header
const char *pHeaderEnd = strstr(pData, "\r\n\r\n");
if (!pHeaderEnd)
return 0;
// Check method (only GET is allowed for now)
if (!SEqual2(pData, "GET "))
{
RespondNotImplemented(addr, "Method not implemented");
return rInBuf.getSize();
}
// Check target
// TODO
RespondReference(addr);
return rInBuf.getSize();
}
void C4Network2RefServer::RespondNotImplemented(const C4NetIO::addr_t &addr, const char *szMessage)
{
// Send the message
StdStrBuf Data = FormatString("HTTP/1.0 501 %s\r\n\r\n", szMessage);
Send(C4NetIOPacket(Data.getData(), Data.getLength(), false, addr));
// Close the connection
Close(addr);
}
void C4Network2RefServer::RespondReference(const C4NetIO::addr_t &addr)
{
CStdLock RefLock(&RefCSec);
// Pack
StdStrBuf PacketData = DecompileToBuf<StdCompilerINIWrite>(mkNamingPtrAdapt(pReference, "Reference"));
// Create header
StdStrBuf Header = FormatString(
"HTTP/1.1 200 Found\r\n"
"Content-Length: %lu\r\n"
"Content-Type: text/plain; charset=UTF-8\r\n"
"Server: " C4ENGINENAME "/" C4VERSION "\r\n"
"\r\n",
static_cast<unsigned long>(PacketData.getLength()));
// Send back
Send(C4NetIOPacket(Header, Header.getLength(), false, addr));
Send(C4NetIOPacket(PacketData, PacketData.getLength(), false, addr));
// Close the connection
Close(addr);
}
// *** C4Network2HTTPClient
C4Network2HTTPClient::C4Network2HTTPClient()
: fBinary(false), fBusy(false), fSuccess(false), fConnected(false), iDataOffset(0), iDownloadedSize(0), iTotalSize(0),
pNotify(nullptr)
{
C4NetIOTCP::SetCallback(this);
}
C4Network2HTTPClient::~C4Network2HTTPClient()
{
}
void C4Network2HTTPClient::PackPacket(const C4NetIOPacket &rPacket, StdBuf &rOutBuf)
{
// Just append the packet
rOutBuf.Append(rPacket);
}
size_t C4Network2HTTPClient::UnpackPacket(const StdBuf &rInBuf, const C4NetIO::addr_t &addr)
{
// since new data arrived, increase timeout time
ResetRequestTimeout();
// Check for complete header
if (!iDataOffset)
{
// Copy data into string buffer (terminate)
StdStrBuf Data; Data.Copy(getBufPtr<char>(rInBuf), rInBuf.getSize());
const char *pData = Data.getData();
// Header complete?
const char *pContent = SSearch(pData, "\r\n\r\n");
if (!pContent)
return 0;
// Read the header
if (!ReadHeader(Data))
{
fBusy = fSuccess = false;
Close(addr);
return rInBuf.getSize();
}
}
iDownloadedSize = rInBuf.getSize() - iDataOffset;
// Check if the packet is complete
if (iTotalSize > iDownloadedSize)
{
return 0;
}
// Get data, uncompress it if needed
StdBuf Data = rInBuf.getPart(iDataOffset, iTotalSize);
if (fCompressed)
if (!Decompress(&Data))
{
fBusy = fSuccess = false;
Close(addr);
return rInBuf.getSize();
}
// Save the result
if (fBinary)
ResultBin.Copy(Data);
else
ResultString.Copy(getBufPtr<char>(Data), Data.getSize());
fBusy = false; fSuccess = true;
// Callback
OnPacket(C4NetIOPacket(Data, addr), this);
// Done
Close(addr);
return rInBuf.getSize();
}
bool C4Network2HTTPClient::ReadHeader(StdStrBuf Data)
{
const char *pData = Data.getData();
const char *pContent = SSearch(pData, "\r\n\r\n");
if (!pContent)
return 0;
// Parse header line
int iHTTPVer1, iHTTPVer2, iResponseCode, iStatusStringPtr;
if (sscanf(pData, "HTTP/%d.%d %d %n", &iHTTPVer1, &iHTTPVer2, &iResponseCode, &iStatusStringPtr) != 3)
{
Error = "Invalid status line!";
return false;
}
// Check HTTP version
if (iHTTPVer1 != 1)
{
Error.Format("Unsupported HTTP version: %d.%d!", iHTTPVer1, iHTTPVer2);
return false;
}
// Check code
if (iResponseCode != 200)
{
// Get status string
StdStrBuf StatusString; StatusString.CopyUntil(pData + iStatusStringPtr, '\r');
// Create error message
Error.Format("HTTP server responded %d: %s", iResponseCode, StatusString.getData());
return false;
}
// Get content length
const char *pContentLength = SSearch(pData, "\r\nContent-Length:");
int iContentLength;
if (!pContentLength || pContentLength > pContent ||
sscanf(pContentLength, "%d", &iContentLength) != 1)
{
Error.Format("Invalid server response: Content-Length is missing!");
return false;
}
iTotalSize = iContentLength;
iDataOffset = (pContent - pData);
// Get content encoding
const char *pContentEncoding = SSearch(pData, "\r\nContent-Encoding:");
if (pContentEncoding)
{
while (*pContentEncoding == ' ') pContentEncoding++;
StdStrBuf Encoding; Encoding.CopyUntil(pContentEncoding, '\r');
if (Encoding == "gzip")
fCompressed = true;
else
fCompressed = false;
}
else
fCompressed = false;
// Okay
return true;
}
bool C4Network2HTTPClient::Decompress(StdBuf *pData)
{
size_t iSize = pData->getSize();
// Create buffer
uint32_t iOutSize = *getBufPtr<uint32_t>(*pData, pData->getSize() - sizeof(uint32_t));
iOutSize = std::min<uint32_t>(iOutSize, iSize * 1000);
StdBuf Out; Out.New(iOutSize);
// Prepare stream
z_stream zstrm;
ZeroMem(&zstrm, sizeof(zstrm));
zstrm.next_in = const_cast<Byte *>(getBufPtr<Byte>(*pData));
zstrm.avail_in = pData->getSize();
zstrm.next_out = getMBufPtr<Byte>(Out);
zstrm.avail_out = Out.getSize();
// Inflate...
if (inflateInit2(&zstrm, 15 + 16) != Z_OK)
{
Error.Format("Could not decompress data!");
return false;
}
// Inflate!
if (inflate(&zstrm, Z_FINISH) != Z_STREAM_END)
{
inflateEnd(&zstrm);
Error.Format("Could not decompress data!");
return false;
}
// Return the buffer
Out.SetSize(zstrm.total_out);
pData->Take(std::move(Out));
// Okay
inflateEnd(&zstrm);
return true;
}
bool C4Network2HTTPClient::OnConn(const C4NetIO::addr_t &AddrPeer, const C4NetIO::addr_t &AddrConnect, const C4NetIO::addr_t *pOwnAddr, C4NetIO *pNetIO)
{
// Make sure we're actually waiting for this connection
if (fConnected || (AddrConnect != ServerAddr && AddrConnect != ServerAddrFallback))
return false;
// Save pack peer address
PeerAddr = AddrPeer;
// Send the request
if (!Send(C4NetIOPacket(Request, AddrPeer)))
{
Error.Format("Unable to send HTTP request: %s", Error.getData());
}
Request.Clear();
fConnected = true;
return true;
}
void C4Network2HTTPClient::OnDisconn(const C4NetIO::addr_t &AddrPeer, C4NetIO *pNetIO, const char *szReason)
{
// Got no complete packet? Failure...
if (!fSuccess && Error.isNull())
{
fBusy = false;
Error.Format("Unexpected disconnect: %s", szReason);
}
fConnected = false;
// Notify
if (pNotify)
pNotify->PushEvent(Ev_HTTP_Response, this);
}
void C4Network2HTTPClient::OnPacket(const class C4NetIOPacket &rPacket, C4NetIO *pNetIO)
{
// Everything worthwhile was already done in UnpackPacket. Only do notify callback
if (pNotify)
pNotify->PushEvent(Ev_HTTP_Response, this);
}
bool C4Network2HTTPClient::Execute(int iMaxTime)
{
// Check timeout
if (fBusy)
{
if (C4TimeMilliseconds::Now() > HappyEyeballsTimeout)
{
HappyEyeballsTimeout = C4TimeMilliseconds::PositiveInfinity;
Application.InteractiveThread.ThreadLogS("HTTP: Starting fallback connection to %s (%s)", Server.getData(), ServerAddrFallback.ToString().getData());
Connect(ServerAddrFallback);
}
if (time(nullptr) > iRequestTimeout)
{
Cancel("Request timeout");
return true;
}
}
// Execute normally
return C4NetIOTCP::Execute(iMaxTime);
}
C4TimeMilliseconds C4Network2HTTPClient::GetNextTick(C4TimeMilliseconds tNow)
{
C4TimeMilliseconds tNetIOTCPTick = C4NetIOTCP::GetNextTick(tNow);
if (!fBusy)
return tNetIOTCPTick;
C4TimeMilliseconds tHTTPClientTick = tNow + 1000 * std::max<time_t>(iRequestTimeout - time(nullptr), 0);
C4TimeMilliseconds HappyEyeballsTick = tNow + std::max(HappyEyeballsTimeout - C4TimeMilliseconds::Now(), 0);
return std::min({tNetIOTCPTick, tHTTPClientTick, HappyEyeballsTick});
}
bool C4Network2HTTPClient::Query(const StdBuf &Data, bool fBinary)
{
if (Server.isNull()) return false;
// Cancel previous request
if (fBusy)
Cancel("Cancelled");
// No result known yet
ResultString.Clear();
// store mode
this->fBinary = fBinary;
// Create request
StdStrBuf Header;
if (Data.getSize())
Header.Format(
"POST %s HTTP/1.0\r\n"
"Host: %s\r\n"
"Connection: Close\r\n"
"Content-Length: %lu\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"Accept-Charset: utf-8\r\n"
"Accept-Encoding: gzip\r\n"
"Accept-Language: %s\r\n"
"User-Agent: " C4ENGINENAME "/" C4VERSION "\r\n"
"\r\n",
RequestPath.getData(),
Server.getData(),
static_cast<unsigned long>(Data.getSize()),
Config.General.LanguageEx);
else
Header.Format(
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"Connection: Close\r\n"
"Accept-Charset: utf-8\r\n"
"Accept-Encoding: gzip\r\n"
"Accept-Language: %s\r\n"
"User-Agent: " C4ENGINENAME "/" C4VERSION "\r\n"
"\r\n",
RequestPath.getData(),
Server.getData(),
Config.General.LanguageEx);
// Compose query
Request.Take(Header.GrabPointer(), Header.getLength());
Request.Append(Data);
// Start connecting
if (!Connect(ServerAddr))
return false;
// Also try the fallback address after some time (if there is one)
if (!ServerAddrFallback.IsNull())
HappyEyeballsTimeout = C4TimeMilliseconds::Now() + C4Network2HTTPHappyEyeballsTimeout;
else
HappyEyeballsTimeout = C4TimeMilliseconds::PositiveInfinity;
// Okay, request will be performed when connection is complete
fBusy = true;
iDataOffset = 0;
ResetRequestTimeout();
ResetError();
return true;
}
void C4Network2HTTPClient::ResetRequestTimeout()
{
// timeout C4Network2HTTPQueryTimeout seconds from this point
iRequestTimeout = time(nullptr) + C4Network2HTTPQueryTimeout;
}
void C4Network2HTTPClient::Cancel(const char *szReason)
{
// Close connection - and connection attempt
Close(ServerAddr); Close(ServerAddrFallback); Close(PeerAddr);
// Reset flags
fBusy = fSuccess = fConnected = fBinary = false;
iDownloadedSize = iTotalSize = iDataOffset = 0;
Error = szReason;
}
void C4Network2HTTPClient::Clear()
{
fBusy = fSuccess = fConnected = fBinary = false;
iDownloadedSize = iTotalSize = iDataOffset = 0;
ResultBin.Clear();
ResultString.Clear();
Error.Clear();
}
bool C4Network2HTTPClient::SetServer(const char *szServerAddress)
{
// Split address
const char *pRequestPath;
if ((pRequestPath = strchr(szServerAddress, '/')))
{
Server.CopyUntil(szServerAddress, '/');
RequestPath = pRequestPath;
}
else
{
Server = szServerAddress;
RequestPath = "/";
}
// Resolve address
ServerAddr.SetAddress(Server);
if (ServerAddr.IsNull())
{
SetError(FormatString("Could not resolve server address %s!", Server.getData()).getData());
return false;
}
ServerAddr.SetDefaultPort(GetDefaultPort());
if (ServerAddr.GetFamily() == C4NetIO::HostAddress::IPv6)
{
// Try to find a fallback IPv4 address for Happy Eyeballs.
ServerAddrFallback.SetAddress(Server, C4NetIO::HostAddress::IPv4);
ServerAddrFallback.SetDefaultPort(GetDefaultPort());
}
else
ServerAddrFallback.Clear();
// Remove port
const char *firstColon = strchr(Server.getData(), ':');
const char *lastColon = strrchr(Server.getData(), ':');
if (firstColon)
// hostname/IPv4 address or IPv6 address with port (e.g. [::1]:1234)
if (firstColon == lastColon || (Server[0] == '[' && *(lastColon - 1) == ']'))
Server.SetLength(lastColon - Server.getData());
// Done
ResetError();
return true;
}
// *** C4Network2UpdateClient
bool C4Network2UpdateClient::QueryUpdateURL()
{
// Perform an Query query
return Query(nullptr, false);
}
bool C4Network2UpdateClient::GetUpdateURL(StdStrBuf *pUpdateURL)
{
// Sanity check
if (isBusy() || !isSuccess()) return false;
// Parse response
try
{
CompileFromBuf<StdCompilerINIRead>(mkNamingAdapt(
mkNamingAdapt(mkParAdapt(*pUpdateURL, StdCompiler::RCT_All), "UpdateURL", ""),
C4ENGINENAME), ResultString);
}
catch (StdCompiler::Exception *pExc)
{
SetError(pExc->Msg.getData());
return false;
}
// done; version OK!
return true;
}
bool C4Network2UpdateClient::GetVersion(StdStrBuf *pVersion)
{
// Sanity check
if (isBusy() || !isSuccess()) return false;
// Parse response
try
{
CompileFromBuf<StdCompilerINIRead>(mkNamingAdapt(
mkNamingAdapt(mkParAdapt(*pVersion, StdCompiler::RCT_All), "Version", ""),
C4ENGINENAME), ResultString);
}
catch (StdCompiler::Exception *pExc)
{
SetError(pExc->Msg.getData());
return false;
}
// done; version OK!
return true;
}
// *** C4Network2RefClient
bool C4Network2RefClient::QueryReferences()
{
// Perform an Query query
return Query(nullptr, false);
}
bool C4Network2RefClient::GetReferences(C4Network2Reference **&rpReferences, int32_t &rRefCount)
{
// Sanity check
if (isBusy() || !isSuccess()) return false;
// local update test
try
{
// Create compiler
StdCompilerINIRead Comp;
Comp.setInput(ResultString);
Comp.Begin();
// Read reference count
Comp.Value(mkNamingCountAdapt(rRefCount, "Reference"));
// Create reference array and initialize
rpReferences = new C4Network2Reference *[rRefCount];
for (int i = 0; i < rRefCount; i++)
rpReferences[i] = nullptr;
// Get references
Comp.Value(mkNamingAdapt(mkArrayAdaptMap(rpReferences, rRefCount, mkPtrAdaptNoNull<C4Network2Reference>), "Reference"));
mkPtrAdaptNoNull<C4Network2Reference>(*rpReferences);
// Done
Comp.End();
}
catch (StdCompiler::Exception *pExc)
{
SetError(pExc->Msg.getData());
return false;
}
// Set source ip
for (int i = 0; i < rRefCount; i++)
rpReferences[i]->SetSourceAddress(getServerAddress());
// Done
ResetError();
return true;
}