diff --git a/apps/labrinth/src/queue/analytics.rs b/apps/labrinth/src/queue/analytics.rs index 117a51fa2..4269edaeb 100644 --- a/apps/labrinth/src/queue/analytics.rs +++ b/apps/labrinth/src/queue/analytics.rs @@ -6,7 +6,6 @@ use dashmap::{DashMap, DashSet}; use redis::cmd; use sqlx::PgPool; use std::collections::HashMap; -use std::net::Ipv6Addr; const DOWNLOADS_NAMESPACE: &str = "downloads"; const VIEWS_NAMESPACE: &str = "views"; @@ -33,23 +32,8 @@ impl AnalyticsQueue { } } - fn strip_ip(ip: Ipv6Addr) -> u64 { - if let Some(ip) = ip.to_ipv4_mapped() { - let octets = ip.octets(); - u64::from_be_bytes([ - octets[0], octets[1], octets[2], octets[3], 0, 0, 0, 0, - ]) - } else { - let octets = ip.octets(); - u64::from_be_bytes([ - octets[0], octets[1], octets[2], octets[3], octets[4], - octets[5], octets[6], octets[7], - ]) - } - } - pub fn add_view(&self, page_view: PageView) { - let ip_stripped = Self::strip_ip(page_view.ip); + let ip_stripped = crate::util::ip::strip_ip(page_view.ip); self.views_queue .entry((ip_stripped, page_view.project_id)) @@ -57,7 +41,7 @@ impl AnalyticsQueue { .push(page_view); } pub fn add_download(&self, download: Download) { - let ip_stripped = Self::strip_ip(download.ip); + let ip_stripped = crate::util::ip::strip_ip(download.ip); self.downloads_queue .insert((ip_stripped, download.project_id), download); } diff --git a/apps/labrinth/src/routes/analytics.rs b/apps/labrinth/src/routes/analytics.rs index 36fe6febc..c8709d9e0 100644 --- a/apps/labrinth/src/routes/analytics.rs +++ b/apps/labrinth/src/routes/analytics.rs @@ -13,7 +13,7 @@ use actix_web::{HttpRequest, HttpResponse}; use serde::Deserialize; use sqlx::PgPool; use std::collections::HashMap; -use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr}; +use std::net::Ipv4Addr; use std::sync::Arc; use url::Url; @@ -39,16 +39,6 @@ pub const FILTERED_HEADERS: &[&str] = &[ "x-vercel-ip-latitude", "x-vercel-ip-country", ]; - -pub fn convert_to_ip_v6(src: &str) -> Result { - let ip_addr: IpAddr = src.parse()?; - - Ok(match ip_addr { - IpAddr::V4(x) => x.to_ipv6_mapped(), - IpAddr::V6(x) => x, - }) -} - #[derive(Deserialize)] pub struct UrlInput { url: String, @@ -101,7 +91,7 @@ pub async fn page_view_ingest( }) .collect::>(); - let ip = convert_to_ip_v6( + let ip = crate::util::ip::convert_to_ip_v6( if let Some(header) = headers.get("cf-connecting-ip") { header } else { diff --git a/apps/labrinth/src/routes/internal/admin.rs b/apps/labrinth/src/routes/internal/admin.rs index 411abb12d..68987d1da 100644 --- a/apps/labrinth/src/routes/internal/admin.rs +++ b/apps/labrinth/src/routes/internal/admin.rs @@ -108,7 +108,7 @@ pub async fn count_download( ApiError::InvalidInput("invalid download URL specified!".to_string()) })?; - let ip = crate::routes::analytics::convert_to_ip_v6(&download_body.ip) + let ip = crate::util::ip::convert_to_ip_v6(&download_body.ip) .unwrap_or_else(|_| Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped()); analytics_queue.add_download(Download { diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 31da67cdc..a362066ed 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1468,6 +1468,8 @@ pub struct NewAccount { pub sign_up_newsletter: Option, } +const NEW_ACCOUNT_LIMITER_NAMESPACE: &str = "new_account_ips"; + #[post("create")] pub async fn create_account_with_password( req: HttpRequest, @@ -1533,19 +1535,6 @@ pub async fn create_account_with_password( )); } - let flow = Flow::ConfirmEmail { - user_id, - confirm_email: new_account.email.clone(), - } - .insert(Duration::hours(24), &redis) - .await?; - - send_email_verify( - new_account.email.clone(), - flow, - &format!("Welcome to Modrinth, {}!", new_account.username), - )?; - crate::database::models::User { id: user_id, github_id: None, @@ -1577,6 +1566,49 @@ pub async fn create_account_with_password( let session = issue_session(req, user_id, &mut transaction, &redis).await?; let res = crate::models::sessions::Session::from(session, true, None); + // We limit each ip to creating 5 accounts in a six hour period + let ip = crate::util::ip::convert_to_ip_v6(&res.ip).map_err(|_| { + ApiError::InvalidInput("unable to parse user ip!".to_string()) + })?; + let stripped_ip = crate::util::ip::strip_ip(ip).to_string(); + + let mut conn = redis.connect().await?; + let uses = if let Some(res) = conn + .get(NEW_ACCOUNT_LIMITER_NAMESPACE, &stripped_ip) + .await? + { + res.parse::().unwrap_or(0) + } else { + 0 + }; + + if uses >= 5 { + return Err(ApiError::InvalidInput( + "IP has been rate-limited.".to_string(), + )); + } + + conn.set( + NEW_ACCOUNT_LIMITER_NAMESPACE, + &stripped_ip, + &(uses + 1).to_string(), + Some(60 * 60 * 6), + ) + .await?; + + let flow = Flow::ConfirmEmail { + user_id, + confirm_email: new_account.email.clone(), + } + .insert(Duration::hours(24), &redis) + .await?; + + send_email_verify( + new_account.email.clone(), + flow, + &format!("Welcome to Modrinth, {}!", new_account.username), + )?; + if new_account.sign_up_newsletter.unwrap_or(false) { sign_up_beehiiv(&new_account.email).await?; } diff --git a/apps/labrinth/src/util/ip.rs b/apps/labrinth/src/util/ip.rs new file mode 100644 index 000000000..9f4a4ccad --- /dev/null +++ b/apps/labrinth/src/util/ip.rs @@ -0,0 +1,25 @@ +use std::net::{AddrParseError, IpAddr, Ipv6Addr}; + +pub fn convert_to_ip_v6(src: &str) -> Result { + let ip_addr: IpAddr = src.parse()?; + + Ok(match ip_addr { + IpAddr::V4(x) => x.to_ipv6_mapped(), + IpAddr::V6(x) => x, + }) +} + +pub fn strip_ip(ip: Ipv6Addr) -> u64 { + if let Some(ip) = ip.to_ipv4_mapped() { + let octets = ip.octets(); + u64::from_be_bytes([ + octets[0], octets[1], octets[2], octets[3], 0, 0, 0, 0, + ]) + } else { + let octets = ip.octets(); + u64::from_be_bytes([ + octets[0], octets[1], octets[2], octets[3], octets[4], octets[5], + octets[6], octets[7], + ]) + } +} diff --git a/apps/labrinth/src/util/mod.rs b/apps/labrinth/src/util/mod.rs index b7271c706..94eefffd8 100644 --- a/apps/labrinth/src/util/mod.rs +++ b/apps/labrinth/src/util/mod.rs @@ -7,6 +7,7 @@ pub mod env; pub mod ext; pub mod guards; pub mod img; +pub mod ip; pub mod ratelimit; pub mod redis; pub mod routes;