diff --git a/Cargo.toml b/Cargo.toml index 33e65e8..17e3175 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } @@ -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", diff --git a/README.md b/README.md index 7c9e709..05f299a 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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 diff --git a/examples/server.rs b/examples/server.rs index e68c53c..e936e7b 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -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/ @@ -18,32 +20,16 @@ async fn main() -> Result<(), Box> { 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(()) } diff --git a/src/lib.rs b/src/lib.rs index d8e0fb7..84236c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..68c3241 --- /dev/null +++ b/src/server.rs @@ -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( + self, + f: MakeResponseFn, + ) -> impl Future>> + where + MakeResponseFn: Fn() -> ResponseFn, + ResponseFn: Fn(Request) -> ResponseFuture, + ResponseFuture: Future, E>>, + B: Body + 'static, + ::Error: std::error::Error + Send + Sync, + E: std::error::Error + Send + Sync + 'static; +} + +impl UnixListenerExt for UnixListener { + fn serve( + self, + f: MakeServiceFn, + ) -> impl Future>> + where + MakeServiceFn: Fn() -> ResponseFn, + ResponseFn: Fn(Request) -> ResponseFuture, + ResponseFuture: Future, E>>, + B: Body + 'static, + ::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?; + } + } + } +}