Skip to content

Commit

Permalink
feat: worked example in full
Browse files Browse the repository at this point in the history
  • Loading branch information
0xAlcibiades committed Sep 11, 2024
1 parent 479cc86 commit 9ea325e
Show file tree
Hide file tree
Showing 5 changed files with 936 additions and 117 deletions.
167 changes: 151 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
![CI](https://github.com/warlock-labs/hyper-server/actions/workflows/CI.yml/badge.svg)
[![codecov](https://codecov.io/gh/warlock-labs/hyper-server/branch/master/graph/badge.svg?token=8W5MEJQSW6)](https://codecov.io/gh/warlock-labs/hyper-server)

A high-performance, modular server implementation built on [hyper], designed to
work seamlessly with [axum], [tonic], [tower], and other tower-compatible
A high-performance, modular server implementation built on [hyper], designed to
work seamlessly with [axum], [tonic], [tower], and other tower-compatible
frameworks.

## Features
Expand All @@ -19,7 +19,6 @@ frameworks.
- Flexible integration with [tower] the ecosystem, supporting various backends:
- [axum]
- [tonic]
- [tungstenite]
- Any `hyper::service::Service`, `tower::Service`, or `tower::Layer` composition

## Installation
Expand All @@ -33,13 +32,144 @@ hyper-server = "0.7.0"

## Usage

Here's an example of how to use hyper-server with a simple tower service:
Here's an example of how to use hyper-server with a simple tower lambda service via TCP/TLS/HTTP2 transport:

```rust

use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use bytes::Bytes;
use http_body_util::Full;
use hyper::{Request, Response};
use hyper::body::Incoming;
use hyper_util::rt::TokioExecutor;
use hyper_util::server::conn::auto::Builder as HttpConnectionBuilder;
use hyper_util::service::TowerToHyperService;
use rustls::ServerConfig;
use tokio::net::TcpListener;
use tokio_stream::wrappers::TcpListenerStream;
use tower::{Layer, ServiceBuilder};
use tracing::{trace, debug, info};

use hyper_server::{load_certs, load_private_key, serve_http_with_shutdown};

// Define a simple service that responds with "Hello, World!"
async fn hello(_: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::new(Bytes::from("Hello, World!"))))
}

// Define a Custom middleware to add a header to all responses, for example
struct AddHeaderLayer;

impl<S> Layer<S> for AddHeaderLayer {
type Service = AddHeaderService<S>;

fn layer(&self, service: S) -> Self::Service {
AddHeaderService { inner: service }
}
}

#[derive(Clone)]
struct AddHeaderService<S> {
inner: S,
}

impl<S, B> tower::Service<Request<B>> for AddHeaderService<S>
where
S: tower::Service<Request<B>, Response=Response<Full<Bytes>>>,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output=Result<Self::Response, Self::Error>> + Send>,
>;

fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, req: Request<B>) -> Self::Future {
trace!("Adding custom header to response");
let future = self.inner.call(req);
Box::pin(async move {
let mut resp = future.await?;
resp.headers_mut()
.insert("X-Custom-Header", "Hello from middleware!".parse().unwrap());
Ok(resp)
})
}
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 8443));
// 1. Set up the TCP listener
let listener = TcpListener::bind(addr).await?;
info!("Listening on https://{}", addr);
let incoming = TcpListenerStream::new(listener);

// 2. Create the HTTP connection builder
let builder = HttpConnectionBuilder::new(TokioExecutor::new());

// 3. Set up the Tower service with middleware
let svc = tower::service_fn(hello);
let svc = ServiceBuilder::new()
.layer(AddHeaderLayer) // Custom middleware
.service(svc);

// 4. Convert the Tower service to a Hyper service
let svc = TowerToHyperService::new(svc);

// 5. Set up TLS config
let certs = load_certs("examples/sample.pem")?;
let key = load_private_key("examples/sample.rsa")?;

let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
let tls_config = Arc::new(config);

// 6. Set up graceful shutdown
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();

// Spawn a task to send the shutdown signal after 1 second
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(1)).await;
let _ = shutdown_tx.send(());
debug!("Shutdown signal sent");
});

// 7. Start the server
info!("Starting HTTPS server...");
serve_http_with_shutdown(
svc,
incoming,
builder,
Some(tls_config),
Some(async {
shutdown_rx.await.ok();
info!("Shutdown signal received, starting graceful shutdown");
}),
)
.await?;

info!("Server has shut down");
// Et voilà!
// A flexible, high-performance server with custom services, middleware, http, tls, tcp, and graceful shutdown
Ok(())
}
```

For more advanced usage and examples, including TLS configuration and custom service implementations, please refer to the [examples directory](/examples).
For more advanced usage and examples, please refer to, or contribute to,
the [examples directory](/examples).

## Architecture

Expand All @@ -52,18 +182,18 @@ hyper-server provides a layered, composable architecture:
5. Middleware: `tower::Service`
6. Application: Your choice of tower-compatible framework (axum, tonic, etc.) or custom service implementation

This structure allows for easy customization and extension at each layer. You
can integrate your own implementations at any level of the stack, providing
This structure allows for easy customization and extension at each layer. You
can integrate your own implementations at any level of the stack, providing
maximum flexibility for your specific use case.

## Security

hyper-server takes security seriously. We use `rustls` for TLS support, which
provides modern, secure defaults. However, please ensure that you configure
your server appropriately for your use case, especially when deploying in
hyper-server takes security seriously. We use `rustls` for TLS support, which
provides modern, secure defaults. However, please ensure that you configure
your server appropriately for your use case, especially when deploying in
production environments.

If you discover any security-related issues, please email team@warlock.xyz
If you discover any security-related issues, please email team@warlock.xyz
instead of using the issue tracker.

## API
Expand All @@ -76,9 +206,9 @@ hyper-server's MSRV is `1.80`.

## Contributing

We welcome contributions to hyper-server! Our contributing guidelines are
inspired by the Rule of St. Benedict, emphasizing humility, listening,
and community. Before contributing, please familiarize yourself with these
We welcome contributions to hyper-server! Our contributing guidelines are
inspired by the Rule of St. Benedict, emphasizing humility, listening,
and community. Before contributing, please familiarize yourself with these
principles at [The Rule of St. Benedict](http://www.benedictfriend.org/the-rule.html).

Key points for contributors:
Expand All @@ -90,12 +220,17 @@ Key points for contributors:

## License

This project is licensed under the MIT License — see
This project is licensed under the MIT License — see
the [LICENSE](/LICENSE) file for details.

[axum]: https://crates.io/crates/axum

[hyper]: https://crates.io/crates/hyper

[rustls]: https://crates.io/crates/rustls

[tower]: https://crates.io/crates/tower

[tonic]: https://crates.io/crates/tonic

[tungstenite]: https://crates.io/crates/tungstenite
131 changes: 131 additions & 0 deletions examples/full.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use bytes::Bytes;
use http_body_util::Full;
use hyper::body::Incoming;
use hyper::{Request, Response};
use hyper_util::rt::TokioExecutor;
use hyper_util::server::conn::auto::Builder as HttpConnectionBuilder;
use hyper_util::service::TowerToHyperService;
use rustls::ServerConfig;
use tokio::net::TcpListener;
use tokio_stream::wrappers::TcpListenerStream;
use tower::{Layer, ServiceBuilder};
use tracing::{debug, info, trace};

use hyper_server::{load_certs, load_private_key, serve_http_with_shutdown};

// Define a simple service that responds with "Hello, World!"
async fn hello(_: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::new(Bytes::from("Hello, World!"))))
}

// Define a Custom middleware to add a header to all responses, for example
struct AddHeaderLayer;

impl<S> Layer<S> for AddHeaderLayer {
type Service = AddHeaderService<S>;

fn layer(&self, service: S) -> Self::Service {
AddHeaderService { inner: service }
}
}

#[derive(Clone)]
struct AddHeaderService<S> {
inner: S,
}

impl<S, B> tower::Service<Request<B>> for AddHeaderService<S>
where
S: tower::Service<Request<B>, Response = Response<Full<Bytes>>>,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
>;

fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, req: Request<B>) -> Self::Future {
trace!("Adding custom header to response");
let future = self.inner.call(req);
Box::pin(async move {
let mut resp = future.await?;
resp.headers_mut()
.insert("X-Custom-Header", "Hello from middleware!".parse().unwrap());
Ok(resp)
})
}
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 8443));
// 1. Set up the TCP listener
let listener = TcpListener::bind(addr).await?;
info!("Listening on https://{}", addr);
let incoming = TcpListenerStream::new(listener);

