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};
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}