Allow HTTP to be used for configuring the server

This commit is contained in:
Maurus Decimus
2026-04-27 07:48:36 +02:00
parent cd4b08544c
commit 242bea67d9
8 changed files with 45 additions and 25 deletions

View File

@@ -2,9 +2,20 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [0.16.2] - 2026-05-XX
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
## Changed
- Allow HTTP to be used for configuring the server.
## Fixed
## [0.16.1] - 2026-04-25 ## [0.16.1] - 2026-04-25
This version includes **multiple breaking changes**. 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. 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 ## Added
- OIDC: Extract username from JWT token. - OIDC: Extract username from JWT token.
@@ -31,7 +42,6 @@ This version includes **multiple breaking changes**. If you are upgrading from v
- Fix `CAA` record updates. - Fix `CAA` record updates.
- Check zone subdomains when finding zones - Check zone subdomains when finding zones
## [0.16.0] - 2026-04-20 ## [0.16.0] - 2026-04-20
This version includes **multiple breaking changes**. 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. This version includes **multiple breaking changes**. 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.

View File

@@ -14,7 +14,7 @@ pub mod token;
pub const DEVICE_CODE_LEN: usize = 40; pub const DEVICE_CODE_LEN: usize = 40;
pub const USER_CODE_LEN: usize = 8; pub const USER_CODE_LEN: usize = 8;
pub const RANDOM_CODE_LEN: usize = 32; pub const RANDOM_CODE_LEN: usize = 32;
pub const CLIENT_ID_MAX_LEN: usize = 20; pub const CLIENT_ID_MAX_LEN: usize = 60;
pub const USER_CODE_ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // No 0, O, I, 1 pub const USER_CODE_ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // No 0, O, I, 1

View File

