diff --git a/CHANGELOG.md b/CHANGELOG.md index db8db82..8256a55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.1.10] - 2024-08-08 + +## Added +- Metrics configuration. +- HTTP endpoint access controls. + +### Changed + +### Fixed +- Unfiltered data store select options on SQL directory creation (fixes #17). + ## [0.1.9] - 2024-08-01 ## Added diff --git a/Cargo.lock b/Cargo.lock index b415142..ee22cdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -189,9 +189,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "camino" @@ -201,9 +201,9 @@ checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" [[package]] name = "cc" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" [[package]] name = "cfg-if" @@ -545,9 +545,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", "miniz_oxide", @@ -1392,11 +1392,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy 0.6.6", + "zerocopy", ] [[package]] @@ -1570,9 +1570,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -1655,9 +1655,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.204" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" dependencies = [ "serde_derive", ] @@ -1675,9 +1675,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" dependencies = [ "proc-macro2", "quote", @@ -1686,9 +1686,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", "memchr", @@ -1729,9 +1729,9 @@ dependencies = [ [[package]] name = "serde_test" -version = "1.0.176" +version = "1.0.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" dependencies = [ "serde", ] @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "webadmin" -version = "0.1.9" +version = "0.1.10" dependencies = [ "ahash", "base64", @@ -2307,9 +2307,9 @@ dependencies = [ [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys", ] @@ -2325,9 +2325,9 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] @@ -2417,34 +2417,14 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -[[package]] -name = "zerocopy" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" -dependencies = [ - "byteorder", - "zerocopy-derive 0.6.6", -] - [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy-derive" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "byteorder", + "zerocopy-derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ff2a2de..9a8a267 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art" keywords = ["web", "admin", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.1.9" +version = "0.1.10" edition = "2021" resolver = "2" diff --git a/src/main.rs b/src/main.rs index 235c86c..3d9d921 100644 --- a/src/main.rs +++ b/src/main.rs @@ -427,7 +427,7 @@ pub fn build_schemas() -> Arc { .build_tls() .build_server() .build_listener() - .build_tracing() + .build_telemetry() .build_smtp_inbound() .build_smtp_outbound() .build_mail_auth() diff --git a/src/pages/config/mod.rs b/src/pages/config/mod.rs index 78e99c1..aa01233 100644 --- a/src/pages/config/mod.rs +++ b/src/pages/config/mod.rs @@ -272,6 +272,10 @@ impl LayoutBuilder { .create("Network") .route("/network/edit") .insert() + // HTTP + .create("HTTP") + .route("/http/edit") + .insert() // System .create("System") .route("/system/edit") @@ -301,6 +305,9 @@ impl LayoutBuilder { .create("Logging & Tracing") .route("/tracing") .insert() + .create("Metrics") + .route("/metrics/edit") + .insert() .create("Webhooks") .route("/web-hooks") .insert() diff --git a/src/pages/config/schema/mod.rs b/src/pages/config/schema/mod.rs index da004d3..6fb4230 100644 --- a/src/pages/config/schema/mod.rs +++ b/src/pages/config/schema/mod.rs @@ -42,6 +42,10 @@ pub const V_QUEUE_NOTIFY_NUM: &str = "notify_num"; pub const V_QUEUE_EXPIRES_IN: &str = "expires_in"; pub const V_QUEUE_LAST_STATUS: &str = "last_status"; pub const V_QUEUE_LAST_ERROR: &str = "last_error"; +pub const V_URL: &str = "url"; +pub const V_URL_PATH: &str = "url_path"; +pub const V_HEADERS: &str = "headers"; +pub const V_METHOD: &str = "method"; pub const CONNECTION_VARS: &[&str] = &[ V_LISTENER, @@ -52,6 +56,19 @@ pub const CONNECTION_VARS: &[&str] = &[ V_PROTOCOL, V_TLS, ]; +pub const HTTP_VARS: &[&str] = &[ + V_LISTENER, + V_REMOTE_IP, + V_REMOTE_PORT, + V_LOCAL_IP, + V_LOCAL_PORT, + V_PROTOCOL, + V_TLS, + V_URL, + V_URL_PATH, + V_HEADERS, + V_METHOD, +]; pub const RCPT_DOMAIN_VARS: &[&str] = &[V_RECIPIENT_DOMAIN]; pub const SMTP_EHLO_VARS: &[&str] = &[ V_LISTENER, diff --git a/src/pages/config/schema/server.rs b/src/pages/config/schema/server.rs index ed094fd..cfb7da9 100644 --- a/src/pages/config/schema/server.rs +++ b/src/pages/config/schema/server.rs @@ -6,11 +6,11 @@ use crate::core::schema::*; -use super::{tracing::EVENT_NAMES, CONNECTION_VARS}; +use super::{tracing::EVENT_NAMES, HTTP_VARS}; impl Builder { pub fn build_server(self) -> Self { - let connect_expr = ExpressionValidator::new(CONNECTION_VARS, &[]); + let http_expr = ExpressionValidator::new(HTTP_VARS, &[]); self.new_schema("network") // Default hostname @@ -34,6 +34,34 @@ impl Builder { ) .default("8192") .build() + // Network fields + .add_network_fields(false) + // Forms + .new_form_section() + .title("Network settings") + .fields([ + "lookup.default.hostname", + "server.max-connections", + "server.proxy.trusted-networks", + ]) + .build() + .new_form_section() + .title("Socket options") + .fields([ + "server.socket.backlog", + "server.socket.ttl", + "server.socket.linger", + "server.socket.tos", + "server.socket.send-buffer-size", + "server.socket.recv-buffer-size", + "server.socket.nodelay", + "server.socket.reuse-addr", + "server.socket.reuse-port", + ]) + .build() + .build() + // HTTP settings + .new_schema("http") // HTTP base URL .new_field("server.http.url") .label("Base URL") @@ -41,13 +69,24 @@ impl Builder { .typ(Type::Expression) .input_check( [], - [ - Validator::Required, - Validator::IsValidExpression(connect_expr), - ], + [Validator::Required, Validator::IsValidExpression(http_expr)], ) .default("protocol + '://' + key_get('default', 'hostname') + ':' + local_port") .build() + // HTTP endpoint security + .new_field("server.http.allowed-endpoint") + .label("Allowed endpoints") + .help(concat!( + "An expression that determines whether access to an endpoint is allowed. ", + "The expression should an HTTP status code (200, 403, etc.)" + )) + .typ(Type::Expression) + .input_check( + [], + [Validator::Required, Validator::IsValidExpression(http_expr)], + ) + .default("200") + .build() // Use X-Forwarded-For .new_field("server.http.use-x-forwarded") .label("Obtain remote IP from Forwarded header") @@ -85,39 +124,20 @@ impl Builder { .typ(Type::Array) .input_check([Transformer::Trim], []) .build() - // Network fields - .add_network_fields(false) - // Forms - .new_form_section() - .title("Network settings") - .fields([ - "lookup.default.hostname", - "server.max-connections", - "server.proxy.trusted-networks", - ]) - .build() .new_form_section() .title("HTTP Settings") .fields([ "server.http.url", "server.http.headers", - "server.http.hsts", "server.http.use-x-forwarded", - "server.http.permissive-cors", ]) .build() .new_form_section() - .title("Socket options") + .title("HTTP Security") .fields([ - "server.socket.backlog", - "server.socket.ttl", - "server.socket.linger", - "server.socket.tos", - "server.socket.send-buffer-size", - "server.socket.recv-buffer-size", - "server.socket.nodelay", - "server.socket.reuse-addr", - "server.socket.reuse-port", + "server.http.allowed-endpoint", + "server.http.hsts", + "server.http.permissive-cors", ]) .build() .build() diff --git a/src/pages/config/schema/tracing.rs b/src/pages/config/schema/tracing.rs index b3faf20..a27adbe 100644 --- a/src/pages/config/schema/tracing.rs +++ b/src/pages/config/schema/tracing.rs @@ -7,7 +7,7 @@ use crate::core::schema::*; impl Builder { - pub fn build_tracing(self) -> Self { + pub fn build_telemetry(self) -> Self { self.new_schema("tracing") .names("tracer", "tracers") .prefix("tracer") @@ -277,6 +277,117 @@ impl Builder { .list_subtitle("Manage custom event logging levels") .list_fields(["_id", "_value"]) .build() + // Metrics + .new_schema("metrics") + // OT Transport + .new_field("metrics.open-telemetry.transport") + .typ(Type::Select { + typ: SelectType::Single, + source: Source::Static(&[ + ("disabled", "Disabled"), + ("http", "HTTP"), + ("grpc", "gRPC"), + ]), + }) + .label("Transport") + .help("The transport protocol for Open Telemetry") + .input_check([], [Validator::Required]) + .default("disabled") + .build() + // OT Endpoint + .new_field("metrics.open-telemetry.endpoint") + .typ(Type::Input) + .label("Endpoint") + .help("The endpoint for Open Telemetry") + .placeholder("https://tracing.example.com/v1/otel") + .input_check([Transformer::Trim], [Validator::Required, Validator::IsUrl]) + .display_if_eq("metrics.open-telemetry.transport", ["http", "grpc"]) + .build() + // OT Headers + .new_field("metrics.open-telemetry.headers") + .typ(Type::Array) + .label("HTTP Headers") + .help("The headers to be sent with OpenTelemetry requests") + .display_if_eq("metrics.open-telemetry.transport", ["http"]) + .build() + // OT Timeout + .new_field("metrics.open-telemetry.timeout") + .label("Timeout") + .help(concat!( + "Maximum amount of time that Stalwart will wait for a response ", + "from the OpenTelemetry endpoint" + )) + .default("10s") + .typ(Type::Duration) + .display_if_eq("metrics.open-telemetry.transport", ["http", "grpc"]) + .input_check([], [Validator::Required]) + .build() + // OT Throttle + .new_field("metrics.open-telemetry.interval") + .label("Push interval") + .help(concat!( + "The minimum amount of time that must pass between ", + "each push request to the OpenTelemetry endpoint" + )) + .default("1m") + .display_if_eq("metrics.open-telemetry.transport", ["http", "grpc"]) + .typ(Type::Duration) + .input_check([], [Validator::Required]) + .build() + // Prometheus auth + .new_field("metrics.prometheus.enable") + .typ(Type::Boolean) + .label("Enable endpoint") + .help("Enable the Prometheus metrics endpoint") + .default("false") + .build() + .new_field("metrics.prometheus.auth.username") + .label("Username") + .help(concat!( + "The Prometheus endpoint's username for Basic authentication" + )) + .typ(Type::Input) + .input_check([Transformer::Trim], []) + .build() + .new_field("metrics.prometheus.auth.secret") + .label("Secret") + .help(concat!( + "The Prometheus endpoint's secret for Basic authentication" + )) + .typ(Type::Secret) + .build() + // Disabled events + .new_field("metrics.disabled-events") + .label("Disabled Metrics") + .help("Which events to disable for metrics") + .typ(Type::Select { + typ: SelectType::ManyWithSearch, + source: Source::StaticId(EVENT_NAMES), + }) + .build() + .new_form_section() + .title("OpenTelemetry Push Metrics") + .fields([ + "metrics.open-telemetry.transport", + "metrics.open-telemetry.endpoint", + "metrics.open-telemetry.timeout", + "metrics.open-telemetry.interval", + "metrics.open-telemetry.headers", + ]) + .build() + .new_form_section() + .title("Prometheus Pull Metrics") + .fields([ + "metrics.prometheus.auth.username", + "metrics.prometheus.auth.secret", + "metrics.prometheus.enable", + ]) + .build() + .new_form_section() + .title("Override metrics") + .fields(["metrics.disabled-events"]) + .build() + .build() } } @@ -556,6 +667,12 @@ pub static EVENT_NAMES: &[&str] = &[ "manage.missing-parameter", "manage.not-found", "manage.not-supported", + "message-ingest.duplicate", + "message-ingest.error", + "message-ingest.ham", + "message-ingest.imap-append", + "message-ingest.jmap-append", + "message-ingest.spam", "milter.action-accept", "milter.action-connection-failure", "milter.action-discard", @@ -647,10 +764,14 @@ pub static EVENT_NAMES: &[&str] = &[ "queue.concurrency-limit-exceeded", "queue.lock-busy", "queue.locked", + "queue.queue-autogenerated", + "queue.queue-dsn", + "queue.queue-message", + "queue.queue-message-submission", + "queue.queue-report", "queue.quota-exceeded", "queue.rate-limit-exceeded", "queue.rescheduled", - "queue.scheduled", "resource.bad-parameters", "resource.download-external", "resource.error", @@ -769,17 +890,19 @@ pub static EVENT_NAMES: &[&str] = &[ "spf.soft-fail", "spf.temp-error", "store.assert-value-failed", + "store.blob-delete", "store.blob-missing-marker", + "store.blob-read", + "store.blob-write", "store.crypto-error", "store.data-corruption", + "store.data-iterate", + "store.data-write", "store.decompress-error", "store.deserialize-error", "store.elasticsearch-error", "store.filesystem-error", "store.foundationdb-error", - "store.ingest", - "store.ingest-duplicate", - "store.ingest-error", "store.ldap-bind", "store.ldap-error", "store.ldap-query", @@ -795,6 +918,13 @@ pub static EVENT_NAMES: &[&str] = &[ "store.sql-query", "store.sqlite-error", "store.unexpected-error", + "telemetry.journal-error", + "telemetry.log-error", + "telemetry.otel-expoter-error", + "telemetry.otel-metrics-exporter-error", + "telemetry.prometheus-exporter-error", + "telemetry.update", + "telemetry.webhook-error", "tls-rpt.record-fetch", "tls-rpt.record-fetch-error", "tls.certificate-not-found", @@ -803,9 +933,4 @@ pub static EVENT_NAMES: &[&str] = &[ "tls.multiple-certificates-available", "tls.no-certificates-available", "tls.not-configured", - "tracing.journal-error", - "tracing.log-error", - "tracing.otel-error", - "tracing.update", - "tracing.webhook-error", ];