Skip to main content

humus_terra/
lib.rs

1#![warn(missing_docs)]
2
3//!
4//! # humus-terra
5//!
6//! humus-terra is an **intuitive** and **robust** framework for writing web-servers based on HTTP2.
7//!
8//! # Features
9//!
10//! - HTTP/2
11//! - Asynchronous Design
12//!
13
14mod encrypt;
15mod error;
16pub mod response;
17pub mod route;
18pub mod terminal;
19pub mod tokens;
20
21use http_body_util::Full;
22use hyper::body::{Bytes, Incoming};
23use hyper::server::conn::http2;
24use hyper::service::service_fn;
25use hyper::{Request, Response, StatusCode};
26use hyper_util::rt::TokioIo;
27
28use crate::response::ResponseBuilder;
29use crate::route::{configure_all, match_route, shutdown_all, Route};
30use std::convert::Infallible;
31use std::error::Error;
32use std::net::SocketAddr;
33use std::sync::Arc;
34use std::time::Duration;
35use tokio::net::TcpListener;
36
37#[derive(Clone)]
38struct TokioExecutor;
39
40impl<F> hyper::rt::Executor<F> for TokioExecutor
41where
42    F: std::future::Future + Send + 'static,
43    F::Output: Send + 'static,
44{
45    fn execute(&self, fut: F) {
46        tokio::task::spawn(fut);
47    }
48}
49
50/// An abstraction for hosting and routing.
51pub struct App {
52    port: u16,
53    shutdown_duration: Duration,
54    root_route: Arc<dyn Route + Send + Sync>,
55}
56
57impl App {
58    ///
59    /// Create new application with specified settings
60    ///
61    /// - *port*: Port to be used for hosting application
62    /// - *shutdown_duration*: Timeout from `SIGINT` for finalising resources and connections
63    /// - *root_route*: Implementation of root route
64    ///
65    /// # Examples
66    ///
67    /// ```ignore
68    /// use std::time::Duration;
69    /// use humus_terra::App;
70    ///
71    /// let app = App::new(8080, Duration::from_secs(10), ...);
72    /// ```
73    ///
74    pub fn new(
75        port: u16,
76        shutdown_duration: Duration,
77        root_route: Arc<dyn Route + Send + Sync>,
78    ) -> Self {
79        Self {
80            port,
81            shutdown_duration,
82            root_route,
83        }
84    }
85
86    async fn configure(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
87        configure_all(self.root_route.clone()).await
88    }
89
90    async fn map(&self, request: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
91        let route = match match_route(request.uri().path(), self.root_route.clone()) {
92            None => {
93                return Ok(ResponseBuilder::new()
94                    .status(StatusCode::NOT_FOUND)
95                    .body(Full::from(Bytes::new()))
96                    .unwrap())
97            }
98            Some(route) => route,
99        };
100
101        match route.handle(request).await {
102            Ok(response) => Ok(response),
103            Err(error) => Ok(ResponseBuilder::new()
104                .status(StatusCode::INTERNAL_SERVER_ERROR)
105                .body(Full::from(error.to_string()))
106                .unwrap()),
107        }
108    }
109
110    async fn shutdown(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
111        shutdown_all(self.root_route.clone()).await
112    }
113
114    /// Run the configured application.
115    ///
116    /// This function executes the main loop of the application. It will block
117    /// until the application is shutdown. If the application is triggered
118    /// with `SIGINT`, it will exit the main loop and finalise resources.
119    /// Instead of terminating the entire programme, the invocation of this
120    /// function will simply return after finalisation.
121    ///
122    /// If the application fails to close all connections within the specified
123    /// time limit, it will log a message but will not panic or forcibly shut
124    /// down the system.
125    ///
126    /// # Examples
127    ///
128    /// ```ignore
129    /// use std::sync::Arc;
130    /// use std::time::Duration;
131    /// use humus_terra::App;
132    ///
133    /// let app = App::new(8080, Duration::from_secs(10), ...);
134    ///
135    /// async move {
136    ///     App::main(Arc::new(app)).await?;
137    /// }
138    /// ```
139    ///
140    pub async fn main(self: Arc<Self>) -> Result<(), Box<dyn Error + Send + Sync>> {
141        self.configure().await?;
142
143        let addr = SocketAddr::from(([127, 0, 0, 1], self.port));
144        let listener = TcpListener::bind(addr).await?;
145
146        let graceful = hyper_util::server::graceful::GracefulShutdown::new();
147        let mut signal = std::pin::pin!(async {
148            tokio::signal::ctrl_c()
149                .await
150                .expect("failed to install CTRL+C signal handler");
151        });
152
153        loop {
154            tokio::select! {
155                Ok((stream, _)) = listener.accept() => {
156                    let io = TokioIo::new(stream);
157                    let app = self.clone();
158
159                    tokio::task::spawn(async move {
160                        if let Err(err) = http2::Builder::new(TokioExecutor)
161                            .serve_connection(io, service_fn(move |req| {
162                                let scoped_app = app.clone();
163                                async move { scoped_app.clone().map(req).await }
164                            }))
165                            .await {
166                            log!(fail "HTTP2 error: {}", err);
167                        }
168                    });
169                },
170
171                _ = &mut signal => {
172                    log!(info "Shutting down...");
173                    self.shutdown().await?;
174                    break;
175                }
176            }
177        }
178
179        tokio::select! {
180            _ = graceful.shutdown() => {
181                log!(info "All connections gracefully closed");
182            },
183            _ = tokio::time::sleep(self.shutdown_duration) => {
184                log!(info "Timed out waiting for connections");
185            }
186        }
187
188        Ok(())
189    }
190}