diff --git a/dCommon/TrafficStats.cpp b/dCommon/TrafficStats.cpp index 179b51bdf..e3c7c05b5 100644 --- a/dCommon/TrafficStats.cpp +++ b/dCommon/TrafficStats.cpp @@ -208,16 +208,23 @@ namespace TrafficStats { if (list.size() > limit) list.resize(limit); } - void Recorder::HttpClient(const std::string& address, bool fromServer, uint64_t bytesIn, uint64_t bytesOut) { + void Recorder::HttpClient(const std::string& address, bool fromServer, uint64_t bytesIn, uint64_t bytesOut, uint32_t accountId, const std::string& user) { + // One entry per address and signed-in account: people sharing an address (behind one NAT or proxy) stay apart + const auto key = accountId ? address + '\n' + std::to_string(accountId) : address; std::lock_guard lock(m_Mutex); - auto it = m_HttpClients.find(address); + auto it = m_HttpClients.find(key); if (it == m_HttpClients.end()) { const bool full = m_HttpClients.size() >= MAX_HTTP_CLIENTS; - it = m_HttpClients.try_emplace(full ? std::string() : address).first; - it->second.address = it->first; + it = m_HttpClients.try_emplace(full ? std::string() : key).first; + it->second.address = full ? std::string() : address; it->second.http = true; + if (!full) { + it->second.accountId = accountId; + it->second.account = user; + } } auto& client = it->second; + if (client.account.empty() && !user.empty() && client.accountId == accountId) client.account = user; // a WebSocket upgrade knows only the account if (fromServer) client.peer = Peer::SERVERS; client.packetsIn++; client.packetsOut++; diff --git a/dCommon/TrafficStats.h b/dCommon/TrafficStats.h index 99ce3a197..2ced14758 100644 --- a/dCommon/TrafficStats.h +++ b/dCommon/TrafficStats.h @@ -160,7 +160,7 @@ namespace TrafficStats { uint64_t bytesIn{}, bytesOut{}; uint32_t resends{}; uint32_t pingMs{}; - uint32_t accountId{}; // a logged-in player's (world servers), else 0 + uint32_t accountId{}; // a logged-in player's (world servers) or a signed-in dashboard account's (HTTP), else 0 uint64_t characterId{}; std::string account, character; @@ -198,8 +198,10 @@ namespace TrafficStats { void Http(int64_t now, const std::string& route, uint16_t status, uint64_t microseconds, uint64_t bytesOut, bool fromServer = false); // A request this server made to another server's web server (the dashboard to the UGC server); any thread void HttpOut(int64_t now, uint64_t bytesIn); - // An HTTP client's request by its address (bytes of the request and of the answer's body) - void HttpClient(const std::string& address, bool fromServer, uint64_t bytesIn, uint64_t bytesOut); + // An HTTP client's request by its address (bytes of the request and of the answer's body). `accountId` and `user`: + // the account the request was signed in as (the dashboard's session or API key), 0 and "" when none; each + // signed-in account on an address is counted apart + void HttpClient(const std::string& address, bool fromServer, uint64_t bytesIn, uint64_t bytesOut, uint32_t accountId = 0, const std::string& user = {}); // Evaluated when a report is taken (on the thread that takes it) void SetGauge(const std::string& name, std::function source); diff --git a/dWeb/Web.cpp b/dWeb/Web.cpp index 197023d7f..cde0ba749 100644 --- a/dWeb/Web.cpp +++ b/dWeb/Web.cpp @@ -264,6 +264,8 @@ namespace { bool fromServer{}; std::string address; uint64_t requestBytes{}; + uint32_t accountId{}; + std::string user; }; std::unordered_map g_DeferredTiming; @@ -284,11 +286,12 @@ namespace { std::filesystem::remove(reply.file, ec); } + // `accountId` and `user`: who the request was signed in as (0 and "" for none), for the Network page's web clients void CountRequest(const std::string& route, uint16_t status, TrafficClock::time_point started, uint64_t bytes, bool fromServer, - const std::string& address, uint64_t requestBytes) { + const std::string& address, uint64_t requestBytes, uint32_t accountId = 0, const std::string& user = {}) { const auto micros = std::chrono::duration_cast(TrafficClock::now() - started).count(); TrafficStats::Local().Http(TrafficStats::Now(), route, status, static_cast(std::max(micros, 0)), bytes, fromServer); - TrafficStats::Local().HttpClient(address, fromServer, requestBytes, bytes); + TrafficStats::Local().HttpClient(address, fromServer, requestBytes, bytes, accountId, user); } } @@ -303,6 +306,9 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms const bool fromServer = http_msg && mg_http_get_header(const_cast(http_msg), TrafficStats::SERVER_HEADER) != nullptr; const auto clientAddress = GetClientIP(connection); const uint64_t requestBytes = http_msg ? http_msg->message.len : 0; + // Who the request was signed in as, once the middleware has looked (the dashboard's session or API key) + uint32_t signedInAccount = 0; + std::string signedInUser; if (!http_msg) { reply.status = eHTTPStatusCode::BAD_REQUEST; @@ -389,7 +395,7 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms } } - CountRequest("GET /ws", level ? 101 : 401, started, 0, fromServer, clientAddress, requestBytes); + CountRequest("GET /ws", level ? 101 : 401, started, 0, fromServer, clientAddress, requestBytes, level ? level->accountId : 0); if (level) { mg_ws_upgrade(connection, const_cast(http_msg), NULL); g_AuthenticatedWSConnections[connection] = { level->level, level->accountId, connectToken, apiToken, @@ -497,6 +503,11 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms } } + if (context.isAuthenticated) { + signedInAccount = context.accountId; + signedInUser = context.authenticatedUser; + } + // Call handler only if all middleware passed. A failing handler (e.g. a database error) answers 500 // instead of taking the whole server down. if (chainPassed) { @@ -523,14 +534,14 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms g_Deferred.SetReplyOptions(connection->id, reply.headers, cc && mg_strcasecmp(*cc, mg_str("close")) == 0); // Requests the answers never came for (the client left) are forgotten now and then if (g_DeferredTiming.size() > 10000) g_DeferredTiming.clear(); - g_DeferredTiming[connection->id] = { std::move(trafficRoute), started, fromServer, clientAddress, requestBytes }; + g_DeferredTiming[connection->id] = { std::move(trafficRoute), started, fromServer, clientAddress, requestBytes, signedInAccount, std::move(signedInUser) }; return; } // The handler deferred and then failed: its late answer is dropped if (g_Deferred.IsPending(connection->id)) g_Deferred.Close(connection->id); SendReply(connection, reply, http_msg); - CountRequest(trafficRoute, static_cast(reply.status), started, ReplyBytes(reply), fromServer, clientAddress, requestBytes); + CountRequest(trafficRoute, static_cast(reply.status), started, ReplyBytes(reply), fromServer, clientAddress, requestBytes, signedInAccount, signedInUser); RemoveSentFile(reply); } @@ -812,7 +823,7 @@ void Web::SendDeferredReplies() { SendReply(connection, finished.reply, nullptr); if (const auto timing = g_DeferredTiming.find(finished.connection); timing != g_DeferredTiming.end()) { CountRequest(timing->second.route, static_cast(finished.reply.status), timing->second.started, ReplyBytes(finished.reply), timing->second.fromServer, - timing->second.address, timing->second.requestBytes); + timing->second.address, timing->second.requestBytes, timing->second.accountId, timing->second.user); g_DeferredTiming.erase(timing); } RemoveSentFile(finished.reply); diff --git a/tests/dCommonTests/TrafficStatsTests.cpp b/tests/dCommonTests/TrafficStatsTests.cpp index 5be94a8cf..fabd6a1e3 100644 --- a/tests/dCommonTests/TrafficStatsTests.cpp +++ b/tests/dCommonTests/TrafficStatsTests.cpp @@ -267,3 +267,28 @@ TEST(TrafficStatsTest, RecorderSplitsHttpByWhoAsked) { EXPECT_EQ(report.seconds[0].httpOutRequests, 1u); EXPECT_EQ(report.seconds[0].httpOutBytesIn, 700u); } + +TEST(TrafficStatsTest, HttpClientsApartBySignedInAccount) { + Recorder r; + r.HttpClient("203.0.113.5", false, 100, 1000, 7, "alice"); + r.HttpClient("203.0.113.5", false, 100, 1000, 7, "alice"); + r.HttpClient("203.0.113.5", false, 50, 500, 9, "bob"); + r.HttpClient("203.0.113.5", false, 10, 20); // not signed in (the sign-in page) + const auto report = r.Take(1); + ASSERT_EQ(report.connections.size(), 3u); + for (const auto& c : report.connections) { + EXPECT_EQ(c.address, "203.0.113.5"); + EXPECT_TRUE(c.http); + if (c.accountId == 7) { + EXPECT_EQ(c.account, "alice"); + EXPECT_EQ(c.packetsIn, 2u); + EXPECT_EQ(c.bytesOut, 2000u); + } else if (c.accountId == 9) { + EXPECT_EQ(c.account, "bob"); + } else { + EXPECT_EQ(c.accountId, 0u); + EXPECT_TRUE(c.account.empty()); + EXPECT_EQ(c.bytesIn, 10u); + } + } +}