diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index 92e928b46..bda6d9a72 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -11,18 +11,33 @@ use crate::{ }; use utils::config::Config; -use super::CONNECTION_VARS; +use super::*; + +pub(crate) const HTTP_VARS: &[u32; 11] = &[ + V_LISTENER, + V_REMOTE_IP, + V_REMOTE_PORT, + V_LOCAL_IP, + V_LOCAL_PORT, + V_PROTOCOL, + V_TLS, + V_URL, + V_URL_PATH, + V_HEADERS, + V_METHOD, +]; impl Default for Network { fn default() -> Self { Self { blocked_ips: Default::default(), allowed_ips: Default::default(), - url: IfBlock::new::<()>( + http_response_url: IfBlock::new::<()>( "server.http.url", [], "protocol + '://' + key_get('default', 'hostname') + ':' + local_port", ), + http_allowed_endpoint: IfBlock::new::<()>("server.http.allowed-endpoint", [], "200"), } } } @@ -34,9 +49,15 @@ impl Network { allowed_ips: AllowedIps::parse(config), ..Default::default() }; - let token_map = &TokenMap::default().with_variables(CONNECTION_VARS); + let token_map = &TokenMap::default().with_variables(HTTP_VARS); - for (value, key) in [(&mut network.url, "server.http.url")] { + for (value, key) in [ + (&mut network.http_response_url, "server.http.url"), + ( + &mut network.http_allowed_endpoint, + "server.http.allowed-endpoint", + ), + ] { if let Some(if_block) = IfBlock::try_parse(config, key, token_map) { *value = if_block; } diff --git a/crates/common/src/expr/eval.rs b/crates/common/src/expr/eval.rs index a6ad559ae..371a04769 100644 --- a/crates/common/src/expr/eval.rs +++ b/crates/common/src/expr/eval.rs @@ -6,6 +6,7 @@ use std::{borrow::Cow, cmp::Ordering, fmt::Display}; +use hyper::StatusCode; use trc::EvalEvent; use crate::Core; @@ -665,3 +666,17 @@ impl<'x> TryFrom> for usize { value.to_usize().ok_or(()) } } + +impl<'x> TryFrom> for StatusCode { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value.to_integer() { + Some(v) => match StatusCode::from_u16(v as u16) { + Ok(status) => Ok(status), + Err(_) => Err(()), + }, + None => Err(()), + } + } +} diff --git a/crates/common/src/expr/mod.rs b/crates/common/src/expr/mod.rs index 5c5b56494..0c190de3a 100644 --- a/crates/common/src/expr/mod.rs +++ b/crates/common/src/expr/mod.rs @@ -31,6 +31,10 @@ pub const V_QUEUE_NOTIFY_NUM: u32 = 17; pub const V_QUEUE_EXPIRES_IN: u32 = 18; pub const V_QUEUE_LAST_STATUS: u32 = 19; pub const V_QUEUE_LAST_ERROR: u32 = 20; +pub const V_URL: u32 = 21; +pub const V_URL_PATH: u32 = 22; +pub const V_HEADERS: u32 = 23; +pub const V_METHOD: u32 = 24; pub const VARIABLES_MAP: &[(&str, u32)] = &[ ("rcpt", V_RECIPIENT), @@ -54,6 +58,10 @@ pub const VARIABLES_MAP: &[(&str, u32)] = &[ ("expires_in", V_QUEUE_EXPIRES_IN), ("last_status", V_QUEUE_LAST_STATUS), ("last_error", V_QUEUE_LAST_ERROR), + ("url", V_URL), + ("url_path", V_URL_PATH), + ("headers", V_HEADERS), + ("method", V_METHOD), ]; use regex::Regex; diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 7eff8bcde..21c6e890d 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -67,7 +67,8 @@ pub struct Core { pub struct Network { pub blocked_ips: BlockedIps, pub allowed_ips: AllowedIps, - pub url: IfBlock, + pub http_response_url: IfBlock, + pub http_allowed_endpoint: IfBlock, } #[derive(Debug)] diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 9c976ec2f..0edafa469 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -59,6 +59,13 @@ impl JMAP { let mut path = req.uri().path().split('/'); path.next(); + // Validate endpoint access + let ctx = HttpContext::new(&session, &req); + match ctx.has_endpoint_access(&self.core).await { + StatusCode::OK => (), + status => return Ok(status.into_http_response()), + } + match path.next().unwrap_or_default() { "jmap" => { match (path.next().unwrap_or_default(), req.method()) { @@ -185,7 +192,7 @@ impl JMAP { return Ok(self .handle_session_resource( - session.resolve_url(&self.core).await, + ctx.resolve_response_url(&self.core).await, access_token, ) .await? @@ -196,7 +203,7 @@ impl JMAP { self.is_anonymous_allowed(&session.remote_ip).await?; return Ok(JsonResponse::new(OAuthMetadata::new( - session.resolve_url(&self.core).await, + ctx.resolve_response_url(&self.core).await, )) .into_http_response()); } @@ -248,12 +255,9 @@ impl JMAP { ("device", &Method::POST) => { self.is_anonymous_allowed(&session.remote_ip).await?; + let url = ctx.resolve_response_url(&self.core).await; return self - .handle_device_auth( - &mut req, - session.resolve_url(&self.core).await, - session.session_id, - ) + .handle_device_auth(&mut req, url, session.session_id) .await; } ("token", &Method::POST) => { @@ -526,33 +530,70 @@ impl SessionManager for JmapSessionManager { } } -impl ResolveVariable for HttpSessionData { - fn resolve_variable(&self, variable: u32) -> common::expr::Variable<'_> { - match variable { - V_REMOTE_IP => self.remote_ip.to_string().into(), - V_REMOTE_PORT => self.remote_port.into(), - V_LOCAL_IP => self.local_ip.to_string().into(), - V_LOCAL_PORT => self.local_port.into(), - V_TLS => self.is_tls.into(), - V_PROTOCOL => if self.is_tls { "https" } else { "http" }.into(), - V_LISTENER => self.instance.id.as_str().into(), - _ => common::expr::Variable::default(), - } +struct HttpContext<'x> { + session: &'x HttpSessionData, + req: &'x HttpRequest, +} + +impl<'x> HttpContext<'x> { + fn new(session: &'x HttpSessionData, req: &'x HttpRequest) -> Self { + Self { session, req } + } + + pub async fn resolve_response_url(&self, core: &Core) -> String { + core.eval_if( + &core.network.http_response_url, + self, + self.session.session_id, + ) + .await + .unwrap_or_else(|| { + format!( + "http{}://{}:{}", + if self.session.is_tls { "s" } else { "" }, + self.session.local_ip, + self.session.local_port + ) + }) + } + + pub async fn has_endpoint_access(&self, core: &Core) -> StatusCode { + core.eval_if( + &core.network.http_allowed_endpoint, + self, + self.session.session_id, + ) + .await + .unwrap_or(StatusCode::OK) } } -impl HttpSessionData { - pub async fn resolve_url(&self, core: &Core) -> String { - core.eval_if(&core.network.url, self, self.session_id) - .await - .unwrap_or_else(|| { - format!( - "http{}://{}:{}", - if self.is_tls { "s" } else { "" }, - self.local_ip, - self.local_port - ) - }) +impl<'x> ResolveVariable for HttpContext<'x> { + fn resolve_variable(&self, variable: u32) -> Variable<'_> { + match variable { + V_REMOTE_IP => self.session.remote_ip.to_string().into(), + V_REMOTE_PORT => self.session.remote_port.into(), + V_LOCAL_IP => self.session.local_ip.to_string().into(), + V_LOCAL_PORT => self.session.local_port.into(), + V_TLS => self.session.is_tls.into(), + V_PROTOCOL => if self.session.is_tls { "https" } else { "http" }.into(), + V_LISTENER => self.session.instance.id.as_str().into(), + V_URL => self.req.uri().to_string().into(), + V_URL_PATH => self.req.uri().path().into(), + V_METHOD => self.req.method().as_str().into(), + V_HEADERS => self + .req + .headers() + .iter() + .map(|(h, v)| { + Variable::String( + format!("{}: {}", h.as_str(), v.to_str().unwrap_or_default()).into(), + ) + }) + .collect::>() + .into(), + _ => Variable::default(), + } } } @@ -903,6 +944,17 @@ impl ToHttpResponse for HtmlResponse { impl ToHttpResponse for StatusCode { fn into_http_response(self) -> HttpResponse { - HttpResponse::new_empty(self) + HttpResponse::new_text( + self, + "application/problem+json", + serde_json::to_string(&RequestError { + p_type: jmap_proto::error::request::RequestErrorType::Other, + status: self.as_u16(), + title: None, + detail: self.canonical_reason().unwrap_or_default().into(), + limit: None, + }) + .unwrap_or_default(), + ) } } diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index 388f6f8a4..07b2cf0d5 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -13,14 +13,17 @@ use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use utils::map::ttl_dashmap::TtlMap; -use crate::{api::http::HttpSessionData, JMAP}; +use crate::{ + api::{http::HttpSessionData, HttpRequest}, + JMAP, +}; use super::AccessToken; impl JMAP { pub async fn authenticate_headers( &self, - req: &hyper::Request, + req: &HttpRequest, session: &HttpSessionData, ) -> trc::Result<(InFlight, Arc)> { if let Some((mechanism, token)) = req.authorization() { @@ -187,7 +190,7 @@ pub trait HttpHeaders { fn authorization_basic(&self) -> Option<&str>; } -impl HttpHeaders for hyper::Request { +impl HttpHeaders for HttpRequest { fn authorization(&self) -> Option<(&str, &str)> { self.headers() .get(header::AUTHORIZATION)