Skip to content

Commit 7d17f34

Browse files
authored
perf: avoid unnecessary parsing of user-agents (#4449)
2 parents 7e715fb + 0cfea60 commit 7d17f34

File tree

33 files changed

+112
-111
lines changed

33 files changed

+112
-111
lines changed

crates/data-model/src/compat/session.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use serde::Serialize;
1111
use ulid::Ulid;
1212

1313
use super::Device;
14-
use crate::{InvalidTransitionError, UserAgent};
14+
use crate::InvalidTransitionError;
1515

1616
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
1717
pub enum CompatSessionState {
@@ -76,7 +76,7 @@ pub struct CompatSession {
7676
pub user_session_id: Option<Ulid>,
7777
pub created_at: DateTime<Utc>,
7878
pub is_synapse_admin: bool,
79-
pub user_agent: Option<UserAgent>,
79+
pub user_agent: Option<String>,
8080
pub last_active_at: Option<DateTime<Utc>>,
8181
pub last_active_ip: Option<IpAddr>,
8282
}

crates/data-model/src/oauth2/device_code_grant.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use oauth2_types::scope::Scope;
1111
use serde::Serialize;
1212
use ulid::Ulid;
1313

14-
use crate::{BrowserSession, InvalidTransitionError, Session, UserAgent};
14+
use crate::{BrowserSession, InvalidTransitionError, Session};
1515

1616
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1717
#[serde(rename_all = "snake_case", tag = "state")]
@@ -192,7 +192,7 @@ pub struct DeviceCodeGrant {
192192
pub ip_address: Option<IpAddr>,
193193

194194
/// The user agent used to request this device code grant.
195-
pub user_agent: Option<UserAgent>,
195+
pub user_agent: Option<String>,
196196
}
197197

198198
impl std::ops::Deref for DeviceCodeGrant {

crates/data-model/src/oauth2/session.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use oauth2_types::scope::Scope;
1111
use serde::Serialize;
1212
use ulid::Ulid;
1313

14-
use crate::{InvalidTransitionError, UserAgent};
14+
use crate::InvalidTransitionError;
1515

1616
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
1717
pub enum SessionState {
@@ -80,7 +80,7 @@ pub struct Session {
8080
pub user_session_id: Option<Ulid>,
8181
pub client_id: Ulid,
8282
pub scope: Scope,
83-
pub user_agent: Option<UserAgent>,
83+
pub user_agent: Option<String>,
8484
pub last_active_at: Option<DateTime<Utc>>,
8585
pub last_active_ip: Option<IpAddr>,
8686
}

crates/data-model/src/user_agent.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44
// SPDX-License-Identifier: AGPL-3.0-only
55
// Please see LICENSE in the repository root for full details.
66

7+
use std::sync::LazyLock;
8+
79
use serde::Serialize;
810
use woothee::{parser::Parser, woothee::VALUE_UNKNOWN};
911

12+
static CUSTOM_USER_AGENT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
13+
regex::Regex::new(r"^(?P<name>[^/]+)/(?P<version>[^ ]+) \((?P<segments>.+)\)$").unwrap()
14+
});
15+
16+
static ELECTRON_USER_AGENT_REGEX: LazyLock<regex::Regex> =
17+
LazyLock::new(|| regex::Regex::new(r"(?m)\w+/[\w.]+").unwrap());
18+
1019
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
1120
#[serde(rename_all = "snake_case")]
1221
pub enum DeviceType {
@@ -37,10 +46,7 @@ impl std::ops::Deref for UserAgent {
3746

3847
impl UserAgent {
3948
fn parse_custom(user_agent: &str) -> Option<(&str, &str, &str, &str, Option<&str>)> {
40-
let regex = regex::Regex::new(r"^(?P<name>[^/]+)/(?P<version>[^ ]+) \((?P<segments>.+)\)$")
41-
.unwrap();
42-
43-
let captures = regex.captures(user_agent)?;
49+
let captures = CUSTOM_USER_AGENT_REGEX.captures(user_agent)?;
4450
let name = captures.name("name")?.as_str();
4551
let version = captures.name("version")?.as_str();
4652
let segments: Vec<&str> = captures
@@ -73,9 +79,8 @@ impl UserAgent {
7379
}
7480

7581
fn parse_electron(user_agent: &str) -> Option<(&str, &str)> {
76-
let regex = regex::Regex::new(r"(?m)\w+/[\w.]+").unwrap();
7782
let omit_keys = ["Mozilla", "AppleWebKit", "Chrome", "Electron", "Safari"];
78-
return regex
83+
return ELECTRON_USER_AGENT_REGEX
7984
.find_iter(user_agent)
8085
.map(|caps| caps.as_str().split_once('/').unwrap())
8186
.find(|pair| !omit_keys.contains(&pair.0));

crates/data-model/src/users.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ use serde::Serialize;
1212
use ulid::Ulid;
1313
use url::Url;
1414

15-
use crate::UserAgent;
16-
1715
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1816
pub struct User {
1917
pub id: Ulid,
@@ -81,7 +79,7 @@ pub enum AuthenticationMethod {
8179
pub struct UserRecoverySession {
8280
pub id: Ulid,
8381
pub email: String,
84-
pub user_agent: UserAgent,
82+
pub user_agent: String,
8583
pub ip_address: Option<IpAddr>,
8684
pub locale: String,
8785
pub created_at: DateTime<Utc>,
@@ -137,7 +135,7 @@ pub struct BrowserSession {
137135
pub user: User,
138136
pub created_at: DateTime<Utc>,
139137
pub finished_at: Option<DateTime<Utc>>,
140-
pub user_agent: Option<UserAgent>,
138+
pub user_agent: Option<String>,
141139
pub last_active_at: Option<DateTime<Utc>>,
142140
pub last_active_ip: Option<IpAddr>,
143141
}
@@ -159,9 +157,9 @@ impl BrowserSession {
159157
user,
160158
created_at: now,
161159
finished_at: None,
162-
user_agent: Some(UserAgent::parse(
160+
user_agent: Some(
163161
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned()
164-
)),
162+
),
165163
last_active_at: Some(now),
166164
last_active_ip: None,
167165
})
@@ -213,7 +211,7 @@ pub struct UserRegistration {
213211
pub password: Option<UserRegistrationPassword>,
214212
pub post_auth_action: Option<serde_json::Value>,
215213
pub ip_address: Option<IpAddr>,
216-
pub user_agent: Option<UserAgent>,
214+
pub user_agent: Option<String>,
217215
pub created_at: DateTime<Utc>,
218216
pub completed_at: Option<DateTime<Utc>>,
219217
}

crates/handlers/src/admin/model.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ impl
206206
user_session_id: session.user_session_id,
207207
redirect_uri: sso_login.map(|sso| sso.redirect_uri),
208208
created_at: session.created_at,
209-
user_agent: session.user_agent.map(|ua| ua.raw),
209+
user_agent: session.user_agent,
210210
last_active_at: session.last_active_at,
211211
last_active_ip: session.last_active_ip,
212212
finished_at,
@@ -313,7 +313,7 @@ impl From<mas_data_model::Session> for OAuth2Session {
313313
user_session_id: session.user_session_id,
314314
client_id: session.client_id,
315315
scope: session.scope.to_string(),
316-
user_agent: session.user_agent.map(|ua| ua.raw),
316+
user_agent: session.user_agent,
317317
last_active_at: session.last_active_at,
318318
last_active_ip: session.last_active_ip,
319319
}
@@ -406,7 +406,7 @@ impl From<mas_data_model::BrowserSession> for UserSession {
406406
created_at: value.created_at,
407407
finished_at: value.finished_at,
408408
user_id: value.user.id,
409-
user_agent: value.user_agent.map(|ua| ua.raw),
409+
user_agent: value.user_agent,
410410
last_active_at: value.last_active_at,
411411
last_active_ip: value.last_active_ip,
412412
}

crates/handlers/src/compat/login.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ use axum_extra::typed_header::TypedHeader;
1111
use chrono::Duration;
1212
use hyper::StatusCode;
1313
use mas_axum_utils::record_error;
14-
use mas_data_model::{
15-
CompatSession, CompatSsoLoginState, Device, SiteConfig, TokenType, User, UserAgent,
16-
};
14+
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, SiteConfig, TokenType, User};
1715
use mas_matrix::HomeserverConnection;
1816
use mas_storage::{
1917
BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess,
@@ -276,7 +274,7 @@ pub(crate) async fn post(
276274
user_agent: Option<TypedHeader<headers::UserAgent>>,
277275
MatrixJsonBody(input): MatrixJsonBody<RequestBody>,
278276
) -> Result<impl IntoResponse, RouteError> {
279-
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
277+
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
280278
let login_type = input.credentials.login_type();
281279
let (mut session, user) = match (password_manager.is_enabled(), input.credentials) {
282280
(

crates/handlers/src/graphql/model/browser_sessions.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ impl BrowserSession {
8181

8282
/// The user-agent with which the session was created.
8383
pub async fn user_agent(&self) -> Option<UserAgent> {
84-
self.0.user_agent.clone().map(UserAgent::from)
84+
self.0
85+
.user_agent
86+
.clone()
87+
.map(mas_data_model::UserAgent::parse)
88+
.map(UserAgent::from)
8589
}
8690

8791
/// The last IP address used by the session.

crates/handlers/src/graphql/model/compat_sessions.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ impl CompatSession {
9898

9999
/// The user-agent with which the session was created.
100100
pub async fn user_agent(&self) -> Option<UserAgent> {
101-
self.session.user_agent.clone().map(UserAgent::from)
101+
self.session
102+
.user_agent
103+
.clone()
104+
.map(mas_data_model::UserAgent::parse)
105+
.map(UserAgent::from)
102106
}
103107

104108
/// The associated SSO login, if any.

crates/handlers/src/graphql/model/oauth.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ impl OAuth2Session {
6161

6262
/// The user-agent with which the session was created.
6363
pub async fn user_agent(&self) -> Option<UserAgent> {
64-
self.0.user_agent.clone().map(UserAgent::from)
64+
self.0
65+
.user_agent
66+
.clone()
67+
.map(mas_data_model::UserAgent::parse)
68+
.map(UserAgent::from)
6569
}
6670

6771
/// The state of the session.

crates/handlers/src/oauth2/device/authorize.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ use mas_axum_utils::{
1313
client_authorization::{ClientAuthorization, CredentialsVerificationError},
1414
record_error,
1515
};
16-
use mas_data_model::UserAgent;
1716
use mas_keystore::Encrypter;
1817
use mas_router::UrlBuilder;
1918
use mas_storage::{BoxClock, BoxRepository, BoxRng, oauth2::OAuth2DeviceCodeGrantParams};
@@ -137,7 +136,7 @@ pub(crate) async fn post(
137136

138137
let expires_in = Duration::microseconds(20 * 60 * 1000 * 1000);
139138

140-
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
139+
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
141140
let ip_address = activity_tracker.ip();
142141

143142
let device_code = Alphanumeric.sample_string(&mut rng, 32);

crates/handlers/src/oauth2/token.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use mas_axum_utils::{
1616
record_error,
1717
};
1818
use mas_data_model::{
19-
AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, SiteConfig, TokenType, UserAgent,
19+
AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, SiteConfig, TokenType,
2020
};
2121
use mas_keystore::{Encrypter, Keystore};
2222
use mas_matrix::HomeserverConnection;
@@ -285,7 +285,7 @@ pub(crate) async fn post(
285285
user_agent: Option<TypedHeader<headers::UserAgent>>,
286286
client_authorization: ClientAuthorization<AccessTokenRequest>,
287287
) -> Result<impl IntoResponse, RouteError> {
288-
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
288+
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
289289
let client = client_authorization
290290
.credentials
291291
.fetch(&mut repo)
@@ -415,7 +415,7 @@ async fn authorization_code_grant(
415415
site_config: &SiteConfig,
416416
mut repo: BoxRepository,
417417
homeserver: &Arc<dyn HomeserverConnection>,
418-
user_agent: Option<UserAgent>,
418+
user_agent: Option<String>,
419419
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
420420
// Check that the client is allowed to use this grant type
421421
if !client.grant_types.contains(&GrantType::AuthorizationCode) {
@@ -596,7 +596,7 @@ async fn refresh_token_grant(
596596
client: &Client,
597597
site_config: &SiteConfig,
598598
mut repo: BoxRepository,
599-
user_agent: Option<UserAgent>,
599+
user_agent: Option<String>,
600600
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
601601
// Check that the client is allowed to use this grant type
602602
if !client.grant_types.contains(&GrantType::RefreshToken) {
@@ -749,7 +749,7 @@ async fn client_credentials_grant(
749749
site_config: &SiteConfig,
750750
mut repo: BoxRepository,
751751
mut policy: Policy,
752-
user_agent: Option<UserAgent>,
752+
user_agent: Option<String>,
753753
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
754754
// Check that the client is allowed to use this grant type
755755
if !client.grant_types.contains(&GrantType::ClientCredentials) {
@@ -771,7 +771,7 @@ async fn client_credentials_grant(
771771
grant_type: mas_policy::GrantType::ClientCredentials,
772772
requester: mas_policy::Requester {
773773
ip_address: activity_tracker.ip(),
774-
user_agent: user_agent.clone().map(|ua| ua.raw),
774+
user_agent: user_agent.clone(),
775775
},
776776
})
777777
.await?;
@@ -828,7 +828,7 @@ async fn device_code_grant(
828828
site_config: &SiteConfig,
829829
mut repo: BoxRepository,
830830
homeserver: &Arc<dyn HomeserverConnection>,
831-
user_agent: Option<UserAgent>,
831+
user_agent: Option<String>,
832832
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
833833
// Check that the client is allowed to use this grant type
834834
if !client.grant_types.contains(&GrantType::DeviceCode) {

crates/handlers/src/upstream_oauth2/link.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use mas_axum_utils::{
1919
csrf::{CsrfExt, ProtectedForm},
2020
record_error,
2121
};
22-
use mas_data_model::UserAgent;
2322
use mas_jose::jwt::Jwt;
2423
use mas_matrix::HomeserverConnection;
2524
use mas_policy::Policy;
@@ -233,7 +232,7 @@ pub(crate) async fn get(
233232
user_agent: Option<TypedHeader<headers::UserAgent>>,
234233
Path(link_id): Path<Ulid>,
235234
) -> Result<impl IntoResponse, RouteError> {
236-
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
235+
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
237236
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
238237
let (session_id, post_auth_action) = sessions_cookie
239238
.lookup_link(link_id)
@@ -502,7 +501,7 @@ pub(crate) async fn get(
502501
email: None,
503502
requester: mas_policy::Requester {
504503
ip_address: activity_tracker.ip(),
505-
user_agent: user_agent.clone().map(|ua| ua.raw),
504+
user_agent: user_agent.clone(),
506505
},
507506
})
508507
.await?;
@@ -568,7 +567,7 @@ pub(crate) async fn post(
568567
Path(link_id): Path<Ulid>,
569568
Form(form): Form<ProtectedForm<FormData>>,
570569
) -> Result<Response, RouteError> {
571-
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
570+
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
572571
let form = cookie_jar.verify_form(&clock, form)?;
573572

574573
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
@@ -786,7 +785,7 @@ pub(crate) async fn post(
786785
email: email.as_deref(),
787786
requester: mas_policy::Requester {
788787
ip_address: activity_tracker.ip(),
789-
user_agent: user_agent.clone().map(|ua| ua.raw),
788+
user_agent: user_agent.clone(),
790789
},
791790
})
792791
.await?;

crates/handlers/src/views/login.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use mas_axum_utils::{
1717
cookies::CookieJar,
1818
csrf::{CsrfExt, ProtectedForm},
1919
};
20-
use mas_data_model::{UserAgent, oauth2::LoginHint};
20+
use mas_data_model::oauth2::LoginHint;
2121
use mas_i18n::DataLocale;
2222
use mas_matrix::HomeserverConnection;
2323
use mas_router::{UpstreamOAuth2Authorize, UrlBuilder};
@@ -146,7 +146,7 @@ pub(crate) async fn post(
146146
user_agent: Option<TypedHeader<headers::UserAgent>>,
147147
Form(form): Form<ProtectedForm<LoginForm>>,
148148
) -> Result<Response, FancyError> {
149-
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
149+
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
150150
if !site_config.password_login_enabled {
151151
// XXX: is it necessary to have better errors here?
152152
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());

crates/handlers/src/views/recovery/start.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use mas_axum_utils::{
1818
cookies::CookieJar,
1919
csrf::{CsrfExt, ProtectedForm},
2020
};
21-
use mas_data_model::{SiteConfig, UserAgent};
21+
use mas_data_model::SiteConfig;
2222
use mas_router::UrlBuilder;
2323
use mas_storage::{
2424
BoxClock, BoxRepository, BoxRng,
@@ -102,7 +102,7 @@ pub(crate) async fn post(
102102
return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
103103
}
104104

105-
let user_agent = UserAgent::parse(user_agent.as_str().to_owned());
105+
let user_agent = user_agent.as_str().to_owned();
106106
let ip_address = activity_tracker.ip();
107107

108108
let form = cookie_jar.verify_form(&clock, form)?;

0 commit comments

Comments
 (0)