1#![allow(clippy::disallowed_types)]
3
4use 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
83const SHUTDOWN_GRACE_PERIOD: Duration = Duration::from_secs(3);
87pub 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
93pub 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#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct LayerConfig {
119 pub body_limit: usize,
122 pub buffer_size: usize,
126 pub concurrency: usize,
129 pub handling_timeout: Duration,
133 pub default_fallback: bool,
143}
144
145impl Default for LayerConfig {
146 fn default() -> Self {
147 Self {
148 body_limit: 16384,
150 buffer_size: 4096,
154 concurrency: 4096,
155 handling_timeout: SERVER_HANDLER_TIMEOUT,
156 default_fallback: true,
157 }
158 }
159}
160
161pub fn build_server_url(
172 listener_addr: SocketAddr,
174 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
190pub fn build_server_fut(
202 bind_addr: SocketAddr,
203 router: Router<()>,
204 layer_config: LayerConfig,
205 maybe_tls_and_dns: Option<(Arc<rustls::ServerConfig>, &str)>,
207 server_span_name: &str,
208 server_span: tracing::Span,
209 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
227pub fn build_server_fut_with_listener(
231 listener: TcpListener,
232 router: Router<()>,
233 layer_config: LayerConfig,
234 maybe_tls_and_dns: Option<(Arc<rustls::ServerConfig>, &str)>,
236 server_span_name: &str,
237 server_span: tracing::Span,
238 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 let router = if layer_config.default_fallback {
250 router.fallback(default_fallback)
251 } else {
252 router
253 };
254
255 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 let outer_middleware = tower::ServiceBuilder::new()
278 .check_service::<HyperService, HyperReq, AxumResp, Infallible>()
279 .layer(trace::server::trace_layer(server_span.clone()))
282 .check_service::<HyperService, HyperReq, TraceResp, Infallible>()
283 .layer(tower::util::MapResponseLayer::new(
286 middleware::post_process_response,
287 ))
288 .check_service::<HyperService, HyperReq, TraceResp, Infallible>();
289
290 let inner_middleware = tower::ServiceBuilder::new()
294 .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
295 .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 .layer(DefaultBodyLimit::max(layer_config.body_limit))
308 .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
309 .layer(MapRequestLayer::new(axum::RequestExt::with_limited_body))
312 .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
313 .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 .layer(LoadShedLayer::new())
323 .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
324 .layer(BufferLayer::new(layer_config.buffer_size))
329 .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
330 .layer(ConcurrencyLimitLayer::new(layer_config.concurrency))
333 .check_service::<AxumService, AxumReq, AxumResp, Infallible>()
334 .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 .layer(TimeoutLayer::new(layer_config.handling_timeout))
345 .check_service::<AxumService, AxumReq, AxumResp, Infallible>();
346
347 let layered_router = router.layer(inner_middleware);
349 let router_service = layered_router.into_service::<hyper::body::Incoming>();
351 let layered_service = Layer::layer(&outer_middleware, router_service);
353 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 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 .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 handle.graceful_shutdown(Some(SHUTDOWN_GRACE_PERIOD));
397 };
398
399 let combined_fut = async {
400 tokio::pin!(server_fut);
401 tokio::select! {
402 biased; () = 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
416pub fn spawn_server_task(
420 bind_addr: SocketAddr,
421 router: Router<()>,
422 layer_config: LayerConfig,
423 maybe_tls_and_dns: Option<(Arc<rustls::ServerConfig>, &str)>,
425 server_span_name: Cow<'static, str>,
426 server_span: tracing::Span,
427 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
448pub fn spawn_server_task_with_listener(
450 listener: TcpListener,
451 router: Router<()>,
452 layer_config: LayerConfig,
453 maybe_tls_and_dns: Option<(Arc<rustls::ServerConfig>, &str)>,
455 server_span_name: Cow<'static, str>,
456 server_span: tracing::Span,
457 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
477pub 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<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#[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::from_request(req, state)
595 .await
596 .map(Self)
597 .map_err(LxRejection::from)
598 }
599}
600
601impl 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 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
626pub struct LxRejection {
631 kind: LxRejectionKind,
633 source_msg: String,
635}
636
637enum LxRejectionKind {
639 Bytes,
642 Json,
644 Path,
646 Query,
648
649 Unauthenticated,
652 Unauthorized,
654 BadEndpoint,
656 BodyLengthOverLimit,
658 Ed25519,
660 Proxy,
662}
663
664impl 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 let kind = CommonErrorKind::Rejection;
741 let kind_msg = self.kind.to_msg();
743 let source_msg = &self.source_msg;
744 let msg = format!("Rejection: {kind_msg}: {source_msg}");
745 warn!("{msg}");
747 let common_error = CommonApiError { kind, msg };
748 common_error.into_response()
749 }
750}
751
752impl LxRejectionKind {
753 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
771pub mod extract {
774 use axum::extract::FromRequestParts;
775
776 use super::*;
777
778 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 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
855pub mod middleware {
858 use axum::extract::State;
859 use http::HeaderName;
860
861 use super::*;
862
863 pub static POST_PROCESS_HEADER: HeaderName =
865 HeaderName::from_static("lx-post-process");
866
867 pub async fn check_content_length_header<B>(
874 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 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 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
927pub 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 source_msg: format!("{method} {path}"),
940 }
941}