Skip to main content

ordinary_utils/
server.rs

1// Copyright (C) 2026 Ordinary Labs, LLC.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use axum::extract::ConnectInfo;
6use axum::http::{HeaderName, HeaderValue, Request, Version};
7use axum::routing::get;
8use futures_util::stream::StreamExt;
9use std::net::{IpAddr, SocketAddr};
10use std::time::Duration;
11use tower::ServiceBuilder;
12use tower_http::timeout::TimeoutLayer;
13use tracing::{Instrument, Span};
14
15use crate::middleware::{ServiceKind, apply_common_middleware};
16use crate::tcp::Sni;
17use anyhow::anyhow;
18use axum::Router;
19use axum::body::Body;
20use axum::handler::Handler;
21use axum::response::Response;
22use base64::{Engine as B64Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
23use blake2::{
24    Blake2bVar,
25    digest::{Update, VariableOutput},
26};
27use bytes::Bytes;
28use http_body_util::Full;
29use hyper::header::{AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, SET_COOKIE};
30use hyper::{HeaderMap, StatusCode, Uri, header};
31use ordinary_config::RedactedHashAlg;
32use rcgen::{CertifiedKey, generate_simple_self_signed};
33use rustls_acme::{AcmeState, EventError, EventOk};
34use std::any::Any;
35use std::fmt;
36use std::fmt::{Debug, Display};
37use std::fs::File;
38use std::io::Write;
39use std::path::Path;
40use std::sync::Arc;
41use tokio::sync::watch::{Receiver, Sender};
42use tokio_rustls::{
43    rustls::ServerConfig,
44    rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
45};
46use tracing::field::DisplayValue;
47#[cfg(tracing_unstable)]
48use valuable::{Mappable, Valuable, Value, Visit};
49
50pub const X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
51pub const X_CORRELATION_ID: HeaderName = HeaderName::from_static("x-correlation-id");
52pub const REPORTING_ENDPOINTS: HeaderName = HeaderName::from_static("reporting-endpoints");
53pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
54pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
55pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
56
57#[derive(PartialEq, Clone)]
58pub enum ProvisionMode {
59    Localhost,
60    Staging,
61    Production,
62}
63
64#[derive(Clone)]
65pub enum SecurityMode<T: AsRef<Path>> {
66    Insecure,
67    Secure(T, ProvisionMode),
68}
69
70pub struct WrappedRedactedHashingAlg(pub RedactedHashAlg);
71
72impl WrappedRedactedHashingAlg {
73    fn hash(&self, header_value: &str) -> String {
74        let span = tracing::info_span!("redacted:hash");
75
76        span.in_scope(|| match self.0 {
77            RedactedHashAlg::Blake2 => {
78                let mut out = [0u8; 32];
79
80                let mut hasher = match Blake2bVar::new(32) {
81                    Ok(v) => v,
82                    Err(err) => {
83                        tracing::error!(%err);
84                        return "redacted".into();
85                    }
86                };
87
88                hasher.update(header_value.as_bytes());
89                if let Err(err) = hasher.finalize_variable(&mut out) {
90                    tracing::error!(%err);
91                    return "redacted".into();
92                }
93
94                b64.encode(&out[0..6])
95            }
96            RedactedHashAlg::Blake3 => {
97                b64.encode(&blake3::hash(header_value.as_bytes()).as_bytes()[0..6])
98            }
99        })
100    }
101}
102pub struct HeadersDebug<'a>(
103    pub &'a HeaderMap,
104    pub Arc<Option<WrappedRedactedHashingAlg>>,
105);
106
107#[cfg(tracing_unstable)]
108impl Valuable for HeadersDebug<'_> {
109    fn as_value(&self) -> Value<'_> {
110        Value::Mappable(self)
111    }
112
113    fn visit(&self, visit: &mut dyn Visit) {
114        for (k, v) in self.0 {
115            if let Ok(v) = v.to_str() {
116                if k == AUTHORIZATION || k == PROXY_AUTHORIZATION || k == COOKIE || k == SET_COOKIE
117                {
118                    if let Some(hasher) = &*self.1 {
119                        visit.visit_entry(k.as_str().as_value(), hasher.hash(v).as_value());
120                    } else {
121                        visit.visit_entry(k.as_str().as_value(), "redacted".as_value());
122                    }
123                } else {
124                    visit.visit_entry(k.as_str().as_value(), v.as_value());
125                }
126            }
127        }
128    }
129}
130
131#[cfg(tracing_unstable)]
132impl Mappable for HeadersDebug<'_> {
133    fn size_hint(&self) -> (usize, Option<usize>) {
134        self.0.iter().size_hint()
135    }
136}
137
138impl Debug for HeadersDebug<'_> {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        use std::fmt::Write;
141
142        f.write_char('{')?;
143
144        let mut is_first = true;
145
146        for (k, v) in self.0 {
147            if let Ok(v) = v.to_str() {
148                if is_first {
149                    is_first = false;
150                    f.write_char('"')?;
151                } else {
152                    f.write_str(",\"")?;
153                }
154
155                f.write_str(k.as_str())?;
156                f.write_str("\":\"")?;
157
158                if k == AUTHORIZATION || k == PROXY_AUTHORIZATION || k == COOKIE || k == SET_COOKIE
159                {
160                    if let Some(hasher) = &*self.1 {
161                        f.write_str(hasher.hash(v).as_str())?;
162                    } else {
163                        f.write_str("redacted")?;
164                    }
165
166                    f.write_char('"')?;
167                } else {
168                    f.write_str(v)?;
169                    f.write_char('"')?;
170                }
171            }
172        }
173
174        f.write_char('}')
175    }
176}
177
178#[must_use]
179pub fn get_http_version_str(v: Version) -> &'static str {
180    match v {
181        Version::HTTP_09 => "0.9",
182        Version::HTTP_10 => "1.0",
183        Version::HTTP_11 => "1.1",
184        Version::HTTP_2 => "2.0",
185        Version::HTTP_3 => "3.0",
186        _ => unreachable!(),
187    }
188}
189
190#[must_use]
191pub fn get_display_ip(log_ips: bool, req: &Request<Body>) -> Option<DisplayValue<IpAddr>> {
192    log_ips
193        .then(|| {
194            req.extensions()
195                .get::<ConnectInfo<SocketAddr>>()
196                .map(|addr| tracing::field::display(get_mapped_ip_for_addr(&addr.0)))
197        })
198        .flatten()
199}
200
201#[must_use]
202pub fn get_mapped_ip_for_addr(addr: &SocketAddr) -> IpAddr {
203    let ip = addr.ip();
204
205    if let IpAddr::V6(ipv6) = &ip
206        && let Some(ipv4) = ipv6.to_ipv4_mapped()
207    {
208        IpAddr::V4(ipv4)
209    } else {
210        ip
211    }
212}
213
214pub fn get_bearer_token_as_bytes(headers: &HeaderMap) -> anyhow::Result<Vec<u8>> {
215    if let Some(auth_header) = headers.get("authorization")
216        && let Ok(str_val) = auth_header.to_str()
217        && let Some(b64_token) = str_val.strip_prefix("Bearer ")
218        && let Ok(token) = b64.decode(b64_token)
219    {
220        Ok(token)
221    } else {
222        Err(anyhow!("failed to get token as bytes"))
223    }
224}
225
226pub fn get_host_fwd(headers: &HeaderMap, uri: &Uri, sni: Option<&Sni>) -> Option<String> {
227    if let Some(forwarded_values) = headers.get(header::FORWARDED)
228        && let Ok(forwarded_values_str) = forwarded_values.to_str()
229        && let Some(first_value) = forwarded_values_str.split(',').next()
230        && let Some(host) = first_value.split(';').find_map(|pair| {
231            let (key, value) = pair.split_once('=')?;
232            key.trim()
233                .eq_ignore_ascii_case("host")
234                .then(|| value.trim().trim_matches('"'))
235        })
236    {
237        return Some(host.to_owned());
238    }
239
240    if let Some(host) = headers
241        .get(X_FORWARDED_HOST)
242        .and_then(|host| host.to_str().ok())
243    {
244        return Some(host.to_owned());
245    }
246
247    if let Some(host) = headers
248        .get(header::HOST)
249        .and_then(|host| host.to_str().ok())
250    {
251        return Some(host.to_owned());
252    }
253
254    if let Some(sni) = sni {
255        return Some(sni.0.clone());
256    }
257
258    if let Some(authority) = uri.authority() {
259        return authority.as_str().rsplit('@').next().map(ToOwned::to_owned);
260    }
261
262    None
263}
264
265pub struct LatencyDisplay(pub f64);
266
267impl Display for LatencyDisplay {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        let mut t = self.0;
270
271        for unit in ["ns", "µs", "ms", "s"] {
272            if t < 10.0 {
273                return write!(f, "{t:.2}{unit}");
274            } else if t < 100.0 {
275                return write!(f, "{t:.1}{unit}");
276            } else if t < 1000.0 {
277                return write!(f, "{t:.0}{unit}");
278            }
279            t /= 1000.0;
280        }
281        write!(f, "{:.0}s", t * 1000.0)
282    }
283}
284
285#[allow(clippy::needless_pass_by_value)]
286pub fn response_for_panic(_: Box<dyn Any + Send + 'static>) -> Response<Full<Bytes>> {
287    #[allow(clippy::declare_interior_mutable_const)]
288    const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8");
289
290    let mut res = Response::new(Full::new(Bytes::from_static(b"500 Internal Server Error")));
291
292    *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
293    res.headers_mut().insert(header::CONTENT_TYPE, TEXT_PLAIN);
294
295    res
296}
297
298pub fn rustls_server_config(
299    key: impl AsRef<Path>,
300    cert: impl AsRef<Path>,
301) -> anyhow::Result<Arc<ServerConfig>> {
302    let key = PrivateKeyDer::from_pem_file(key)?;
303
304    let certs = CertificateDer::pem_file_iter(cert)?.flatten().collect();
305
306    let mut config = ServerConfig::builder()
307        .with_no_client_auth()
308        .with_single_cert(certs, key)?;
309
310    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
311
312    Ok(Arc::new(config))
313}
314
315/// writes crt.pem and key.pem to directory
316pub fn generate_self_signed_localhost_certs(cert_dir_path: impl AsRef<Path>) -> anyhow::Result<()> {
317    std::fs::create_dir_all(&cert_dir_path)?;
318
319    let cert_path = cert_dir_path.as_ref().join("crt.pem");
320    let key_path = cert_dir_path.as_ref().join("key.pem");
321
322    if !cert_path.exists() || !key_path.exists() {
323        let subject_alt_names = vec!["localhost".to_string()];
324
325        let CertifiedKey { cert, signing_key } =
326            match generate_simple_self_signed(subject_alt_names) {
327                Ok(ck) => {
328                    tracing::info!("generated self-signed localhost cert");
329                    ck
330                }
331                Err(err) => {
332                    tracing::error!("failed to generate self-signed localhost cert");
333                    return Err(err.into());
334                }
335            };
336
337        let cert = cert.pem();
338        let key = signing_key.serialize_pem();
339
340        let mut cert_file = File::create(cert_path)?;
341        let mut key_file = File::create(key_path)?;
342
343        cert_file.write_all(cert.as_bytes())?;
344        cert_file.flush()?;
345        key_file.write_all(key.as_bytes())?;
346        key_file.flush()?;
347    }
348
349    Ok(())
350}
351
352pub fn acme_task(
353    acme_span_clone: Span,
354    mut state: AcmeState<std::io::Error>,
355    signal_tx: Sender<()>,
356    mut terminate_rx: Option<Receiver<bool>>,
357) {
358    tokio::spawn(async move {
359        async {
360            tracing::info!("ready");
361
362            loop {
363                let event = if let Some(terminate_rx) = terminate_rx.as_mut() {
364                    tokio::select! {
365                        state = state.next() => state,
366                        () = signal_tx.closed() => {
367                            tracing::warn!("not accepting new connections");
368                            break;
369                        },
370                        _ = terminate_rx.changed() => {
371                            tracing::warn!("not accepting new connections");
372                            break;
373                        }
374                    }
375                } else {
376                    tokio::select! {
377                        state = state.next() => state,
378                        () = signal_tx.closed() => {
379                            tracing::warn!("not accepting new connections");
380                            break;
381                        }
382                    }
383                };
384
385                if let Some(event) = event {
386                    match event {
387                        Ok(evt) => match evt {
388                            EventOk::DeployedNewCert => {
389                                tracing::info!(evt.deploy = %"new", "cert");
390                            }
391                            EventOk::CertCacheStore => {
392                                tracing::info!(evt.cache = %"stored", "cert");
393                            }
394                            EventOk::AccountCacheStore => {
395                                tracing::info!(evt.cache = %"stored", "account");
396                            }
397                            EventOk::DeployedCachedCert => {
398                                tracing::info!(evt.deploy = %"cached", "cert");
399                            }
400                        },
401                        Err(err) => match err {
402                            EventError::AccountCacheStore(err) => {
403                                tracing::error!(%err, evt.cache = %"store", "account");
404                            }
405                            EventError::CertCacheStore(err) => {
406                                tracing::error!(%err, evt.cache = %"store", "cert");
407                            }
408                            EventError::AccountCacheLoad(err) => {
409                                tracing::error!(%err, evt.cache = %"load", "account");
410                            }
411                            EventError::CachedCertParse(err) => {
412                                tracing::error!(%err, evt.parse = %"cache", "cert");
413                            }
414                            EventError::NewCertParse(err) => {
415                                tracing::error!(%err, evt.parse = %"new", "cert");
416                            }
417                            EventError::CertCacheLoad(err) => {
418                                tracing::error!(%err, evt.cache = %"load", "cert");
419                            }
420                            EventError::Order(err) => {
421                                tracing::error!(%err, "order");
422                            }
423                        },
424                    }
425                } else {
426                    break;
427                }
428            }
429        }
430        .instrument(acme_span_clone)
431        .await;
432    });
433}
434
435#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
436pub fn redirect_service<H, T, S>(
437    span_clone: &Span,
438    redacted_hash: Arc<Option<WrappedRedactedHashingAlg>>,
439    log_ips: bool,
440    log_headers: bool,
441    handler: H,
442    state: S,
443    api_domain: Option<String>,
444) -> Router
445where
446    H: Handler<T, S>,
447    T: 'static,
448    S: Clone + Send + Sync + 'static,
449{
450    let router = Router::new()
451        .route("/healthz", get(|| async { StatusCode::OK }))
452        .fallback(handler);
453
454    apply_common_middleware(
455        router,
456        &state,
457        Some(span_clone),
458        String::new(),
459        log_headers,
460        log_ips,
461        redacted_hash,
462        ServiceKind::Redirect,
463        Some(api_domain.unwrap_or("redirect".to_owned())),
464    )
465    .layer(ServiceBuilder::new().layer(TimeoutLayer::with_status_code(
466        StatusCode::REQUEST_TIMEOUT,
467        Duration::from_secs(5),
468    )))
469}