Allow HTTP to be used for configuring the server
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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."));
|
||||||
|
|||||||
@@ -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",),
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user