Skip to content

Commit

Permalink
HTTP endpoint access controls (closes #266 closes #329 closes #542)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdecimus committed Aug 8, 2024
1 parent bba371c commit 3b053ad
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 40 deletions.
29 changes: 25 additions & 4 deletions crates/common/src/config/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}
}
}
Expand All @@ -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;
}
Expand Down
15 changes: 15 additions & 0 deletions crates/common/src/expr/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use std::{borrow::Cow, cmp::Ordering, fmt::Display};

use hyper::StatusCode;
use trc::EvalEvent;

use crate::Core;
Expand Down Expand Up @@ -665,3 +666,17 @@ impl<'x> TryFrom<Variable<'x>> for usize {
value.to_usize().ok_or(())
}
}

impl<'x> TryFrom<Variable<'x>> for StatusCode {
type Error = ();

fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
match value.to_integer() {
Some(v) => match StatusCode::from_u16(v as u16) {
Ok(status) => Ok(status),
Err(_) => Err(()),
},
None => Err(()),
}
}
}
8 changes: 8 additions & 0 deletions crates/common/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
116 changes: 84 additions & 32 deletions crates/jmap/src/api/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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?
Expand All @@ -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());
}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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::<Vec<_>>()
.into(),
_ => Variable::default(),
}
}
}

Expand Down Expand Up @@ -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(),
)
}
}
9 changes: 6 additions & 3 deletions crates/jmap/src/auth/authenticate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<hyper::body::Incoming>,
req: &HttpRequest,
session: &HttpSessionData,
) -> trc::Result<(InFlight, Arc<AccessToken>)> {
if let Some((mechanism, token)) = req.authorization() {
Expand Down Expand Up @@ -187,7 +190,7 @@ pub trait HttpHeaders {
fn authorization_basic(&self) -> Option<&str>;
}

impl HttpHeaders for hyper::Request<hyper::body::Incoming> {
impl HttpHeaders for HttpRequest {
fn authorization(&self) -> Option<(&str, &str)> {
self.headers()
.get(header::AUTHORIZATION)
Expand Down

0 comments on commit 3b053ad

Please sign in to comment.