mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat(web): API key traffic is its own Network row, named after the key
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -208,9 +208,12 @@ namespace TrafficStats {
|
|||||||
if (list.size() > limit) list.resize(limit);
|
if (list.size() > limit) list.resize(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Recorder::HttpClient(const std::string& address, bool fromServer, uint64_t bytesIn, uint64_t bytesOut, uint32_t accountId, const std::string& user) {
|
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 std::string& apiKeyName) {
|
||||||
const auto key = accountId ? address + '\n' + std::to_string(accountId) : address;
|
// One entry per address and signed-in account: people sharing an address (behind one NAT or proxy) stay apart.
|
||||||
|
// An API key is its own entry, named after the key, apart from its owner's browser sessions
|
||||||
|
const auto key = accountId ? address + '\n' + std::to_string(accountId) + (apiKeyName.empty() ? std::string() : "\nkey:" + apiKeyName) : address;
|
||||||
|
const auto label = apiKeyName.empty() || user.empty() ? user : user + " (API key \"" + apiKeyName + "\")";
|
||||||
std::lock_guard lock(m_Mutex);
|
std::lock_guard lock(m_Mutex);
|
||||||
auto it = m_HttpClients.find(key);
|
auto it = m_HttpClients.find(key);
|
||||||
if (it == m_HttpClients.end()) {
|
if (it == m_HttpClients.end()) {
|
||||||
@@ -220,11 +223,11 @@ namespace TrafficStats {
|
|||||||
it->second.http = true;
|
it->second.http = true;
|
||||||
if (!full) {
|
if (!full) {
|
||||||
it->second.accountId = accountId;
|
it->second.accountId = accountId;
|
||||||
it->second.account = user;
|
it->second.account = label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto& client = it->second;
|
auto& client = it->second;
|
||||||
if (client.account.empty() && !user.empty() && client.accountId == accountId) client.account = user; // a WebSocket upgrade knows only the account
|
if (client.account.empty() && !label.empty() && client.accountId == accountId) client.account = label; // a WebSocket upgrade knows only the account
|
||||||
if (fromServer) client.peer = Peer::SERVERS;
|
if (fromServer) client.peer = Peer::SERVERS;
|
||||||
client.packetsIn++;
|
client.packetsIn++;
|
||||||
client.packetsOut++;
|
client.packetsOut++;
|
||||||
|
|||||||
@@ -201,7 +201,8 @@ namespace TrafficStats {
|
|||||||
// An HTTP client's request by its address (bytes of the request and of the answer's body). `accountId` and `user`:
|
// 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
|
// 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
|
// 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 = {});
|
void HttpClient(const std::string& address, bool fromServer, uint64_t bytesIn, uint64_t bytesOut, uint32_t accountId = 0, const std::string& user = {},
|
||||||
|
const std::string& apiKeyName = {});
|
||||||
|
|
||||||
// Evaluated when a report is taken (on the thread that takes it)
|
// Evaluated when a report is taken (on the thread that takes it)
|
||||||
void SetGauge(const std::string& name, std::function<double()> source);
|
void SetGauge(const std::string& name, std::function<double()> source);
|
||||||
|
|||||||
17
dWeb/Web.cpp
17
dWeb/Web.cpp
@@ -266,6 +266,7 @@ namespace {
|
|||||||
uint64_t requestBytes{};
|
uint64_t requestBytes{};
|
||||||
uint32_t accountId{};
|
uint32_t accountId{};
|
||||||
std::string user;
|
std::string user;
|
||||||
|
std::string apiKey; // the API key's name, when the request used one
|
||||||
};
|
};
|
||||||
std::unordered_map<unsigned long, DeferredTiming> g_DeferredTiming;
|
std::unordered_map<unsigned long, DeferredTiming> g_DeferredTiming;
|
||||||
|
|
||||||
@@ -288,10 +289,10 @@ namespace {
|
|||||||
|
|
||||||
// `accountId` and `user`: who the request was signed in as (0 and "" for none), for the Network page's web clients
|
// `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,
|
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, uint32_t accountId = 0, const std::string& user = {}) {
|
const std::string& address, uint64_t requestBytes, uint32_t accountId = 0, const std::string& user = {}, const std::string& apiKeyName = {}) {
|
||||||
const auto micros = std::chrono::duration_cast<std::chrono::microseconds>(TrafficClock::now() - started).count();
|
const auto micros = std::chrono::duration_cast<std::chrono::microseconds>(TrafficClock::now() - started).count();
|
||||||
TrafficStats::Local().Http(TrafficStats::Now(), route, status, static_cast<uint64_t>(std::max<int64_t>(micros, 0)), bytes, fromServer);
|
TrafficStats::Local().Http(TrafficStats::Now(), route, status, static_cast<uint64_t>(std::max<int64_t>(micros, 0)), bytes, fromServer);
|
||||||
TrafficStats::Local().HttpClient(address, fromServer, requestBytes, bytes, accountId, user);
|
TrafficStats::Local().HttpClient(address, fromServer, requestBytes, bytes, accountId, user, apiKeyName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +309,7 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms
|
|||||||
const uint64_t requestBytes = http_msg ? http_msg->message.len : 0;
|
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)
|
// Who the request was signed in as, once the middleware has looked (the dashboard's session or API key)
|
||||||
uint32_t signedInAccount = 0;
|
uint32_t signedInAccount = 0;
|
||||||
std::string signedInUser;
|
std::string signedInUser, signedInKey; // signedInKey: the API key's name when the request used one
|
||||||
|
|
||||||
if (!http_msg) {
|
if (!http_msg) {
|
||||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||||
@@ -395,7 +396,8 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CountRequest("GET /ws", level ? 101 : 401, started, 0, fromServer, clientAddress, requestBytes, level ? level->accountId : 0);
|
CountRequest("GET /ws", level ? 101 : 401, started, 0, fromServer, clientAddress, requestBytes, level ? level->accountId : 0, {},
|
||||||
|
level && level->apiKey ? level->apiKey->name : std::string());
|
||||||
if (level) {
|
if (level) {
|
||||||
mg_ws_upgrade(connection, const_cast<mg_http_message*>(http_msg), NULL);
|
mg_ws_upgrade(connection, const_cast<mg_http_message*>(http_msg), NULL);
|
||||||
g_AuthenticatedWSConnections[connection] = { level->level, level->accountId, connectToken, apiToken,
|
g_AuthenticatedWSConnections[connection] = { level->level, level->accountId, connectToken, apiToken,
|
||||||
@@ -506,6 +508,7 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms
|
|||||||
if (context.isAuthenticated) {
|
if (context.isAuthenticated) {
|
||||||
signedInAccount = context.accountId;
|
signedInAccount = context.accountId;
|
||||||
signedInUser = context.authenticatedUser;
|
signedInUser = context.authenticatedUser;
|
||||||
|
if (context.apiKey) signedInKey = context.apiKey->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call handler only if all middleware passed. A failing handler (e.g. a database error) answers 500
|
// Call handler only if all middleware passed. A failing handler (e.g. a database error) answers 500
|
||||||
@@ -534,14 +537,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);
|
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
|
// Requests the answers never came for (the client left) are forgotten now and then
|
||||||
if (g_DeferredTiming.size() > 10000) g_DeferredTiming.clear();
|
if (g_DeferredTiming.size() > 10000) g_DeferredTiming.clear();
|
||||||
g_DeferredTiming[connection->id] = { std::move(trafficRoute), started, fromServer, clientAddress, requestBytes, signedInAccount, std::move(signedInUser) };
|
g_DeferredTiming[connection->id] = { std::move(trafficRoute), started, fromServer, clientAddress, requestBytes, signedInAccount, std::move(signedInUser), std::move(signedInKey) };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The handler deferred and then failed: its late answer is dropped
|
// The handler deferred and then failed: its late answer is dropped
|
||||||
if (g_Deferred.IsPending(connection->id)) g_Deferred.Close(connection->id);
|
if (g_Deferred.IsPending(connection->id)) g_Deferred.Close(connection->id);
|
||||||
|
|
||||||
SendReply(connection, reply, http_msg);
|
SendReply(connection, reply, http_msg);
|
||||||
CountRequest(trafficRoute, static_cast<uint16_t>(reply.status), started, ReplyBytes(reply), fromServer, clientAddress, requestBytes, signedInAccount, signedInUser);
|
CountRequest(trafficRoute, static_cast<uint16_t>(reply.status), started, ReplyBytes(reply), fromServer, clientAddress, requestBytes, signedInAccount, signedInUser, signedInKey);
|
||||||
RemoveSentFile(reply);
|
RemoveSentFile(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,7 +826,7 @@ void Web::SendDeferredReplies() {
|
|||||||
SendReply(connection, finished.reply, nullptr);
|
SendReply(connection, finished.reply, nullptr);
|
||||||
if (const auto timing = g_DeferredTiming.find(finished.connection); timing != g_DeferredTiming.end()) {
|
if (const auto timing = g_DeferredTiming.find(finished.connection); timing != g_DeferredTiming.end()) {
|
||||||
CountRequest(timing->second.route, static_cast<uint16_t>(finished.reply.status), timing->second.started, ReplyBytes(finished.reply), timing->second.fromServer,
|
CountRequest(timing->second.route, static_cast<uint16_t>(finished.reply.status), timing->second.started, ReplyBytes(finished.reply), timing->second.fromServer,
|
||||||
timing->second.address, timing->second.requestBytes, timing->second.accountId, timing->second.user);
|
timing->second.address, timing->second.requestBytes, timing->second.accountId, timing->second.user, timing->second.apiKey);
|
||||||
g_DeferredTiming.erase(timing);
|
g_DeferredTiming.erase(timing);
|
||||||
}
|
}
|
||||||
RemoveSentFile(finished.reply);
|
RemoveSentFile(finished.reply);
|
||||||
|
|||||||
@@ -292,3 +292,20 @@ TEST(TrafficStatsTest, HttpClientsApartBySignedInAccount) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(TrafficStatsTest, ApiKeyTrafficIsNamedAfterTheKey) {
|
||||||
|
Recorder r;
|
||||||
|
r.HttpClient("203.0.113.5", false, 100, 1000, 7, "alice"); // alice's browser
|
||||||
|
r.HttpClient("203.0.113.5", false, 50, 500, 7, "alice", "status bot"); // alice's API key
|
||||||
|
r.HttpClient("203.0.113.5", false, 50, 500, 7, "alice", "status bot");
|
||||||
|
const auto report = r.Take(1);
|
||||||
|
ASSERT_EQ(report.connections.size(), 2u);
|
||||||
|
bool sawKey = false, sawBrowser = false;
|
||||||
|
for (const auto& c : report.connections) {
|
||||||
|
EXPECT_EQ(c.accountId, 7u);
|
||||||
|
if (c.account == "alice (API key \"status bot\")") { sawKey = true; EXPECT_EQ(c.packetsIn, 2u); }
|
||||||
|
else if (c.account == "alice") sawBrowser = true;
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(sawKey);
|
||||||
|
EXPECT_TRUE(sawBrowser);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user