Files
DarkflameServer/dDashboardServer/routes/UgcFetch.cpp
Aaron Kimbrell 9e8f161dd8 feat(traffic): count traffic by peer and by connection
Every server now splits its packet counts by peer: its own connections
(players on auth and worlds), its master link, or other servers (the
worlds on chat, every server on master, a world's chat link, which is
now counted too). HTTP requests carrying X-Darkflame-Server count as
another server's, the dashboard counts its own requests to the UGC
server, and each HTTP client address is counted. The report also gets
each RakNet connection's statistics (worlds name the player on it),
trimmed to the 32 busiest with the rest summed. Counting stays on the
main loop, except the dashboard's UGC fetches, which only touch the
locked recorder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:28:39 -05:00

110 lines
4.4 KiB
C++

#include "UgcFetch.h"
#include <chrono>
#include <mutex>
#include <curl/curl.h>
#include "dConfig.h"
#include "Game.h"
#include "RouteUtils.h"
#include "TrafficStats.h"
#include "TtlCache.h"
namespace {
std::mutex g_CacheMutex;
TtlCache<std::string, std::shared_ptr<const UgcFetch::Fetched>> g_FileCache(std::chrono::seconds(15), 32 * 1024 * 1024);
TtlCache<std::string, std::shared_ptr<const UgcFetch::Fetched>> g_StatusCache(std::chrono::seconds(2), 1024 * 1024);
constexpr size_t MAX_FETCH_BYTES = 128 * 1024 * 1024;
size_t Collect(char* data, size_t size, size_t count, void* userData) {
auto* out = static_cast<std::string*>(userData);
if (out->size() + size * count > MAX_FETCH_BYTES) return 0;
out->append(data, size * count);
return size * count;
}
// Sends a request set up by setup (a GET unless it says otherwise)
template <typename Setup>
std::shared_ptr<const UgcFetch::Fetched> Perform(const std::string& url, long timeout, Setup&& setup) {
auto out = std::make_shared<UgcFetch::Fetched>();
CURL* curl = curl_easy_init();
if (!curl) {
out->error = "could not start a request";
return out;
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "http,https");
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "DarkflameServer-Dashboard");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Collect);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out->body);
// Names the dashboard to the UGC server, whose traffic report then counts this as the dashboard's (not a player's)
curl_slist* identify = curl_slist_append(nullptr, (std::string(TrafficStats::SERVER_HEADER) + ": dashboard").c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, identify);
setup(curl);
const auto code = curl_easy_perform(curl);
TrafficStats::Local().HttpOut(TrafficStats::Now(), out->body.size());
if (code == CURLE_OK) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &out->status);
else out->error = curl_easy_strerror(code);
curl_easy_cleanup(curl);
curl_slist_free_all(identify);
return out;
}
}
namespace UgcFetch {
std::string InternalUrl() {
auto url = Game::config->GetValue("ugc_internal_url");
if (url.empty()) url = "http://127.0.0.1:2008";
while (url.ends_with('/')) url.pop_back();
return url;
}
std::shared_ptr<const Fetched> Get(const std::string& url) {
return Perform(url, 20L, [](CURL*) {});
}
std::shared_ptr<const Fetched> CachedGet(const std::string& url, eCache cache) {
auto& store = cache == eCache::STATUS ? g_StatusCache : g_FileCache;
{
std::lock_guard lock(g_CacheMutex);
if (auto hit = store.Get(url)) return *hit;
}
auto fetched = Get(url);
if (fetched->status == 200 || fetched->status == 404) {
std::lock_guard lock(g_CacheMutex);
store.Put(url, fetched, fetched->body.size() + 64);
}
return fetched;
}
std::shared_ptr<const Fetched> AdminPost(const std::string& url, const std::string& key, const std::string& body) {
curl_slist* headers = curl_slist_append(nullptr, "Content-Type: application/json");
headers = curl_slist_append(headers, (std::string(TrafficStats::SERVER_HEADER) + ": dashboard").c_str());
headers = curl_slist_append(headers, ("X-Ugc-Admin-Key: " + key).c_str());
auto out = Perform(url, 120L, [&](CURL* curl) {
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));
});
curl_slist_free_all(headers);
return out;
}
void ReplyError(HTTPReply& reply, const Fetched& fetched, std::string_view shownUrl) {
using RouteUtils::JsonError;
if (fetched.status == 408) return JsonError(reply, eHTTPStatusCode::REQUEST_TIMEOUT, "The UGC server is making it; try again in a moment");
if (fetched.status == 404) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "The UGC server has not made it");
if (fetched.status == 0) {
const std::string at = shownUrl.empty() ? std::string() : " at " + std::string(shownUrl);
return JsonError(reply, eHTTPStatusCode::BAD_GATEWAY, "The UGC server doesn't answer" + at + " (" + fetched.error + ")");
}
JsonError(reply, eHTTPStatusCode::BAD_GATEWAY, "The UGC server answered " + std::to_string(fetched.status));
}
}