Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption location-sharing privacy self-hosted federated
at auth 5.1 kB view raw
1use std::{ 2 sync::atomic::{AtomicU64, Ordering}, 3 time::Instant, 4}; 5 6use axum::{ 7 body::{Body, Bytes, to_bytes}, 8 http::{HeaderMap, Method, Request}, 9 middleware::Next, 10 response::Response, 11}; 12use base64::{Engine, prelude::BASE64_STANDARD}; 13use serde_json::Value; 14 15static REQ_SEQ: AtomicU64 = AtomicU64::new(1); 16 17fn format_x_auth(headers: &HeaderMap) -> Option<String> { 18 let v = headers.get("x-auth")?; 19 let s = v.to_str().ok()?.to_string(); 20 21 const MAX: usize = 120; 22 if s.len() > MAX { 23 Some(format!("{}… (len={})", &s[..MAX], s.len())) 24 } else { 25 Some(s) 26 } 27} 28 29fn status_emoji(status: axum::http::StatusCode) -> &'static str { 30 if status.is_success() { 31 "" 32 } else if status.is_redirection() { 33 "" 34 } else if status.is_client_error() { 35 "" 36 } else if status.is_server_error() { 37 "" 38 } else { 39 "" 40 } 41} 42 43/// Convert JSON into a "key: value" style display. 44/// - Objects: `key: value` (nested objects/arrays are indented) 45/// - Arrays: `- item` (nested indented) 46/// If not JSON: UTF-8 text, else base64. 47fn body_as_kv(bytes: &Bytes) -> String { 48 if bytes.is_empty() { 49 return "<empty>".to_string(); 50 } 51 52 if let Ok(v) = serde_json::from_slice::<Value>(bytes) { 53 let mut out = String::new(); 54 write_value(&mut out, &v, 0); 55 return out.trim_end().to_string(); 56 } 57 58 match std::str::from_utf8(bytes) { 59 Ok(s) => s.to_string(), 60 Err(_) => format!("<non-utf8; base64>\n{}", BASE64_STANDARD.encode(bytes)), 61 } 62} 63 64fn write_value(out: &mut String, v: &Value, indent: usize) { 65 match v { 66 Value::Object(map) => { 67 for (k, val) in map { 68 write_key_value(out, k, val, indent); 69 } 70 } 71 Value::Array(arr) => { 72 for item in arr { 73 write_array_item(out, item, indent); 74 } 75 } 76 _ => { 77 // Root primitive 78 out.push_str(&indent_str(indent)); 79 out.push_str(&format_primitive(v)); 80 out.push('\n'); 81 } 82 } 83} 84 85fn write_key_value(out: &mut String, key: &str, val: &Value, indent: usize) { 86 let pad = indent_str(indent); 87 88 match val { 89 Value::Object(_) | Value::Array(_) => { 90 out.push_str(&pad); 91 out.push_str(key); 92 out.push_str(":\n"); 93 write_value(out, val, indent + 2); 94 } 95 _ => { 96 out.push_str(&pad); 97 out.push_str(key); 98 out.push_str(": "); 99 out.push_str(&format_primitive(val)); 100 out.push('\n'); 101 } 102 } 103} 104 105fn write_array_item(out: &mut String, item: &Value, indent: usize) { 106 let pad = indent_str(indent); 107 108 match item { 109 Value::Object(_) | Value::Array(_) => { 110 out.push_str(&pad); 111 out.push_str("-\n"); 112 write_value(out, item, indent + 2); 113 } 114 _ => { 115 out.push_str(&pad); 116 out.push_str("- "); 117 out.push_str(&format_primitive(item)); 118 out.push('\n'); 119 } 120 } 121} 122 123fn format_primitive(v: &Value) -> String { 124 match v { 125 Value::String(s) => s.clone(), 126 Value::Number(n) => n.to_string(), 127 Value::Bool(b) => b.to_string(), 128 Value::Null => "null".to_string(), 129 // Shouldn’t happen here (we route these elsewhere), but safe fallback 130 Value::Object(_) | Value::Array(_) => "<complex>".to_string(), 131 } 132} 133 134fn indent_str(spaces: usize) -> String { 135 " ".repeat(spaces) 136} 137 138fn indent_block(s: &str, spaces: usize) -> String { 139 let pad = " ".repeat(spaces); 140 s.lines() 141 .map(|line| format!("{pad}{line}\n")) 142 .collect::<String>() 143 .trim_end_matches('\n') 144 .to_string() 145} 146 147pub async fn log_req_res_bodies(req: Request<Body>, next: Next) -> Response { 148 // Avoid noisy CORS preflight logs 149 if req.method() == Method::OPTIONS { 150 return next.run(req).await; 151 } 152 153 let id = REQ_SEQ.fetch_add(1, Ordering::Relaxed); 154 let start = Instant::now(); 155 156 let method = req.method().clone(); 157 let uri = req.uri().clone(); 158 let req_headers = req.headers().clone(); 159 160 // Read + restore request body 161 let (req_parts, req_body) = req.into_parts(); 162 let req_bytes = to_bytes(req_body, usize::MAX).await.unwrap(); 163 let req = Request::from_parts(req_parts, Body::from(req_bytes.clone())); 164 165 // Run handler 166 let res = next.run(req).await; 167 168 let status = res.status(); 169 let res_headers = res.headers().clone(); 170 171 // Read + restore response body 172 let (res_parts, res_body) = res.into_parts(); 173 let res_bytes = to_bytes(res_body, usize::MAX).await.unwrap(); 174 let res = Response::from_parts(res_parts, Body::from(res_bytes.clone())); 175 176 let ms = start.elapsed().as_millis(); 177 let sep = "════════════════════════════════════════════════════════════════"; 178 179 let mut out = String::new(); 180 out.push('\n'); 181 out.push_str(sep); 182 out.push('\n'); 183 out.push_str(&format!( 184 "🚦 #{id} {method} {uri} {} {status}{ms}ms\n", 185 status_emoji(status) 186 )); 187 188 out.push('\n'); 189 190 out.push_str("📥 Request\n"); 191 if let Some(xauth) = format_x_auth(&req_headers) { 192 out.push_str(&format!(" 🔐 x-auth: {xauth}\n")); 193 } 194 out.push_str(&indent_block(&body_as_kv(&req_bytes), 2)); 195 out.push('\n'); 196 197 out.push_str("📤 Response\n"); 198 out.push_str(&indent_block(&body_as_kv(&res_bytes), 2)); 199 out.push('\n'); 200 201 out.push_str(sep); 202 out.push('\n'); 203 204 tracing::info!("{out}"); 205 206 let _ = res_headers; 207 res 208}