This commit is contained in:
mdecimus 2024-04-17 16:00:50 +02:00
parent 929d84468f
commit 3cc3b726ea
13 changed files with 59 additions and 21 deletions

View file

@ -2,6 +2,21 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [0.7.2] - 2024-04-17
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin version.
## Added
- Support for DNS-01 and HTTP-01 ACME challenges (#226)
- Configurable external resources (#355)
### Changed
### Fixed
- Startup failure when Elasticsearch is down/starting up (#334)
- URL decode path elements in REST API.
## [0.7.1] - 2024-04-12
To upgrade replace the `stalwart-mail` binary.

2
Cargo.lock generated
View file

@ -6622,7 +6622,7 @@ name = "utils"
version = "0.7.2"
dependencies = [
"ahash 0.8.11",
"base64 0.21.7",
"base64 0.22.0",
"blake3",
"chrono",
"dashmap",

View file

@ -91,7 +91,7 @@ Key features:
- Self-service portal for password reset and encryption-at-rest key management.
- **Secure and robust**:
- Encryption at rest with **S/MIME** or **OpenPGP**.
- Automatic TLS certificate provisioning with [ACME](https://datatracker.ietf.org/doc/html/rfc8555).
- Automatic TLS certificate provisioning with [ACME](https://datatracker.ietf.org/doc/html/rfc8555) using `TLS-ALPN-01`, `DNS-01` or `HTTP-01` challenges.
- OAuth 2.0 [authorization code](https://www.rfc-editor.org/rfc/rfc8628) and [device authorization](https://www.rfc-editor.org/rfc/rfc8628) flows.
- Automated blocking of hosts that cause multiple authentication errors (aka **fail2ban**).
- Access Control Lists (ACLs).

View file

@ -203,10 +203,10 @@ fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option<DnsUpdater> {
match config.value_require(("acme", acme_id, "provider"))? {
"rfc2136-tsig" => {
let algorithm: TsigAlgorithm = config
.value_require(("acme", acme_id, "algorithm"))?
.value_require(("acme", acme_id, "tsig-algorithm"))?
.parse()
.map_err(|_| {
config.new_parse_error(("acme", acme_id, "algorithm"), "Invalid algorithm")
config.new_parse_error(("acme", acme_id, "tsig-algorithm"), "Invalid algorithm")
})
.ok()?;
let key = STANDARD

View file

@ -46,6 +46,8 @@ use crate::{
JMAP,
};
use super::decode_path_element;
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum Algorithm {
Rsa,
@ -76,7 +78,7 @@ impl JMAP {
async fn handle_get_public_key(&self, path: Vec<&str>) -> HttpResponse {
let signature_id = match path.get(1) {
Some(signature_id) => *signature_id,
Some(signature_id) => decode_path_element(signature_id),
None => {
return RequestError::not_found().into_http_response();
}

View file

@ -39,6 +39,8 @@ use crate::{
JMAP,
};
use super::decode_path_element;
#[derive(Debug, Serialize, Deserialize)]
struct DnsRecord {
#[serde(rename = "type")]
@ -82,7 +84,8 @@ impl JMAP {
}
(Some(domain), &Method::GET) => {
// Obtain DNS records
match self.build_dns_records(domain).await {
let domain = decode_path_element(domain);
match self.build_dns_records(domain.as_ref()).await {
Ok(records) => JsonResponse::new(json!({
"data": records,
}))
@ -92,7 +95,8 @@ impl JMAP {
}
(Some(domain), &Method::POST) => {
// Create domain
match self.core.storage.data.create_domain(domain).await {
let domain = decode_path_element(domain);
match self.core.storage.data.create_domain(domain.as_ref()).await {
Ok(_) => {
// Set default domain name if missing
if matches!(
@ -103,7 +107,7 @@ impl JMAP {
.core
.storage
.config
.set([("lookup.default.domain", *domain)])
.set([("lookup.default.domain", domain.as_ref())])
.await
{
tracing::error!("Failed to set default domain name: {}", err);
@ -120,7 +124,8 @@ impl JMAP {
}
(Some(domain), &Method::DELETE) => {
// Delete domain
match self.core.storage.data.delete_domain(domain).await {
let domain = decode_path_element(domain);
match self.core.storage.data.delete_domain(domain.as_ref()).await {
Ok(_) => JsonResponse::new(json!({
"data": (),
}))

View file

@ -128,3 +128,12 @@ impl From<String> for ManagementApiError {
}
}
}
pub fn decode_path_element(item: &str) -> Cow<'_, str> {
// Bit hackish but avoids an extra dependency
form_urlencoded::parse(item.as_bytes())
.into_iter()
.next()
.map(|(k, _)| k)
.unwrap_or_else(|| item.into())
}

View file

@ -42,7 +42,7 @@ use crate::{
JMAP,
};
use super::ManagementApiError;
use super::{decode_path_element, ManagementApiError};
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct PrincipalResponse {
@ -151,7 +151,8 @@ impl JMAP {
}
(Some(name), method) => {
// Fetch, update or delete principal
let account_id = match self.core.storage.data.get_account_id(name).await {
let name = decode_path_element(name);
let account_id = match self.core.storage.data.get_account_id(name.as_ref()).await {
Ok(Some(account_id)) => account_id,
Ok(None) => {
return RequestError::blank(

View file

@ -45,6 +45,8 @@ use crate::{
JMAP,
};
use super::decode_path_element;
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct Message {
pub id: QueueId,
@ -122,7 +124,7 @@ impl JMAP {
match (
path.get(1).copied().unwrap_or_default(),
path.get(2).copied(),
path.get(2).copied().map(decode_path_element),
req.method(),
) {
("messages", None, &Method::GET) => {
@ -439,7 +441,7 @@ impl JMAP {
}
("reports", Some(report_id), &Method::GET) => {
let mut result = None;
if let Some(report_id) = parse_queued_report_id(report_id) {
if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) {
match report_id {
QueueClass::DmarcReportHeader(event) => {
let mut rua = Vec::new();
@ -475,7 +477,7 @@ impl JMAP {
}
}
("reports", Some(report_id), &Method::DELETE) => {
if let Some(report_id) = parse_queued_report_id(report_id) {
if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) {
match report_id {
QueueClass::DmarcReportHeader(event) => {
self.smtp.delete_dmarc_report(event).await;

View file

@ -40,6 +40,8 @@ use crate::{
JMAP,
};
use super::decode_path_element;
enum ReportType {
Dmarc,
Tls,
@ -50,7 +52,7 @@ impl JMAP {
pub async fn handle_manage_reports(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
match (
path.get(1).copied().unwrap_or_default(),
path.get(2).copied(),
path.get(2).copied().map(decode_path_element),
req.method(),
) {
(class @ ("dmarc" | "tls" | "arf"), None, &Method::GET) => {
@ -159,7 +161,7 @@ impl JMAP {
}
}
(class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::GET) => {
if let Some(report_id) = parse_incoming_report_id(class, report_id) {
if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) {
match &report_id {
ReportClass::Tls { .. } => match self
.core
@ -215,7 +217,7 @@ impl JMAP {
}
}
(class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::DELETE) => {
if let Some(report_id) = parse_incoming_report_id(class, report_id) {
if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) {
let mut batch = BatchBuilder::new();
batch.clear(ValueClass::Report(report_id));
let result = self.core.storage.data.write(batch.build()).await.is_ok();

View file

@ -32,7 +32,7 @@ use crate::{
JMAP,
};
use super::ManagementApiError;
use super::{decode_path_element, ManagementApiError};
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
@ -269,7 +269,9 @@ impl JMAP {
}
}
(Some(prefix), &Method::DELETE) if !prefix.is_empty() => {
match self.core.storage.config.clear(prefix).await {
let prefix = decode_path_element(prefix);
match self.core.storage.config.clear(prefix.as_ref()).await {
Ok(_) => JsonResponse::new(json!({
"data": (),
}))

View file

@ -21,7 +21,7 @@ chrono = "0.4"
rand = "0.8.5"
webpki-roots = { version = "0.26"}
ring = { version = "0.17" }
base64 = "0.21"
base64 = "0.22"
serde_json = "1.0"
rcgen = "0.13"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}

View file

@ -8,7 +8,7 @@ version: '3'
services:
pebble:
image: letsencrypt/pebble:latest
command: pebble -config /test/config/pebble-config.json -strict -dnsserver 8.8.8.8:53 #-dnsserver 10.30.50.3:8053
command: pebble -config /test/config/pebble-config.json -strict -dnsserver 10.30.50.3:8053 #-dnsserver 8.8.8.8:53
ports:
- 14000:14000 # HTTPS ACME API
- 15000:15000 # HTTPS Management API