HTTP: Add redirectRoot option to Http object

This commit is contained in:
Maurus Decimus
2026-06-17 19:30:36 +02:00
parent 8778e1bc45
commit b1880c8670
10 changed files with 45 additions and 21 deletions

View File

@@ -7,8 +7,10 @@ All notable changes to this project will be documented in this file. This projec
If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions.
## Added
- OAuth Profile for Open Public Clients ([draft-ietf-mailmaint-oauth-public](https://datatracker.ietf.org/doc/draft-ietf-mailmaint-oauth-public/))
- OAuth client secret verification for confidential clients.
- OAuth:
- OAuth Profile for Open Public Clients ([draft-ietf-mailmaint-oauth-public](https://datatracker.ietf.org/doc/draft-ietf-mailmaint-oauth-public/))
- Client secret verification for confidential clients.
- HTTP: Add `redirectRoot` option to `Http` object to allow redirecting requests to the root path to a different path (e.g. `/account`).
## Changed

View File

@@ -62,6 +62,7 @@ pub struct Http {
pub allowed_endpoint: IfBlock,
pub response_headers: Vec<(hyper::header::HeaderName, hyper::header::HeaderValue)>,
pub use_forwarded: bool,
pub redirect_root: Option<String>,
}
#[derive(Clone)]
@@ -448,6 +449,7 @@ impl Http {
rate_anonymous: http.rate_limit_anonymous,
response_headers: http_headers,
use_forwarded: http.use_x_forwarded,
redirect_root: http.redirect_root,
}
}
}

View File

@@ -233,8 +233,7 @@ impl OAuthApiHandler for Server {
// Parse and validate PKCE challenge (RFC 7636).
let pkce_challenge = match code_challenge {
Some(challenge) => {
match code_challenge_method.as_deref().unwrap_or("plain") {
Some(challenge) => match code_challenge_method.as_deref().unwrap_or("plain") {
"S256" => PkceCodeChallenge::S256(challenge),
"plain" if stateless_client.is_none() => {
PkceCodeChallenge::Plain(challenge)
@@ -244,13 +243,12 @@ impl OAuthApiHandler for Server {
.into_err()
.details("Unsupported PKCE code_challenge_method."));
}
}
}
},
None => {
if stateless_client.is_some() {
return Err(trc::AuthEvent::Error
.into_err()
.details("A PKCE code_challenge with the S256 method is required."));
return Err(trc::AuthEvent::Error.into_err().details(
"A PKCE code_challenge with the S256 method is required.",
));
}
PkceCodeChallenge::None
}

View File

@@ -611,6 +611,8 @@ impl ParseHttp for Server {
if path.next().is_none() {
if !external.is_empty() {
return Ok(HttpResponse::redirect(format!("/{external}/")));
} else if let Some(url) = &self.core.network.http.redirect_root {
return Ok(HttpResponse::redirect(url.clone()));
}
} else if let Some(resource) = self
.inner

View File

@@ -948,6 +948,7 @@ pub enum Property {
Recipients = 484,
Records = 256,
RecurrenceId = 805,
RedirectRoot = 911,
RedirectUris = 605,
Refresh = 419,
RefreshTokenExpiry = 617,

View File

@@ -1101,6 +1101,7 @@ impl EnumImpl for Property {
b"recipients" => Property::Recipients,
b"records" => Property::Records,
b"recurrenceId" => Property::RecurrenceId,
b"redirectRoot" => Property::RedirectRoot,
b"redirectUris" => Property::RedirectUris,
b"refresh" => Property::Refresh,
b"refreshTokenExpiry" => Property::RefreshTokenExpiry,
@@ -2017,6 +2018,7 @@ impl EnumImpl for Property {
Property::Recipients => "recipients",
Property::Records => "records",
Property::RecurrenceId => "recurrenceId",
Property::RedirectRoot => "redirectRoot",
Property::RedirectUris => "redirectUris",
Property::Refresh => "refresh",
Property::RefreshTokenExpiry => "refreshTokenExpiry",
@@ -2937,6 +2939,7 @@ impl EnumImpl for Property {
484 => Some(Property::Recipients),
256 => Some(Property::Records),
805 => Some(Property::RecurrenceId),
911 => Some(Property::RedirectRoot),
605 => Some(Property::RedirectUris),
419 => Some(Property::Refresh),
617 => Some(Property::RefreshTokenExpiry),
@@ -3158,7 +3161,7 @@ impl EnumImpl for Property {
}
}
const COUNT: usize = 911;
const COUNT: usize = 912;
}
impl serde::Serialize for Property {

View File

@@ -2930,6 +2930,8 @@ pub struct Http {
pub response_headers: VecMap<String, String>,
#[serde(rename = "useXForwarded")]
pub use_x_forwarded: bool,
#[serde(rename = "redirectRoot")]
pub redirect_root: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

View File

@@ -21203,7 +21203,7 @@ impl RegistryJsonPropertyPatch for GroupAccount {
impl ObjectImpl for Http {
const FLAGS: u64 = OBJ_SINGLETON;
const VERSION: u8 = 0;
const VERSION: u8 = 1;
const OBJECT: ObjectType = ObjectType::Http;
fn validate(&self, errors: &mut Vec<ValidationError>) -> bool {
@@ -21222,6 +21222,11 @@ impl ObjectImpl for Http {
errors.push(ValidationError::required(Property::ResponseHeaders));
}
}
if let Some(value) = &self.redirect_root {
if value.is_empty() {
errors.push(ValidationError::required(Property::RedirectRoot));
}
}
errors.len() == neb
}
@@ -21256,6 +21261,7 @@ impl Pickle for Http {
self.use_permissive_cors.pickle(out);
self.response_headers.pickle(out);
self.use_x_forwarded.pickle(out);
self.redirect_root.pickle(out);
}
fn unpickle(stream: &mut crate::pickle::PickledStream<'_>) -> Option<Self> {
@@ -21267,6 +21273,9 @@ impl Pickle for Http {
this.use_permissive_cors = Pickle::unpickle(stream)?;
this.response_headers = Pickle::unpickle(stream)?;
this.use_x_forwarded = Pickle::unpickle(stream)?;
if stream.version() >= 1 {
this.redirect_root = Pickle::unpickle(stream)?;
}
Some(this)
}
}
@@ -21290,13 +21299,14 @@ impl Default for Http {
use_permissive_cors: false,
response_headers: Default::default(),
use_x_forwarded: false,
redirect_root: Some("/account".to_string()),
}
}
}
impl IntoValue for Http {
fn into_value(self) -> JmapValue<'static> {
let mut map = jmap_tools::Map::with_capacity(9);
let mut map = jmap_tools::Map::with_capacity(10);
map.insert_unchecked(
Property::RateLimitAuthenticated,
self.rate_limit_authenticated.into_value(),
@@ -21319,6 +21329,7 @@ impl IntoValue for Http {
self.response_headers.into_value(),
);
map.insert_unchecked(Property::UseXForwarded, self.use_x_forwarded.into_value());
map.insert_unchecked(Property::RedirectRoot, self.redirect_root.into_value());
JmapValue::Object(map)
}
}
@@ -21341,6 +21352,9 @@ impl RegistryJsonPropertyPatch for Http {
.response_headers
.patch(pointer.with_validators(&[StringValidator::Trim]), value),
Some(Property::UseXForwarded) => self.use_x_forwarded.patch(pointer, value),
Some(Property::RedirectRoot) => self
.redirect_root
.patch(pointer.with_validators(&[StringValidator::Trim]), value),
Some(Property::Type) => Ok(MaybeUnpatched::Unpatched {
property: Property::Type,
value,

Binary file not shown.

View File

@@ -1 +1 @@
fleWj8pl8amEMbzM8N8ku4pLHlm4yYln_xEcXSCADBw
qtv0KUvdELIzxfAvonxvf3PW10RnWMB9iADw0FtHMF4