1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2013-2017, The OpenClonk Team and contributors
6 * Copyright (c) 2017-2022, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18#include "C4Include.h"
19#include "C4Game.h"
20#include "C4Version.h"
21#include "C4Network2Reference.h"
22
23#include <format>
24#include <stdexcept>
25
26#include <fcntl.h>
27
28// *** C4Network2Reference
29
30C4Network2Reference::C4Network2Reference()
31 : Icon(0), Time(0), Frame(0), StartTime(0), LeaguePerformance(0),
32 JoinAllowed(true), PasswordNeeded(false), OfficialServer(false),
33 NetpuncherGameID{} {}
34
35C4Network2Reference::~C4Network2Reference() {}
36
37void C4Network2Reference::SetSourceAddress(const C4Network2EndpointAddress &ip)
38{
39 source = ip;
40 for (auto &addr : Addrs)
41 {
42 if (addr.GetAddr().IsNullHost())
43 {
44 addr.GetAddr().SetHost(ip);
45 }
46 }
47}
48
49void C4Network2Reference::InitLocal(C4Game *pGame)
50{
51 // Copy all game parameters
52 Parameters = pGame->Parameters;
53
54 // Discard player resources (we don't want these infos in the reference)
55 // Add league performance (but only after game end)
56 C4ClientPlayerInfos *pClientInfos; C4PlayerInfo *pPlayerInfo;
57 int32_t i, j;
58 for (i = 0; pClientInfos = Parameters.PlayerInfos.GetIndexedInfo(iIndex: i); i++)
59 for (j = 0; pPlayerInfo = pClientInfos->GetPlayerInfo(iIndex: j); j++)
60 {
61 pPlayerInfo->DiscardResource();
62 if (pGame->GameOver)
63 pPlayerInfo->SetLeaguePerformance(
64 pGame->RoundResults.GetLeaguePerformance(idPlayer: pPlayerInfo->GetID()));
65 }
66
67 // Special additional information in reference
68 Icon = pGame->C4S.Head.Icon;
69 GameStatus = pGame->Network.Status;
70 Time = pGame->Time;
71 Frame = pGame->FrameCounter;
72 StartTime = pGame->StartTime;
73 LeaguePerformance = pGame->RoundResults.GetLeaguePerformance();
74 Comment = Config.Network.Comment;
75 JoinAllowed = pGame->Network.isJoinAllowed();
76 PasswordNeeded = pGame->Network.isPassworded();
77 NetpuncherGameID = pGame->Network.getNetpuncherGameID();
78 NetpuncherAddr = pGame->Network.getNetpuncherAddr();
79 Game.Set();
80
81 // Addresses
82 for (const auto &addr : pGame->Clients.getLocal()->getNetClient()->getAddresses())
83 {
84 Addrs.emplace_back(args: addr.Addr);
85 }
86}
87
88void C4Network2Reference::CompileFunc(StdCompiler *pComp)
89{
90 pComp->Value(rStruct: mkNamingAdapt(rValue&: Icon, szName: "Icon", rDefault: 0));
91 pComp->Value(rStruct: mkParAdapt(rObj&: GameStatus, rPar: true));
92 pComp->Value(rStruct: mkNamingAdapt(rValue&: Time, szName: "Time", rDefault: 0));
93 pComp->Value(rStruct: mkNamingAdapt(rValue&: Frame, szName: "Frame", rDefault: 0));
94 pComp->Value(rStruct: mkNamingAdapt(rValue&: StartTime, szName: "StartTime", rDefault: 0));
95 pComp->Value(rStruct: mkNamingAdapt(rValue&: LeaguePerformance, szName: "LeaguePerformance", rDefault: 0));
96 pComp->Value(rStruct: mkNamingAdapt(rValue&: Comment, szName: "Comment", rDefault: ""));
97 pComp->Value(rStruct: mkNamingAdapt(rValue&: JoinAllowed, szName: "JoinAllowed", rDefault: true));
98 pComp->Value(rStruct: mkNamingAdapt(rValue&: PasswordNeeded, szName: "PasswordNeeded", rDefault: false));
99 pComp->Value(rStruct: mkNamingAdapt(rValue: mkSTLContainerAdapt(rTarget&: Addrs), szName: "Address"));
100 pComp->Value(rStruct: mkNamingAdapt(rValue&: Game.sEngineName, szName: "Game", rDefault: "None"));
101 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdapt(array&: Game.iVer, default_: 0), szName: "Version"));
102 pComp->Value(rStruct: mkNamingAdapt(rValue&: Game.iBuild, szName: "Build", rDefault: -1));
103 pComp->Value(rStruct: mkNamingAdapt(rValue&: OfficialServer, szName: "OfficialServer", rDefault: false));
104
105 pComp->Value(rStruct&: Parameters);
106
107 pComp->Value(rStruct: mkNamingAdapt(rValue&: NetpuncherGameID, szName: "NetpuncherID", rDefault: C4NetpuncherID(), fPrefillDefault: false, fStoreDefault: false));
108 pComp->Value(rStruct: mkNamingAdapt(rValue&: NetpuncherAddr, szName: "NetpuncherAddr", rDefault: "", fPrefillDefault: false, fStoreDefault: false));
109}
110
111int32_t C4Network2Reference::getSortOrder() const // Don't go over 100, because that's for the masterserver...
112{
113 C4GameVersion verThis;
114 int iOrder = 0;
115 // Official server
116 if (isOfficialServer() && !Config.Network.UseAlternateServer) iOrder += 50;
117 // Joinable
118 if (isJoinAllowed() && (getGameVersion() == verThis)) iOrder += 25;
119 // League game
120 if (Parameters.isLeague()) iOrder += 5;
121 // In lobby
122 if (getGameStatus().isLobbyActive()) iOrder += 3;
123 // No password needed
124 if (!isPasswordNeeded()) iOrder += 1;
125 // Done
126 return iOrder;
127}
128
129// *** C4Network2RefServer
130
131C4Network2RefServer::C4Network2RefServer()
132 : pReference(nullptr) {}
133
134C4Network2RefServer::~C4Network2RefServer()
135{
136 Clear();
137}
138
139void C4Network2RefServer::Clear()
140{
141 C4NetIOTCP::Close();
142 delete pReference; pReference = nullptr;
143}
144
145void C4Network2RefServer::SetReference(C4Network2Reference *pNewReference)
146{
147 CStdLock RefLock(&RefCSec);
148 delete pReference; pReference = pNewReference;
149}
150
151void C4Network2RefServer::PackPacket(const C4NetIOPacket &rPacket, StdBuf &rOutBuf)
152{
153 // Just append the packet
154 rOutBuf.Append(Buf2: rPacket);
155}
156
157size_t C4Network2RefServer::UnpackPacket(const StdBuf &rInBuf, const C4NetIO::addr_t &addr)
158{
159 const char *pData = rInBuf.getPtr<char>();
160 // Check for complete header
161 const char *pHeaderEnd = strstr(haystack: pData, needle: "\r\n\r\n");
162 if (!pHeaderEnd)
163 return 0;
164 // Check method (only GET is allowed for now)
165 if (!SEqual2(szStr1: pData, szStr2: "GET "))
166 {
167 RespondMethodNotAllowed(addr);
168 return rInBuf.getSize();
169 }
170 // Check target
171 // TODO
172 RespondReference(addr);
173 return rInBuf.getSize();
174}
175
176void C4Network2RefServer::RespondMethodNotAllowed(const C4NetIO::addr_t &addr)
177{
178 // Send the message
179 const std::string data{"HTTP/1.0 405 Method Not Allowed\r\n\r\n"};
180 Send(rPacket: C4NetIOPacket(data.c_str(), data.size(), false, addr));
181 // Close the connection
182 Close(addr);
183}
184
185void C4Network2RefServer::RespondReference(const C4NetIO::addr_t &addr)
186{
187 CStdLock RefLock(&RefCSec);
188 // Pack
189 const std::string packetData{DecompileToBuf<StdCompilerINIWrite>(SrcStruct: mkNamingPtrAdapt(rpObj&: pReference, szNaming: "Reference"))};
190 // Create header
191 const char *const charset{C4Config::GetCharsetCodeName(charset: Config.General.LanguageCharset)};
192
193 const std::string header{std::format(
194 fmt: "HTTP/1.0 200 OK\r\n"
195 "Content-Length: {}\r\n"
196 "Content-Type: text/plain; charset={}\r\n"
197 "Server: " C4ENGINENAME "/" C4VERSION "\r\n"
198 "\r\n",
199 args: packetData.size(),
200 args: charset)};
201 // Send back
202 Send(rPacket: C4NetIOPacket(header.c_str(), header.size(), false, addr));
203 Send(rPacket: C4NetIOPacket(packetData.c_str(), packetData.size(), false, addr));
204 // Close the connection
205 Close(addr);
206}
207
208// *** C4Network2HTTPClient
209
210class C4Network2HTTPClient::Impl
211{
212public:
213 virtual ~Impl() = default;
214
215public:
216 virtual bool Init() = 0;
217 virtual bool Query(const StdBuf &Data, bool binary, C4HTTPClient::Headers headers) = 0;
218
219 virtual bool isBusy() const = 0;
220 virtual bool isSuccess() const = 0;
221 virtual bool isConnected() const = 0;
222 virtual std::size_t getTotalSize() const = 0;
223 virtual std::size_t getDownloadedSize() const = 0;
224 virtual const StdBuf &getResultBin() const = 0;
225 virtual const StdStrBuf &getResultString() const = 0;
226 virtual const char *getURL() const = 0;
227 virtual const char *getServerName() const = 0;
228 virtual const C4NetIO::addr_t &getServerAddress() const = 0;
229 virtual const char *GetError() const = 0;
230 virtual void ResetError() = 0;
231 virtual bool Cancel(std::string_view reason) = 0;
232 virtual void Clear() = 0;
233
234 virtual bool SetServer(std::string_view serverAddress, std::uint16_t defaultPort) = 0;
235 virtual void SetNotify(C4InteractiveThread *thread) = 0;
236
237 virtual bool Execute(int maxTime) = 0;
238 virtual int GetTimeout() { return StdSync::Infinite; }
239
240#ifdef _WIN32
241 virtual HANDLE GetEvent() = 0;
242#else
243 virtual void GetFDs(std::vector<pollfd> &fds) = 0;
244#endif
245
246 virtual void SetError(std::string_view error) = 0;
247};
248
249class C4Network2HTTPClientImplCurl : public C4Network2HTTPClient::Impl
250{
251public:
252 C4Network2HTTPClientImplCurl(std::shared_ptr<spdlog::logger> logger) : logger{std::move(logger)} {}
253 ~C4Network2HTTPClientImplCurl() override = default;
254
255public:
256 bool Init() override;
257 bool Query(const StdBuf &Data, bool binary, C4HTTPClient::Headers headers = {}) override;
258
259 bool isBusy() const override { return busy.load(m: std::memory_order_acquire); }
260 bool isSuccess() const override { return success.load(m: std::memory_order_acquire); }
261 bool isConnected() const override { return downloadedSize.load(m: std::memory_order_acquire) + totalSize.load(m: std::memory_order_acquire) != 0; }
262 std::size_t getTotalSize() const override { return totalSize.load(m: std::memory_order_acquire); }
263 std::size_t getDownloadedSize() const override { return downloadedSize.load(m: std::memory_order_acquire); }
264 const StdBuf &getResultBin() const override { assert(binary); return resultBin; }
265 const StdStrBuf &getResultString() const override { assert(!binary); return resultString; }
266 const char *getURL() const override { return url.c_str(); }
267 const char *getServerName() const override { return serverName.c_str(); }
268 const C4NetIO::addr_t &getServerAddress() const override { return serverAddress; }
269 virtual const char *GetError() const override { return !error.empty() && *error.c_str() ? error.c_str() : nullptr; }
270 void ResetError() override { error.clear(); }
271 bool Cancel(std::string_view reason) override;
272 void Clear() override;
273
274 bool SetServer(std::string_view serverAddress, std::uint16_t defaultPort) override;
275 void SetNotify(class C4InteractiveThread *thread) override { this->thread.store(p: thread, m: std::memory_order_release); }
276
277 bool Execute(int iMaxTime = StdSync::Infinite) override { return true; }
278
279#ifdef _WIN32
280 HANDLE GetEvent() override { return nullptr; }
281#else
282 void GetFDs(std::vector<pollfd> &fds) override {}
283#endif
284
285protected:
286 void SetError(const std::string_view error) override { this->error = error; }
287
288private:
289 C4Task::Hot<void> QueryAsync(C4Task::Hot<C4HTTPClient::Result> &&queryTask);
290
291protected:
292 StdBuf resultBin;
293 StdStrBuf resultString;
294
295private:
296 std::shared_ptr<spdlog::logger> logger;
297 std::optional<C4HTTPClient> client;
298 C4Task::Hot<void> task;
299 StdBuf data;
300
301private:
302 std::string url;
303 std::string serverName;
304 std::atomic_bool binary{false};
305
306 std::atomic_bool busy{false};
307 std::atomic_bool success{false};
308
309 std::string error;
310 C4NetIO::addr_t serverAddress;
311
312 std::atomic_size_t totalSize;
313 std::atomic_size_t downloadedSize;
314
315 std::atomic<class C4InteractiveThread *> thread{nullptr};
316};
317
318// mini HTTP client
319class C4Network2HTTPClientImplNetIO : public C4Network2HTTPClient::Impl, public C4NetIOTCP, private C4NetIO::CBClass
320{
321public:
322 C4Network2HTTPClientImplNetIO(std::shared_ptr<spdlog::logger> logger);
323 virtual ~C4Network2HTTPClientImplNetIO() override;
324
325private:
326 std::shared_ptr<spdlog::logger> logger;
327 // Address information
328 C4NetIO::addr_t ServerAddr, ServerAddrFallback, PeerAddr;
329 StdStrBuf Server, RequestPath;
330
331 bool fBinary;
332 bool fBusy, fSuccess, fConnected;
333 size_t iDataOffset;
334 StdBuf Request;
335 time_t iRequestTimeout;
336 std::chrono::time_point<std::chrono::steady_clock> HappyEyeballsTimeout;
337
338 // Response header data
339 size_t iDownloadedSize, iTotalSize;
340 bool fCompressed;
341
342 // Event queue to use for notify when something happens
343 class C4InteractiveThread *pNotify;
344
345protected:
346 StdBuf ResultBin; // set if fBinary
347 StdStrBuf ResultString; // set if !fBinary
348
349protected:
350 // Overridden
351 virtual void PackPacket(const C4NetIOPacket &rPacket, StdBuf &rOutBuf) override;
352 virtual size_t UnpackPacket(const StdBuf &rInBuf, const C4NetIO::addr_t &addr) override;
353
354 // Callbacks
355 bool OnConn(const C4NetIO::addr_t &AddrPeer, const C4NetIO::addr_t &AddrConnect, const addr_t *pOwnAddr, C4NetIO *pNetIO) override;
356 void OnDisconn(const C4NetIO::addr_t &AddrPeer, C4NetIO *pNetIO, const char *szReason) override;
357 void OnPacket(const class C4NetIOPacket &rPacket, C4NetIO *pNetIO) override;
358
359 void ResetRequestTimeout();
360 virtual int32_t GetDefaultPort() { return 80; }
361
362public:
363 bool Init() override { return C4NetIOTCP::Init(); }
364 bool Init(const std::uint16_t port) override { return C4NetIOTCP::Init(iPort: port); }
365 bool Query(const StdBuf &Data, bool fBinary, C4HTTPClient::Headers headers) override;
366
367 bool isBusy() const override { return fBusy; }
368 bool isSuccess() const override { return fSuccess; }
369 bool isConnected() const override { return fConnected; }
370 size_t getTotalSize() const override { return iTotalSize; }
371 size_t getDownloadedSize() const override { return iDownloadedSize; }
372 const StdBuf &getResultBin() const override { assert(fBinary); return ResultBin; }
373 const StdStrBuf &getResultString() const override { assert(!fBinary); return ResultString; }
374 const char *getServerName() const override { return Server.getData(); }
375 const char *getURL() const override { return ""; }
376 const C4NetIO::addr_t &getServerAddress() const override { return ServerAddr; }
377 const char *GetError() const override { return C4NetIOTCP::GetError(); }
378 void ResetError() override { return C4NetIOTCP::ResetError(); }
379 bool Cancel(std::string_view reason) override;
380 void Clear() override;
381
382 bool SetServer(std::string_view serverAddress, std::uint16_t defaultPort) override;
383
384 void SetNotify(class C4InteractiveThread *pnNotify) override { pNotify = pnNotify; }
385
386 // Overridden
387 bool Execute(int iMaxTime = StdSync::Infinite) override;
388 int GetTimeout() override;
389
390#ifdef _WIN32
391 HANDLE GetEvent() override { return C4NetIOTCP::GetEvent(); }
392#else
393 void GetFDs(std::vector<pollfd> &fds) { C4NetIOTCP::GetFDs(fds); }
394#endif
395
396protected:
397 void SetError(const std::string_view error) override { C4NetIO::SetError(strnError: error.data()); }
398
399private:
400 bool ReadHeader(const StdStrBuf &Data);
401 bool Decompress(StdBuf *pData);
402
403private:
404 static constexpr std::chrono::milliseconds C4Network2HTTPHappyEyeballsTimeout{300};
405 static constexpr int C4Network2HTTPQueryTimeout{20}; // (s)
406};
407
408#define IMPL(type) static_cast<std::unique_ptr<C4Network2HTTPClient::Impl>>(std::make_unique<type>(Application.LogSystem.CreateLoggerWithDifferentName(Config.Logging.Network2HTTPClient, #type)))
409
410C4Network2HTTPClient::C4Network2HTTPClient()
411 : impl{Config.Network.UseCurl
412 ? IMPL(C4Network2HTTPClientImplCurl)
413 : IMPL(C4Network2HTTPClientImplNetIO)
414 }
415{
416}
417
418#undef IMPL
419
420C4Network2HTTPClient::~C4Network2HTTPClient()
421{
422 Cancel(reason: {});
423}
424
425bool C4Network2HTTPClient::Init() { return impl->Init(); }
426bool C4Network2HTTPClient::Query(const StdBuf &Data, const bool binary, C4HTTPClient::Headers headers) { return impl->Query(Data, binary, headers: std::move(headers)); }
427
428bool C4Network2HTTPClient::isBusy() const { return impl->isBusy(); }
429bool C4Network2HTTPClient::isSuccess() const { return impl->isSuccess(); }
430bool C4Network2HTTPClient::isConnected() const { return impl->isConnected(); }
431std::size_t C4Network2HTTPClient::getTotalSize() const { return impl->getTotalSize(); }
432std::size_t C4Network2HTTPClient::getDownloadedSize() const { return impl->getDownloadedSize(); }
433const StdBuf &C4Network2HTTPClient::getResultBin() const { return impl->getResultBin(); }
434const StdStrBuf &C4Network2HTTPClient::getResultString() const { return impl->getResultString(); }
435const char *C4Network2HTTPClient::getURL() const { return impl->getURL(); }
436const char *C4Network2HTTPClient::getServerName() const { return impl->getServerName(); }
437const C4NetIO::addr_t &C4Network2HTTPClient::getServerAddress() const { return impl->getServerAddress(); }
438const char *C4Network2HTTPClient::GetError() const { return impl->GetError(); }
439void C4Network2HTTPClient::ResetError() { impl->ResetError(); }
440bool C4Network2HTTPClient::Cancel(const std::string_view reason) { return impl->Cancel(reason); }
441void C4Network2HTTPClient::Clear() { impl->Clear(); }
442
443bool C4Network2HTTPClient::SetServer(const std::string_view serverAddress, const std::uint16_t defaultPort) { return impl->SetServer(serverAddress, defaultPort); }
444void C4Network2HTTPClient::SetNotify(C4InteractiveThread *const thread) { impl->SetNotify(thread); }
445
446bool C4Network2HTTPClient::Execute(const int maxTime) { return impl->Execute(maxTime); }
447int C4Network2HTTPClient::GetTimeout() { return impl->GetTimeout(); }
448
449#ifdef _WIN32
450HANDLE C4Network2HTTPClient::GetEvent() { return impl->GetEvent(); }
451#else
452void C4Network2HTTPClient::GetFDs(std::vector<pollfd> &fds) { impl->GetFDs(fds); }
453#endif
454
455void C4Network2HTTPClient::SetError(const char *const error) { impl->SetError(error); }
456
457bool C4Network2HTTPClientImplCurl::Init() try
458{
459 client.emplace();
460 return true;
461}
462catch (const std::runtime_error &e)
463{
464 SetError(e.what());
465 return false;
466}
467
468bool C4Network2HTTPClientImplCurl::Query(const StdBuf &Data, const bool binary, C4HTTPClient::Headers headers)
469{
470 if (!Cancel(reason: {}))
471 {
472 return false;
473 }
474
475 data = Data;
476 this->binary.store(i: binary, m: std::memory_order_release);
477 success.store(i: false, m: std::memory_order_release);
478
479 downloadedSize.store(i: 0, m: std::memory_order_release);
480 totalSize.store(i: 0, m: std::memory_order_release);
481
482 try
483 {
484 C4HTTPClient::Request request{.Uri: {url}, .Binary: binary, .Data: {reinterpret_cast<const std::byte *>(this->data.getData()), this->data.getSize()}};
485
486 const auto progressCallback = [this](const std::size_t totalSize, const std::size_t downloadedSize)
487 {
488 this->totalSize.store(i: totalSize, m: std::memory_order_release);
489 this->downloadedSize.store(i: downloadedSize, m: std::memory_order_release);
490 return true;
491 };
492
493 task = QueryAsync(queryTask: this->data.isNull() ? client->GetAsync(request: std::move(request), progressCallback, headers: std::move(headers)) : client->PostAsync(request: std::move(request), progressCallback, headers: std::move(headers)));
494 return true;
495 }
496 catch (const std::runtime_error &e)
497 {
498 SetError(e.what());
499 return false;
500 }
501}
502
503bool C4Network2HTTPClientImplCurl::Cancel(const std::string_view reason)
504{
505 if (task)
506 {
507 try
508 {
509 std::move(task).CancelAndWait();
510 }
511 catch (const std::runtime_error &)
512 {
513 SetError("Could not cancel");
514 return false;
515 }
516 }
517
518 SetError(reason);
519 return true;
520}
521
522void C4Network2HTTPClientImplCurl::Clear()
523{
524 Cancel(reason: {});
525 resultBin.Clear();
526 resultString.Clear();
527 ResetError();
528 serverAddress.Clear();
529 url.clear();
530}
531
532bool C4Network2HTTPClientImplCurl::SetServer(const std::string_view serverAddress, const std::uint16_t defaultPort) try
533{
534 const auto uri = C4HTTPClient::Uri::ParseOldStyle(serverAddress: std::string{serverAddress}, port: defaultPort);
535 url = uri.GetPart(part: C4HTTPClient::Uri::Part::Uri);
536 serverName = uri.GetPart(part: C4HTTPClient::Uri::Part::Host);
537 return true;
538}
539catch (const std::runtime_error &e)
540{
541 SetError(e.what());
542 return false;
543}
544
545C4Task::Hot<void> C4Network2HTTPClientImplCurl::QueryAsync(C4Task::Hot<C4HTTPClient::Result> &&queryTask)
546{
547 busy.store(i: true, m: std::memory_order_release);
548
549 {
550 const struct Cleanup
551 {
552 ~Cleanup()
553 {
554 busy.store(i: false, m: std::memory_order_release);
555 }
556
557 std::atomic_bool &busy;
558 } cleanup{.busy: busy};
559
560 try
561 {
562 auto result = co_await std::move(queryTask);
563
564 if (binary)
565 {
566 resultBin = std::move(result.Buffer);
567 }
568 else
569 {
570 resultString.Copy(pnData: reinterpret_cast<const char *>(result.Buffer.getData()), iChars: result.Buffer.getSize());
571 }
572
573 success.store(i: true, m: std::memory_order_release);
574 serverAddress = result.ServerAddress;
575 }
576 catch (const std::runtime_error &e)
577 {
578 SetError(e.what());
579 success.store(i: false, m: std::memory_order_release);
580 }
581 }
582
583 if (auto *const thread = this->thread.load(m: std::memory_order_acquire); thread)
584 {
585 thread->PushEvent(eEventType: Ev_HTTP_Response, data: this);
586 }
587}
588
589C4Network2HTTPClientImplNetIO::C4Network2HTTPClientImplNetIO(std::shared_ptr<spdlog::logger> logger)
590 : logger{std::move(logger)}, fBusy(false), fSuccess(false), fConnected(false), iDownloadedSize(0), iTotalSize(0), fBinary(false), iDataOffset(0),
591 pNotify(nullptr)
592{
593 C4NetIOTCP::SetCallback(this);
594}
595
596C4Network2HTTPClientImplNetIO::~C4Network2HTTPClientImplNetIO() {}
597
598void C4Network2HTTPClientImplNetIO::PackPacket(const C4NetIOPacket &rPacket, StdBuf &rOutBuf)
599{
600 // Just append the packet
601 rOutBuf.Append(Buf2: rPacket);
602}
603
604size_t C4Network2HTTPClientImplNetIO::UnpackPacket(const StdBuf &rInBuf, const C4NetIO::addr_t &addr)
605{
606 // since new data arrived, increase timeout time
607 ResetRequestTimeout();
608 // Check for complete header
609 if (!iDataOffset)
610 {
611 // Copy data into string buffer (terminate)
612 StdStrBuf Data; Data.Copy(pnData: rInBuf.getPtr<char>(), iChars: rInBuf.getSize());
613 const char *pData = Data.getData();
614 // Header complete?
615 const char *pContent = SSearch(szString: pData, szIndex: "\r\n\r\n");
616 if (!pContent)
617 return 0;
618 // Read the header
619 if (!ReadHeader(Data))
620 {
621 fBusy = fSuccess = false;
622 Close(addr);
623 return rInBuf.getSize();
624 }
625 }
626 iDownloadedSize = rInBuf.getSize() - iDataOffset;
627 // Check if the packet is complete
628 if (iTotalSize > iDownloadedSize)
629 {
630 return 0;
631 }
632 // Get data, uncompress it if needed
633 StdBuf Data = rInBuf.getPart(iStart: iDataOffset, inSize: iTotalSize);
634 if (fCompressed)
635 if (!Decompress(pData: &Data))
636 {
637 fBusy = fSuccess = false;
638 Close(addr);
639 return rInBuf.getSize();
640 }
641 // Save the result
642 if (fBinary)
643 ResultBin.Copy(Buf2: Data);
644 else
645 ResultString.Copy(pnData: Data.getPtr<char>(), iChars: Data.getSize());
646 fBusy = false; fSuccess = true;
647 // Callback
648 OnPacket(rPacket: C4NetIOPacket(Data, addr), pNetIO: this);
649 // Done
650 Close(addr);
651 return rInBuf.getSize();
652}
653
654bool C4Network2HTTPClientImplNetIO::ReadHeader(const StdStrBuf &Data)
655{
656 const char *pData = Data.getData();
657 const char *pContent = SSearch(szString: pData, szIndex: "\r\n\r\n");
658 if (!pContent)
659 return 0;
660 // Parse header line
661 int iHTTPVer1, iHTTPVer2, iResponseCode, iStatusStringPtr;
662 if (sscanf(s: pData, format: "HTTP/%d.%d %d %n", &iHTTPVer1, &iHTTPVer2, &iResponseCode, &iStatusStringPtr) != 3)
663 {
664 Error = "Invalid status line!";
665 return false;
666 }
667 // Check HTTP version
668 if (iHTTPVer1 != 1)
669 {
670 Error.Copy(pnData: std::format(fmt: "Unsupported HTTP version: {}.{}!", args&: iHTTPVer1, args&: iHTTPVer2).c_str());
671 return false;
672 }
673 // Check code
674 if (iResponseCode != 200)
675 {
676 // Get status string
677 StdStrBuf StatusString; StatusString.CopyUntil(szString: pData + iStatusStringPtr, cUntil: '\r');
678 // Create error message
679 Error.Copy(pnData: std::format(fmt: "HTTP server responded {}: {}", args&: iResponseCode, args: StatusString.getData()).c_str());
680 return false;
681 }
682 // Get content length
683 const char *pContentLength = SSearch(szString: pData, szIndex: "\r\nContent-Length:");
684 int iContentLength;
685 if (!pContentLength || pContentLength > pContent ||
686 sscanf(s: pContentLength, format: "%d", &iContentLength) != 1)
687 {
688 Error.Copy(pnData: "Invalid server response: Content-Length is missing!");
689 return false;
690 }
691 iTotalSize = iContentLength;
692 iDataOffset = (pContent - pData);
693 // Get content encoding
694 const char *pContentEncoding = SSearch(szString: pData, szIndex: "\r\nContent-Encoding:");
695 if (pContentEncoding)
696 {
697 while (*pContentEncoding == ' ') pContentEncoding++;
698 StdStrBuf Encoding; Encoding.CopyUntil(szString: pContentEncoding, cUntil: '\r');
699 if (Encoding == "gzip")
700 fCompressed = true;
701 else
702 fCompressed = false;
703 }
704 else
705 fCompressed = false;
706 // Okay
707 return true;
708}
709
710bool C4Network2HTTPClientImplNetIO::Decompress(StdBuf *pData)
711{
712 size_t iSize = pData->getSize();
713 // Create buffer
714 uint32_t iOutSize = *pData->getPtr<uint32_t>(pos: pData->getSize() - sizeof(uint32_t));
715 iOutSize = std::min<uint32_t>(a: iOutSize, b: iSize * 1000);
716 StdBuf Out; Out.New(inSize: iOutSize);
717 // Prepare stream
718 z_stream zstrm{};
719 zstrm.next_in = const_cast<Byte *>(pData->getPtr<Byte>());
720 zstrm.avail_in = pData->getSize();
721 zstrm.next_out = Out.getMPtr<Byte>();
722 zstrm.avail_out = Out.getSize();
723 // Inflate...
724 if (inflateInit2(&zstrm, 15 + 16) != Z_OK)
725 {
726 Error.Copy(pnData: "Could not decompress data!");
727 return false;
728 }
729 // Inflate!
730 if (inflate(strm: &zstrm, Z_FINISH) != Z_STREAM_END)
731 {
732 inflateEnd(strm: &zstrm);
733 Error.Copy(pnData: "Could not decompress data!");
734 return false;
735 }
736 // Return the buffer
737 Out.SetSize(zstrm.total_out);
738 pData->Take(Buf2&: Out);
739 // Okay
740 inflateEnd(strm: &zstrm);
741 return true;
742}
743
744bool C4Network2HTTPClientImplNetIO::OnConn(const C4NetIO::addr_t &AddrPeer, const C4NetIO::addr_t &AddrConnect, const C4NetIO::addr_t *pOwnAddr, C4NetIO *pNetIO)
745{
746 // Make sure we're actually waiting for this connection
747 if (fConnected || (AddrConnect != ServerAddr && AddrConnect != ServerAddrFallback))
748 return false;
749 // Save pack peer address
750 PeerAddr = AddrPeer;
751 // Send the request
752 if (!Send(rPacket: C4NetIOPacket(Request, AddrPeer)))
753 {
754 Error.Copy(pnData: std::format(fmt: "Unable to send HTTP request: {}", args: Error.getData()).c_str());
755 }
756 Request.Clear();
757 fConnected = true;
758 return true;
759}
760
761void C4Network2HTTPClientImplNetIO::OnDisconn(const C4NetIO::addr_t &AddrPeer, C4NetIO *pNetIO, const char *szReason)
762{
763 // Got no complete packet? Failure...
764 if (!fSuccess && Error.isNull())
765 {
766 fBusy = false;
767 Error.Copy(pnData: "Unexpected disconnect: ");
768 Error.Append(pnData: szReason);
769 }
770 fConnected = false;
771 // Notify
772 if (pNotify)
773 pNotify->PushEvent(eEventType: Ev_HTTP_Response, data: this);
774}
775
776void C4Network2HTTPClientImplNetIO::OnPacket(const class C4NetIOPacket &rPacket, C4NetIO *pNetIO)
777{
778 // Everything worthwhile was already done in UnpackPacket. Only do notify callback
779 if (pNotify)
780 pNotify->PushEvent(eEventType: Ev_HTTP_Response, data: this);
781}
782
783bool C4Network2HTTPClientImplNetIO::Execute(int iMaxTime)
784{
785 // Check timeout
786 if (fBusy)
787 {
788 if (std::chrono::steady_clock::now() > HappyEyeballsTimeout)
789 {
790 HappyEyeballsTimeout = decltype(HappyEyeballsTimeout)::max();
791 logger->info(fmt: "Starting fallback connection to {} ({})", args: Server.getData(), args: ServerAddrFallback.ToString());
792 Connect(addr: ServerAddrFallback);
793 }
794
795 if (time(timer: nullptr) > iRequestTimeout)
796 {
797 Cancel(reason: "Request timeout");
798 return true;
799 }
800 }
801 // Execute normally
802 return C4NetIOTCP::Execute(iMaxTime);
803}
804
805int C4Network2HTTPClientImplNetIO::GetTimeout()
806{
807 if (!fBusy)
808 return C4NetIOTCP::GetTimeout();
809 return MaxTimeout(iTimeout1: C4NetIOTCP::GetTimeout(), iTimeout2: static_cast<int>(1000 * std::max<time_t>(a: time(timer: nullptr) - iRequestTimeout, b: 0)));
810}
811
812bool C4Network2HTTPClientImplNetIO::Query(const StdBuf &Data, bool fBinary, C4HTTPClient::Headers headers)
813{
814 if (Server.isNull()) return false;
815 // Cancel previous request
816 if (fBusy)
817 Cancel(reason: "Cancelled");
818 // No result known yet
819 ResultString.Clear();
820 // store mode
821 this->fBinary = fBinary;
822 // Create request
823 const char *szCharset = C4Config::GetCharsetCodeName(charset: Config.General.LanguageCharset);
824 std::string header;
825 if (Data.getSize())
826 header = std::format(
827 fmt: "POST {} HTTP/1.0\r\n"
828 "Host: {}\r\n"
829 "Connection: Close\r\n"
830 "Content-Length: {}\r\n"
831 "Content-Type: text/plain; encoding={}\r\n"
832 "Accept-Charset: {}\r\n"
833 "Accept-Encoding: gzip\r\n"
834 "Accept-Language: {}\r\n"
835 "User-Agent: " C4ENGINENAME "/" C4VERSION "\r\n"
836 "\r\n",
837 args: RequestPath.getData(),
838 args: Server.getData(),
839 args: Data.getSize(),
840 args&: szCharset,
841 args&: szCharset,
842 args: +Config.General.LanguageEx);
843 else
844 header = std::format(
845 fmt: "GET {} HTTP/1.0\r\n"
846 "Host: {}\r\n"
847 "Connection: Close\r\n"
848 "Accept-Charset: {}\r\n"
849 "Accept-Encoding: gzip\r\n"
850 "Accept-Language: {}\r\n"
851 "User-Agent: " C4ENGINENAME "/" C4VERSION "\r\n"
852 "\r\n",
853 args: RequestPath.getData(),
854 args: Server.getData(),
855 args&: szCharset,
856 args: +Config.General.LanguageEx);
857
858 for (const auto &[key, value] : headers)
859 {
860 header += std::format(fmt: "{}: {}\r\n", args: key, args: value);
861 }
862
863 // Compose query
864 Request.Copy(pnData: header.c_str(), inSize: header.size());
865 Request.Append(Buf2: Data);
866
867 bool enableFallback{!ServerAddrFallback.IsNull()};
868 // Start connecting
869 if (!Connect(addr: ServerAddr))
870 {
871 if (!enableFallback)
872 {
873 return false;
874 }
875
876 std::swap(a&: ServerAddr, b&: ServerAddrFallback);
877 enableFallback = false;
878 if (!Connect(addr: ServerAddr))
879 {
880 return false;
881 }
882 }
883 if (enableFallback)
884 {
885 HappyEyeballsTimeout = std::chrono::steady_clock::now() + C4Network2HTTPHappyEyeballsTimeout;
886 }
887 else
888 {
889 HappyEyeballsTimeout = decltype(HappyEyeballsTimeout)::max();
890 }
891
892 // Okay, request will be performed when connection is complete
893 fBusy = true;
894 iDataOffset = 0;
895 ResetRequestTimeout();
896 ResetError();
897 return true;
898}
899
900void C4Network2HTTPClientImplNetIO::ResetRequestTimeout()
901{
902 // timeout C4Network2HTTPQueryTimeout seconds from this point
903 iRequestTimeout = time(timer: nullptr) + C4Network2HTTPQueryTimeout;
904}
905
906bool C4Network2HTTPClientImplNetIO::Cancel(const std::string_view reason)
907{
908 // Close connection - and connection attempt
909 Close(addr: ServerAddr);
910 Close(addr: ServerAddrFallback);
911 Close(addr: PeerAddr);
912
913 // Reset flags
914 fBusy = fSuccess = fConnected = fBinary = false;
915 iDownloadedSize = iTotalSize = iDataOffset = 0;
916 Error.Copy(pnData: reason.data(), iChars: reason.size());
917 return true;
918}
919
920void C4Network2HTTPClientImplNetIO::Clear()
921{
922 fBusy = fSuccess = fConnected = fBinary = false;
923 iDownloadedSize = iTotalSize = iDataOffset = 0;
924 ResultBin.Clear();
925 ResultString.Clear();
926 Error.Clear();
927}
928
929bool C4Network2HTTPClientImplNetIO::SetServer(const std::string_view serverAddress, std::uint16_t defaultPort)
930{
931 if (defaultPort == 0)
932 {
933 defaultPort = GetDefaultPort();
934 }
935
936 try
937 {
938 const auto uri = C4HTTPClient::Uri::ParseOldStyle(serverAddress: std::string{serverAddress}, port: defaultPort);
939
940 Server = std::format(fmt: "{}:{}", args: static_cast<std::string>(uri.GetPart(part: C4HTTPClient::Uri::Part::Host)), args: static_cast<std::string>(uri.GetPart(part: C4HTTPClient::Uri::Part::Port))).c_str();
941 RequestPath = uri.GetPart(part: C4HTTPClient::Uri::Part::Path);
942 }
943 catch (const std::runtime_error &e)
944 {
945 SetError(e.what());
946 return false;
947 }
948
949 // Resolve address
950 ServerAddr.SetAddress(addr: Server);
951 if (ServerAddr.IsNull())
952 {
953 SetError(std::format(fmt: "Could not resolve server address {}!", args: Server.getData()).c_str());
954 return false;
955 }
956 ServerAddr.SetDefaultPort(defaultPort);
957
958 if (ServerAddr.GetFamily() == C4Network2HostAddress::IPv6)
959 {
960 // Try to find a fallback IPv4 address for Happy Eyeballs.
961 ServerAddrFallback.SetAddress(addr: Server, family: C4Network2HostAddress::IPv4);
962 ServerAddrFallback.SetDefaultPort(defaultPort);
963 }
964 else
965 {
966 ServerAddrFallback.Clear();
967 }
968
969 // Remove port
970 const auto &firstColon = std::strchr(s: Server.getData(), c: ':');
971 const auto &lastColon = std::strrchr(s: Server.getData(), c: ':');
972 if (firstColon)
973 {
974 // Hostname/IPv4 address or IPv6 address with port (e.g. [::1]:1234)
975 if (firstColon == lastColon || (Server[0] == '[' && lastColon[-1] == ']'))
976 Server.SetLength(lastColon - Server.getData());
977 }
978
979 // Done
980 ResetError();
981 return true;
982}
983
984// *** C4Network2RefClient
985
986bool C4Network2RefClient::QueryReferences()
987{
988 // invalidate version
989 fVerSet = false;
990 // Perform an Query query
991 return Query(Data: StdBuf{}, binary: false);
992}
993
994bool C4Network2RefClient::GetReferences(C4Network2Reference ** &rpReferences, int32_t &rRefCount)
995{
996 // Sanity check
997 if (isBusy() || !isSuccess()) return false;
998 // Parse response
999 MasterVersion.Set(szEngine: "", iVer1: 0, iVer2: 0, iVer3: 0, iVer4: 0, iVerBuild: 0);
1000 fVerSet = false;
1001 // local update test
1002 try
1003 {
1004 // Create compiler
1005 StdCompilerINIRead Comp;
1006 Comp.setInput(getResultString());
1007 Comp.Begin();
1008 // get current version
1009 Comp.Value(rStruct: mkNamingAdapt(rValue: mkInsertAdapt(rObj: mkInsertAdapt(rObj: mkInsertAdapt(
1010 rObj: mkNamingAdapt(rValue: mkParAdapt(rObj&: MasterVersion, rPar: false), szName: "Version"),
1011 rIns: mkNamingAdapt(rValue: mkParAdapt(rObj&: MessageOfTheDay, rPar: StdCompiler::RCT_All), szName: "MOTD", rDefault: "")),
1012 rIns: mkNamingAdapt(rValue: mkParAdapt(rObj&: MessageOfTheDayHyperlink, rPar: StdCompiler::RCT_All), szName: "MOTDURL", rDefault: "")),
1013 rIns: mkNamingAdapt(rValue: mkParAdapt(rObj&: LeagueServerRedirect, rPar: StdCompiler::RCT_All), szName: "LeagueServerRedirect", rDefault: "")),
1014 C4ENGINENAME));
1015 // Read reference count
1016 Comp.Value(rStruct: mkNamingCountAdapt(iCount&: rRefCount, szName: "Reference"));
1017 // Create reference array and initialize
1018 rpReferences = new C4Network2Reference *[rRefCount];
1019 for (int i = 0; i < rRefCount; i++)
1020 rpReferences[i] = nullptr;
1021 // Get references
1022 Comp.Value(rStruct: mkNamingAdapt(rValue: mkArrayAdaptMapS(array: rpReferences, size: rRefCount, map: mkPtrAdaptNoNull<C4Network2Reference>), szName: "Reference"));
1023 mkPtrAdaptNoNull<C4Network2Reference>(rpObj&: *rpReferences);
1024 // Done
1025 Comp.End();
1026 }
1027 catch (const StdCompiler::Exception &e)
1028 {
1029 SetError(e.what());
1030 return false;
1031 }
1032 // Set source ip
1033 for (int i = 0; i < rRefCount; i++)
1034 rpReferences[i]->SetSourceAddress(getServerAddress());
1035 // validate version
1036 if (MasterVersion.iVer[0]) fVerSet = true;
1037 // Done
1038 ResetError();
1039 return true;
1040}
1041