JMAP Registry API implementation - part 10
This commit is contained in:
@@ -4,129 +4,32 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
net::{IpAddr, SocketAddr},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use common::{
|
||||
Server,
|
||||
auth::{AccessToken, oauth::GrantType},
|
||||
config::smtp::{
|
||||
queue::MxConfig,
|
||||
resolver::{Policy, Tlsa},
|
||||
},
|
||||
psl,
|
||||
};
|
||||
use http_body_util::{StreamBody, combinators::BoxBody};
|
||||
use hyper::{
|
||||
Method, StatusCode,
|
||||
body::{Bytes, Frame},
|
||||
};
|
||||
use mail_auth::{
|
||||
AuthenticatedMessage, DkimResult, DmarcResult, IpLookupStrategy, IprevOutput, IprevResult,
|
||||
SpfOutput, SpfResult,
|
||||
dmarc::{self, verify::DmarcParameters},
|
||||
mta_sts::TlsRpt,
|
||||
spf::verify::SpfParameters,
|
||||
};
|
||||
use hyper::body::{Bytes, Frame};
|
||||
use mail_auth::{IpLookupStrategy, mta_sts::TlsRpt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use smtp::outbound::{
|
||||
client::{SmtpClient, StartTlsResult},
|
||||
dane::{dnssec::TlsaLookup, verify::TlsaVerify},
|
||||
lookup::{DnsLookup, ToNextHop},
|
||||
mta_sts::{lookup::MtaStsLookup, verify::VerifyPolicy},
|
||||
};
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::{io::AsyncWriteExt, sync::mpsc};
|
||||
use utils::url_params::UrlParams;
|
||||
|
||||
use http_proto::{request::decode_path_element, *};
|
||||
|
||||
pub trait TroubleshootApi: Sync + Send {
|
||||
fn handle_diagnose_api_request(
|
||||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
access_token: &AccessToken,
|
||||
body: Option<Vec<u8>>,
|
||||
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl TroubleshootApi for Server {
|
||||
async fn handle_diagnose_api_request(
|
||||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
access_token: &AccessToken,
|
||||
body: Option<Vec<u8>>,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let account_id = access_token.account_id();
|
||||
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
path.get(2).copied(),
|
||||
req.method(),
|
||||
) {
|
||||
("token", None, &Method::GET) => {
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.encode_access_token(GrantType::Diagnose, account_id, "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
("delivery", Some(target), &Method::GET) => {
|
||||
let timeout = Duration::from_secs(
|
||||
params
|
||||
.parse::<u64>("timeout")
|
||||
.filter(|interval| *interval >= 1)
|
||||
.unwrap_or(30),
|
||||
);
|
||||
|
||||
let mut rx = spawn_delivery_diagnose(
|
||||
self.clone(),
|
||||
decode_path_element(target).to_lowercase(),
|
||||
timeout,
|
||||
);
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/event-stream")
|
||||
.with_cache_control("no-store")
|
||||
.with_stream_body(BoxBody::new(StreamBody::new(async_stream::stream! {
|
||||
while let Some(stage) = rx.recv().await {
|
||||
yield Ok(stage.to_frame());
|
||||
}
|
||||
yield Ok(DeliveryStage::Completed.to_frame());
|
||||
}))))
|
||||
}
|
||||
("dmarc", None, &Method::POST) => {
|
||||
let request = serde_json::from_slice::<DmarcTroubleshootRequest>(
|
||||
body.as_deref().unwrap_or_default(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err)
|
||||
})?;
|
||||
let response = dmarc_diagnose(self, request).await.ok_or_else(|| {
|
||||
trc::EventType::Resource(trc::ResourceEvent::BadParameters)
|
||||
.reason("Failed to parse message body")
|
||||
})?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": response,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
enum DeliveryStage {
|
||||
pub(crate) enum DeliveryStage {
|
||||
MxLookupStart {
|
||||
domain: String,
|
||||
},
|
||||
@@ -253,7 +156,7 @@ enum DeliveryStage {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct MX {
|
||||
pub(crate) struct MX {
|
||||
pub exchanges: Vec<String>,
|
||||
pub preference: u16,
|
||||
}
|
||||
@@ -267,7 +170,7 @@ pub enum ReportUri {
|
||||
}
|
||||
|
||||
impl DeliveryStage {
|
||||
fn to_frame(&self) -> Frame<Bytes> {
|
||||
pub fn to_frame(&self) -> Frame<Bytes> {
|
||||
let payload = format!(
|
||||
"event: event\ndata: [{}]\n\n",
|
||||
serde_json::to_string(self).unwrap_or_default()
|
||||
@@ -285,7 +188,7 @@ impl ElapsedMs for Instant {
|
||||
self.elapsed().as_millis() as u64
|
||||
}
|
||||
}
|
||||
fn spawn_delivery_diagnose(
|
||||
pub(crate) fn spawn_delivery_diagnose(
|
||||
server: Server,
|
||||
domain_or_email: String,
|
||||
timeout: Duration,
|
||||
@@ -816,388 +719,3 @@ async fn delivery_diagnose(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct DmarcTroubleshootRequest {
|
||||
#[serde(rename = "remoteIp")]
|
||||
remote_ip: IpAddr,
|
||||
#[serde(rename = "ehloDomain")]
|
||||
ehlo_domain: String,
|
||||
#[serde(rename = "mailFrom")]
|
||||
mail_from: String,
|
||||
body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct DmarcTroubleshootResponse {
|
||||
#[serde(rename = "spfEhloDomain")]
|
||||
spf_ehlo_domain: String,
|
||||
#[serde(rename = "spfEhloResult")]
|
||||
spf_ehlo_result: AuthResult,
|
||||
#[serde(rename = "spfMailFromDomain")]
|
||||
spf_mail_from_domain: String,
|
||||
#[serde(rename = "spfMailFromResult")]
|
||||
spf_mail_from_result: AuthResult,
|
||||
#[serde(rename = "ipRevResult")]
|
||||
ip_rev_result: AuthResult,
|
||||
#[serde(rename = "ipRevPtr")]
|
||||
ip_rev_ptr: Vec<String>,
|
||||
#[serde(rename = "dkimResults")]
|
||||
dkim_results: Vec<AuthResult>,
|
||||
#[serde(rename = "dkimPass")]
|
||||
dkim_pass: bool,
|
||||
#[serde(rename = "arcResult")]
|
||||
arc_result: AuthResult,
|
||||
#[serde(rename = "dmarcResult")]
|
||||
dmarc_result: AuthResult,
|
||||
#[serde(rename = "dmarcPass")]
|
||||
dmarc_pass: bool,
|
||||
#[serde(rename = "dmarcPolicy")]
|
||||
dmarc_policy: DmarcPolicy,
|
||||
elapsed: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum AuthResult {
|
||||
Pass,
|
||||
Fail { details: Option<String> },
|
||||
SoftFail { details: Option<String> },
|
||||
TempError { details: Option<String> },
|
||||
PermError { details: Option<String> },
|
||||
Neutral { details: Option<String> },
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum DmarcPolicy {
|
||||
None,
|
||||
Quarantine,
|
||||
Reject,
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
async fn dmarc_diagnose(
|
||||
server: &Server,
|
||||
request: DmarcTroubleshootRequest,
|
||||
) -> Option<DmarcTroubleshootResponse> {
|
||||
let remote_ip = request.remote_ip;
|
||||
let ehlo_domain = request.ehlo_domain.to_lowercase();
|
||||
let mail_from = request.mail_from.to_lowercase();
|
||||
let mail_from_domain = mail_from.rsplit_once('@').map(|(_, domain)| domain);
|
||||
|
||||
let local_host = &server.core.network.server_name;
|
||||
|
||||
let now = Instant::now();
|
||||
let ehlo_spf_output = server
|
||||
.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.verify_spf(
|
||||
server
|
||||
.inner
|
||||
.cache
|
||||
.build_auth_parameters(SpfParameters::verify_ehlo(
|
||||
remote_ip,
|
||||
&ehlo_domain,
|
||||
local_host,
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
let iprev = server
|
||||
.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.verify_iprev(server.inner.cache.build_auth_parameters(remote_ip))
|
||||
.await;
|
||||
let mail_spf_output = if let Some(mail_from_domain) = mail_from_domain {
|
||||
server
|
||||
.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.check_host(server.inner.cache.build_auth_parameters(SpfParameters::new(
|
||||
remote_ip,
|
||||
mail_from_domain,
|
||||
&ehlo_domain,
|
||||
local_host,
|
||||
&mail_from,
|
||||
)))
|
||||
.await
|
||||
} else {
|
||||
server
|
||||
.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.check_host(server.inner.cache.build_auth_parameters(SpfParameters::new(
|
||||
remote_ip,
|
||||
&ehlo_domain,
|
||||
&ehlo_domain,
|
||||
local_host,
|
||||
&format!("postmaster@{ehlo_domain}"),
|
||||
)))
|
||||
.await
|
||||
};
|
||||
|
||||
let body = request
|
||||
.body
|
||||
.unwrap_or_else(|| format!("From: {mail_from}\r\nSubject: test\r\n\r\ntest"));
|
||||
let auth_message = AuthenticatedMessage::parse_with_opts(body.as_bytes(), true)?;
|
||||
|
||||
let dkim_output = server
|
||||
.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.verify_dkim(server.inner.cache.build_auth_parameters(&auth_message))
|
||||
.await;
|
||||
let dkim_pass = dkim_output
|
||||
.iter()
|
||||
.any(|d| matches!(d.result(), DkimResult::Pass));
|
||||
|
||||
let arc_output = server
|
||||
.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.verify_arc(server.inner.cache.build_auth_parameters(&auth_message))
|
||||
.await;
|
||||
|
||||
let dmarc_output = server
|
||||
.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.verify_dmarc(server.inner.cache.build_auth_parameters(DmarcParameters {
|
||||
message: &auth_message,
|
||||
dkim_output: &dkim_output,
|
||||
rfc5321_mail_from_domain: mail_from_domain.unwrap_or(ehlo_domain.as_str()),
|
||||
spf_output: &mail_spf_output,
|
||||
domain_suffix_fn: |domain| psl::domain_str(domain).unwrap_or(domain),
|
||||
}))
|
||||
.await;
|
||||
let dmarc_pass = matches!(dmarc_output.spf_result(), DmarcResult::Pass)
|
||||
|| matches!(dmarc_output.dkim_result(), DmarcResult::Pass);
|
||||
let dmarc_result = if dmarc_pass {
|
||||
DmarcResult::Pass
|
||||
} else if dmarc_output.spf_result() != &DmarcResult::None {
|
||||
dmarc_output.spf_result().clone()
|
||||
} else if dmarc_output.dkim_result() != &DmarcResult::None {
|
||||
dmarc_output.dkim_result().clone()
|
||||
} else {
|
||||
DmarcResult::None
|
||||
};
|
||||
|
||||
Some(DmarcTroubleshootResponse {
|
||||
spf_ehlo_domain: ehlo_spf_output.domain().to_string(),
|
||||
spf_ehlo_result: (&ehlo_spf_output).into(),
|
||||
spf_mail_from_domain: mail_spf_output.domain().to_string(),
|
||||
spf_mail_from_result: (&mail_spf_output).into(),
|
||||
ip_rev_ptr: iprev
|
||||
.ptr
|
||||
.as_ref()
|
||||
.map(|ptr| ptr.iter().map(|s| s.to_string()).collect())
|
||||
.unwrap_or_default(),
|
||||
ip_rev_result: (&iprev).into(),
|
||||
dkim_pass,
|
||||
dkim_results: dkim_output
|
||||
.iter()
|
||||
.map(|result| result.result().into())
|
||||
.collect(),
|
||||
arc_result: arc_output.result().into(),
|
||||
dmarc_result: (&dmarc_result).into(),
|
||||
dmarc_policy: (&dmarc_output.policy()).into(),
|
||||
dmarc_pass,
|
||||
elapsed: now.elapsed_ms(),
|
||||
})
|
||||
}
|
||||
|
||||
impl From<&SpfOutput> for AuthResult {
|
||||
fn from(value: &SpfOutput) -> Self {
|
||||
match value.result() {
|
||||
SpfResult::Pass => AuthResult::Pass,
|
||||
SpfResult::Fail => AuthResult::Fail {
|
||||
details: value.explanation().map(|e| e.to_string()),
|
||||
},
|
||||
SpfResult::SoftFail => AuthResult::SoftFail {
|
||||
details: value.explanation().map(|e| e.to_string()),
|
||||
},
|
||||
SpfResult::Neutral => AuthResult::Neutral {
|
||||
details: value.explanation().map(|e| e.to_string()),
|
||||
},
|
||||
SpfResult::TempError => AuthResult::TempError {
|
||||
details: value.explanation().map(|e| e.to_string()),
|
||||
},
|
||||
SpfResult::PermError => AuthResult::PermError {
|
||||
details: value.explanation().map(|e| e.to_string()),
|
||||
},
|
||||
SpfResult::None => AuthResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthResult> for SpfOutput {
|
||||
fn from(value: AuthResult) -> Self {
|
||||
match value {
|
||||
AuthResult::Pass => SpfOutput::new(String::new()).with_result(SpfResult::Pass),
|
||||
AuthResult::Fail { .. } => SpfOutput::new(String::new()).with_result(SpfResult::Fail),
|
||||
AuthResult::SoftFail { .. } => {
|
||||
SpfOutput::new(String::new()).with_result(SpfResult::SoftFail)
|
||||
}
|
||||
AuthResult::Neutral { .. } => {
|
||||
SpfOutput::new(String::new()).with_result(SpfResult::Neutral)
|
||||
}
|
||||
AuthResult::TempError { .. } => {
|
||||
SpfOutput::new(String::new()).with_result(SpfResult::TempError)
|
||||
}
|
||||
AuthResult::PermError { .. } => {
|
||||
SpfOutput::new(String::new()).with_result(SpfResult::PermError)
|
||||
}
|
||||
AuthResult::None => SpfOutput::new(String::new()).with_result(SpfResult::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&IprevOutput> for AuthResult {
|
||||
fn from(value: &IprevOutput) -> Self {
|
||||
match &value.result {
|
||||
IprevResult::Pass => AuthResult::Pass,
|
||||
IprevResult::Fail(error) => AuthResult::Fail {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
IprevResult::TempError(error) => AuthResult::TempError {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
IprevResult::PermError(error) => AuthResult::PermError {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
IprevResult::None => AuthResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthResult> for IprevResult {
|
||||
fn from(value: AuthResult) -> Self {
|
||||
match value {
|
||||
AuthResult::Pass => IprevResult::Pass,
|
||||
AuthResult::Fail { details } => {
|
||||
IprevResult::Fail(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::TempError { details } => {
|
||||
IprevResult::TempError(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::PermError { details } => {
|
||||
IprevResult::PermError(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::None => IprevResult::None,
|
||||
_ => IprevResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&DkimResult> for AuthResult {
|
||||
fn from(value: &DkimResult) -> Self {
|
||||
match value {
|
||||
DkimResult::Pass => AuthResult::Pass,
|
||||
DkimResult::Neutral(error) => AuthResult::Neutral {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
DkimResult::Fail(error) => AuthResult::Fail {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
DkimResult::PermError(error) => AuthResult::PermError {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
DkimResult::TempError(error) => AuthResult::TempError {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
DkimResult::None => AuthResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthResult> for DkimResult {
|
||||
fn from(value: AuthResult) -> Self {
|
||||
match value {
|
||||
AuthResult::Pass => DkimResult::Pass,
|
||||
AuthResult::Neutral { details } => {
|
||||
DkimResult::Neutral(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::Fail { details } => {
|
||||
DkimResult::Fail(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::PermError { details } => {
|
||||
DkimResult::PermError(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::TempError { details } => {
|
||||
DkimResult::TempError(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
_ => DkimResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&DmarcResult> for AuthResult {
|
||||
fn from(value: &DmarcResult) -> Self {
|
||||
match value {
|
||||
DmarcResult::Pass => AuthResult::Pass,
|
||||
DmarcResult::Fail(error) => AuthResult::Fail {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
DmarcResult::TempError(error) => AuthResult::TempError {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
DmarcResult::PermError(error) => AuthResult::PermError {
|
||||
details: error.to_string().into(),
|
||||
},
|
||||
DmarcResult::None => AuthResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthResult> for DmarcResult {
|
||||
fn from(value: AuthResult) -> Self {
|
||||
match value {
|
||||
AuthResult::Pass => DmarcResult::Pass,
|
||||
AuthResult::Fail { details } => {
|
||||
DmarcResult::Fail(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::TempError { details } => {
|
||||
DmarcResult::TempError(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::PermError { details } => {
|
||||
DmarcResult::PermError(mail_auth::Error::Io(details.unwrap_or_default()))
|
||||
}
|
||||
AuthResult::None => DmarcResult::None,
|
||||
_ => DmarcResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dmarc::Policy> for DmarcPolicy {
|
||||
fn from(value: &dmarc::Policy) -> Self {
|
||||
match value {
|
||||
dmarc::Policy::None => DmarcPolicy::None,
|
||||
dmarc::Policy::Quarantine => DmarcPolicy::Quarantine,
|
||||
dmarc::Policy::Reject => DmarcPolicy::Reject,
|
||||
dmarc::Policy::Unspecified => DmarcPolicy::Unspecified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DmarcPolicy> for dmarc::Policy {
|
||||
fn from(value: DmarcPolicy) -> Self {
|
||||
match value {
|
||||
DmarcPolicy::None => dmarc::Policy::None,
|
||||
DmarcPolicy::Quarantine => dmarc::Policy::Quarantine,
|
||||
DmarcPolicy::Reject => dmarc::Policy::Reject,
|
||||
DmarcPolicy::Unspecified => dmarc::Policy::Unspecified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,23 @@ pub mod telemetry;
|
||||
// SPDX-SnippetEnd
|
||||
pub mod diagnose;
|
||||
|
||||
use crate::management::diagnose::TroubleshootApi;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body};
|
||||
use hyper::{StatusCode, header};
|
||||
use crate::management::diagnose::{DeliveryStage, spawn_delivery_diagnose};
|
||||
use common::{
|
||||
Server,
|
||||
auth::{AccessToken, oauth::GrantType},
|
||||
};
|
||||
use http_body_util::{StreamBody, combinators::BoxBody};
|
||||
use http_proto::{
|
||||
HttpRequest, HttpResponse, HttpSessionData, JsonResponse, ToHttpResponse,
|
||||
request::{decode_path_element, fetch_body},
|
||||
};
|
||||
use hyper::{Method, StatusCode, header};
|
||||
use jmap::api::{ToJmapHttpResponse, ToRequestError};
|
||||
use jmap_proto::error::request::RequestError;
|
||||
use registry::schema::enums::Permission;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
use utils::url_params::UrlParams;
|
||||
|
||||
pub trait ManagementApi: Sync + Send {
|
||||
fn handle_api_manage_request(
|
||||
@@ -41,37 +51,116 @@ impl ManagementApi for Server {
|
||||
let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>();
|
||||
|
||||
match path.first().copied().unwrap_or_default() {
|
||||
"diagnose" => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::Troubleshoot)?;
|
||||
"token" => {
|
||||
let account_id = access_token.account_id();
|
||||
match path.get(1).copied() {
|
||||
// SPDX-SnippetBegin
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
Some("tracing") if self.core.is_enterprise_edition() => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::TracingLive)?;
|
||||
|
||||
self.handle_diagnose_api_request(req, path, access_token, body)
|
||||
.await
|
||||
}
|
||||
// SPDX-SnippetBegin
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
"telemetry" => {
|
||||
// WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED
|
||||
// Any attempt to modify, bypass, or disable this license validation mechanism
|
||||
// constitutes a severe violation of the Stalwart Enterprise License Agreement.
|
||||
// Such actions may result in immediate termination of your license, legal action,
|
||||
// and substantial financial penalties. Stalwart Labs LLC actively monitors for
|
||||
// unauthorized modifications and will pursue all available legal remedies against
|
||||
// violators to the fullest extent of the law, including but not limited to claims
|
||||
// for copyright infringement, breach of contract, and fraud.
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.encode_access_token(GrantType::LiveTracing, account_id, "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
Some("metrics") if self.core.is_enterprise_edition() => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::MetricsLive)?;
|
||||
|
||||
if self.core.is_enterprise_edition() {
|
||||
use crate::management::telemetry::TelemetryApi;
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.encode_access_token(GrantType::LiveMetrics, account_id, "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
// SPDX-SnippetEnd
|
||||
Some("delivery") => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::Troubleshoot)?;
|
||||
|
||||
self.handle_telemetry_api_request(req, path, access_token)
|
||||
.await
|
||||
} else {
|
||||
Err(trc::ResourceEvent::NotFound.ctx(trc::Key::Details, "Enterprise feature"))
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.encode_access_token(GrantType::Diagnose, account_id, "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
Some("tracing") | Some("metrics") => {
|
||||
Err(trc::ResourceEvent::NotFound
|
||||
.ctx(trc::Key::Details, "Enterprise feature"))
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
}
|
||||
}
|
||||
// SPDX-SnippetEnd
|
||||
"live" => {
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let account_id = access_token.account_id();
|
||||
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
path.get(2).copied(),
|
||||
req.method(),
|
||||
) {
|
||||
("delivery", Some(target), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::Troubleshoot)?;
|
||||
|
||||
let timeout = Duration::from_secs(
|
||||
params
|
||||
.parse::<u64>("timeout")
|
||||
.filter(|interval| *interval >= 1)
|
||||
.unwrap_or(30),
|
||||
);
|
||||
|
||||
let mut rx = spawn_delivery_diagnose(
|
||||
self.clone(),
|
||||
decode_path_element(target).to_lowercase(),
|
||||
timeout,
|
||||
);
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/event-stream")
|
||||
.with_cache_control("no-store")
|
||||
.with_stream_body(BoxBody::new(StreamBody::new(
|
||||
async_stream::stream! {
|
||||
while let Some(stage) = rx.recv().await {
|
||||
yield Ok(stage.to_frame());
|
||||
}
|
||||
yield Ok(DeliveryStage::Completed.to_frame());
|
||||
},
|
||||
))))
|
||||
}
|
||||
// SPDX-SnippetBegin
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
("traces", _, &Method::GET) if self.core.is_enterprise_edition() => {
|
||||
use crate::management::telemetry::TelemetryApi;
|
||||
|
||||
self.handle_telemetry_api_request(req, true, access_token)
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
("metrics", _, &Method::GET) if self.core.is_enterprise_edition() => {
|
||||
use crate::management::telemetry::TelemetryApi;
|
||||
|
||||
self.handle_telemetry_api_request(req, false, access_token)
|
||||
.await
|
||||
}
|
||||
// SPDX-SnippetEnd
|
||||
("traces" | "metrics", _, &Method::GET) => {
|
||||
Err(trc::ResourceEvent::NotFound
|
||||
.ctx(trc::Key::Details, "Enterprise feature"))
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,15 @@
|
||||
*
|
||||
*/
|
||||
|
||||
use common::{
|
||||
Server,
|
||||
auth::{AccessToken, oauth::GrantType},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use http_body_util::{StreamBody, combinators::BoxBody};
|
||||
use http_proto::*;
|
||||
use hyper::{
|
||||
Method, StatusCode,
|
||||
StatusCode,
|
||||
body::{Bytes, Frame},
|
||||
};
|
||||
use mail_parser::DateTime;
|
||||
use registry::schema::enums::Permission;
|
||||
use serde_json::json;
|
||||
use std::future::Future;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
@@ -38,7 +34,7 @@ pub trait TelemetryApi: Sync + Send {
|
||||
fn handle_telemetry_api_request(
|
||||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
is_tracing: bool,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
|
||||
}
|
||||
@@ -47,48 +43,40 @@ impl TelemetryApi for Server {
|
||||
async fn handle_telemetry_api_request(
|
||||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
is_tracing: bool,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let account_id = access_token.account_id();
|
||||
let todo = "use same format as in JMAP API";
|
||||
if is_tracing {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::TracingLive)?;
|
||||
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
path.get(2).copied(),
|
||||
req.method(),
|
||||
) {
|
||||
("traces", Some("live"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::TracingLive)?;
|
||||
let mut key_filters = AHashMap::new();
|
||||
let mut filter = None;
|
||||
|
||||
let mut key_filters = AHashMap::new();
|
||||
let mut filter = None;
|
||||
|
||||
for (key, value) in params.into_inner() {
|
||||
if key == "filter" {
|
||||
filter = value.into_owned().into();
|
||||
} else if let Some(key) = Key::try_parse(key.to_ascii_lowercase().as_str()) {
|
||||
key_filters.insert(key, value.into_owned());
|
||||
}
|
||||
for (key, value) in params.into_inner() {
|
||||
if key == "filter" {
|
||||
filter = value.into_owned().into();
|
||||
} else if let Some(key) = Key::try_parse(key.to_ascii_lowercase().as_str()) {
|
||||
key_filters.insert(key, value.into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
let (_, mut rx) = SubscriberBuilder::new("live-tracer".to_string())
|
||||
.with_interests(Box::new(Bitset::all()))
|
||||
.with_lossy(false)
|
||||
.register();
|
||||
let throttle = Duration::from_secs(1);
|
||||
let ping_interval = Duration::from_secs(30);
|
||||
let ping_payload = Bytes::from(format!(
|
||||
"event: ping\ndata: {{\"interval\": {}}}\n\n",
|
||||
ping_interval.as_millis()
|
||||
));
|
||||
let mut last_ping = Instant::now();
|
||||
let mut events = Vec::new();
|
||||
let mut active_span_ids = AHashSet::new();
|
||||
let (_, mut rx) = SubscriberBuilder::new("live-tracer".to_string())
|
||||
.with_interests(Box::new(Bitset::all()))
|
||||
.with_lossy(false)
|
||||
.register();
|
||||
let throttle = Duration::from_secs(1);
|
||||
let ping_interval = Duration::from_secs(30);
|
||||
let ping_payload = Bytes::from(format!(
|
||||
"event: ping\ndata: {{\"interval\": {}}}\n\n",
|
||||
ping_interval.as_millis()
|
||||
));
|
||||
let mut last_ping = Instant::now();
|
||||
let mut events = Vec::new();
|
||||
let mut active_span_ids = AHashSet::new();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK)
|
||||
Ok(HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/event-stream")
|
||||
.with_cache_control("no-store")
|
||||
.with_stream_body(BoxBody::new(StreamBody::new(
|
||||
@@ -185,68 +173,47 @@ impl TelemetryApi for Server {
|
||||
}
|
||||
},
|
||||
))))
|
||||
}
|
||||
("live", Some("tracing-token"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::TracingLive)?;
|
||||
} else {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::MetricsLive)?;
|
||||
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.encode_access_token(GrantType::LiveTracing, account_id, "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
("live", Some("metrics-token"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::MetricsLive)?;
|
||||
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.encode_access_token(GrantType::LiveMetrics, account_id, "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
("metrics", Some("live"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.enforce_permission(Permission::MetricsLive)?;
|
||||
|
||||
let interval = Duration::from_secs(
|
||||
params
|
||||
.parse::<u64>("interval")
|
||||
.filter(|interval| *interval >= 1)
|
||||
.unwrap_or(30),
|
||||
);
|
||||
let mut event_types = AHashSet::new();
|
||||
let mut metric_types = AHashSet::new();
|
||||
for metric_name in params.get("metrics").unwrap_or_default().split(',') {
|
||||
let metric_name = metric_name.trim();
|
||||
if !metric_name.is_empty() {
|
||||
if let Some(event_type) = EventType::parse(metric_name) {
|
||||
event_types.insert(event_type);
|
||||
} else if let Some(metric_type) = MetricType::parse(metric_name) {
|
||||
metric_types.insert(metric_type);
|
||||
}
|
||||
let interval = Duration::from_secs(
|
||||
params
|
||||
.parse::<u64>("interval")
|
||||
.filter(|interval| *interval >= 1)
|
||||
.unwrap_or(30),
|
||||
);
|
||||
let mut event_types = AHashSet::new();
|
||||
let mut metric_types = AHashSet::new();
|
||||
for metric_name in params.get("metrics").unwrap_or_default().split(',') {
|
||||
let metric_name = metric_name.trim();
|
||||
if !metric_name.is_empty() {
|
||||
if let Some(event_type) = EventType::parse(metric_name) {
|
||||
event_types.insert(event_type);
|
||||
} else if let Some(metric_type) = MetricType::parse(metric_name) {
|
||||
metric_types.insert(metric_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh expensive metrics
|
||||
for metric_type in [
|
||||
MetricType::QueueCount,
|
||||
MetricType::UserCount,
|
||||
MetricType::DomainCount,
|
||||
] {
|
||||
if metric_types.contains(&metric_type) {
|
||||
let value = match metric_type {
|
||||
MetricType::QueueCount => self.total_queued_messages().await?,
|
||||
MetricType::UserCount => self.total_accounts().await? as u64,
|
||||
MetricType::DomainCount => self.total_domains().await? as u64,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Collector::update_gauge(metric_type, value);
|
||||
}
|
||||
// Refresh expensive metrics
|
||||
for metric_type in [
|
||||
MetricType::QueueCount,
|
||||
MetricType::UserCount,
|
||||
MetricType::DomainCount,
|
||||
] {
|
||||
if metric_types.contains(&metric_type) {
|
||||
let value = match metric_type {
|
||||
MetricType::QueueCount => self.total_queued_messages().await?,
|
||||
MetricType::UserCount => self.total_accounts().await? as u64,
|
||||
MetricType::DomainCount => self.total_domains().await? as u64,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Collector::update_gauge(metric_type, value);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK)
|
||||
Ok(HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/event-stream")
|
||||
.with_cache_control("no-store")
|
||||
.with_stream_body(BoxBody::new(StreamBody::new(
|
||||
@@ -309,8 +276,6 @@ impl TelemetryApi for Server {
|
||||
}
|
||||
},
|
||||
))))
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user