forked from jmug/stoatchat
chore(thanos): strip down codebase to just API routes
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
use crate::util::result::Error;
|
||||
|
||||
use mongodb::bson::doc;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Validate, Serialize, Deserialize)]
|
||||
pub struct IdempotencyKey {
|
||||
#[validate(length(min = 1, max = 64))]
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
impl IdempotencyKey {
|
||||
// Backwards compatibility.
|
||||
// Issue #109
|
||||
pub fn consume_nonce(&mut self, v: Option<String>) {
|
||||
if let Some(v) = v {
|
||||
self.key = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref TOKEN_CACHE: std::sync::Mutex<lru::LruCache<String, ()>> = std::sync::Mutex::new(lru::LruCache::new(100));
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for IdempotencyKey {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
if let Some(key) = request
|
||||
.headers()
|
||||
.get("Idempotency-Key")
|
||||
.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 let Ok(mut cache) = TOKEN_CACHE.lock() {
|
||||
if cache.get(&idempotency.key).is_some() {
|
||||
return Outcome::Failure((Status::Conflict, Error::DuplicateNonce));
|
||||
}
|
||||
|
||||
cache.put(idempotency.key.clone(), ());
|
||||
return Outcome::Success(idempotency);
|
||||
}
|
||||
}
|
||||
|
||||
Outcome::Success(IdempotencyKey { key: ulid::Ulid::new().to_string() })
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod result;
|
||||
pub mod variables;
|
||||
pub mod ratelimit;
|
||||
// pub mod result;
|
||||
// pub mod variables;
|
||||
// pub mod ratelimit;
|
||||
pub mod regex;
|
||||
pub mod idempotency;
|
||||
// pub mod idempotency;
|
||||
@@ -1,180 +0,0 @@
|
||||
use rocket::{async_trait, http::Status, request::{Outcome, FromRequest}, response};
|
||||
use std::{collections::{HashMap, hash_map::DefaultHasher}, time};
|
||||
use crate::{database::User, util::result::Error};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[inline]
|
||||
fn now() -> f64 {
|
||||
// will this ever actually panic?
|
||||
time::SystemTime::now().duration_since(time::SystemTime::UNIX_EPOCH).unwrap().as_secs_f64()
|
||||
}
|
||||
|
||||
struct Ratelimit {
|
||||
pub rate: u8,
|
||||
pub per: u8,
|
||||
window: f64,
|
||||
tokens: u8,
|
||||
pub last: f64,
|
||||
}
|
||||
|
||||
impl Ratelimit {
|
||||
pub fn new(rate: u8, per: u8) -> Self {
|
||||
Self {
|
||||
rate,
|
||||
per,
|
||||
window: 0f64,
|
||||
tokens: rate,
|
||||
last: 0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tokens(&self, current: Option<f64>) -> u8 {
|
||||
let current = current.unwrap_or(now());
|
||||
if current > (self.window + self.per as f64) {
|
||||
self.rate
|
||||
} else {
|
||||
self.tokens
|
||||
}
|
||||
}
|
||||
|
||||
fn update_ratelimit(&mut self) -> Option<f64> {
|
||||
let current = now();
|
||||
self.last = current;
|
||||
|
||||
self.tokens = self.get_tokens(Some(current));
|
||||
|
||||
if self.tokens == self.rate {
|
||||
self.window = current
|
||||
}
|
||||
|
||||
if self.tokens == 0 {
|
||||
return Some(self.per as f64 - (current - self.window))
|
||||
}
|
||||
|
||||
self.tokens -= 1;
|
||||
|
||||
if self.tokens == 0 {
|
||||
self.window = current
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Ratelimit {
|
||||
fn clone(&self) -> Ratelimit {
|
||||
Ratelimit::new(self.rate, self.per)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RatelimitMapping {
|
||||
cache: HashMap<u64, Ratelimit>,
|
||||
cooldown: Ratelimit
|
||||
}
|
||||
|
||||
impl RatelimitMapping {
|
||||
pub fn new(rate: u8, per: u8) -> Self {
|
||||
RatelimitMapping {
|
||||
cache: HashMap::new(),
|
||||
cooldown: Ratelimit::new(rate, per)
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_cache(&mut self) {
|
||||
let current = now();
|
||||
self.cache.retain(|_, v| current < v.last + v.per as f64);
|
||||
}
|
||||
|
||||
pub fn get_bucket(&mut self, key: u64) -> &mut Ratelimit {
|
||||
self.verify_cache();
|
||||
self.cache.entry(key).or_insert(self.cooldown.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Ratelimiter {
|
||||
bucket: u64,
|
||||
limit: u8,
|
||||
remaining: u8,
|
||||
reset: f64
|
||||
}
|
||||
|
||||
pub struct RatelimitState(Arc<Mutex<HashMap<&'static str, RatelimitMapping>>>);
|
||||
|
||||
impl RatelimitState {
|
||||
pub fn new() -> Self {
|
||||
let mut hashmap = HashMap::new();
|
||||
hashmap.insert("message_send", RatelimitMapping::new(10, 10));
|
||||
let arc = Arc::new(Mutex::new(hashmap));
|
||||
RatelimitState(arc)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for Ratelimiter {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let res = request.local_cache_async(async {
|
||||
if let Some(route) = request.route() {
|
||||
if let Some(route_name) = &route.name {
|
||||
let user = request.guard::<User>().await.unwrap();
|
||||
|
||||
let state = request.guard::<&rocket::State<RatelimitState>>().await.unwrap().inner();
|
||||
let arc = Arc::clone(&state.0);
|
||||
let mut mutex = arc.lock().unwrap();
|
||||
let mapping = mutex.get_mut(route_name.as_ref()).unwrap();
|
||||
|
||||
// create a unique key tied to the user id and route they use
|
||||
let key = format!("{}:{}:{}", user.id, route.method.as_str(), route.uri.as_str());
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
key.hash(&mut hasher);
|
||||
let hashed = hasher.finish();
|
||||
|
||||
let bucket = mapping.get_bucket(hashed);
|
||||
let ret = bucket.update_ratelimit();
|
||||
|
||||
if let Some(retry_after) = ret {
|
||||
Err(Error::TooManyRequests { retry_after })
|
||||
} else {
|
||||
Ok(Ratelimiter {
|
||||
bucket: hashed,
|
||||
limit: bucket.rate,
|
||||
remaining: bucket.get_tokens(None),
|
||||
reset: bucket.window + (bucket.per as f64)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}).await;
|
||||
|
||||
match res {
|
||||
Ok(rl) => Outcome::Success(*rl),
|
||||
Err(e) => Outcome::Failure((Status::TooManyRequests, e.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RatelimitResponse<R>(pub R);
|
||||
|
||||
impl<'r, 'o: 'r, R: response::Responder<'r, 'o>> response::Responder<'r, 'o> for RatelimitResponse<R> {
|
||||
fn respond_to(self, request: &'r rocket::Request<'_>) -> response::Result<'o> {
|
||||
let res: &Result<Ratelimiter, Error> = request.local_cache(|| unreachable!());
|
||||
let ratelimiter = res.as_ref().unwrap();
|
||||
|
||||
Ok(response::Builder::new(rocket::Response::new())
|
||||
.raw_header("x-ratelimit-bucket", ratelimiter.bucket.to_string())
|
||||
.raw_header("x-ratelimit-limit", ratelimiter.limit.to_string())
|
||||
.raw_header("x-ratelimit-remaining", ratelimiter.remaining.to_string())
|
||||
.raw_header("x-ratelimit-reset", ratelimiter.reset.to_string())
|
||||
.merge(self.0.respond_to(request)?)
|
||||
.finalize())
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
use json;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::request::Request;
|
||||
use rocket::response::{self, Responder, Response};
|
||||
use serde::Serialize;
|
||||
use std::io::Cursor;
|
||||
use validator::ValidationErrors;
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Error {
|
||||
LabelMe,
|
||||
|
||||
// ? Onboarding related errors.
|
||||
AlreadyOnboarded,
|
||||
|
||||
// ? User related errors.
|
||||
UsernameTaken,
|
||||
UnknownUser,
|
||||
AlreadyFriends,
|
||||
AlreadySentRequest,
|
||||
Blocked,
|
||||
BlockedByOther,
|
||||
NotFriends,
|
||||
|
||||
// ? Channel related errors.
|
||||
UnknownChannel,
|
||||
UnknownAttachment,
|
||||
UnknownMessage,
|
||||
CannotEditMessage,
|
||||
CannotJoinCall,
|
||||
TooManyAttachments,
|
||||
TooManyReplies,
|
||||
EmptyMessage,
|
||||
CannotRemoveYourself,
|
||||
GroupTooLarge {
|
||||
max: usize,
|
||||
},
|
||||
AlreadyInGroup,
|
||||
NotInGroup,
|
||||
|
||||
// ? Server related errors.
|
||||
UnknownServer,
|
||||
InvalidRole,
|
||||
Banned,
|
||||
TooManyServers{
|
||||
max: usize,
|
||||
},
|
||||
|
||||
// ? Bot related errors.
|
||||
ReachedMaximumBots,
|
||||
IsBot,
|
||||
BotIsPrivate,
|
||||
|
||||
// ? General errors.
|
||||
TooManyIds,
|
||||
FailedValidation {
|
||||
error: ValidationErrors,
|
||||
},
|
||||
DatabaseError {
|
||||
operation: &'static str,
|
||||
with: &'static str,
|
||||
},
|
||||
InternalError,
|
||||
MissingPermission,
|
||||
InvalidOperation,
|
||||
InvalidCredentials,
|
||||
DuplicateNonce,
|
||||
VosoUnavailable,
|
||||
NotFound,
|
||||
NoEffect,
|
||||
TooManyRequests {
|
||||
retry_after: f64
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EmptyResponse;
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
impl<'r> Responder<'r, 'static> for EmptyResponse {
|
||||
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
||||
Response::build()
|
||||
.status(rocket::http::Status { code: 204 })
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP response builder for Error enum
|
||||
impl<'r> Responder<'r, 'static> for Error {
|
||||
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
||||
let status = match self {
|
||||
Error::LabelMe => Status::InternalServerError,
|
||||
|
||||
Error::AlreadyOnboarded => Status::Forbidden,
|
||||
|
||||
Error::UnknownUser => Status::NotFound,
|
||||
Error::UsernameTaken => Status::Conflict,
|
||||
Error::AlreadyFriends => Status::Conflict,
|
||||
Error::AlreadySentRequest => Status::Conflict,
|
||||
Error::Blocked => Status::Conflict,
|
||||
Error::BlockedByOther => Status::Forbidden,
|
||||
Error::NotFriends => Status::Forbidden,
|
||||
|
||||
Error::UnknownChannel => Status::NotFound,
|
||||
Error::UnknownMessage => Status::NotFound,
|
||||
Error::UnknownAttachment => Status::BadRequest,
|
||||
Error::CannotEditMessage => Status::Forbidden,
|
||||
Error::CannotJoinCall => Status::BadRequest,
|
||||
Error::TooManyAttachments => Status::BadRequest,
|
||||
Error::TooManyReplies => Status::BadRequest,
|
||||
Error::EmptyMessage => Status::UnprocessableEntity,
|
||||
Error::CannotRemoveYourself => Status::BadRequest,
|
||||
Error::GroupTooLarge { .. } => Status::Forbidden,
|
||||
Error::AlreadyInGroup => Status::Conflict,
|
||||
Error::NotInGroup => Status::NotFound,
|
||||
|
||||
Error::UnknownServer => Status::NotFound,
|
||||
Error::InvalidRole => Status::NotFound,
|
||||
Error::Banned => Status::Forbidden,
|
||||
Error::TooManyServers { .. } => Status::Forbidden,
|
||||
|
||||
Error::ReachedMaximumBots => Status::BadRequest,
|
||||
Error::IsBot => Status::BadRequest,
|
||||
Error::BotIsPrivate => Status::Forbidden,
|
||||
|
||||
Error::FailedValidation { .. } => Status::UnprocessableEntity,
|
||||
Error::DatabaseError { .. } => Status::InternalServerError,
|
||||
Error::InternalError => Status::InternalServerError,
|
||||
Error::MissingPermission => Status::Forbidden,
|
||||
Error::InvalidOperation => Status::BadRequest,
|
||||
Error::TooManyIds => Status::BadRequest,
|
||||
Error::InvalidCredentials => Status::Forbidden,
|
||||
Error::DuplicateNonce => Status::Conflict,
|
||||
Error::VosoUnavailable => Status::BadRequest,
|
||||
Error::NotFound => Status::NotFound,
|
||||
Error::NoEffect => Status::Ok,
|
||||
Error::TooManyRequests { .. } => Status::TooManyRequests,
|
||||
};
|
||||
|
||||
// Serialize the error data structure into JSON.
|
||||
let string = json!(self).to_string();
|
||||
|
||||
// Build and send the request.
|
||||
Response::build()
|
||||
.sized_body(string.len(), Cursor::new(string))
|
||||
.header(ContentType::new("application", "json"))
|
||||
.status(status)
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use log::warn;
|
||||
|
||||
lazy_static! {
|
||||
// Application Settings
|
||||
pub static ref MONGO_URI: String =
|
||||
env::var("REVOLT_MONGO_URI").expect("Missing REVOLT_MONGO_URI environment variable.");
|
||||
pub static ref REDIS_URI: String =
|
||||
env::var("REVOLT_REDIS_URI").expect("Missing REVOLT_REDIS_URI environment variable.");
|
||||
pub static ref WS_HOST: String =
|
||||
env::var("REVOLT_WS_HOST").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
|
||||
pub static ref PUBLIC_URL: String =
|
||||
env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable.");
|
||||
pub static ref APP_URL: String =
|
||||
env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable.");
|
||||
pub static ref EXTERNAL_WS_URL: String =
|
||||
env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable.");
|
||||
|
||||
pub static ref AUTUMN_URL: String =
|
||||
env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
|
||||
pub static ref JANUARY_URL: String =
|
||||
env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
|
||||
pub static ref VOSO_URL: String =
|
||||
env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
|
||||
pub static ref VOSO_WS_HOST: String =
|
||||
env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string());
|
||||
pub static ref VOSO_MANAGE_TOKEN: String =
|
||||
env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string());
|
||||
|
||||
pub static ref HCAPTCHA_KEY: String =
|
||||
env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string());
|
||||
pub static ref HCAPTCHA_SITEKEY: String =
|
||||
env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string());
|
||||
pub static ref VAPID_PRIVATE_KEY: String =
|
||||
env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable.");
|
||||
pub static ref VAPID_PUBLIC_KEY: String =
|
||||
env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable.");
|
||||
|
||||
// Application Flags
|
||||
pub static ref INVITE_ONLY: bool = env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1");
|
||||
pub static ref USE_EMAIL: bool = env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or(
|
||||
env::var("REVOLT_SMTP_HOST").is_ok()
|
||||
&& env::var("REVOLT_SMTP_USERNAME").is_ok()
|
||||
&& env::var("REVOLT_SMTP_PASSWORD").is_ok()
|
||||
&& env::var("REVOLT_SMTP_FROM").is_ok(),
|
||||
|v| v == *"1"
|
||||
);
|
||||
pub static ref USE_HCAPTCHA: bool = env::var("REVOLT_HCAPTCHA_KEY").is_ok();
|
||||
pub static ref USE_AUTUMN: bool = env::var("AUTUMN_PUBLIC_URL").is_ok();
|
||||
pub static ref USE_JANUARY: bool = env::var("JANUARY_PUBLIC_URL").is_ok();
|
||||
pub static ref USE_VOSO: bool = env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok();
|
||||
|
||||
// SMTP Settings
|
||||
pub static ref SMTP_HOST: String =
|
||||
env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string());
|
||||
pub static ref SMTP_USERNAME: String =
|
||||
env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string());
|
||||
pub static ref SMTP_PASSWORD: String =
|
||||
env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string());
|
||||
pub static ref SMTP_FROM: String = env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_string());
|
||||
|
||||
// Application Logic Settings
|
||||
pub static ref MAX_GROUP_SIZE: usize =
|
||||
env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap();
|
||||
pub static ref MAX_BOT_COUNT: usize =
|
||||
env::var("REVOLT_MAX_BOT_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap();
|
||||
pub static ref MAX_EMBED_COUNT: usize =
|
||||
env::var("REVOLT_MAX_EMBED_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap();
|
||||
pub static ref MAX_SERVER_COUNT: usize =
|
||||
env::var("REVOLT_MAX_SERVER_COUNT").unwrap_or_else(|_| "100".to_string()).parse().unwrap();
|
||||
pub static ref EARLY_ADOPTER_BADGE: i64 =
|
||||
env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap();
|
||||
}
|
||||
|
||||
pub fn preflight_checks() {
|
||||
format!("{}", *APP_URL);
|
||||
format!("{}", *MONGO_URI);
|
||||
format!("{}", *PUBLIC_URL);
|
||||
format!("{}", *EXTERNAL_WS_URL);
|
||||
|
||||
format!("{}", *VAPID_PRIVATE_KEY);
|
||||
format!("{}", *VAPID_PUBLIC_KEY);
|
||||
|
||||
if *USE_EMAIL == false {
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
|
||||
panic!("Running in production without email is not recommended, set REVOLT_UNSAFE_NO_EMAIL=1 to override.");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
warn!("No SMTP settings specified! Remember to configure email.");
|
||||
}
|
||||
|
||||
if *USE_HCAPTCHA == false {
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
|
||||
panic!("Running in production without CAPTCHA is not recommended, set REVOLT_UNSAFE_NO_CAPTCHA=1 to override.");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
warn!("No Captcha key specified! Remember to add hCaptcha key.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user