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