// 2. Create the HTTP connection builder
let builder = HttpConnectionBuilder::new(TokioExecutor::new());

// 3. Set up the Tower service with middleware
let svc = tower::service_fn(hello);
let svc = ServiceBuilder::new()
.layer(AddHeaderLayer) // Custom middleware
.service(svc);

// 4. Convert the Tower service to a Hyper service
let svc = TowerToHyperService::new(svc);

// 5. Set up TLS config
let certs = load_certs("examples/sample.pem")?;
let key = load_private_key("examples/sample.rsa")?;

let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
let tls_config = Arc::new(config);

// 6. Set up graceful shutdown
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();

// Spawn a task to send the shutdown signal after 1 second
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(1)).await;
let _ = shutdown_tx.send(());
debug!("Shutdown signal sent");
});

// 7. Start the server
info!("Starting HTTPS server...");
serve_http_with_shutdown(
svc,
incoming,
builder,
Some(tls_config),
Some(async {
shutdown_rx.await.ok();
info!("Shutdown signal received, starting graceful shutdown");
}),
)
.await?;

info!("Server has shut down");
// Et voilà!
// A flexible, high-performance server with custom services, middleware, http, tls, tcp, and graceful shutdown
Ok(())
}
Loading

0 comments on commit 9ea325e

Please sign in to comment.