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