/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use std::{borrow::Cow, fmt::Display}; #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] pub enum RequestLimitError { #[serde(rename = "maxSizeRequest")] SizeRequest, #[serde(rename = "maxSizeUpload")] SizeUpload, #[serde(rename = "maxCallsInRequest")] CallsIn, #[serde(rename = "maxConcurrentRequests")] ConcurrentRequest, #[serde(rename = "maxConcurrentUpload")] ConcurrentUpload, } #[derive(Debug, serde::Serialize, serde::Deserialize)] pub enum RequestErrorType { #[serde(rename = "urn:ietf:params:jmap:error:unknownCapability")] UnknownCapability, #[serde(rename = "urn:ietf:params:jmap:error:notJSON")] NotJSON, #[serde(rename = "urn:ietf:params:jmap:error:notRequest")] NotRequest, #[serde(rename = "urn:ietf:params:jmap:error:limit")] Limit, #[serde(rename = "about:blank")] Other, } #[derive(Debug, Clone)] pub struct RateLimitPolicy { pub name: &'static str, pub limit: u64, pub remaining: u64, pub window: Option, pub reset: Option, pub unit: RateLimitUnit, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RateLimitUnit { Requests, ContentBytes, ConcurrentRequests, } impl RateLimitUnit { pub fn as_str(self) -> &'static str { match self { RateLimitUnit::Requests => "requests", RateLimitUnit::ContentBytes => "content-bytes", RateLimitUnit::ConcurrentRequests => "concurrent-requests", } } } #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct RequestError<'x> { #[serde(rename = "type")] pub p_type: RequestErrorType, pub status: u16, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, pub detail: Cow<'x, str>, #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, #[serde(skip)] pub rate_limit: Vec, #[serde(skip)] pub retry_after: Option, } impl<'x> RequestError<'x> { pub fn blank( status: u16, title: impl Into>, detail: impl Into>, ) -> Self { RequestError { p_type: RequestErrorType::Other, status, title: Some(title.into()), detail: detail.into(), limit: None, rate_limit: Vec::new(), retry_after: None, } } pub fn with_rate_limit(mut self, policy: RateLimitPolicy) -> Self { if let Some(reset) = policy.reset && self.retry_after.is_none_or(|r| reset > r) { self.retry_after = Some(reset); } self.rate_limit.push(policy); self } pub fn with_retry_after(mut self, seconds: u64) -> Self { if self.retry_after.is_none_or(|r| seconds > r) { self.retry_after = Some(seconds); } self } pub fn internal_server_error() -> Self { RequestError::blank( 500, "Internal Server Error", concat!( "There was a problem while processing your request. ", "Please contact the system administrator if this problem persists." ), ) } pub fn unavailable() -> Self { RequestError::blank( 503, "Temporarily Unavailable", concat!( "There was a temporary problem while processing your request. ", "Please try again in a few moments." ), ) } pub fn invalid_parameters() -> Self { RequestError::blank( 400, "Invalid Parameters", "One or multiple parameters could not be parsed.", ) } pub fn forbidden() -> Self { RequestError::blank( 403, "Forbidden", "You do not have enough permissions to access this resource.", ) } pub fn over_blob_quota(max_files: usize, max_bytes: usize) -> Self { RequestError::blank( 429, "Quota exceeded", format!( "You have exceeded the blob upload quota of {} files or {} bytes.", max_files, max_bytes ), ) } pub fn over_quota() -> Self { RequestError::blank( 403, "Quota exceeded", "You have exceeded your account quota.", ) } pub fn tenant_over_quota() -> Self { RequestError::blank( 403, "Tenant quota exceeded", "Your organization has exceeded its quota.", ) } pub fn too_many_requests() -> Self { RequestError::blank( 429, "Too Many Requests", "Your request has been rate limited. Please try again in a few seconds.", ) } pub fn too_many_auth_attempts() -> Self { RequestError::blank( 429, "Too Many Authentication Attempts", "Your request has been rate limited. Please try again in a few minutes.", ) } pub fn limit(limit_type: RequestLimitError) -> Self { RequestError { p_type: RequestErrorType::Limit, status: 400, title: None, detail: match limit_type { RequestLimitError::SizeRequest => concat!( "The request is larger than the server ", "is willing to process." ), RequestLimitError::SizeUpload => concat!( "The uploaded file is larger than the server ", "is willing to process." ), RequestLimitError::CallsIn => concat!( "The request exceeds the maximum number ", "of calls in a single request." ), RequestLimitError::ConcurrentRequest => concat!( "The request exceeds the maximum number ", "of concurrent requests." ), RequestLimitError::ConcurrentUpload => concat!( "The request exceeds the maximum number ", "of concurrent uploads." ), } .into(), limit: Some(limit_type), rate_limit: Vec::new(), retry_after: None, } } pub fn not_found() -> Self { RequestError::blank( 404, "Not Found", "The requested resource does not exist on this server.", ) } pub fn unauthorized() -> Self { RequestError::blank(401, "Unauthorized", "You have to authenticate first.") } pub fn unknown_capability(capability: &'_ str) -> RequestError<'_> { RequestError { p_type: RequestErrorType::UnknownCapability, limit: None, title: None, status: 400, detail: format!( concat!( "The Request object used capability ", "'{}', which is not supported", "by this server." ), capability ) .into(), rate_limit: Vec::new(), retry_after: None, } } pub fn not_json(detail: &'_ str) -> RequestError<'_> { RequestError { p_type: RequestErrorType::NotJSON, limit: None, title: None, status: 400, detail: format!("Failed to parse JSON: {detail}").into(), rate_limit: Vec::new(), retry_after: None, } } pub fn not_request(detail: impl Into>) -> RequestError<'x> { RequestError { p_type: RequestErrorType::NotRequest, limit: None, title: None, status: 400, detail: detail.into(), rate_limit: Vec::new(), retry_after: None, } } } impl RateLimitPolicy { pub fn new(name: &'static str, limit: u64) -> Self { RateLimitPolicy { name, limit, remaining: 0, window: None, reset: None, unit: RateLimitUnit::Requests, } } pub fn with_window(mut self, window: u64) -> Self { self.window = Some(window); self } pub fn with_reset(mut self, reset: u64) -> Self { self.reset = Some(reset); self } pub fn with_remaining(mut self, remaining: u64) -> Self { self.remaining = remaining; self } pub fn with_unit(mut self, unit: RateLimitUnit) -> Self { self.unit = unit; self } pub fn fmt_policy(&self, out: &mut String) { use std::fmt::Write; let _ = write!(out, "\"{}\";q={}", self.name, self.limit); if let Some(window) = self.window { let _ = write!(out, ";w={window}"); } if !matches!(self.unit, RateLimitUnit::Requests) { let _ = write!(out, ";qu=\"{}\"", self.unit.as_str()); } } pub fn fmt_state(&self, out: &mut String) { use std::fmt::Write; let _ = write!(out, "\"{}\";r={}", self.name, self.remaining); if let Some(reset) = self.reset { let _ = write!(out, ";t={reset}"); } } } impl<'x> RequestError<'x> { pub fn rate_limit_policy_header(&self) -> Option { if self.rate_limit.is_empty() { return None; } let mut out = String::new(); for (i, policy) in self.rate_limit.iter().enumerate() { if i > 0 { out.push_str(", "); } policy.fmt_policy(&mut out); } Some(out) } pub fn rate_limit_state_header(&self) -> Option { if self.rate_limit.is_empty() { return None; } let mut out = String::new(); for (i, policy) in self.rate_limit.iter().enumerate() { if i > 0 { out.push_str(", "); } policy.fmt_state(&mut out); } Some(out) } } impl Display for RequestError<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.detail) } } #[cfg(test)] mod tests { use super::*; #[test] fn rate_limit_headers_match_spec() { // Spec example: RateLimit-Policy: "burst";q=100;w=60,"daily";q=1000;w=86400 let mut p1 = String::new(); RateLimitPolicy::new("burst", 100) .with_window(60) .fmt_policy(&mut p1); assert_eq!(p1, r#""burst";q=100;w=60"#); // Spec example: RateLimit-Policy: "peruser";q=65535;qu="content-bytes";w=10 let mut p2 = String::new(); RateLimitPolicy::new("peruser", 65535) .with_window(10) .with_unit(RateLimitUnit::ContentBytes) .fmt_policy(&mut p2); assert_eq!(p2, r#""peruser";q=65535;w=10;qu="content-bytes""#); // Spec example: RateLimit: "default";r=50;t=30 let mut s = String::new(); RateLimitPolicy::new("default", 100) .with_remaining(50) .with_reset(30) .fmt_state(&mut s); assert_eq!(s, r#""default";r=50;t=30"#); // Two policies in one header let err = RequestError::too_many_requests() .with_rate_limit( RateLimitPolicy::new("burst", 100) .with_window(60) .with_reset(30), ) .with_rate_limit( RateLimitPolicy::new("daily", 1000) .with_window(86400) .with_reset(3600), ); assert_eq!( err.rate_limit_policy_header().as_deref(), Some(r#""burst";q=100;w=60, "daily";q=1000;w=86400"#), ); assert_eq!( err.rate_limit_state_header().as_deref(), Some(r#""burst";r=0;t=30, "daily";r=0;t=3600"#), ); assert_eq!(err.retry_after, Some(3600)); } }