@@ -81,7 +81,7 @@ impl ManagementApi for Server {
if let Some(email) = path.get(1).copied() { if let Some(email) = path.get(1).copied() {
self.is_http_anonymous_request_allowed(session.remote_ip) self.is_http_anonymous_request_allowed(session.remote_ip)
.await?; .await?;
self.handle_discover_request(decode_path_element(email).as_ref()) self.handle_discover_request(session, decode_path_element(email).as_ref())
.await .await
} else { } else {
Err(trc::ResourceEvent::NotFound.into_err()) Err(trc::ResourceEvent::NotFound.into_err())

View File

@@ -49,6 +49,7 @@ pub struct OAuthMetadata {
pub trait OAuthApiHandler: Sync + Send { pub trait OAuthApiHandler: Sync + Send {
fn handle_discover_request( fn handle_discover_request(
&self, &self,
session: &HttpSessionData,
account_name: &str, account_name: &str,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send; ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
@@ -114,7 +115,11 @@ pub enum LoginResponse {
} }
impl OAuthApiHandler for Server { impl OAuthApiHandler for Server {
async fn handle_discover_request(&self, account_name: &str) -> trc::Result<HttpResponse> { async fn handle_discover_request(
&self,
session: &HttpSessionData,
account_name: &str,
) -> trc::Result<HttpResponse> {
let account_name = account_name.trim().to_lowercase(); let account_name = account_name.trim().to_lowercase();
if let Some(domain_name) = account_name.try_domain_part() if let Some(domain_name) = account_name.try_domain_part()
&& let Some(endpoint) = self && let Some(endpoint) = self
@@ -126,7 +131,7 @@ impl OAuthApiHandler for Server {
.no_cache() .no_cache()
.into_http_response()) .into_http_response())
} else { } else {
self.handle_oidc_metadata().await self.handle_oidc_metadata(!session.is_tls).await
} }
} }
@@ -155,13 +160,13 @@ impl OAuthApiHandler for Server {
if client_id.len() > CLIENT_ID_MAX_LEN { if client_id.len() > CLIENT_ID_MAX_LEN {
return Err(trc::AuthEvent::Error return Err(trc::AuthEvent::Error
.into_err() .into_err()
.details("Client ID is invalid.")); .details("Client ID is too long."));
} else if redirect_uri } else if redirect_uri
.as_ref() .as_ref()
.is_some_and(|uri| uri.starts_with("http://")) .is_some_and(|uri| uri.starts_with("http://"))
{ {
#[cfg(not(feature = "dev_mode"))] #[cfg(not(feature = "dev_mode"))]
if !self.registry().is_recovery_mode() { if !self.registry().is_recovery_mode() && code_challenge.is_none() {
return Err(trc::AuthEvent::Error return Err(trc::AuthEvent::Error
.into_err() .into_err()
.details("Redirect URI must be HTTPS.")); .details("Redirect URI must be HTTPS."));

View File

@@ -33,7 +33,10 @@ pub trait OpenIdHandler: Sync + Send {
account_id: u32, account_id: u32,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send; ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_oidc_metadata(&self) -> impl Future<Output = trc::Result<HttpResponse>> + Send; fn handle_oidc_metadata(
&self,
strip_base_url: bool,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
} }
impl OpenIdHandler for Server { impl OpenIdHandler for Server {
@@ -52,8 +55,12 @@ impl OpenIdHandler for Server {
.into_http_response()) .into_http_response())
} }
async fn handle_oidc_metadata(&self) -> trc::Result<HttpResponse> { async fn handle_oidc_metadata(&self, strip_base_url: bool) -> trc::Result<HttpResponse> {
let base_url = &self.core.network.http.url_https; let base_url = if strip_base_url {
""
} else {
&self.core.network.http.url_https
};
Ok(JsonResponse::new(OpenIdMetadata { Ok(JsonResponse::new(OpenIdMetadata {
authorization_endpoint: format!("{base_url}/login",), authorization_endpoint: format!("{base_url}/login",),

View File

@@ -278,7 +278,7 @@ impl ParseHttp for Server {
self.is_http_anonymous_request_allowed(session.remote_ip) self.is_http_anonymous_request_allowed(session.remote_ip)
.await?; .await?;
return self.handle_oidc_metadata().await; return self.handle_oidc_metadata(false).await;
} }
("acme-challenge", &Method::GET) if self.has_acme_http_providers() => { ("acme-challenge", &Method::GET) if self.has_acme_http_providers() => {
if let Some(token) = path.next() { if let Some(token) = path.next() {

View File

@@ -38,6 +38,7 @@ use store::{
write::{AnyKey, BatchBuilder}, write::{AnyKey, BatchBuilder},
}; };
use types::id::Id; use types::id::Id;
use utils::is_valid_domain;
pub(crate) async fn bootstrap_get( pub(crate) async fn bootstrap_get(
mut get: RegistryGetResponse<'_>, mut get: RegistryGetResponse<'_>,
@@ -505,15 +506,6 @@ pub(crate) async fn bootstrap_set(
Ok(set) Ok(set)
} }
fn is_valid_domain(hostname: &str) -> bool {
const RESERVED_TLDS: &[&str] = &["test", "localhost", "local", "internal"];
psl::domain_str(hostname).is_some()
|| RESERVED_TLDS.contains(&hostname)
|| hostname
.rsplit_once('.')
.is_some_and(|(_, tld)| RESERVED_TLDS.contains(&tld))
}
async fn write_object(registry: &RegistryStore, object: &Object) -> Result<Id, SetError<Property>> { async fn write_object(registry: &RegistryStore, object: &Object) -> Result<Id, SetError<Property>> {
match registry.write(RegistryWrite::insert(object)).await { match registry.write(RegistryWrite::insert(object)).await {
Ok(RegistryWriteResult::Success(id)) => Ok(id), Ok(RegistryWriteResult::Success(id)) => Ok(id),

View File

@@ -340,12 +340,18 @@ pub fn sanitize_domain(domain: &str) -> Option<String> {
} }
} }
if found_dot if found_dot && last_ch != '.' && is_valid_domain(&result) {
&& last_ch != '.'
&& psl::domain(result.as_bytes()).is_some_and(|d| d.suffix().typ().is_some())
{
Some(result) Some(result)
} else { } else {
None None
} }
} }
pub fn is_valid_domain(domain: &str) -> bool {
const RESERVED_TLDS: &[&str] = &["test", "localhost", "local", "internal"];
psl::domain(domain.as_bytes()).is_some_and(|d| d.suffix().typ().is_some())
|| RESERVED_TLDS.contains(&domain)
|| domain
.rsplit_once('.')
.is_some_and(|(_, tld)| RESERVED_TLDS.contains(&tld))
}