summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2024-04-17 16:00:50 +0200
committermdecimus <mauro@stalw.art>2024-04-17 16:00:50 +0200
commit3cc3b726ea1f843c69a889035db089691b9801e2 (patch)
treeee395adfdb53e41235106fb3f52104fc8f1c7b15
parent929d84468fd8a20e173757b30d025ee88d4b7ccc (diff)
v0.7.2v0.7.2
-rw-r--r--CHANGELOG.md15
-rw-r--r--Cargo.lock2
-rw-r--r--README.md2
-rw-r--r--crates/common/src/config/server/tls.rs4
-rw-r--r--crates/jmap/src/api/management/dkim.rs4
-rw-r--r--crates/jmap/src/api/management/domain.rs13
-rw-r--r--crates/jmap/src/api/management/mod.rs9
-rw-r--r--crates/jmap/src/api/management/principal.rs5
-rw-r--r--crates/jmap/src/api/management/queue.rs8
-rw-r--r--crates/jmap/src/api/management/report.rs8
-rw-r--r--crates/jmap/src/api/management/settings.rs6
-rw-r--r--crates/utils/Cargo.toml2
-rw-r--r--tests/resources/acme/docker-compose-pebble.yaml2
13 files changed, 59 insertions, 21 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03520774..e4b111ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/Cargo.lock b/Cargo.lock
index 127ce4f9..a4f55a98 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/README.md b/README.md
index 0ce72b4d..69f7cda7 100644
--- a/README.md
+++ b/README.md
@@ -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).
diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs
index 0d3d4182..414fcc16 100644
--- a/crates/common/src/config/server/tls.rs
+++ b/crates/common/src/config/server/tls.rs
@@ -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
diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs
index ab98fbca..5d00e30f 100644
--- a/crates/jmap/src/api/management/dkim.rs
+++ b/crates/jmap/src/api/management/dkim.rs
@@ -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();
}
diff --git a/crates/jmap/src/api/management/domain.rs b/crates/jmap/src/api/management/domain.rs
index f19c8a87..7fe884f0 100644
--- a/crates/jmap/src/api/management/domain.rs
+++ b/crates/jmap/src/api/management/domain.rs
@@ -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": (),
}))
diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs
index 0e9c4b35..463b7507 100644
--- a/crates/jmap/src/api/management/mod.rs
+++ b/crates/jmap/src/api/management/mod.rs
@@ -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())
+}
diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs
index 625b7179..e7bc606b 100644
--- a/crates/jmap/src/api/management/principal.rs
+++ b/crates/jmap/src/api/management/principal.rs
@@ -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(
diff --git a/crates/jmap/src/api/management/queue.rs b/crates/jmap/src/api/management/queue.rs
index 7446bebc..2677940d 100644
--- a/crates/jmap/src/api/management/queue.rs
+++ b/crates/jmap/src/api/management/queue.rs
@@ -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;
diff --git a/crates/jmap/src/api/management/report.rs b/crates/jmap/src/api/management/report.rs
index 14cfb5d6..f4eb5185 100644
--- a/crates/jmap/src/api/management/report.rs
+++ b/crates/jmap/src/api/management/report.rs
@@ -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();
diff --git a/crates/jmap/src/api/management/settings.rs b/crates/jmap/src/api/management/settings.rs
index 5e3952f4..de8f9595 100644
--- a/crates/jmap/src/api/management/settings.rs
+++ b/crates/jmap/src/api/management/settings.rs
@@ -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": (),
}))
diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
index a086f47f..59b14be3 100644
--- a/crates/utils/Cargo.toml
+++ b/crates/utils/Cargo.toml
@@ -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"]}
diff --git a/tests/resources/acme/docker-compose-pebble.yaml b/tests/resources/acme/docker-compose-pebble.yaml
index 4330e19d..5806fe59 100644
--- a/tests/resources/acme/docker-compose-pebble.yaml
+++ b/tests/resources/acme/docker-compose-pebble.yaml
@@ -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