scuffle_http/
lib.rs

1//! An HTTP server with support for HTTP/1, HTTP/2 and HTTP/3.
2//!
3//! It abstracts away [`hyper`](https://crates.io/crates/hyper) and [`h3`](https://crates.io/crates/h3) to provide a rather simple interface for creating and running a server that can handle all three protocols.
4//!
5//! See the [examples](./examples) directory for usage examples.
6//!
7//! ## Why do we need this?
8//!
9//! This crate is designed to be a simple and easy to use HTTP server that supports HTTP/1, HTTP/2 and HTTP/3.
10//!
11//! Currently, there are simply no other crates that provide support for all three protocols with a unified API.
12//! This crate aims to fill that gap.
13//!
14//! ## Feature Flags
15//!
16//! - `tower`: Enables support for [`tower`](https://crates.io/crates/tower) services. Enabled by default.
17//! - `http1`: Enables support for HTTP/1. Enabled by default.
18//! - `http2`: Enables support for HTTP/2. Enabled by default.
19//! - `http3`: Enables support for HTTP/3. Disabled by default.
20//! - `tracing`: Enables logging with [`tracing`](https://crates.io/crates/tracing). Disabled by default.
21//! - `tls-rustls`: Enables support for TLS with [`rustls`](https://crates.io/crates/rustls). Disabled by default.
22//! - `http3-tls-rustls`: Enables both `http3` and `tls-rustls` features. Disabled by default.
23//!
24//! ## Example
25//!
26//! The following example demonstrates how to create a simple HTTP server (without TLS) that responds with "Hello, world!" to all requests on port 3000.
27//!
28//! ```rust
29//! # use scuffle_future_ext::FutureExt;
30//! # tokio_test::block_on(async {
31//! # let run = async {
32//! let service = scuffle_http::service::fn_http_service(|req| async move {
33//!     scuffle_http::Response::builder()
34//!         .status(scuffle_http::http::StatusCode::OK)
35//!         .header(scuffle_http::http::header::CONTENT_TYPE, "text/plain")
36//!         .body("Hello, world!".to_string())
37//! });
38//! let service_factory = scuffle_http::service::service_clone_factory(service);
39//!
40//! scuffle_http::HttpServer::builder()
41//!     .service_factory(service_factory)
42//!     .bind("[::]:3000".parse().unwrap())
43//!     .build()
44//!     .run()
45//!     .await
46//!     .expect("server failed");
47//! # };
48//! # run.with_timeout(std::time::Duration::from_secs(1)).await.expect_err("test should have timed out");
49//! # });
50//! ```
51//!
52//! ## Status
53//!
54//! This crate is currently under development and is not yet stable.
55//!
56//! Unit tests are not yet fully implemented. Use at your own risk.
57//!
58//! ### Missing Features
59//!
60//! - HTTP/3 webtransport support
61//! - Upgrading to websocket connections from HTTP/3 connections (this is usually done via HTTP/1.1 anyway)
62//!
63//! ## License
64//!
65//! This project is licensed under the [MIT](./LICENSE.MIT) or [Apache-2.0](./LICENSE.Apache-2.0) license.
66//! You can choose between one of them if you use this work.
67//!
68//! `SPDX-License-Identifier: MIT OR Apache-2.0`
69#![cfg_attr(docsrs, feature(doc_cfg))]
70#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
71
72#[cfg(all(feature = "http3", not(feature = "tls-rustls")))]
73compile_error!("feature \"tls-rustls\" must be enabled when \"http3\" is enabled.");
74
75#[cfg(any(feature = "http1", feature = "http2", feature = "http3"))]
76#[cfg_attr(docsrs, doc(cfg(any(feature = "http1", feature = "http2", feature = "http3"))))]
77pub mod backend;
78pub mod body;
79pub mod error;
80mod server;
81pub mod service;
82
83pub use http;
84pub use http::Response;
85pub use server::{HttpServer, HttpServerBuilder};
86
87pub type IncomingRequest = http::Request<body::IncomingBody>;
88
89#[cfg(test)]
90#[cfg_attr(all(test, coverage_nightly), coverage(off))]
91mod tests {
92    use std::convert::Infallible;
93    use std::time::Duration;
94
95    use scuffle_future_ext::FutureExt;
96
97    use crate::service::{fn_http_service, service_clone_factory};
98    use crate::HttpServer;
99
100    fn get_available_addr() -> std::io::Result<std::net::SocketAddr> {
101        let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
102        listener.local_addr()
103    }
104
105    const RESPONSE_TEXT: &str = "Hello, world!";
106
107    #[allow(dead_code)]
108    async fn test_server<F, S>(builder: crate::HttpServerBuilder<F, S>, versions: &[reqwest::Version])
109    where
110        F: crate::service::HttpServiceFactory + std::fmt::Debug + Clone + Send + 'static,
111        F::Error: std::error::Error + Send,
112        F::Service: Clone + std::fmt::Debug + Send + 'static,
113        <F::Service as crate::service::HttpService>::Error: std::error::Error + Send + Sync,
114        <F::Service as crate::service::HttpService>::ResBody: Send,
115        <<F::Service as crate::service::HttpService>::ResBody as http_body::Body>::Data: Send,
116        <<F::Service as crate::service::HttpService>::ResBody as http_body::Body>::Error: std::error::Error + Send + Sync,
117        S: crate::server::http_server_builder::State,
118        S::ServiceFactory: crate::server::http_server_builder::IsSet,
119        S::Bind: crate::server::http_server_builder::IsUnset,
120        S::Ctx: crate::server::http_server_builder::IsUnset,
121    {
122        let addr = get_available_addr().expect("failed to get available address");
123        let (ctx, handler) = scuffle_context::Context::new();
124
125        let server = builder.bind(addr).ctx(ctx).build();
126
127        let handle = tokio::spawn(async move {
128            server.run().await.expect("server run failed");
129        });
130
131        // Wait for the server to start
132        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
133
134        let url = format!("http://{}/", addr);
135
136        for version in versions {
137            let mut builder = reqwest::Client::builder().danger_accept_invalid_certs(true);
138
139            if *version == reqwest::Version::HTTP_3 {
140                builder = builder.http3_prior_knowledge();
141            } else if *version == reqwest::Version::HTTP_2 {
142                builder = builder.http2_prior_knowledge();
143            } else {
144                builder = builder.http1_only();
145            }
146
147            let client = builder.build().expect("failed to build client");
148
149            let request = client
150                .request(reqwest::Method::GET, &url)
151                .version(*version)
152                .body(RESPONSE_TEXT.to_string())
153                .build()
154                .expect("failed to build request");
155
156            let resp = client
157                .execute(request)
158                .await
159                .expect("failed to get response")
160                .text()
161                .await
162                .expect("failed to get text");
163
164            assert_eq!(resp, RESPONSE_TEXT);
165        }
166
167        handler.shutdown().await;
168        handle.await.expect("task failed");
169    }
170
171    #[cfg(feature = "tls-rustls")]
172    #[allow(dead_code)]
173    async fn test_tls_server<F, S>(builder: crate::HttpServerBuilder<F, S>, versions: &[reqwest::Version])
174    where
175        F: crate::service::HttpServiceFactory + std::fmt::Debug + Clone + Send + 'static,
176        F::Error: std::error::Error + Send,
177        F::Service: Clone + std::fmt::Debug + Send + 'static,
178        <F::Service as crate::service::HttpService>::Error: std::error::Error + Send + Sync,
179        <F::Service as crate::service::HttpService>::ResBody: Send,
180        <<F::Service as crate::service::HttpService>::ResBody as http_body::Body>::Data: Send,
181        <<F::Service as crate::service::HttpService>::ResBody as http_body::Body>::Error: std::error::Error + Send + Sync,
182        S: crate::server::http_server_builder::State,
183        S::ServiceFactory: crate::server::http_server_builder::IsSet,
184        S::Bind: crate::server::http_server_builder::IsUnset,
185        S::Ctx: crate::server::http_server_builder::IsUnset,
186    {
187        let addr = get_available_addr().expect("failed to get available address");
188        let (ctx, handler) = scuffle_context::Context::new();
189
190        let server = builder.bind(addr).ctx(ctx).build();
191
192        let handle = tokio::spawn(async move {
193            server.run().await.expect("server run failed");
194        });
195
196        // Wait for the server to start
197        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
198
199        let url = format!("https://{}/", addr);
200
201        for version in versions {
202            let mut builder = reqwest::Client::builder().danger_accept_invalid_certs(true).https_only(true);
203
204            if *version == reqwest::Version::HTTP_3 {
205                builder = builder.http3_prior_knowledge();
206            } else if *version == reqwest::Version::HTTP_2 {
207                builder = builder.http2_prior_knowledge();
208            } else {
209                builder = builder.http1_only();
210            }
211
212            let client = builder.build().expect("failed to build client");
213
214            let request = client
215                .request(reqwest::Method::GET, &url)
216                .version(*version)
217                .body(RESPONSE_TEXT.to_string())
218                .build()
219                .expect("failed to build request");
220
221            let resp = client
222                .execute(request)
223                .await
224                .unwrap_or_else(|_| panic!("failed to get response version {:?}", version))
225                .text()
226                .await
227                .expect("failed to get text");
228
229            assert_eq!(resp, RESPONSE_TEXT);
230        }
231
232        handler.shutdown().await;
233        handle.await.expect("task failed");
234    }
235
236    #[tokio::test]
237    #[cfg(feature = "http2")]
238    async fn http2_server() {
239        let builder = HttpServer::builder().service_factory(service_clone_factory(fn_http_service(|_| async {
240            Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
241        })));
242
243        #[cfg(feature = "http1")]
244        let builder = builder.enable_http1(false);
245
246        test_server(builder, &[reqwest::Version::HTTP_2]).await;
247    }
248
249    #[tokio::test]
250    #[cfg(all(feature = "http1", feature = "http2"))]
251    async fn http12_server() {
252        let server = HttpServer::builder()
253            .service_factory(service_clone_factory(fn_http_service(|_| async {
254                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
255            })))
256            .enable_http1(true)
257            .enable_http2(true);
258
259        test_server(server, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
260    }
261
262    #[cfg(feature = "tls-rustls")]
263    fn rustls_config() -> rustls::ServerConfig {
264        rustls::crypto::aws_lc_rs::default_provider()
265            .install_default()
266            .expect("failed to install aws lc provider");
267
268        let certfile = std::fs::File::open("../../assets/cert.pem").expect("cert not found");
269        let certs = rustls_pemfile::certs(&mut std::io::BufReader::new(certfile))
270            .collect::<Result<Vec<_>, _>>()
271            .expect("failed to load certs");
272        let keyfile = std::fs::File::open("../../assets/key.pem").expect("key not found");
273        let key = rustls_pemfile::private_key(&mut std::io::BufReader::new(keyfile))
274            .expect("failed to load key")
275            .expect("no key found");
276
277        rustls::ServerConfig::builder()
278            .with_no_client_auth()
279            .with_single_cert(certs, key)
280            .expect("failed to build config")
281    }
282
283    #[tokio::test]
284    #[cfg(all(feature = "tls-rustls", feature = "http1"))]
285    async fn rustls_http1_server() {
286        let builder = HttpServer::builder()
287            .service_factory(service_clone_factory(fn_http_service(|_| async {
288                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
289            })))
290            .rustls_config(rustls_config());
291
292        #[cfg(feature = "http2")]
293        let builder = builder.enable_http2(false);
294
295        test_tls_server(builder, &[reqwest::Version::HTTP_11]).await;
296    }
297
298    #[tokio::test]
299    #[cfg(all(feature = "tls-rustls", feature = "http3"))]
300    async fn rustls_http3_server() {
301        let builder = HttpServer::builder()
302            .service_factory(service_clone_factory(fn_http_service(|_| async {
303                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
304            })))
305            .rustls_config(rustls_config())
306            .enable_http3(true);
307
308        #[cfg(feature = "http2")]
309        let builder = builder.enable_http2(false);
310
311        #[cfg(feature = "http1")]
312        let builder = builder.enable_http1(false);
313
314        test_tls_server(builder, &[reqwest::Version::HTTP_3]).await;
315    }
316
317    #[tokio::test]
318    #[cfg(all(feature = "tls-rustls", feature = "http1", feature = "http2"))]
319    async fn rustls_http12_server() {
320        let builder = HttpServer::builder()
321            .service_factory(service_clone_factory(fn_http_service(|_| async {
322                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
323            })))
324            .rustls_config(rustls_config())
325            .enable_http1(true)
326            .enable_http2(true);
327
328        test_tls_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
329    }
330
331    #[tokio::test]
332    #[cfg(all(feature = "tls-rustls", feature = "http1", feature = "http2", feature = "http3"))]
333    async fn rustls_http123_server() {
334        let builder = HttpServer::builder()
335            .service_factory(service_clone_factory(fn_http_service(|_| async {
336                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
337            })))
338            .rustls_config(rustls_config())
339            .enable_http1(true)
340            .enable_http2(true)
341            .enable_http3(true);
342
343        test_tls_server(
344            builder,
345            &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2, reqwest::Version::HTTP_3],
346        )
347        .await;
348    }
349
350    #[tokio::test]
351    async fn no_backend() {
352        let addr = get_available_addr().expect("failed to get available address");
353
354        let builder = HttpServer::builder()
355            .service_factory(service_clone_factory(fn_http_service(|_| async {
356                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
357            })))
358            .bind(addr);
359
360        #[cfg(feature = "http1")]
361        let builder = builder.enable_http1(false);
362
363        #[cfg(feature = "http2")]
364        let builder = builder.enable_http2(false);
365
366        builder
367            .build()
368            .run()
369            .with_timeout(Duration::from_millis(100))
370            .await
371            .expect("server timed out")
372            .expect("server failed");
373    }
374
375    #[tokio::test]
376    #[cfg(feature = "tls-rustls")]
377    async fn rustls_no_backend() {
378        let addr = get_available_addr().expect("failed to get available address");
379
380        let builder = HttpServer::builder()
381            .service_factory(service_clone_factory(fn_http_service(|_| async {
382                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
383            })))
384            .rustls_config(rustls_config())
385            .bind(addr);
386
387        #[cfg(feature = "http1")]
388        let builder = builder.enable_http1(false);
389
390        #[cfg(feature = "http2")]
391        let builder = builder.enable_http2(false);
392
393        builder
394            .build()
395            .run()
396            .with_timeout(Duration::from_millis(100))
397            .await
398            .expect("server timed out")
399            .expect("server failed");
400    }
401
402    #[tokio::test]
403    #[cfg(all(feature = "tower", feature = "http1", feature = "http2"))]
404    async fn tower_make_service() {
405        let builder = HttpServer::builder()
406            .tower_make_service_factory(tower::service_fn(|_| async {
407                Ok::<_, Infallible>(tower::service_fn(|_| async move {
408                    Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
409                }))
410            }))
411            .enable_http1(true)
412            .enable_http2(true);
413
414        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
415    }
416
417    #[tokio::test]
418    #[cfg(all(feature = "tower", feature = "http1", feature = "http2"))]
419    async fn tower_custom_make_service() {
420        let builder = HttpServer::builder()
421            .custom_tower_make_service_factory(
422                tower::service_fn(|target| async move {
423                    assert_eq!(target, 42);
424                    Ok::<_, Infallible>(tower::service_fn(|_| async move {
425                        Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
426                    }))
427                }),
428                42,
429            )
430            .enable_http1(true)
431            .enable_http2(true);
432
433        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
434    }
435
436    #[tokio::test]
437    #[cfg(all(feature = "tower", feature = "http1", feature = "http2"))]
438    async fn tower_make_service_with_addr() {
439        use std::net::SocketAddr;
440
441        let builder = HttpServer::builder()
442            .tower_make_service_with_addr(tower::service_fn(|addr: SocketAddr| async move {
443                assert!(addr.ip().is_loopback());
444                Ok::<_, Infallible>(tower::service_fn(|_| async move {
445                    Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
446                }))
447            }))
448            .enable_http1(true)
449            .enable_http2(true);
450
451        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
452    }
453
454    #[tokio::test]
455    #[cfg(all(feature = "http1", feature = "http2"))]
456    async fn fn_service_factory() {
457        use crate::service::fn_http_service_factory;
458
459        let builder = HttpServer::builder()
460            .service_factory(fn_http_service_factory(|_| async {
461                Ok::<_, Infallible>(fn_http_service(|_| async {
462                    Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
463                }))
464            }))
465            .enable_http1(true)
466            .enable_http2(true);
467
468        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
469    }
470
471    #[tokio::test]
472    #[cfg(all(
473        feature = "http1",
474        feature = "http2",
475        feature = "http3",
476        feature = "tls-rustls",
477        feature = "tower"
478    ))]
479    async fn axum_service() {
480        let router = axum::Router::new().route(
481            "/",
482            axum::routing::get(|req: String| async move {
483                assert_eq!(req, RESPONSE_TEXT);
484                http::Response::new(RESPONSE_TEXT.to_string())
485            }),
486        );
487
488        let builder = HttpServer::builder()
489            .tower_make_service_factory(router.into_make_service())
490            .rustls_config(rustls_config())
491            .enable_http3(true)
492            .enable_http1(true)
493            .enable_http2(true);
494
495        test_tls_server(
496            builder,
497            &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2, reqwest::Version::HTTP_3],
498        )
499        .await;
500    }
501
502    #[tokio::test]
503    #[cfg(all(feature = "http1", feature = "http2"))]
504    async fn tracked_body() {
505        use crate::body::TrackedBody;
506
507        #[derive(Clone)]
508        struct TestTracker;
509
510        impl crate::body::Tracker for TestTracker {
511            type Error = Infallible;
512
513            fn on_data(&self, size: usize) -> Result<(), Self::Error> {
514                assert_eq!(size, RESPONSE_TEXT.len());
515                Ok(())
516            }
517        }
518
519        let builder = HttpServer::builder()
520            .service_factory(service_clone_factory(fn_http_service(|req| async {
521                let req = req.map(|b| TrackedBody::new(b, TestTracker));
522                let body = req.into_body();
523                Ok::<_, Infallible>(http::Response::new(body))
524            })))
525            .enable_http1(true)
526            .enable_http2(true);
527
528        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
529    }
530
531    #[tokio::test]
532    #[cfg(all(feature = "http1", feature = "http2"))]
533    async fn tracked_body_error() {
534        use crate::body::TrackedBody;
535
536        #[derive(Clone)]
537        struct TestTracker;
538
539        impl crate::body::Tracker for TestTracker {
540            type Error = &'static str;
541
542            fn on_data(&self, size: usize) -> Result<(), Self::Error> {
543                assert_eq!(size, RESPONSE_TEXT.len());
544                Err("test")
545            }
546        }
547
548        let builder = HttpServer::builder()
549            .service_factory(service_clone_factory(fn_http_service(|req| async {
550                let req = req.map(|b| TrackedBody::new(b, TestTracker));
551                let body = req.into_body();
552                // Use axum to convert the body to bytes
553                let bytes = axum::body::to_bytes(axum::body::Body::new(body), usize::MAX).await;
554                assert_eq!(bytes.expect_err("expected error").to_string(), "tracker error: test");
555
556                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
557            })))
558            .enable_http1(true)
559            .enable_http2(true);
560
561        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
562    }
563
564    #[tokio::test]
565    #[cfg(all(feature = "http2", feature = "http3", feature = "tls-rustls"))]
566    async fn response_trailers() {
567        #[derive(Default)]
568        struct TestBody {
569            data_sent: bool,
570        }
571
572        impl http_body::Body for TestBody {
573            type Data = bytes::Bytes;
574            type Error = Infallible;
575
576            fn poll_frame(
577                mut self: std::pin::Pin<&mut Self>,
578                _cx: &mut std::task::Context<'_>,
579            ) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
580                if !self.data_sent {
581                    self.as_mut().data_sent = true;
582                    let data = http_body::Frame::data(bytes::Bytes::from_static(RESPONSE_TEXT.as_bytes()));
583                    std::task::Poll::Ready(Some(Ok(data)))
584                } else {
585                    let mut trailers = http::HeaderMap::new();
586                    trailers.insert("test", "test".parse().unwrap());
587                    std::task::Poll::Ready(Some(Ok(http_body::Frame::trailers(trailers))))
588                }
589            }
590        }
591
592        let builder = HttpServer::builder()
593            .service_factory(service_clone_factory(fn_http_service(|_req| async {
594                let mut resp = http::Response::new(TestBody::default());
595                resp.headers_mut().insert("trailers", "test".parse().unwrap());
596                Ok::<_, Infallible>(resp)
597            })))
598            .rustls_config(rustls_config())
599            .enable_http3(true)
600            .enable_http2(true);
601
602        #[cfg(feature = "http1")]
603        let builder = builder.enable_http1(false);
604
605        test_tls_server(builder, &[reqwest::Version::HTTP_2, reqwest::Version::HTTP_3]).await;
606    }
607}