Skip to main content

churust_core/
lib.rs

1//! Churust core kernel: the engine, routing, request pipeline, and extractors
2//! that power the [Churust](https://crates.io/crates/churust) web framework.
3//!
4//! Churust is a Ktor-inspired, async-first web framework for Rust. This crate
5//! (`churust-core`) is the foundation every other Churust crate builds on. It
6//! provides:
7//!
8//! - A fluent [`Churust::server`] builder ([`AppBuilder`]) for assembling an
9//!   [`App`] from routes, shared state, middleware, and configuration.
10//! - A trie-based [`Router`] supporting static segments, `{param}` captures, and
11//!   trailing `{name...}` wildcards.
12//! - The per-request [`Call`] context — the single object every handler receives.
13//! - An onion-style middleware [pipeline] ordered by [`Phase`].
14//! - Type-safe [extractors](crate::extract) ([`Path`], [`Query`], [`State`],
15//!   [`BearerToken`]) plus the [`FromCall`]/[`FromCallParts`] traits that let
16//!   handlers take typed arguments.
17//! - A flexible [`Response`]/[`IntoResponse`] model and a status-carrying
18//!   [`Error`] type.
19//! - Layered [`Config`] loading (defaults < `churust.toml` < `CHURUST_*` env <
20//!   code) and optional TLS (feature `tls`).
21//! - An in-process [`TestClient`] for fast, socket-free integration tests.
22//!
23//! # Example
24//!
25//! Build an app, register a route, and exercise it with the in-process test
26//! client (no socket is bound, so this runs in any environment):
27//!
28//! ```
29//! use churust_core::{Churust, Call, TestClient};
30//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
31//! let app = Churust::server()
32//!     .routing(|r| {
33//!         r.get("/", |_c: Call| async { "Hello, Churust!" });
34//!     })
35//!     .build();
36//!
37//! let res = TestClient::new(app).get("/").send().await;
38//! assert_eq!(res.status().as_u16(), 200);
39//! assert_eq!(res.text(), "Hello, Churust!");
40//! # });
41//! ```
42//!
43//! To actually serve traffic, call [`App::start`] (binds a socket and serves
44//! until Ctrl-C) or [`AppBuilder::start`].
45
46#![deny(missing_docs)]
47#![doc(
48    html_logo_url = "https://raw.githubusercontent.com/davthecoder/Churust/main/img/churust_logo.png"
49)]
50
51pub mod body;
52pub use body::Body;
53
54pub mod error;
55pub use error::{Error, IntoError, Result};
56
57pub mod response;
58pub use response::{IntoResponse, Response};
59
60pub mod call;
61pub use call::{BodyStream, Call, Params, PeerAddr};
62
63pub mod handler;
64pub use handler::{boxed, BoxHandler, Handler, IntoHandler};
65
66pub mod state;
67pub use state::StateMap;
68
69pub mod extract;
70pub use extract::{
71    check_body_limit, BearerToken, Either, Form, FromCall, FromCallParts, Header, HeaderName,
72    OptionalFromCallParts, Path, Payload, Query, RouteBodyLimit, State,
73};
74
75#[cfg(feature = "tower")]
76pub mod tower;
77
78pub mod router;
79pub use path::PathPolicy;
80pub use router::{allow_header_value, Match, RouteBuilder, Router};
81
82pub mod pipeline;
83pub use pipeline::{Endpoint, Middleware, Next, Phase};
84
85pub mod config;
86pub use config::{Config, ServerSection, TlsSection};
87
88pub mod app;
89pub use app::{App, AppBuilder, Churust, Plugin, ServerConfig};
90
91pub mod engine;
92
93/// Security response headers applied by default. See [`SecurityHeaders`].
94pub mod security;
95pub use security::SecurityHeaders;
96
97// Percent-decoding for path segments. Internal: the decoding rules are a
98// routing detail, and exposing them would invite decoding at the wrong point in
99// the pipeline.
100/// Percent-decoding and canonicalisation for URL path segments.
101pub mod path;
102mod path_de;
103
104/// Cookies: reading a request's, and building `Set-Cookie`.
105pub mod cookie;
106pub use cookie::{Cookie, SameSite};
107
108/// `multipart/form-data` bodies (feature `multipart`).
109#[cfg(feature = "multipart")]
110pub mod multipart;
111#[cfg(feature = "multipart")]
112pub use multipart::{Field, Multipart, MultipartStream, Part};
113
114/// Sessions carried by a cookie.
115pub mod session;
116pub use session::{CookieStore, Session, SessionStore, Sessions, SESSION_ID_KEY};
117
118/// Login, logout, and the two deadlines that end a login.
119pub mod identity;
120pub use identity::{Authenticated, Identities, Identity};
121
122/// Route guards — predicates that select among routes sharing a method and path.
123pub mod guard;
124pub use guard::{BoxGuard, Guard};
125
126/// Run a blocking operation without stalling the async runtime.
127///
128/// Calling a blocking API directly from a handler occupies a runtime worker for
129/// its duration; enough concurrent calls and the server stops answering
130/// anything. This moves the work to tokio's blocking pool, which is what that
131/// pool is for.
132///
133/// Reach for it around synchronous file I/O, a blocking database driver, or
134/// CPU-heavy work such as password hashing.
135///
136/// ```
137/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
138/// let sum = churust_core::block(|| (1..=1000).sum::<u64>()).await.unwrap();
139/// assert_eq!(sum, 500_500);
140/// # });
141/// ```
142///
143/// A panic inside `f` becomes a `500` rather than taking down the worker,
144/// matching how a panicking handler is already treated.
145pub async fn block<F, T>(f: F) -> Result<T>
146where
147    F: FnOnce() -> T + Send + 'static,
148    T: Send + 'static,
149{
150    tokio::task::spawn_blocking(f).await.map_err(|e| {
151        // A `JoinError` embeds the panic payload, and `Error`'s message is what
152        // is rendered to the client — so formatting it into the message published whatever an
153        // `.expect("connecting to postgres://user:pw@host")` happened to say.
154        // Keep it for the operator, send the client the same bare message a
155        // panicking handler already produces.
156        tracing::error!(error = %e, "blocking task failed");
157        Error::internal("Internal Server Error").with_source(e)
158    })
159}
160
161/// Compare two secrets without leaking their contents through timing.
162///
163/// A plain `==` on strings returns as soon as it finds a differing byte, so the
164/// time it takes reveals how much of a guess was correct. Over enough requests
165/// that is enough to recover a token or password a byte at a time.
166///
167/// Use this in [`Auth::basic`](../churust_auth/index.html) callbacks and
168/// anywhere else a request-supplied value is checked against a secret.
169///
170/// The comparison is constant-time **in the contents**, not in the length: an
171/// early length check short-circuits, which reveals only the length. That is
172/// the same trade every practical implementation makes.
173///
174/// ```
175/// use churust_core::secure_compare;
176///
177/// assert!(secure_compare("hunter2", "hunter2"));
178/// assert!(!secure_compare("hunter2", "hunter3"));
179/// ```
180pub fn secure_compare(a: impl AsRef<[u8]>, b: impl AsRef<[u8]>) -> bool {
181    let (a, b) = (a.as_ref(), b.as_ref());
182    if a.len() != b.len() {
183        return false;
184    }
185    // Fold every byte into the accumulator so the loop cannot exit early.
186    let mut diff = 0u8;
187    for (x, y) in a.iter().zip(b.iter()) {
188        diff |= x ^ y;
189    }
190    diff == 0
191}
192
193#[cfg(feature = "tls")]
194pub mod tls;
195
196/// HTTP/3 over QUIC (feature `http3`).
197#[cfg(feature = "http3")]
198pub mod http3;
199
200#[cfg(feature = "ws")]
201pub mod ws;
202#[cfg(feature = "ws")]
203pub use ws::{WebSocket, WebSocketUpgrade};
204
205#[cfg(feature = "fs")]
206pub mod fs;
207#[cfg(feature = "fs")]
208pub use fs::StaticFiles;
209
210pub mod test;
211pub use test::{TestClient, TestRequest, TestResponse};
212
213#[cfg(test)]
214mod smoke {
215    #[test]
216    fn workspace_builds() {
217        assert_eq!(2 + 2, 4);
218    }
219}