Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption
location-sharing
privacy
self-hosted
federated
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}