v0.1.8
This commit is contained in:
1 parent
b6e8ddb9c7
commit
818cae8592
12 files changed
+253
-33
No files matched your search
+129
-27
@@ -96,12 +96,71 @@ pub async fn handler(
|
||||
.map(|(l, _)| l)
|
||||
.unwrap_or_else(|| "-".into());
|
||||
let size = hdr("content-length").parse::<u64>().unwrap_or(0);
|
||||
let resp = inner(app.clone(), peer, method.clone(), headers, body).await;
|
||||
let mut resp = resp;
|
||||
// request headers for the trace (never credentials or cookies)
|
||||
let hdr_dump: Vec<String> = headers
|
||||
.iter()
|
||||
.filter(|(k, _)| {
|
||||
!matches!(
|
||||
k.as_str(),
|
||||
"authorization" | "cookie" | "proxy-authorization"
|
||||
)
|
||||
})
|
||||
.map(|(k, v)| {
|
||||
format!(
|
||||
"{}: {}",
|
||||
k.as_str(),
|
||||
String::from_utf8_lossy(v.as_bytes())
|
||||
.chars()
|
||||
.filter(|c| !c.is_control())
|
||||
.take(200)
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let typed_user = headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.split_once(' '))
|
||||
.and_then(|(_, d)| {
|
||||
use base64::Engine;
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(d.trim())
|
||||
.ok()
|
||||
})
|
||||
.and_then(|b| String::from_utf8(b).ok())
|
||||
.and_then(|d| {
|
||||
d.split_once(':')
|
||||
.map(|(u, _)| u.chars().take(120).collect::<String>())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let body_bytes = match http_body_util::Limited::new(body, 64 * 1024)
|
||||
.collect()
|
||||
.await
|
||||
{
|
||||
Ok(b) => b.to_bytes(),
|
||||
Err(_) => return StatusCode::PAYLOAD_TOO_LARGE.into_response(),
|
||||
};
|
||||
let resp = inner(
|
||||
app.clone(),
|
||||
peer,
|
||||
method.clone(),
|
||||
headers,
|
||||
Body::from(body_bytes.clone()),
|
||||
)
|
||||
.await;
|
||||
let (mut parts, rbody) = resp.into_parts();
|
||||
let rbytes = rbody
|
||||
.collect()
|
||||
.await
|
||||
.map(|b| b.to_bytes())
|
||||
.unwrap_or_default();
|
||||
parts.headers.remove("content-length");
|
||||
let mut resp = Response::from_parts(parts, Body::from(rbytes.clone()));
|
||||
resp.headers_mut()
|
||||
.insert("X-ActivePush", HeaderValue::from_static("autodiscover"));
|
||||
let code = resp.status().as_u16();
|
||||
let status = match code {
|
||||
200 if String::from_utf8_lossy(&rbytes).contains("<ErrorCode>") => "invalid request",
|
||||
200 => "ok",
|
||||
401 => "auth required",
|
||||
429 => "locked out",
|
||||
@@ -109,6 +168,18 @@ pub async fn handler(
|
||||
_ => "error",
|
||||
};
|
||||
tracing::info!(target: "active_push::autodiscover", ip = %ip, host = %host, path = %uri.path(), method = %method, http = code, user = %user, ua = %ua, "autodiscover {status}");
|
||||
let clip = |b: &[u8]| -> String {
|
||||
String::from_utf8_lossy(&b[..b.len().min(3000)])
|
||||
.chars()
|
||||
.filter(|c| !c.is_control() || *c == '\n')
|
||||
.collect()
|
||||
};
|
||||
app.ad_trace_push(serde_json::json!({
|
||||
"ts": now(), "ip": ip, "host": host, "method": method.as_str(), "path": uri.path(),
|
||||
"typed_user": typed_user, "http": code, "ms": started.elapsed().as_millis() as u64,
|
||||
"request_headers": hdr_dump, "request_body": clip(&body_bytes),
|
||||
"response_body": clip(&rbytes),
|
||||
}));
|
||||
app.metrics.record(Event {
|
||||
ts: now(),
|
||||
user,
|
||||
@@ -199,36 +270,67 @@ async fn inner(
|
||||
if !user_allowed(&login, &s) {
|
||||
return unauthorized();
|
||||
}
|
||||
if let Some((l, p)) = &creds {
|
||||
let key = app.pool.key(l, p);
|
||||
if !app.auth.is_cached(&key) {
|
||||
let mut sess = app.session(l, p);
|
||||
match sess.authenticate().await {
|
||||
Ok(AuthResult::Ok) => {
|
||||
app.auth.success(&user_key);
|
||||
app.auth.cache_ok(key, s.security.auth_cache_secs);
|
||||
sess.close().await;
|
||||
}
|
||||
Ok(AuthResult::Denied) => {
|
||||
app.metrics.auth_failure();
|
||||
if s.security.log_auth_failures {
|
||||
tracing::warn!(target: "active_push::auth", "authentication failed user={l} ip={ip} service=autodiscover");
|
||||
// iOS sends the same password twice: once with "user@domain" and once with the bare "user",
|
||||
// and treats a refusal of either as a failed Autodiscover. When the bare name is the local
|
||||
// part of the requested address, try the full address first (the form most IMAP servers with
|
||||
// several domains know), then the name as given.
|
||||
let mut login = login;
|
||||
if let Some((given, pass)) = &creds {
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
if !given.contains('@') {
|
||||
if let Some((local, _)) = email.rsplit_once('@') {
|
||||
if local.eq_ignore_ascii_case(given) {
|
||||
if let Some(full) = normalize_login(&email, &s) {
|
||||
if user_allowed(&full, &s) {
|
||||
candidates.push(full);
|
||||
}
|
||||
}
|
||||
let (m, w, lo) = (
|
||||
s.security.auth_fail_max,
|
||||
s.security.auth_fail_window_secs,
|
||||
s.security.lockout_secs,
|
||||
);
|
||||
app.auth.fail(&ip_key, m * 3, w, lo);
|
||||
app.auth.fail(&user_key, m, w, lo);
|
||||
return unauthorized();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("autodiscover: backend unavailable: {e:#}");
|
||||
return StatusCode::SERVICE_UNAVAILABLE.into_response();
|
||||
}
|
||||
}
|
||||
candidates.push(given.clone());
|
||||
|
||||
let mut accepted: Option<String> = candidates
|
||||
.iter()
|
||||
.find(|c| app.auth.is_cached(&app.pool.key(c, pass)))
|
||||
.cloned();
|
||||
if accepted.is_none() {
|
||||
for cand in &candidates {
|
||||
let mut sess = app.session(cand, pass);
|
||||
match sess.authenticate().await {
|
||||
Ok(AuthResult::Ok) => {
|
||||
app.auth.success(&user_key);
|
||||
app.auth
|
||||
.cache_ok(app.pool.key(cand, pass), s.security.auth_cache_secs);
|
||||
sess.close().await;
|
||||
accepted = Some(cand.clone());
|
||||
break;
|
||||
}
|
||||
Ok(AuthResult::Denied) => {}
|
||||
Err(e) => {
|
||||
tracing::error!("autodiscover: backend unavailable: {e:#}");
|
||||
return StatusCode::SERVICE_UNAVAILABLE.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match accepted {
|
||||
Some(l) => login = l,
|
||||
None => {
|
||||
app.metrics.auth_failure();
|
||||
if s.security.log_auth_failures {
|
||||
tracing::warn!(target: "active_push::auth", "authentication failed user={given} ip={ip} service=autodiscover");
|
||||
}
|
||||
let (m, w, lo) = (
|
||||
s.security.auth_fail_max,
|
||||
s.security.auth_fail_window_secs,
|
||||
s.security.lockout_secs,
|
||||
);
|
||||
app.auth.fail(&ip_key, m * 3, w, lo);
|
||||
app.auth.fail(&user_key, m, w, lo);
|
||||
return unauthorized();
|
||||
}
|
||||
}
|
||||
}
|
||||
if email.is_empty() || !email.contains('@') {
|
||||
email = crate::backend::primary_email(&s, &login);
|
||||
|
||||
Reference in new issue
Block a user