diff --git a/Cargo.lock b/Cargo.lock index b40286da..506c62d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -72,6 +83,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "ansi_term" version = "0.12.1" @@ -515,7 +532,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60a2c7c80a7850b56df4b8e98e8e4932c34877b8add4f13e8350499cc1e4572" dependencies = [ - "ahash", + "ahash 0.7.6", "base64 0.13.0", "chrono", "hex", @@ -1343,7 +1360,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] @@ -1352,6 +1369,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + [[package]] name = "heck" version = "0.4.0" @@ -1815,6 +1842,15 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "lru" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eedb2bdbad7e0634f83989bf596f497b070130daaa398ab22d84c39e266deec5" +dependencies = [ + "hashbrown 0.14.0", +] + [[package]] name = "lru-cache" version = "0.1.2" @@ -1872,7 +1908,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e52eb6380b6d2a10eb3434aec0885374490f5b82c8aaf5cd487a183c98be834" dependencies = [ - "ahash", + "ahash 0.7.6", "metrics-macros", ] @@ -2906,6 +2942,7 @@ dependencies = [ "indexmap", "iso8601-timestamp 0.2.11", "log", + "lru 0.11.0", "mongodb", "nanoid", "once_cell", @@ -2943,7 +2980,7 @@ dependencies = [ "lettre", "linkify 0.6.0", "log", - "lru", + "lru 0.7.6", "nanoid", "num_enum 0.5.7", "once_cell", @@ -2957,6 +2994,7 @@ dependencies = [ "revolt_rocket_okapi", "rocket", "rocket_authifier", + "rocket_cors", "rocket_empty", "rocket_prometheus", "schemars", @@ -3032,7 +3070,7 @@ dependencies = [ "iso8601-timestamp 0.1.10", "linkify 0.8.1", "log", - "lru", + "lru 0.7.6", "mongodb", "nanoid", "num_enum 0.5.7", diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index 8a255e64..ce5044a6 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -32,6 +32,7 @@ revolt-permissions = { version = "0.6.7", path = "../permissions", features = [ # Utility log = "0.4" +lru = "0.11.0" rand = "0.8.5" ulid = "1.0.0" nanoid = "0.4.0" diff --git a/crates/quark/src/web/idempotency.rs b/crates/core/database/src/util/idempotency.rs similarity index 79% rename from crates/quark/src/web/idempotency.rs rename to crates/core/database/src/util/idempotency.rs index 49004a3a..1db20993 100644 --- a/crates/quark/src/web/idempotency.rs +++ b/crates/core/database/src/util/idempotency.rs @@ -1,6 +1,9 @@ -use crate::{Error, Result}; +use std::num::NonZeroUsize; + +use revolt_result::{create_error, Error, Result}; use async_std::sync::Mutex; +use once_cell::sync::Lazy; use revolt_rocket_okapi::gen::OpenApiGenerator; use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue}; @@ -8,16 +11,14 @@ use rocket::http::Status; use rocket::request::{FromRequest, Outcome}; use schemars::schema::{InstanceType, SchemaObject, SingleOrVec}; use serde::{Deserialize, Serialize}; -use validator::Validate; -use once_cell::sync::Lazy; -#[derive(Validate, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] pub struct IdempotencyKey { - #[validate(length(min = 1, max = 64))] key: String, } -static TOKEN_CACHE: Lazy>> = Lazy::new(|| Mutex::new(lru::LruCache::new(100))); +static TOKEN_CACHE: Lazy>> = + Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap()))); impl IdempotencyKey { // Backwards compatibility. @@ -26,7 +27,7 @@ impl IdempotencyKey { if let Some(v) = v { let mut cache = TOKEN_CACHE.lock().await; if cache.get(&v).is_some() { - return Err(Error::DuplicateNonce); + return Err(create_error!(DuplicateNonce)); } cache.put(v.clone(), ()); @@ -81,14 +82,19 @@ impl<'r> FromRequest<'r> for IdempotencyKey { .next() .map(|k| k.to_string()) { - let idempotency = IdempotencyKey { key }; - if let Err(error) = idempotency.validate() { - return Outcome::Failure((Status::BadRequest, Error::FailedValidation { error })); + if key.len() > 64 { + return Outcome::Failure(( + Status::BadRequest, + create_error!(FailedValidation { + error: "idempotency key too long".to_string(), + }), + )); } + let idempotency = IdempotencyKey { key }; let mut cache = TOKEN_CACHE.lock().await; if cache.get(&idempotency.key).is_some() { - return Outcome::Failure((Status::Conflict, Error::DuplicateNonce)); + return Outcome::Failure((Status::Conflict, create_error!(DuplicateNonce))); } cache.put(idempotency.key.clone(), ()); diff --git a/crates/core/database/src/util/mod.rs b/crates/core/database/src/util/mod.rs index e7efee98..8f4f4c76 100644 --- a/crates/core/database/src/util/mod.rs +++ b/crates/core/database/src/util/mod.rs @@ -1,3 +1,4 @@ pub mod bridge; +pub mod idempotency; pub mod permissions; pub mod reference; diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index d4e663f6..cee8deb5 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -51,6 +51,7 @@ lettre = "0.10.0-alpha.4" rocket = { version = "0.5.0-rc.2", default-features = false, features = [ "json", ] } +rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" } rocket_empty = { version = "0.1.1", features = ["schema"] } rocket_authifier = { version = "1.0.7" } rocket_prometheus = "0.10.0-rc.3" diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 35654a04..d8a2b3b3 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -8,8 +8,10 @@ extern crate serde_json; pub mod routes; pub mod util; +use rocket_cors::{AllowedOrigins, CorsOptions}; use rocket_prometheus::PrometheusMetrics; use std::net::Ipv4Addr; +use std::str::FromStr; use async_std::channel::unbounded; use revolt_quark::authifier::{Authifier, AuthifierEvent}; @@ -62,7 +64,27 @@ async fn rocket() -> _ { async_std::task::spawn(revolt_quark::tasks::start_workers(legacy_db.clone())); // Configure CORS - let cors = revolt_quark::web::cors::new(); + let cors = CorsOptions { + allowed_origins: AllowedOrigins::All, + allowed_methods: [ + "Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch", + ] + .iter() + .map(|s| FromStr::from_str(s).unwrap()) + .collect(), + ..Default::default() + } + .to_cors() + .expect("Failed to create CORS."); + + // Configure Swagger + let swagger = revolt_rocket_okapi::swagger_ui::make_swagger_ui( + &revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { + url: "../openapi.json".to_owned(), + ..Default::default() + }, + ) + .into(); // Configure Rocket let rocket = rocket::build(); @@ -71,14 +93,14 @@ async fn rocket() -> _ { routes::mount(rocket) .attach(prometheus.clone()) .mount("/metrics", prometheus) - .mount("/", revolt_quark::web::cors::catch_all_options_routes()) - .mount("/", revolt_quark::web::ratelimiter::routes()) - .mount("/swagger/", revolt_quark::web::swagger::routes()) + .mount("/", rocket_cors::catch_all_options_routes()) + .mount("/", util::ratelimiter::routes()) + .mount("/swagger/", swagger) .manage(authifier) .manage(db) .manage(legacy_db) .manage(cors.clone()) - .attach(revolt_quark::web::ratelimiter::RatelimitFairing) + .attach(util::ratelimiter::RatelimitFairing) .attach(cors) .configure(rocket::Config { limits: rocket::data::Limits::default().limit("string", 5.megabytes()), diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index 455327e7..04034599 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -1,8 +1,8 @@ +use revolt_database::util::idempotency::IdempotencyKey; use revolt_quark::{ models::{message::DataMessageSend, Message, User}, perms, types::push::MessageAuthor, - web::idempotency::IdempotencyKey, Db, Error, Permission, Ref, Result, }; diff --git a/crates/delta/src/routes/customisation/emoji_create.rs b/crates/delta/src/routes/customisation/emoji_create.rs index 25ba7bf5..dcf28164 100644 --- a/crates/delta/src/routes/customisation/emoji_create.rs +++ b/crates/delta/src/routes/customisation/emoji_create.rs @@ -1,3 +1,5 @@ +use once_cell::sync::Lazy; +use regex::Regex; use revolt_quark::models::emoji::EmojiParent; use revolt_quark::models::{Emoji, File, User}; use revolt_quark::variables::delta::MAX_EMOJI_COUNT; @@ -5,10 +7,13 @@ use revolt_quark::{perms, Db, Error, Permission, Result}; use serde::Deserialize; use validator::Validate; -use crate::util::regex::RE_EMOJI; - use rocket::serde::json::Json; +/// Regex for valid emoji names +/// +/// Alphanumeric and underscores +pub static RE_EMOJI: Lazy = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap()); + /// # Emoji Data #[derive(Validate, Deserialize, JsonSchema)] pub struct DataCreateEmoji { @@ -57,7 +62,9 @@ pub async fn create_emoji( // ! FIXME: hardcoded upper limit let emojis = db.fetch_emoji_by_parent_id(&server.id).await?; if emojis.len() > *MAX_EMOJI_COUNT { - return Err(Error::TooManyEmoji { max: *MAX_EMOJI_COUNT }); + return Err(Error::TooManyEmoji { + max: *MAX_EMOJI_COUNT, + }); } } EmojiParent::Detached => return Err(Error::InvalidOperation), diff --git a/crates/delta/src/routes/onboard/complete.rs b/crates/delta/src/routes/onboard/complete.rs index 150fdf7d..51b43d7c 100644 --- a/crates/delta/src/routes/onboard/complete.rs +++ b/crates/delta/src/routes/onboard/complete.rs @@ -1,4 +1,5 @@ -use crate::util::regex::RE_USERNAME; +use once_cell::sync::Lazy; +use regex::Regex; use revolt_database::{Database, User}; use revolt_models::v0; use revolt_quark::authifier::models::Session; @@ -8,6 +9,12 @@ use rocket::{serde::json::Json, State}; use serde::{Deserialize, Serialize}; use validator::Validate; +/// Regex for valid usernames +/// +/// Block zero width space +/// Block lookalike characters +pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); + /// # New User Data #[derive(Validate, Serialize, Deserialize, JsonSchema)] pub struct DataOnboard { diff --git a/crates/delta/src/routes/users/change_username.rs b/crates/delta/src/routes/users/change_username.rs index 2941a2eb..8d469f4c 100644 --- a/crates/delta/src/routes/users/change_username.rs +++ b/crates/delta/src/routes/users/change_username.rs @@ -1,9 +1,16 @@ -use crate::util::regex::RE_USERNAME; +use once_cell::sync::Lazy; +use regex::Regex; use revolt_quark::{authifier::models::Account, models::User, Database, Error, Result}; use rocket::{serde::json::Json, State}; use serde::{Deserialize, Serialize}; use validator::Validate; +/// Regex for valid usernames +/// +/// Block zero width space +/// Block lookalike characters +pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); + /// # Username Information #[derive(Validate, Serialize, Deserialize, JsonSchema)] pub struct DataChangeUsername { diff --git a/crates/delta/src/routes/users/edit_user.rs b/crates/delta/src/routes/users/edit_user.rs index c1c2ba90..644a727d 100644 --- a/crates/delta/src/routes/users/edit_user.rs +++ b/crates/delta/src/routes/users/edit_user.rs @@ -1,3 +1,5 @@ +use once_cell::sync::Lazy; +use regex::Regex; use revolt_quark::models::user::{FieldsUser, PartialUser, User}; use revolt_quark::models::File; use revolt_quark::{Database, Error, Ref, Result}; @@ -8,7 +10,11 @@ use rocket::State; use serde::{Deserialize, Serialize}; use validator::Validate; -use crate::util::regex::RE_DISPLAY_NAME; +/// Regex for valid display names +/// +/// Block zero width space +/// Block newline and carriage return +pub static RE_DISPLAY_NAME: Lazy = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap()); /// # Profile Data #[derive(Validate, Serialize, Deserialize, Debug, JsonSchema)] diff --git a/crates/delta/src/routes/webhooks/webhook_execute.rs b/crates/delta/src/routes/webhooks/webhook_execute.rs index 0ec30299..fb31fa56 100644 --- a/crates/delta/src/routes/webhooks/webhook_execute.rs +++ b/crates/delta/src/routes/webhooks/webhook_execute.rs @@ -1,8 +1,10 @@ -use revolt_database::{util::reference::Reference, Database}; +use revolt_database::{ + util::{idempotency::IdempotencyKey, reference::Reference}, + Database, +}; use revolt_quark::{ models::message::{DataMessageSend, Message}, types::push::MessageAuthor, - web::idempotency::IdempotencyKey, Db, Error, Result, }; use rocket::{serde::json::Json, State}; diff --git a/crates/delta/src/util/mod.rs b/crates/delta/src/util/mod.rs index 1d9dab60..8f8380fa 100644 --- a/crates/delta/src/util/mod.rs +++ b/crates/delta/src/util/mod.rs @@ -1 +1 @@ -pub mod regex; +pub mod ratelimiter; diff --git a/crates/quark/src/web/ratelimiter.rs b/crates/delta/src/util/ratelimiter.rs similarity index 97% rename from crates/quark/src/web/ratelimiter.rs rename to crates/delta/src/util/ratelimiter.rs index 58f581a3..814cf3e9 100644 --- a/crates/quark/src/web/ratelimiter.rs +++ b/crates/delta/src/util/ratelimiter.rs @@ -1,14 +1,9 @@ -//! Pulled from lightspeed-tv/backend. -//! -//! This will be replaced again in the near future since -//! I don't want duplication between two different projects. - use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; use std::ops::Add; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use crate::authifier::models::Session; +use revolt_quark::authifier::models::Session; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::uri::Origin; use rocket::http::{Method, Status}; diff --git a/crates/delta/src/util/regex.rs b/crates/delta/src/util/regex.rs deleted file mode 100644 index 7c71e4b1..00000000 --- a/crates/delta/src/util/regex.rs +++ /dev/null @@ -1,19 +0,0 @@ -use once_cell::sync::Lazy; -use regex::Regex; - -/// Regex for valid display names -/// -/// Block zero width space -/// Block newline and carriage return -pub static RE_DISPLAY_NAME: Lazy = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap()); - -/// Regex for valid usernames -/// -/// Block zero width space -/// Block lookalike characters -pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); - -/// Regex for valid emoji names -/// -/// Alphanumeric and underscores -pub static RE_EMOJI: Lazy = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap()); diff --git a/crates/quark/src/impl/generic/channels/channel.rs b/crates/quark/src/impl/generic/channels/channel.rs index b9b135e8..d020585f 100644 --- a/crates/quark/src/impl/generic/channels/channel.rs +++ b/crates/quark/src/impl/generic/channels/channel.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use revolt_database::util::idempotency::IdempotencyKey; use ulid::Ulid; use crate::{ @@ -11,8 +12,7 @@ use crate::{ }, tasks::{ack::AckEvent, process_embeds}, types::push::MessageAuthor, - variables::delta::{MAX_ATTACHMENT_COUNT, MAX_REPLY_COUNT, MAX_EMBED_COUNT}, - web::idempotency::IdempotencyKey, + variables::delta::{MAX_ATTACHMENT_COUNT, MAX_EMBED_COUNT, MAX_REPLY_COUNT}, Database, Error, OverrideField, Ref, Result, }; @@ -413,7 +413,10 @@ impl Channel { ) -> Result { Message::validate_sum(&data.content, data.embeds.as_deref().unwrap_or_default())?; - idempotency.consume_nonce(data.nonce).await?; + idempotency + .consume_nonce(data.nonce) + .await + .map_err(|_| Error::InvalidOperation)?; // Check the message is not empty if (data.content.as_ref().map_or(true, |v| v.is_empty())) @@ -497,16 +500,24 @@ impl Channel { // Add attachments to message. let mut attachments = vec![]; - if data.attachments.as_ref().is_some_and(|v| v.len() > *MAX_ATTACHMENT_COUNT) { + if data + .attachments + .as_ref() + .is_some_and(|v| v.len() > *MAX_ATTACHMENT_COUNT) + { return Err(Error::TooManyAttachments { max: *MAX_ATTACHMENT_COUNT, }); } - if data.embeds.as_ref().is_some_and(|v| v.len() > *MAX_EMBED_COUNT) { + if data + .embeds + .as_ref() + .is_some_and(|v| v.len() > *MAX_EMBED_COUNT) + { return Err(Error::TooManyEmbeds { - max: *MAX_EMBED_COUNT - }) + max: *MAX_EMBED_COUNT, + }); } for attachment_id in data.attachments.as_deref().unwrap_or_default() { diff --git a/crates/quark/src/web/cors.rs b/crates/quark/src/web/cors.rs deleted file mode 100644 index 6ce5ec0d..00000000 --- a/crates/quark/src/web/cors.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::str::FromStr; - -pub use rocket_cors::catch_all_options_routes; -use rocket_cors::{AllowedOrigins, Cors}; - -pub fn new() -> Cors { - rocket_cors::CorsOptions { - allowed_origins: AllowedOrigins::All, - allowed_methods: [ - "Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch", - ] - .iter() - .map(|s| FromStr::from_str(s).unwrap()) - .collect(), - ..Default::default() - } - .to_cors() - .expect("Failed to create CORS.") -} diff --git a/crates/quark/src/web/mod.rs b/crates/quark/src/web/mod.rs index cbe9a41c..b9a1cff7 100644 --- a/crates/quark/src/web/mod.rs +++ b/crates/quark/src/web/mod.rs @@ -1,10 +1,5 @@ use crate::Database; use rocket::State; -pub mod cors; -pub mod idempotency; -pub mod ratelimiter; -pub mod swagger; - pub use rocket_empty::EmptyResponse; pub type Db = State; diff --git a/crates/quark/src/web/swagger.rs b/crates/quark/src/web/swagger.rs deleted file mode 100644 index fcfe3a91..00000000 --- a/crates/quark/src/web/swagger.rs +++ /dev/null @@ -1,9 +0,0 @@ -use rocket::Route; - -pub fn routes() -> Vec { - revolt_rocket_okapi::swagger_ui::make_swagger_ui(&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { - url: "../openapi.json".to_owned(), - ..Default::default() - }) - .into() -}