Skip to main content

lexe_api/
server.rs

1// This is the only place where we are allowed to use e.g. `Json` and `Query`.
2#![allow(clippy::disallowed_types)]
3
4//! This module provides various API server utilities.
5//!
6//! # Serving
7//!
8//! Methods to serve a [`Router`] with a fallback handler (for unmatched paths),
9//! tracing / request instrumentation, backpressure, load shedding, concurrency
10//! limits, server-side timeouts, TLS, and graceful shutdown:
11//!
12//! - [`build_server_fut`]
13//! - [`build_server_fut_with_listener`]
14//! - [`spawn_server_task`]
15//! - [`spawn_server_task_with_listener`]
16//!
17//! # Extractors to get data from requests:
18//!
19//! - [`LxJson`] to deserialize from HTTP body JSON
20//! - [`LxQuery`] to deserialize from query strings
21//!
22//! # [`IntoResponse`] types / impls for building Lexe API-conformant responses:
23//!
24//! - [`LxJson`] type for returning success responses as JSON
25//! - All [`ApiError`]s and [`CommonApiError`] impl [`IntoResponse`]
26//! - [`LxRejection`] for notifying clients of bad JSON, query strings, etc.
27//!
28//! [`ApiError`]: lexe_api_core::error::ApiError
29//! [`CommonApiError`]: lexe_api_core::error::CommonApiError
30//! [`Router`]: axum::Router
31//! [`IntoResponse`]: axum::response::IntoResponse
32//! [`LxJson`]: crate::server::LxJson
33//! [`LxQuery`]: crate::server::extract::LxQuery
34//! [`LxRejection`]: crate::server::LxRejection
35//! [`build_server_fut`]: crate::server::build_server_fut
36//! [`build_server_fut_with_listener`]: crate::server::build_server_fut_with_listener
37//! [`spawn_server_task`]: crate::server::spawn_server_task
38//! [`spawn_server_task_with_listener`]: crate::server::spawn_server_task_with_listener
39
40use std::{
41    borrow::Cow,
42    convert::Infallible,
43    fmt::{self, Display},
44    future::Future,
45    net::{SocketAddr, TcpListener},
46    str::FromStr,
47    sync::Arc,
48    time::Duration,
49};
50
51use anyhow::Context;
52use axum::{
53    Router, ServiceExt as AxumServiceExt,
54    error_handling::HandleErrorLayer,
55    extract::{
56        DefaultBodyLimit, FromRequest, OptionalFromRequest,
57        rejection::{
58            BytesRejection, JsonRejection, PathRejection, QueryRejection,
59        },
60    },
61    response::IntoResponse,
62    routing::RouterIntoService,
63};
64use axum_server::tls_rustls::{RustlsAcceptor, RustlsConfig};
65use bytes::Bytes;
66use http::{HeaderValue, StatusCode, header::CONTENT_TYPE};
67use lexe_api_core::{
68    axum_helpers,
69    error::{CommonApiError, CommonErrorKind},
70};
71use lexe_common::api::auth::{self, LexeScope};
72use lexe_crypto::ed25519;
73use lexe_tokio::{notify_once::NotifyOnce, task::LxTask};
74use serde::{Serialize, de::DeserializeOwned};
75use tower::{
76    Layer, buffer::BufferLayer, limit::ConcurrencyLimitLayer,
77    load_shed::LoadShedLayer, timeout::TimeoutLayer, util::MapRequestLayer,
78};
79use tracing::{Instrument, debug, error, info, warn};
80
81use crate::{rest, tls_acceptor::CertInjectorAcceptor, trace};
82
83/// The grace period passed to [`axum_server::Handle::graceful_shutdown`] during
84/// which new connections are refused and we wait for existing connections to
85/// terminate before initiating a hard shutdown.
86const SHUTDOWN_GRACE_PERIOD: Duration = Duration::from_secs(3);
87/// The maximum time we'll wait for a server to complete shutdown.
88pub const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
89lexe_std::const_assert!(
90    SHUTDOWN_GRACE_PERIOD.as_secs() < SERVER_SHUTDOWN_TIMEOUT.as_secs()
91);
92
93/// The default maximum time a server can spend handling a request.
94pub const SERVER_HANDLER_TIMEOUT: Duration = Duration::from_secs(25);
95lexe_std::const_assert!(
96    rest::API_REQUEST_TIMEOUT.as_secs() > SERVER_HANDLER_TIMEOUT.as_secs()
97);
98
99/// A configuration object for Axum / Tower middleware.
100///
101/// Defaults:
102///
103/// ```
104/// # use std::time::Duration;
105/// # use lexe_api::server::LayerConfig;
106/// assert_eq!(
107///     LayerConfig::default(),
108///     LayerConfig {
109///         body_limit: 16384,
110///         buffer_size: 4096,
111///         concurrency: 4096,
112///         handling_timeout: Duration::from_secs(25),
113///         default_fallback: true,
114///     }
115/// );
116/// ```
117#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct LayerConfig {
119    /// The maximum size of the request body in bytes.
120    /// Helps prevent DoS, but may need to be increased for some services.
121    pub body_limit: usize,
122    /// The size of the work buffer for our service.
123    /// Allows the server to immediately work on more queued requests when a
124    /// request completes, and prevents a large backlog from building up.
125    pub buffer_size: usize,
126    /// The maximum # of requests we'll process at once.
127    /// Helps prevent the CPU from maxing out, resulting in thrashing.
128    pub concurrency: usize,
129    /// The maximum time a server can spend handling a request. Helps prevent
130    /// degenerate cases which take abnormally long to process from crowding
131    /// out normal workloads.
132    pub handling_timeout: Duration,
133    /// Whether to add Lexe's default [`Router::fallback`] to the [`Router`].
134    /// The [`Router::fallback`] is called if no routes were matched;
135    /// Lexe's [`default_fallback`] returns a "bad endpoint" rejection along
136    /// with the requested method and path.
137    ///
138    /// If you need to set a custom fallback, set this to [`false`], otherwise
139    /// your custom fallback will be clobbered by Lexe's [`default_fallback`].
140    /// NOTE, however, that the caller is responsible for ensuring that the
141    /// [`Router`] has a fallback configured in this case.
142    pub default_fallback: bool,
143}
144
145impl Default for LayerConfig {
146    fn default() -> Self {
147        Self {
148            // 16KiB is sufficient for most Lexe services.
149            body_limit: 16384,
150            // TODO(max): We are using very high values right now because it
151            // doesn't make sense to constrain anything until we have run some
152            // load tests to profile performance and see what breaks.
153            buffer_size: 4096,
154            concurrency: 4096,
155            handling_timeout: SERVER_HANDLER_TIMEOUT,
156            default_fallback: true,
157        }
158    }
159}
160
161// --- Server helpers --- //
162
163/// Construct a server URL given the [`TcpListener::local_addr`] from by a
164/// server's [`TcpListener`], and its DNS name.
165///
166/// ex: `https://lexe.app` (port=443)
167/// ex: `https://relay.lexe.app:4396`
168/// ex: `http://[::1]:8080`
169//
170// We have a fn to build the url because it's easy to mess up.
171pub fn build_server_url(
172    // The output of `TcpListener::local_addr`
173    listener_addr: SocketAddr,
174    // Primary DNS name
175    maybe_dns: Option<&str>,
176) -> String {
177    match maybe_dns {
178        Some(dns_name) => {
179            let port = listener_addr.port();
180            if port == 443 {
181                format!("https://{dns_name}")
182            } else {
183                format!("https://{dns_name}:{port}")
184            }
185        }
186        None => format!("http://{listener_addr}"),
187    }
188}
189
190/// Constructs an API server future which can be spawned into a task.
191/// Additionally returns the server url.
192///
193/// Use this helper when it is useful to poll multiple futures in a single task
194/// to reduce the amount of task nesting / indirection. If there is only one
195/// future that needs to be driven, use [`spawn_server_task`] instead.
196///
197/// Errors if the [`TcpListener`] failed to bind or return its local address.
198/// Returns the server future along with the bound socket address.
199// Avoids generic parameters to prevent binary bloat.
200// Returns unnamed `impl Future` to avoid Pin<Box<T>> deref cost.
201pub fn build_server_fut(
202    bind_addr: SocketAddr,
203    router: Router<()>,
204    layer_config: LayerConfig,
205    // TLS config + primary DNS name
206    maybe_tls_and_dns: Option<(Arc<rustls::ServerConfig>, &str)>,
207    server_span_name: &str,
208    server_span: tracing::Span,
209    // Send on this channel to begin a graceful shutdown of the server.
210    shutdown: NotifyOnce,
211) -> anyhow::Result<(impl Future<Output = ()>, String)> {
212    let listener =
213        TcpListener::bind(bind_addr).context("Could not bind TCP listener")?;
214    let (server_fut, primary_server_url) = build_server_fut_with_listener(
215        listener,
216        router,
217        layer_config,
218        maybe_tls_and_dns,
219        server_span_name,
220        server_span,
221        shutdown,
222    )
223    .context("Could not build server future")?;
224    Ok((server_fut, primary_server_url))
225}
226
227/// [`build_server_fut`] but takes a [`TcpListener`] instead of [`SocketAddr`].
228// Avoids generic parameters to prevent binary bloat.
229// Returns unnamed `impl Future` to avoid Pin<Box<T>> deref cost.
230pub fn build_server_fut_with_listener(
231    listener: TcpListener,
232    router: Router<()>,
233    layer_config: LayerConfig,
234    // TLS config + primary DNS name
235    maybe_tls_and_dns: Option<(Arc<rustls::ServerConfig>, &str)>,
236    server_span_name: &str,
237    server_span: tracing::Span,
238    // Send on this channel to begin a graceful shutdown of the server.
239    mut shutdown: NotifyOnce,
240) -> anyhow::Result<(impl Future<Output = ()> + use<>, String)> {
241    let (maybe_tls_config, maybe_dns) = maybe_tls_and_dns.unzip();
242    let listener_addr = listener
243        .local_addr()
244        .context("Could not get listener local address")?;
245    let primary_server_url = build_server_url(listener_addr, maybe_dns);
246    info!("Url for {server_span_name}: {primary_server_url}");
247
248    // Add Lexe's default fallback if it is enabled in the LayerConfig.
249    let router = if layer_config.default_fallback {
250        router.fallback(default_fallback)
251    } else {
252        router
253    };
254
255    // Used to annotate the service / request / response types
256    // at each point in the ServiceBuilder chains.
257    type HyperService = RouterIntoService<hyper::body::Incoming, ()>;
258    type AxumService = RouterIntoService<axum::body::Body, ()>;
259    type HyperReq = http::Request<hyper::body::Incoming>;
260    type AxumReq = http::Request<axum::body::Body>;
261    type AxumResp = http::Response<axum::body::Body>;
262    type TraceResp = http::Response<
263        tower_http::trace::ResponseBody<
264            axum::body::Body,
265            tower_http::classify::NeverClassifyEos<anyhow::Error>,
266            (),
267            trace::server::LxOnEos,
268            trace::server::LxOnFailure,
269        >,
270    >;
271
272    // The outer middleware stack which wraps the entire Router.
273    //
274    // Axum docs explain ordering better than tower's ServiceBuilder docs do:
275    // https://docs.rs/axum/latest/axum/middleware/index.html#ordering
276    // Basically, requests go from top to bottom and responses bottom to top.
277    let outer_middleware = tower::ServiceBuilder::new()
278        .check_service::<HyperService, HyperReq, AxumResp, Infallible>()
279        // Log everything on its way in and out, even load-shedded requests.
280        // This layer changes the response type.
281        .layer(trace::server::trace_layer(server_span.clone()))
282        .check_service::<HyperService, HyperReq, TraceResp, Infallible>()
283        // Run our post-processor which can modify responses *after* the Axum
284        // Router has constructed the response.
285        .layer(tower::util::MapResponseLayer::new(
286            middleware::post_process_response,
287        ))
288        .check_service::<HyperService, HyperReq, TraceResp, Infallible>();
289
290    // The inner middleware stack which is cloned to each route in the Router.
291    // We put most of the layers here because it is a lot easier to work with
292    // axum types; moving these outside quickly degenerates into type hell.
293    let inner_middleware = tower::ServiceBuilder::new()
294        .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
295        // Immediately reject anything with a CONTENT_LENGTH over the limit.
296        .layer(axum::middleware::map_request_with_state(
297            layer_config.body_limit,
298            middleware::check_content_length_header,
299        ))
300        .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
301        // Set the default request body limit for all requests. This adds a
302        // `DefaultBodyLimitKind` (private axum type) into the request
303        // extensions so that any inner layers or extractors which call
304        // `axum::RequestExt::[with|into]_limited_body` will pick it up.
305        // NOTE that many of our extractors transitively rely on the Bytes
306        // extractor which will default to a 2MB limit if this is not set.
307        .layer(DefaultBodyLimit::max(layer_config.body_limit))
308        .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
309        // Here, we explicitly apply the body limit from the request extensions,
310        // transforming the request body type into `http_body_util::Limited`.
311        .layer(MapRequestLayer::new(axum::RequestExt::with_limited_body))
312        .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
313        // Handles errors from the load_shed, buffer, and concurrency layers.
314        .layer(HandleErrorLayer::new(|_: tower::BoxError| async move {
315            CommonApiError {
316                kind: CommonErrorKind::AtCapacity,
317                msg: "Service is at capacity; retry later".to_owned(),
318            }
319        }))
320        // Returns an `Err` if the inner service returns `Poll::Pending`.
321        // Helps prevent OOM when combined with the buffer or concurrency layer.
322        .layer(LoadShedLayer::new())
323        .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
324        // Returns Poll::Pending when the buffer is full (backpressure).
325        // Allows the server to immediately work on more queued requests when a
326        // request completes, and prevents a large backlog from building up.
327        // Note that while the layer is often cloned, the buffer itself is not.
328        .layer(BufferLayer::new(layer_config.buffer_size))
329        .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
330        // Returns `Poll::Pending` when the concurrency limit has been reached.
331        // Helps prevent the CPU from maxing out, resulting in thrashing.
332        .layer(ConcurrencyLimitLayer::new(layer_config.concurrency))
333        .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
334        // Handles errors generated by the timeout layer.
335        .layer(HandleErrorLayer::new(|_: tower::BoxError| async move {
336            CommonApiError {
337                kind: CommonErrorKind::Server,
338                msg: "Server timed out handling request".to_owned(),
339            }
340        }))
341        // Returns an error if the inner service takes longer than the timeout
342        // to handle the request. Prevents degenerate cases which take
343        // abnormally long to process from crowding out normal workloads.
344        .layer(TimeoutLayer::new(layer_config.handling_timeout))
345        .check_service::<AxumService, AxumReq, AxumResp, Infallible>();
346
347    // Apply inner middleware
348    let layered_router = router.layer(inner_middleware);
349    // Convert into Service
350    let router_service = layered_router.into_service::<hyper::body::Incoming>();
351    // Apply outer middleware
352    let layered_service = Layer::layer(&outer_middleware, router_service);
353    // Convert into MakeService
354    let make_service = layered_service.into_make_service();
355
356    let handle = axum_server::Handle::new();
357    let handle_clone = handle.clone();
358    let server_fut = async {
359        let serve_result = match maybe_tls_config {
360            Some(tls_config) => {
361                // Use our custom CertInjectorAcceptor to inject verified
362                // TLS client certificates into request extensions.
363                let axum_tls_config = RustlsConfig::from_config(tls_config);
364                let rustls_acceptor = RustlsAcceptor::new(axum_tls_config);
365                let acceptor = CertInjectorAcceptor::new(rustls_acceptor);
366
367                axum_server::from_tcp(listener)
368                    .acceptor(acceptor)
369                    .handle(handle_clone)
370                    .serve(make_service)
371                    .await
372            }
373            None =>
374                axum_server::from_tcp(listener)
375                    .handle(handle_clone)
376                    .serve(make_service)
377                    .await,
378        };
379
380        serve_result
381            // See axum_server::Server::serve docs for why this can't error
382            .expect("No binding + axum MakeService::poll_ready never errors");
383    };
384
385    let graceful_shutdown_fut = async move {
386        shutdown.recv().await;
387        info!("Shutting down API server");
388        // The 'grace period' is a period of time during which new connections
389        // are refused and `axum_server::Server::serve` waits for all current
390        // connections to terminate. If `None`, the server waits indefinitely
391        // for current connections to terminate; if `Some`, the server will
392        // initiate a hard shutdown after the grace period has elapsed. We use
393        // Some(_) with a relatively short grace period because (1) our handlers
394        // shouldn't take long to return and (2) we sometimes see connections
395        // failing to terminate for servers which have a /shutdown endpoint.
396        handle.graceful_shutdown(Some(SHUTDOWN_GRACE_PERIOD));
397    };
398
399    let combined_fut = async {
400        tokio::pin!(server_fut);
401        tokio::select! {
402            biased; // Ensure graceful shutdown future finishes first
403            () = graceful_shutdown_fut => (),
404            _ = &mut server_fut => return error!("Server exited early"),
405        }
406        match tokio::time::timeout(SERVER_SHUTDOWN_TIMEOUT, server_fut).await {
407            Ok(()) => info!("API server finished"),
408            Err(_) => warn!("API server timed out during shutdown"),
409        }
410    }
411    .instrument(server_span);
412
413    Ok((combined_fut, primary_server_url))
414}
415
416/// [`build_server_fut`] but additionally spawns the server future into an
417/// instrumented server task and logs the full URL used to access the server.
418/// Returns the server task and server url.
419pub fn spawn_server_task(
420    bind_addr: SocketAddr,
421    router: Router<()>,
422    layer_config: LayerConfig,
423    // TLS config + primary DNS name
424    maybe_tls_and_dns: Option<(Arc<rustls::ServerConfig>, &str)>,
425    server_span_name: Cow<'static, str>,
426    server_span: tracing::Span,
427    // Send on this channel to begin a graceful shutdown of the server.
428    shutdown: NotifyOnce,
429) -> anyhow::Result<(LxTask<()>, String)> {
430    let listener = TcpListener::bind(bind_addr)
431        .context(bind_addr)
432        .context("Failed to bind TcpListener")?;
433
434    let (server_task, primary_server_url) = spawn_server_task_with_listener(
435        listener,
436        router,
437        layer_config,
438        maybe_tls_and_dns,
439        server_span_name,
440        server_span,
441        shutdown,
442    )
443    .context("spawn_server_task_with_listener failed")?;
444
445    Ok((server_task, primary_server_url))
446}
447
448/// [`spawn_server_task`] but takes [`TcpListener`] instead of [`SocketAddr`].
449pub fn spawn_server_task_with_listener(
450    listener: TcpListener,
451    router: Router<()>,
452    layer_config: LayerConfig,
453    // TLS config + primary DNS name
454    maybe_tls_and_dns: Option<(Arc<rustls::ServerConfig>, &str)>,
455    server_span_name: Cow<'static, str>,
456    server_span: tracing::Span,
457    // Send on this channel to begin a graceful shutdown of the server.
458    shutdown: NotifyOnce,
459) -> anyhow::Result<(LxTask<()>, String)> {
460    let (server_fut, primary_server_url) = build_server_fut_with_listener(
461        listener,
462        router,
463        layer_config,
464        maybe_tls_and_dns,
465        &server_span_name,
466        server_span.clone(),
467        shutdown,
468    )
469    .context("Failed to build server future")?;
470
471    let server_task =
472        LxTask::spawn_with_span(server_span_name, server_span, server_fut);
473
474    Ok((server_task, primary_server_url))
475}
476
477// --- LxJson --- //
478
479/// A version of [`axum::Json`] which conforms to Lexe's (JSON) API.
480/// It can be used as either an extractor or a response.
481///
482/// - As an extractor: rejections return [`LxRejection`].
483/// - As a success response:
484///   - Serialization success returns an [`http::Response`] with JSON body.
485///   - Serialization failure returns a [`ErrorResponse`].
486///
487/// [`axum::Json`] is banned because:
488///
489/// - Rejections return [`JsonRejection`] which is just a string HTTP body.
490/// - Response serialization failures likewise return just a string body.
491///
492/// NOTE: This must only be used for forming *success* API responses,
493/// i.e. `T` in `Result<T, E>`, because its [`IntoResponse`] impl uses
494/// [`StatusCode::OK`]. Our API error types, while also serialized as JSON,
495/// have separate [`IntoResponse`] impls which return error statuses.
496///
497/// [`ErrorResponse`]: lexe_api_core::error::ErrorResponse
498pub struct LxJson<T>(pub T);
499
500impl<T: DeserializeOwned, S: Send + Sync> FromRequest<S> for LxJson<T> {
501    type Rejection = LxRejection;
502
503    async fn from_request(
504        req: http::Request<axum::body::Body>,
505        state: &S,
506    ) -> Result<Self, Self::Rejection> {
507        // `axum::Json`'s from_request impl is fine but its rejection is not
508        <axum::Json<T> as FromRequest<S>>::from_request(req, state)
509            .await
510            .map(|axum::Json(t)| Self(t))
511            .map_err(LxRejection::from)
512    }
513}
514
515impl<T: DeserializeOwned, S: Send + Sync> OptionalFromRequest<S> for LxJson<T> {
516    type Rejection = LxRejection;
517
518    async fn from_request(
519        req: http::Request<axum::body::Body>,
520        state: &S,
521    ) -> Result<Option<Self>, Self::Rejection> {
522        <axum::Json<T> as OptionalFromRequest<S>>::from_request(req, state)
523            .await
524            .map(|opt| opt.map(|axum::Json(t)| Self(t)))
525            .map_err(LxRejection::from)
526    }
527}
528
529impl<T: Serialize> IntoResponse for LxJson<T> {
530    fn into_response(self) -> http::Response<axum::body::Body> {
531        axum_helpers::build_json_response(StatusCode::OK, &self.0)
532    }
533}
534
535impl<T: Clone> Clone for LxJson<T> {
536    fn clone(&self) -> Self {
537        Self(self.0.clone())
538    }
539}
540
541impl<T: Copy> Copy for LxJson<T> {}
542
543impl<T: fmt::Debug> fmt::Debug for LxJson<T> {
544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545        T::fmt(&self.0, f)
546    }
547}
548
549impl<T: Eq + PartialEq> Eq for LxJson<T> {}
550
551impl<T: PartialEq> PartialEq for LxJson<T> {
552    fn eq(&self, other: &Self) -> bool {
553        self.0.eq(&other.0)
554    }
555}
556
557// --- LxBytes --- //
558
559/// A version of [`Bytes`] which conforms to Lexe's (binary) API.
560/// - [`axum`] has implementations of [`FromRequest`] and [`IntoResponse`] for
561///   [`Bytes`], but these implementations are not Lexe API-conformant.
562/// - This type can be used as either an extractor or a success response, and
563///   should always be used instead of [`Bytes`] in these server contexts.
564/// - It is still fine to use [`Bytes`] on the client side.
565///
566/// - As an extractor: rejections return [`LxRejection`].
567/// - As a success response:
568///   - Returns an [`http::Response`] with a binary body.
569///
570///   - Any failure encountered in extraction or creation should produce an
571///     [`ErrorResponse`].
572///
573/// The regular impls are non-conformant because:
574///
575/// - Rejections return [`BytesRejection`] which is just a string HTTP body.
576///
577/// NOTE: This must only be used for forming *success* API responses,
578/// i.e. `LxBytes` in `Result<LxBytes, E>`, because its [`IntoResponse`] impl
579/// uses [`StatusCode::OK`]. Our API error types are serialized as JSON and
580/// have separate [`IntoResponse`] impls which return error statuses.
581///
582/// [`ErrorResponse`]: lexe_api_core::error::ErrorResponse
583#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
584pub struct LxBytes(pub Bytes);
585
586impl<S: Send + Sync> FromRequest<S> for LxBytes {
587    type Rejection = LxRejection;
588
589    async fn from_request(
590        req: http::Request<axum::body::Body>,
591        state: &S,
592    ) -> Result<Self, Self::Rejection> {
593        // `Bytes`'s from_request impl is fine but its rejection is not
594        Bytes::from_request(req, state)
595            .await
596            .map(Self)
597            .map_err(LxRejection::from)
598    }
599}
600
601/// The [`Bytes`] [`IntoResponse`] impl is almost exactly the same,
602/// except it returns the wrong HTTP version.
603impl IntoResponse for LxBytes {
604    fn into_response(self) -> http::Response<axum::body::Body> {
605        let http_body = http_body_util::Full::new(self.0);
606        let axum_body = axum::body::Body::new(http_body);
607
608        axum_helpers::default_response_builder()
609            .header(
610                CONTENT_TYPE,
611                // Or `HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM)`
612                HeaderValue::from_static("application/octet-stream"),
613            )
614            .status(StatusCode::OK)
615            .body(axum_body)
616            .expect("All operations here should be infallible")
617    }
618}
619
620impl<T: Into<Bytes>> From<T> for LxBytes {
621    fn from(t: T) -> Self {
622        Self(t.into())
623    }
624}
625
626// --- LxRejection --- //
627
628/// Our own [`axum::extract::rejection`] type with an [`IntoResponse`] impl
629/// which conforms to Lexe's API. Contains the source rejection's error text.
630pub struct LxRejection {
631    /// Which [`axum::extract::rejection`] this [`LxRejection`] was built from.
632    kind: LxRejectionKind,
633    /// The error text of the source rejection, or additional context.
634    source_msg: String,
635}
636
637/// The source of this [`LxRejection`].
638enum LxRejectionKind {
639    // -- From `axum::extract::rejection` -- //
640    /// [`BytesRejection`]
641    Bytes,
642    /// [`JsonRejection`]
643    Json,
644    /// [`PathRejection`]
645    Path,
646    /// [`QueryRejection`]
647    Query,
648
649    // -- Other -- //
650    /// Bearer authentication failed
651    Unauthenticated,
652    /// Client is not authorized to access this resource
653    Unauthorized,
654    /// Client request did not match any paths in the [`Router`].
655    BadEndpoint,
656    /// Request body length over limit
657    BodyLengthOverLimit,
658    /// [`ed25519::Error`]
659    Ed25519,
660    /// Gateway proxy
661    Proxy,
662}
663
664// Use explicit `.map_err()`s instead of From impls for non-obvious conversions
665impl LxRejection {
666    pub fn from_ed25519(error: ed25519::Error) -> Self {
667        Self {
668            kind: LxRejectionKind::Ed25519,
669            source_msg: format!("{error:#}"),
670        }
671    }
672
673    pub fn from_bearer_auth(error: auth::Error) -> Self {
674        Self {
675            kind: LxRejectionKind::Unauthenticated,
676            source_msg: format!("{error:#}"),
677        }
678    }
679
680    pub fn scope_unauthorized(
681        granted_scope: &LexeScope,
682        requested_scope: &LexeScope,
683    ) -> Self {
684        Self {
685            kind: LxRejectionKind::Unauthorized,
686            source_msg: format!(
687                "granted scope: {granted_scope:?}, requested scope: {requested_scope:?}"
688            ),
689        }
690    }
691
692    pub fn proxy(error: impl Display) -> Self {
693        Self {
694            kind: LxRejectionKind::Proxy,
695            source_msg: format!("{error:#}"),
696        }
697    }
698}
699
700impl From<BytesRejection> for LxRejection {
701    fn from(bytes_rejection: BytesRejection) -> Self {
702        Self {
703            kind: LxRejectionKind::Bytes,
704            source_msg: bytes_rejection.body_text(),
705        }
706    }
707}
708
709impl From<JsonRejection> for LxRejection {
710    fn from(json_rejection: JsonRejection) -> Self {
711        Self {
712            kind: LxRejectionKind::Json,
713            source_msg: json_rejection.body_text(),
714        }
715    }
716}
717
718impl From<PathRejection> for LxRejection {
719    fn from(path_rejection: PathRejection) -> Self {
720        Self {
721            kind: LxRejectionKind::Path,
722            source_msg: path_rejection.body_text(),
723        }
724    }
725}
726
727impl From<QueryRejection> for LxRejection {
728    fn from(query_rejection: QueryRejection) -> Self {
729        Self {
730            kind: LxRejectionKind::Query,
731            source_msg: query_rejection.body_text(),
732        }
733    }
734}
735
736impl IntoResponse for LxRejection {
737    fn into_response(self) -> http::Response<axum::body::Body> {
738        // TODO(phlip9): authn+authz+badendpoint rejections should return
739        // standard status codes, not just 400.
740        let kind = CommonErrorKind::Rejection;
741        // "Bad JSON: Failed to deserialize the JSON body into the target type"
742        let kind_msg = self.kind.to_msg();
743        let source_msg = &self.source_msg;
744        let msg = format!("Rejection: {kind_msg}: {source_msg}");
745        // Log the rejection now since our trace layer can't access this info
746        warn!("{msg}");
747        let common_error = CommonApiError { kind, msg };
748        common_error.into_response()
749    }
750}
751
752impl LxRejectionKind {
753    /// A generic error message for this rejection kind.
754    fn to_msg(&self) -> &'static str {
755        match self {
756            Self::Bytes => "Bad request bytes",
757            Self::Json => "Client provided bad JSON",
758            Self::Path => "Client provided bad path parameter",
759            Self::Query => "Client provided bad query string",
760
761            Self::Unauthenticated => "Invalid bearer auth",
762            Self::Unauthorized => "Not authorized to access this resource",
763            Self::BadEndpoint => "Client requested a non-existent endpoint",
764            Self::BodyLengthOverLimit => "Request body length over limit",
765            Self::Ed25519 => "Ed25519 error",
766            Self::Proxy => "Proxy error",
767        }
768    }
769}
770
771// --- Extractors --- //
772
773pub mod extract {
774    use axum::extract::FromRequestParts;
775
776    use super::*;
777
778    /// Lexe API-compliant version of [`axum::extract::Query`].
779    pub struct LxQuery<T>(pub T);
780
781    impl<T: DeserializeOwned, S: Send + Sync> FromRequestParts<S> for LxQuery<T> {
782        type Rejection = LxRejection;
783
784        async fn from_request_parts(
785            parts: &mut http::request::Parts,
786            state: &S,
787        ) -> Result<Self, Self::Rejection> {
788            axum::extract::Query::from_request_parts(parts, state)
789                .await
790                .map(|axum::extract::Query(t)| Self(t))
791                .map_err(LxRejection::from)
792        }
793    }
794
795    impl<T: Clone> Clone for LxQuery<T> {
796        fn clone(&self) -> Self {
797            Self(self.0.clone())
798        }
799    }
800
801    impl<T: fmt::Debug> fmt::Debug for LxQuery<T> {
802        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
803            T::fmt(&self.0, f)
804        }
805    }
806
807    impl<T: Eq + PartialEq> Eq for LxQuery<T> {}
808
809    impl<T: PartialEq> PartialEq for LxQuery<T> {
810        fn eq(&self, other: &Self) -> bool {
811            self.0.eq(&other.0)
812        }
813    }
814
815    /// Lexe API-compliant version of [`axum::extract::Path`].
816    pub struct LxPath<T>(pub T);
817
818    impl<T: DeserializeOwned + Send, S: Send + Sync> FromRequestParts<S>
819        for LxPath<T>
820    {
821        type Rejection = LxRejection;
822
823        async fn from_request_parts(
824            parts: &mut http::request::Parts,
825            state: &S,
826        ) -> Result<Self, Self::Rejection> {
827            axum::extract::Path::from_request_parts(parts, state)
828                .await
829                .map(|axum::extract::Path(t)| Self(t))
830                .map_err(LxRejection::from)
831        }
832    }
833
834    impl<T: Clone> Clone for LxPath<T> {
835        fn clone(&self) -> Self {
836            Self(self.0.clone())
837        }
838    }
839
840    impl<T: fmt::Debug> fmt::Debug for LxPath<T> {
841        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
842            T::fmt(&self.0, f)
843        }
844    }
845
846    impl<T: Eq + PartialEq> Eq for LxPath<T> {}
847
848    impl<T: PartialEq> PartialEq for LxPath<T> {
849        fn eq(&self, other: &Self) -> bool {
850            self.0.eq(&other.0)
851        }
852    }
853}
854
855// --- Custom middleware --- //
856
857pub mod middleware {
858    use axum::extract::State;
859    use http::HeaderName;
860
861    use super::*;
862
863    /// The header name used for response post-processing signals.
864    pub static POST_PROCESS_HEADER: HeaderName =
865        HeaderName::from_static("lx-post-process");
866
867    /// Checks the `CONTENT_LENGTH` header and returns an early rejection if the
868    /// contained value exceeds our configured body limit. This optimization
869    /// allows us to avoid unnecessary work processing the request further.
870    ///
871    /// NOTE: This does not enforce the body length!! Use [`DefaultBodyLimit`]
872    /// in combination with [`axum::RequestExt::with_limited_body`] to do so.
873    pub async fn check_content_length_header<B>(
874        // `LayerConfig::body_limit`
875        State(config_body_limit): State<usize>,
876        request: http::Request<B>,
877    ) -> Result<http::Request<B>, LxRejection> {
878        let maybe_content_length = request
879            .headers()
880            .get(http::header::CONTENT_LENGTH)
881            .and_then(|value| value.to_str().ok())
882            .and_then(|value_str| usize::from_str(value_str).ok());
883
884        // If a limit is configured and the header value exceeds it, reject.
885        if let Some(content_length) = maybe_content_length
886            && content_length > config_body_limit
887        {
888            return Err(LxRejection {
889                kind: LxRejectionKind::BodyLengthOverLimit,
890                source_msg: "Content length header over limit".to_owned(),
891            });
892        }
893
894        Ok(request)
895    }
896
897    /// A post-processor which can be used to modify the [`http::Response`]s
898    /// returned by an [`axum::Router`]. This is done by signalling the desired
899    /// modification in a fake [`POST_PROCESS_HEADER`] which is also removed
900    /// during post-processing. This can be used to override Axum defaults
901    /// which one does not have access to from within the [`Router`]. Currently,
902    /// this only supports a "remove-content-length" command which removes the
903    /// content-length header set by Axum, but can be easily extended.
904    pub(super) fn post_process_response(
905        mut response: http::Response<axum::body::Body>,
906    ) -> http::Response<axum::body::Body> {
907        let value = match response.headers_mut().remove(&POST_PROCESS_HEADER) {
908            Some(v) => v,
909            None => return response,
910        };
911
912        match value.as_bytes() {
913            b"remove-content-length" => {
914                response.headers_mut().remove(http::header::CONTENT_LENGTH);
915                debug!("Post process: Removed content-length header");
916            }
917            unknown => {
918                let unknown_str = String::from_utf8_lossy(unknown);
919                warn!("Post process: Invalid header value: {unknown_str}");
920            }
921        }
922
923        response
924    }
925}
926
927// --- Helpers --- //
928
929/// Lexe's default fallback [`Handler`](axum::handler::Handler).
930/// Returns a "bad endpoint" rejection along with the requested method and path.
931pub async fn default_fallback(
932    method: http::Method,
933    uri: http::Uri,
934) -> LxRejection {
935    let path = uri.path();
936    LxRejection {
937        kind: LxRejectionKind::BadEndpoint,
938        // e.g. "POST /app/node_info"
939        source_msg: format!("{method} {path}"),
940    }
941}