Improve verbosity (closes #4 closes #19)

This commit is contained in:
Maurus Decimus
2026-07-18 11:41:08 +02:00
parent 9fc9178b67
commit 846fc88f7e
13 changed files with 554 additions and 44 deletions
+89 -11
View File
@@ -7,7 +7,7 @@
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use std::time::{Duration, SystemTime};
use std::time::{Duration, Instant, SystemTime};
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
@@ -15,12 +15,13 @@ use serde_json::Value;
use ureq::Agent;
use ureq::config::{Config, RedirectAuthHeaders};
use ureq::tls::{RootCerts, TlsConfig};
use ureq::{ResponseExt, http::Uri};
use crate::jmap::error::JmapError;
use crate::jmap::inflight::{Permit, Semaphore};
use crate::jmap::retry::{self, Disposition, RateLimitState};
use crate::jmap::session::Limits;
use crate::logging::{LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
const MAX_BODY: u64 = 512 * 1024 * 1024;
@@ -253,7 +254,7 @@ impl HttpClient {
} else {
None
};
self.one_attempt(url, body, content_type)
self.one_attempt(method, url, body, content_type)
};
match attempt_outcome {
Attempt::Ok { status, body, .. } if (200..300).contains(&status) => {
@@ -366,8 +367,16 @@ impl HttpClient {
}
}
fn one_attempt(&self, url: &str, body: Option<&[u8]>, content_type: Option<&str>) -> Attempt {
fn one_attempt(
&self,
method: &str,
url: &str,
body: Option<&[u8]>,
content_type: Option<&str>,
) -> Attempt {
let auth = self.inner.auth.header_value();
let logger = self.logger();
let started = Instant::now();
let result = if let Some(payload) = body {
let mut req = self
.inner
@@ -390,6 +399,12 @@ impl HttpClient {
match result {
Ok(mut resp) => {
let status = resp.status().as_u16();
let final_uri = resp.get_uri().clone();
let response_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let retry_after = resp
.headers()
.get("retry-after")
@@ -411,18 +426,37 @@ impl HttpClient {
})
.collect();
match resp.body_mut().with_config().limit(MAX_BODY).read_to_vec() {
Ok(bytes) => Attempt::Ok {
status,
body: bytes,
retry_after,
rate_limit_headers,
},
Ok(bytes) => {
warn_on_redirect(&logger, method, url, &final_uri);
logger.trace_http(&HttpCall {
proto: "JMAP",
method,
url,
status,
elapsed: started.elapsed(),
note: None,
request: body,
request_type: content_type,
response: &bytes,
response_type: response_type.as_deref(),
});
Attempt::Ok {
status,
body: bytes,
retry_after,
rate_limit_headers,
}
}
Err(e) => Attempt::Transport(JmapError::Transport(format!(
"reading response body: {e}"
))),
}
}
Err(e) => Attempt::Transport(map_ureq_error(e)),
Err(e) => {
let err = map_ureq_error(e);
logger.trace_http_error("JMAP", method, url, &err.to_string(), started.elapsed());
Attempt::Transport(err)
}
}
}
@@ -498,6 +532,26 @@ struct RetryLog<'a> {
body: &'a [u8],
}
fn warn_on_redirect(logger: &Logger, method: &str, requested: &str, final_uri: &Uri) {
let landed = final_uri.to_string();
if cross_host(requested, &landed) {
logger.warn(&format!(
"{method} {requested} was redirected across hosts to {landed}; a \
JMAP endpoint must not redirect: verify the server's advertised base \
URL and that the request host matches its configured hostname"
));
}
}
pub fn cross_host(requested: &str, landed: &str) -> bool {
match (url::Url::parse(requested), url::Url::parse(landed)) {
(Ok(a), Ok(b)) => {
a.host_str() != b.host_str() || a.port_or_known_default() != b.port_or_known_default()
}
_ => false,
}
}
fn transport_disposition(err: &JmapError) -> Disposition {
match err {
JmapError::Connect(_) => Disposition::Fatal,
@@ -600,6 +654,30 @@ mod tests {
assert_eq!(auth.header_value(), "Bearer abc.def");
}
#[test]
fn cross_host_detects_hostname_change() {
assert!(cross_host(
"https://mail.example.com/jmap",
"https://login.example.net/sso"
));
}
#[test]
fn cross_host_ignores_same_host_path_change() {
assert!(!cross_host(
"https://mail.example.com/jmap",
"https://mail.example.com/jmap/api"
));
}
#[test]
fn cross_host_flags_port_change() {
assert!(cross_host(
"https://mail.example.com/jmap",
"https://mail.example.com:8443/jmap"
));
}
#[test]
fn truncate_does_not_panic_on_multibyte_boundary() {
let mut body = vec![b'a'; 510];