Skip to content
This repository was archived by the owner on Dec 24, 2025. It is now read-only.

Commit 846e7b3

Browse files
committed
Parse User-Agent strings
1 parent f89890e commit 846e7b3

File tree

3 files changed

+54
-3
lines changed

3 files changed

+54
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ reqwest = { version = "0.12", features = ["json"] }
2626
serde = { version = "1", features = ["derive"] }
2727
tokio = { version = "1", features = ["net", "rt-multi-thread"] }
2828
uuid = { version = "1", features = ["serde", "v4"] }
29+
regex = "1.11.1"
2930

3031
[dependencies.axum]
3132
version = "0.8"

src/endpoints/global_data.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ use axum::{
66
};
77
use chrono::{DateTime, Utc};
88
use dashmap::DashMap;
9+
use regex::Regex;
910
use reqwest::{Client, StatusCode};
1011
use serde::Serialize;
1112
use serde_json::Value;
1213
use sqlx::{query, PgPool};
13-
use std::{fmt::Write, fs::read_to_string};
14+
use std::{fmt::Write, fs::read_to_string, sync::LazyLock};
1415

1516
const PROJECT_ID: &str = "p2rxzX0q";
1617

@@ -172,18 +173,66 @@ pub async fn metrics(
172173
let data_container = global_data.read().await;
173174
let request_agents = data_container.data.request_user_agents.clone();
174175
for (agent, count) in request_agents {
175-
writeln!(response, "request_count{{user_agent=\"{agent}\"}} {count}");
176+
if let Some((mod_ver, minecraft_ver, note)) = parse_user_agent(agent) {
177+
writeln!(response, "request_count{{mod_version=\"{mod_ver}\", minecraft_version=\"{minecraft_ver}\", mod=\"{note}\"}} {count}");
178+
}
176179
}
177180
data_container.data.request_user_agents.clear();
178181
let gateway_agents = data_container.data.gateway_user_agents.clone();
179182
for (agent, count) in gateway_agents {
180-
writeln!(response, "connections{{user_agent=\"{agent}\"}} {count}");
183+
if let Some((mod_ver, minecraft_ver, note)) = parse_user_agent(agent) {
184+
writeln!(response, "connections{{mod_version=\"{mod_ver}\", minecraft_version=\"{minecraft_ver}\", mod=\"{note}\"}} {count}");
185+
}
181186
}
182187
};
183188

184189
Ok(response)
185190
}
186191

192+
static SEMVER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
193+
Regex::new(
194+
r#"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"#,
195+
)
196+
.unwrap()
197+
});
198+
static MCVER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
199+
Regex::new(
200+
"0\\.\\d+(\\.\\d+)?a?(_\\d+)?|\\d+\\.\\d+(\\.\\d+)?(-pre\\d+|Pre-[Rr]elease \\d+)?|\\d+\\.\\d+(\\.\\d+)?(-rc\\d+| [Rr]elease Candidate \\d+)?|\\d+w\\d+[a-z]|[a-c]\\d\\.\\d+(\\.\\d+)?[a-z]?(_\\d+)?[a-z]?|(Alpha|Beta) v?\\d+\\.\\d+(\\.\\d+)?[a-z]?(_\\d+)?[a-z]?|Inf?dev (0\\.31 )?\\d+(-\\d+)?|(rd|inf?)-\\d+|1\\.RV-Pre1|3D Shareware v1\\.34|23w13a_or_b|24w14potato|25w14craftmine|(.*[Ee]xperimental [Ss]napshot )(\\d+)",
201+
)
202+
.unwrap()
203+
});
204+
static OLD_1_UA: LazyLock<Regex> =
205+
LazyLock::new(|| Regex::new(r".*\((?:(AxolotlClient)/)(.+)(?:\+mc)?(.+)\) .*").unwrap());
206+
static OLD_2_UA: LazyLock<Regex> =
207+
LazyLock::new(|| Regex::new(r".*\((?:(AxolotlClient)/)(.+) \(Minecraft .+\).*").unwrap());
208+
static CURRENT_UA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(AxolotlClient)/(.+) Minecraft/(.+)").unwrap());
209+
static SNAPPER_UA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(Snapper)/(.+)\+(.+)").unwrap());
210+
211+
fn parse_user_agent(agent: String) -> Option<(String, String, String)> {
212+
if agent.starts_with("Java-http-client") {
213+
return None;
214+
}
215+
for regex in [&OLD_1_UA, &OLD_2_UA, &CURRENT_UA, &SNAPPER_UA] {
216+
if regex.is_match(&agent) {
217+
let captures = regex.captures(&agent).unwrap();
218+
let mod_name_capture = captures.get(1);
219+
let mod_ver_capture = captures.get(2);
220+
let mc_ver_capture = captures.get(3);
221+
if mod_name_capture.is_none() || mod_ver_capture.is_none() || mc_ver_capture.is_none() {
222+
return None;
223+
}
224+
let mod_name = mod_name_capture.unwrap().as_str().to_string();
225+
let mod_ver = mod_ver_capture.unwrap().as_str();
226+
let mc_ver = mc_ver_capture.unwrap().as_str();
227+
if !SEMVER_REGEX.is_match(mod_ver) || !MCVER_REGEX.is_match(mc_ver) {
228+
return None;
229+
}
230+
return Some((mod_ver.to_string(), mc_ver.to_string(), mod_name));
231+
}
232+
}
233+
return None;
234+
}
235+
187236
async fn get_total_players(database: &PgPool) -> Result<u32, ApiError> {
188237
Ok(query!("SELECT reltuples AS estimate FROM pg_class where relname = 'players'")
189238
.fetch_one(database)

0 commit comments

Comments
 (0)