Skip to content

Commit

Permalink
Add server extension trait revised for hyper 1.x
Browse files Browse the repository at this point in the history
  • Loading branch information
iamjpotts committed Jun 10, 2024
1 parent 6409d89 commit 255abe5
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 31 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ edition = "2021"
[dependencies]
hex = "0.4"
http-body-util = { version = "0.1", optional = true }
hyper = "1.1"
hyper = "1.3"
hyper-util = { version = "0.1.2", optional = true }
tokio = { version = "1.35", default-features = false, features = ["net"] }
tower-service = { version = "0.3", optional = true }
Expand All @@ -24,7 +24,7 @@ thiserror = "1.0"
tokio = { version = "1.35", features = ["io-std", "io-util", "macros", "rt-multi-thread"] }

[features]
default = ["client"]
default = ["client", "server"]
client = [
"http-body-util",
"hyper/client",
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ A typical server can be built by creating a `tokio::net::UnixListener` and accep
`hyper::service::service_fn` to create a request/response processing function, and connecting the `UnixStream` to it
using `hyper::server::conn::http1::Builder::new().serve_connection()`.

`hyperlocal` provides an extension trait `UnixListenerExt` with an implementation of this.

An example is at [examples/server.rs](./examples/server.rs), runnable via `cargo run --example server`

To test that your server is working you can use an out-of-the-box tool like `curl`
Expand All @@ -73,10 +75,10 @@ It's a Unix system. I know this.
`hyperlocal` also provides bindings for writing unix domain socket based HTTP clients the `Client` interface from the
`hyper-utils` crate.
An example is at [examples/client.rs](./examples/client.rs), runnable via `cargo run --features="server" --example client`
An example is at [examples/client.rs](./examples/client.rs), runnable via `cargo run --example client`
Hyper's client interface makes it easy to send typical HTTP methods like `GET`, `POST`, `DELETE` with factory
methods, `get`, `post`, `delete`, etc. These require an argument that can be tranformed into a `hyper::Uri`.
methods, `get`, `post`, `delete`, etc. These require an argument that can be transformed into a `hyper::Uri`.

Since Unix domain sockets aren't represented with hostnames that resolve to ip addresses coupled with network ports,
your standard over the counter URL string won't do. Instead, use a `hyperlocal::Uri`, which represents both file path to the domain
Expand Down
40 changes: 13 additions & 27 deletions examples/server.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use hyper::{service::service_fn, Response};
use hyper_util::rt::TokioIo;
use std::{error::Error, fs, path::Path};

use hyper::Response;
use tokio::net::UnixListener;

use hyperlocal::UnixListenerExt;

const PHRASE: &str = "It's a Unix system. I know this.\n";

// Adapted from https://hyper.rs/guides/1/server/hello-world/
Expand All @@ -18,32 +20,16 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {

println!("Listening for connections at {}.", path.display());

loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);

println!("Accepting connection.");
listener
.serve(|| {
println!("Accepted connection.");

tokio::task::spawn(async move {
let svc_fn = service_fn(|_req| async {
|_request| async {
let body = PHRASE.to_string();
Ok::<_, hyper::Error>(Response::new(body))
});

match hyper::server::conn::http1::Builder::new()
// On OSX, disabling keep alive prevents serve_connection from
// blocking and later returning an Err derived from E_NOTCONN.
.keep_alive(false)
.serve_connection(io, svc_fn)
.await
{
Ok(()) => {
println!("Accepted connection.");
}
Err(err) => {
eprintln!("Failed to accept connection: {err:?}");
}
};
});
}
}
})
.await?;

Ok(())
}
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@
//!
//! - Client- enables the client extension trait and connector. *Enabled by
//! default*.
//!
//! - Server- enables the server extension trait. *Enabled by default*.

#[cfg(feature = "client")]
mod client;
#[cfg(feature = "client")]
pub use client::{UnixClientExt, UnixConnector};

#[cfg(feature = "server")]
mod server;
#[cfg(feature = "server")]
pub use server::UnixListenerExt;

mod uri;

pub use uri::Uri;
80 changes: 80 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use hyper::{
body::{Body, Incoming},
service::service_fn,
Request, Response,
};
use hyper_util::rt::TokioIo;
use std::future::Future;
use tokio::net::UnixListener;

/// Extension trait for provisioning a hyper HTTP server over a Unix domain
/// socket.
///
/// # Example
///
/// ```rust
/// use hyper::Response;
/// use hyperlocal::UnixListenerExt;
/// use tokio::net::UnixListener;
///
/// let future = async move {
/// let listener = UnixListener::bind("/tmp/hyperlocal.sock").expect("parsed unix path");
///
/// listener
/// .serve(|| {
/// |_request| async {
/// Ok::<_, hyper::Error>(Response::new("Hello, world.".to_string()))
/// }
/// })
/// .await
/// .expect("failed to serve a connection")
/// };
/// ```
pub trait UnixListenerExt {
/// Indefinitely accept and respond to connections.
///
/// Pass a function which will generate the function which responds to
/// all requests for an individual connection.
fn serve<MakeResponseFn, ResponseFn, ResponseFuture, B, E>(
self,
f: MakeResponseFn,
) -> impl Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>>
where
MakeResponseFn: Fn() -> ResponseFn,
ResponseFn: Fn(Request<Incoming>) -> ResponseFuture,
ResponseFuture: Future<Output = Result<Response<B>, E>>,
B: Body + 'static,
<B as Body>::Error: std::error::Error + Send + Sync,
E: std::error::Error + Send + Sync + 'static;
}

impl UnixListenerExt for UnixListener {
fn serve<MakeServiceFn, ResponseFn, ResponseFuture, B, E>(
self,
f: MakeServiceFn,
) -> impl Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>>
where
MakeServiceFn: Fn() -> ResponseFn,
ResponseFn: Fn(Request<Incoming>) -> ResponseFuture,
ResponseFuture: Future<Output = Result<Response<B>, E>>,
B: Body + 'static,
<B as Body>::Error: std::error::Error + Send + Sync,
E: std::error::Error + Send + Sync + 'static,
{
async move {
loop {
let (stream, _) = self.accept().await?;
let io = TokioIo::new(stream);

let svc_fn = service_fn(f());

hyper::server::conn::http1::Builder::new()
// On OSX, disabling keep alive prevents serve_connection from
// blocking and later returning an Err derived from E_NOTCONN.
.keep_alive(false)
.serve_connection(io, svc_fn)
.await?;
}
}
}
}

0 comments on commit 255abe5

Please sign in to comment.