From 92113224ae14901fe82f6086717c9f3d6ca8fac5 Mon Sep 17 00:00:00 2001 From: Thomas Orozco Date: Wed, 24 Feb 2021 12:01:02 +0000 Subject: [PATCH] state: move instantiation into State::from_request This refactors the service implementation a little bit to instantiate the State via a dedicated method. The underlying goal is to allow users to use Gotham without having to use its handler. The motivation for that is to allow: - Not requiring handlers to be unwind safe. - Injecting extra things into the state that came from the socket (besides the client address), such as client TLS certificates. --- Cargo.toml | 2 + examples/custom_service/Cargo.toml | 14 ++++++ examples/custom_service/README.md | 19 +++++++ examples/custom_service/src/main.rs | 78 +++++++++++++++++++++++++++++ gotham/src/lib.rs | 2 +- gotham/src/service/mod.rs | 50 ++---------------- gotham/src/service/trap.rs | 5 +- gotham/src/state/mod.rs | 50 +++++++++++++++++- 8 files changed, 169 insertions(+), 51 deletions(-) create mode 100644 examples/custom_service/Cargo.toml create mode 100644 examples/custom_service/README.md create mode 100644 examples/custom_service/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 7079a3cf9..ac68e22d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,5 +81,7 @@ members = [ # finalizer "examples/finalizers/", + # custom_service + "examples/custom_service/", ] diff --git a/examples/custom_service/Cargo.toml b/examples/custom_service/Cargo.toml new file mode 100644 index 000000000..1efea6cf2 --- /dev/null +++ b/examples/custom_service/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "gotham_examples_custom_service" +description = "Using Gotham in a pre-existing Hyper service" +version = "0.0.0" +publish = false +edition = "2018" + +[dependencies] +anyhow = "1.0" +futures = "0.3" +gotham = { path = "../../gotham" } +http = "0.2" +hyper = { version = "0.14", features = ["full"] } +tokio = { version = "1.0", features = ["full"] } diff --git a/examples/custom_service/README.md b/examples/custom_service/README.md new file mode 100644 index 000000000..bf1021acd --- /dev/null +++ b/examples/custom_service/README.md @@ -0,0 +1,19 @@ +# Custom Service + +An example of using Gotham within a pre-existing Hyper service. This is useful +for advanced use cases where you might want to wrap Gotham in order logic, or +pre-populate your state handing it off to Gotham. + +## License + +Licensed under your option of: + +* [MIT License](../../LICENSE-MIT) +* [Apache License, Version 2.0](../../LICENSE-APACHE) + +## Community + +The following policies guide participation in our project and our community: + +* [Code of conduct](../../CODE_OF_CONDUCT.md) +* [Contributing](../../CONTRIBUTING.md) diff --git a/examples/custom_service/src/main.rs b/examples/custom_service/src/main.rs new file mode 100644 index 000000000..1bdc83c12 --- /dev/null +++ b/examples/custom_service/src/main.rs @@ -0,0 +1,78 @@ +//! An example usage of Gotham from another service. + +use anyhow::{Context as _, Error}; +use futures::future::{BoxFuture, FutureExt}; +use gotham::{ + router::{builder::*, Router}, + service::call_handler, + state::State, +}; +use http::{Request, Response}; +use hyper::{server::conn::Http, service::Service, Body}; +use std::net::SocketAddr; +use std::panic::AssertUnwindSafe; +use std::task; +use tokio::net::TcpListener; + +#[derive(Clone)] +struct MyService { + router: Router, + addr: SocketAddr, +} + +impl Service> for MyService { + type Response = Response; + type Error = Error; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll> { + task::Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + // NOTE: You don't *have* to use call_handler for this (you could use `router.handle`), but + // call_handler will catch panics and return en error response. + let state = State::from_request(req, self.addr); + call_handler(self.router.clone(), AssertUnwindSafe(state)).boxed() + } +} + +pub fn say_hello(state: State) -> (State, &'static str) { + (state, "hello world") +} + +#[tokio::main] +pub async fn main() -> Result<(), Error> { + let router = build_simple_router(|route| { + // For the path "/" invoke the handler "say_hello" + route.get("/").to(say_hello); + }); + + let addr = "127.0.0.1:7878"; + let listener = TcpListener::bind(&addr).await?; + + println!("Listening for requests at http://{}", addr); + + loop { + let (socket, addr) = listener + .accept() + .await + .context("Error accepting connection")?; + + let service = MyService { + router: router.clone(), + addr, + }; + + let task = async move { + Http::new() + .serve_connection(socket, service) + .await + .context("Error serving connection")?; + + Result::<_, Error>::Ok(()) + }; + + tokio::spawn(task); + } +} diff --git a/gotham/src/lib.rs b/gotham/src/lib.rs index 83b8d9670..05c45f57e 100644 --- a/gotham/src/lib.rs +++ b/gotham/src/lib.rs @@ -32,7 +32,7 @@ pub mod helpers; pub mod middleware; pub mod pipeline; pub mod router; -mod service; +pub mod service; pub mod state; /// Test utilities for Gotham and Gotham consumer apps. diff --git a/gotham/src/service/mod.rs b/gotham/src/service/mod.rs index 3a2857410..54718bf73 100644 --- a/gotham/src/service/mod.rs +++ b/gotham/src/service/mod.rs @@ -5,24 +5,19 @@ use std::net::SocketAddr; use std::panic::AssertUnwindSafe; use std::pin::Pin; use std::sync::Arc; -use std::thread; use futures::prelude::*; use futures::task::{self, Poll}; -use http::request; use hyper::service::Service; -use hyper::upgrade::OnUpgrade; use hyper::{Body, Request, Response}; -use log::debug; use crate::handler::NewHandler; - -use crate::helpers::http::request::path::RequestPathSegments; -use crate::state::client_addr::put_client_addr; -use crate::state::{set_request_id, State}; +use crate::state::State; mod trap; +pub use trap::call_handler; + /// Wraps a `NewHandler` which will be used to serve requests. Used in `gotham::os::*` to bind /// incoming connections to `ConnectedGothamService` values. pub(crate) struct GothamService @@ -76,43 +71,8 @@ where } fn call<'a>(&'a mut self, req: Request) -> Self::Future { - let mut state = State::new(); - - put_client_addr(&mut state, self.client_addr); - - let ( - request::Parts { - method, - uri, - version, - headers, - mut extensions, - .. - }, - body, - ) = req.into_parts(); - - state.put(RequestPathSegments::new(uri.path())); - state.put(method); - state.put(uri); - state.put(version); - state.put(headers); - state.put(body); - - if let Some(on_upgrade) = extensions.remove::() { - state.put(on_upgrade); - } - - { - let request_id = set_request_id(&mut state); - debug!( - "[DEBUG][{}][Thread][{:?}]", - request_id, - thread::current().id(), - ); - }; - - trap::call_handler(self.handler.clone(), AssertUnwindSafe(state)).boxed() + let state = State::from_request(req, self.client_addr); + call_handler(self.handler.clone(), AssertUnwindSafe(state)).boxed() } } diff --git a/gotham/src/service/trap.rs b/gotham/src/service/trap.rs index b667a426c..41a19e155 100644 --- a/gotham/src/service/trap.rs +++ b/gotham/src/service/trap.rs @@ -29,10 +29,7 @@ where /// /// Timing information is recorded and logged, except in the case of a panic where the timer is /// moved and cannot be recovered. -pub(super) async fn call_handler( - t: T, - state: AssertUnwindSafe, -) -> anyhow::Result> +pub async fn call_handler(t: T, state: AssertUnwindSafe) -> anyhow::Result> where T: NewHandler + Send + UnwindSafe, { diff --git a/gotham/src/state/mod.rs b/gotham/src/state/mod.rs index 425d5fb93..faf978086 100644 --- a/gotham/src/state/mod.rs +++ b/gotham/src/state/mod.rs @@ -5,16 +5,22 @@ mod data; mod from_state; pub mod request_id; -use log::trace; +use log::{debug, trace}; +use http::request; +use hyper::upgrade::OnUpgrade; +use hyper::{Body, Request}; use std::any::{Any, TypeId}; use std::collections::HashMap; +use std::net::SocketAddr; pub use crate::state::client_addr::client_addr; pub use crate::state::data::StateData; pub use crate::state::from_state::FromState; pub use crate::state::request_id::request_id; +use crate::helpers::http::request::path::RequestPathSegments; +use crate::state::client_addr::put_client_addr; pub(crate) use crate::state::request_id::set_request_id; /// Provides storage for request state, and stores one item of each type. The types used for @@ -69,6 +75,48 @@ impl State { f(&mut State::new()) } + /// Instantiate a new `State` for a given `Request`. This is primarily useful if you're calling + /// Gotham from your own Hyper service. + pub fn from_request(req: Request, client_addr: SocketAddr) -> Self { + let mut state = Self::new(); + + put_client_addr(&mut state, client_addr); + + let ( + request::Parts { + method, + uri, + version, + headers, + mut extensions, + .. + }, + body, + ) = req.into_parts(); + + state.put(RequestPathSegments::new(uri.path())); + state.put(method); + state.put(uri); + state.put(version); + state.put(headers); + state.put(body); + + if let Some(on_upgrade) = extensions.remove::() { + state.put(on_upgrade); + } + + { + let request_id = set_request_id(&mut state); + debug!( + "[DEBUG][{}][Thread][{:?}]", + request_id, + std::thread::current().id(), + ); + }; + + state + } + /// Puts a value into the `State` storage. One value of each type is retained. Successive calls /// to `put` will overwrite the existing value of the same type. ///