diff --git a/Cargo.toml b/Cargo.toml index c4de7cc86..5ed8c0eaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ members = [ "examples/handlers/request_data", "examples/handlers/stateful", "examples/handlers/simple_async_handlers", + "examples/handlers/simple_async_handlers_await", "examples/handlers/async_handlers", "examples/handlers/form_urlencoded", "examples/handlers/multipart", @@ -75,15 +76,10 @@ members = [ "examples/example_contribution_template/name", # TODO: Re-enable when tokio-tungstenite is updated - # "examples/websocket", + "examples/websocket", # finalizer "examples/finalizers/", ] - -[patch.crates-io] -gotham = { path = "gotham" } -gotham_derive = { path = "gotham_derive" } -borrow-bag = { path = "misc/borrow_bag" } diff --git a/README.md b/README.md index cd14b2983..78a853ae5 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ We do acknowledge that sometimes the choices we've made for the Gotham web framework may not suit the needs of all projects. If that is the case for your project there are alternative Rust web frameworks you might like to consider: +1. [Actix-Web](https://github.com/actix/actix-web) 1. [Conduit](https://github.com/conduit-rust/conduit) 1. [Nickel](https://github.com/nickel-org/nickel.rs) 1. [Rocket](https://github.com/SergioBenitez/Rocket) diff --git a/examples/cookies/introduction/Cargo.toml b/examples/cookies/introduction/Cargo.toml index 155840d17..86650e06f 100644 --- a/examples/cookies/introduction/Cargo.toml +++ b/examples/cookies/introduction/Cargo.toml @@ -10,4 +10,4 @@ edition = "2018" gotham = { path = "../../../gotham" } mime = "0.3" -cookie = "0.13" +cookie = "0.14" diff --git a/examples/handlers/README.md b/examples/handlers/README.md index 486ed38ed..fa98ba9a8 100644 --- a/examples/handlers/README.md +++ b/examples/handlers/README.md @@ -18,6 +18,7 @@ We recommend reviewing our handler examples in the order shown below: 1. [Request Data](request_data) - Accessing common request information 1. [Stateful Handlers](stateful) - Keeping state in a handler 1. [Simple Async Handlers](simple_async_handlers) - Async Request Handlers 101 +1. [Simple Async Handlers (.await version)](simple_async_handlers_await) - Request Handlers that use async/.await 1. [Async Handlers](async_handlers) - More complicated async request handlers ## Help diff --git a/examples/handlers/simple_async_handlers/README.md b/examples/handlers/simple_async_handlers/README.md index c06af72b4..e7acb56fa 100644 --- a/examples/handlers/simple_async_handlers/README.md +++ b/examples/handlers/simple_async_handlers/README.md @@ -24,6 +24,9 @@ find yourself doing lots of CPU/memory intensive operations on the web server, then futures are probably not going to help your performance, and you might be better off spawning a new thread per request. +If you came here looking for an example that uses async/.await, please read +[Async Request Handlers (.await version)](../simple_async_handlers_await). + ## Running From the `examples/handlers/async_handlers` directory: diff --git a/examples/handlers/simple_async_handlers_await/Cargo.toml b/examples/handlers/simple_async_handlers_await/Cargo.toml new file mode 100644 index 000000000..cf7b1ca30 --- /dev/null +++ b/examples/handlers/simple_async_handlers_await/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "gotham_examples_handlers_simple_async_handlers_await" +description = "An example that does asynchronous work before responding" +version = "0.0.0" +authors = ["David Laban "] +publish = false +edition = "2018" + +[dependencies] +gotham = { path = "../../../gotham" } +gotham_derive = { path = "../../../gotham_derive" } + +mime = "0.3" +futures = "0.3.1" +serde = "1.0" +serde_derive = "1.0" +tokio = "0.2.9" diff --git a/examples/handlers/simple_async_handlers_await/README.md b/examples/handlers/simple_async_handlers_await/README.md new file mode 100644 index 000000000..dd13ad5d8 --- /dev/null +++ b/examples/handlers/simple_async_handlers_await/README.md @@ -0,0 +1,49 @@ +# Async Request Handlers (.await version) + +The idea of async handlers has already been introduced by the post_handler example in +[Request Data](../request_data), which waits for the POST body asyncronously, and resolves +the response future once it has processed the body. The combinator-based version +of this example can be found at [Async Request Handlers](../simple_async_handlers). + +This example has exactly the same behavior and API as the combinator-based version, +and it can be used as a reference when converting your code to use async/await. + +## Running + +From the `examples/handlers/async_handlers` directory: + +``` +Terminal 1: + Compiling gotham_examples_handlers_simple_async_handlers v0.0.0 (file:///.../gotham/examples/handlers/simple_async_handlers) + Finished dev [unoptimized + debuginfo] target(s) in 8.19 secs + Running `.../gotham/target/debug/gotham_examples_handlers_simple_async_handlers` +Listening for requests at http://127.0.0.1:7878 +sleep for 5 seconds once: starting +sleep for 5 seconds once: finished +sleep for one second 5 times: starting +sleep for one second 5 times: finished + +Terminal 2: +$ curl 'http://127.0.0.1:7878/sleep?seconds=5' +slept for 5 seconds +$ curl 'http://127.0.0.1:7878/loop?seconds=5' +slept for 1 seconds +slept for 1 seconds +slept for 1 seconds +slept for 1 seconds +slept for 1 seconds +``` + +## 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/handlers/simple_async_handlers_await/src/main.rs b/examples/handlers/simple_async_handlers_await/src/main.rs new file mode 100644 index 000000000..3d0a1449f --- /dev/null +++ b/examples/handlers/simple_async_handlers_await/src/main.rs @@ -0,0 +1,162 @@ +//! A basic example showing the request components + +use futures::prelude::*; +use std::pin::Pin; +use std::time::{Duration, Instant}; + +use gotham::hyper::{Body, StatusCode}; + +use gotham::handler::HandlerResult; +use gotham::helpers::http::response::create_response; +use gotham::router::builder::DefineSingleRoute; +use gotham::router::builder::{build_simple_router, DrawRoutes}; +use gotham::router::Router; +use gotham::state::{FromState, State}; +use gotham_derive::{StateData, StaticResponseExtender}; +use serde_derive::Deserialize; + +use tokio::time::delay_until; + +type SleepFuture = Pin> + Send>>; + +#[derive(Deserialize, StateData, StaticResponseExtender)] +struct QueryStringExtractor { + seconds: u64, +} + +/// Sneaky hack to make tests take less time. Nothing to see here ;-). +#[cfg(not(test))] +fn get_duration(seconds: u64) -> Duration { + Duration::from_secs(seconds) +} +#[cfg(test)] +fn get_duration(seconds: u64) -> Duration { + Duration::from_millis(seconds) +} +/// All this function does is return a future that resolves after a number of +/// seconds, with a Vec that tells you how long it slept for. +/// +/// Note that it does not block the thread from handling other requests, because +/// it returns a `Future`, which will be managed by the tokio reactor, and +/// called back once the timeout has expired. +/// +/// Vec is chosen because it is one of the things that you need to resolve +/// a HandlerFuture and respond to a request. +/// +/// Most things that you call to access remote services (e.g databases and +/// web apis) can be coerced into returning futures that yield useful data, +/// so the patterns that you learn in this example should be applicable to +/// real world problems. +fn sleep(seconds: u64) -> SleepFuture { + let when = Instant::now() + get_duration(seconds); + let delay = delay_until(when.into()).map(move |_| { + format!("slept for {} seconds\n", seconds) + .as_bytes() + .to_vec() + }); + + delay.boxed() +} + +/// This handler sleeps for the requested number of seconds, using the `sleep()` +/// helper method, above. +async fn sleep_handler(mut state: State) -> HandlerResult { + let seconds = QueryStringExtractor::take_from(&mut state).seconds; + println!("sleep for {} seconds once: starting", seconds); + // Here, we call the sleep function and turn its old-style future into + // a new-style future. Note that this step doesn't block: it just sets + // up the timer so that we can use it later. + let sleep_future = sleep(seconds); + + // Here is where the serious sleeping happens. We yield execution of + // this block until sleep_future is resolved. + // The Ok("slept for x seconds") value is stored in result. + let data = sleep_future.await; + + // Here, we convert the result from `sleep()` into the form that Gotham + // expects: `state` is owned by this block so we need to return it. + // We also convert any errors that we have into the form that Hyper + // expects, using the helper from IntoHandlerError. + let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, data); + println!("sleep for {} seconds once: finished", seconds); + Ok((state, res)) +} + +/// It calls sleep(1) as many times as needed to make the requested duration. +/// +/// Notice how much easier it is to read than the version in +/// `simple_async_handlers`. +async fn loop_handler(mut state: State) -> HandlerResult { + let seconds = QueryStringExtractor::take_from(&mut state).seconds; + println!("sleep for one second {} times: starting", seconds); + + // The code within this block reads exactly like syncronous code. + // This is the style that you should aim to write your business + // logic in. + let mut accumulator = Vec::new(); + for _ in 0..seconds { + let body = sleep(1).await; + accumulator.extend(body) + } + + let res = create_response( + &state, + StatusCode::OK, + mime::TEXT_PLAIN, + Body::from(accumulator), + ); + println!("sleep for one second {} times: finished", seconds); + Ok((state, res)) +} + +/// Create a `Router`. +fn router() -> Router { + build_simple_router(|route| { + route + .get("/sleep") + .with_query_string_extractor::() + .to_async(sleep_handler); + route + .get("/loop") + .with_query_string_extractor::() + .to_async(loop_handler); + }) +} + +/// Start a server and use a `Router` to dispatch requests. +pub fn main() { + let addr = "127.0.0.1:7878"; + println!("Listening for requests at http://{}", addr); + gotham::start(addr, router()) +} + +#[cfg(test)] +mod tests { + use gotham::test::TestServer; + + use super::*; + + fn assert_returns_ok(url_str: &str, expected_response: &str) { + let test_server = TestServer::new(router()).unwrap(); + let response = test_server.client().get(url_str).perform().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + &String::from_utf8(response.read_body().unwrap()).unwrap(), + expected_response + ); + } + + #[test] + fn sleep_says_how_long_it_slept_for() { + assert_returns_ok("http://localhost/sleep?seconds=2", "slept for 2 seconds\n"); + } + + #[test] + fn loop_breaks_the_time_into_one_second_sleeps() { + assert_returns_ok( + "http://localhost/loop?seconds=2", + "slept for 1 seconds\nslept for 1 seconds\n", + ); + } +} diff --git a/examples/path/globs/README.md b/examples/path/globs/README.md index 615b716ef..42d181a5a 100644 --- a/examples/path/globs/README.md +++ b/examples/path/globs/README.md @@ -24,7 +24,7 @@ Terminal 2: > Host: localhost:7878 > User-Agent: curl/7.54.0 > Accept: */* - > + > < HTTP/1.1 200 OK < Content-Length: 39 < Content-Type: text/plain @@ -34,13 +34,59 @@ Terminal 2: < X-Content-Type-Options: nosniff < X-Runtime-Microseconds: 165 < Date: Mon, 19 Mar 2018 22:17:17 GMT - < + < Got 4 parts: heads shoulders knees * Connection #0 to host localhost left intact toes + + curl -vvv http://localhost:7878/middle/heads/shoulders/knees/toes/foobar + * Trying 127.0.0.1... + * TCP_NODELAY set + * Connected to localhost (127.0.0.1) port 7878 (#0) + > GET /middle/heads/shoulders/knees/toes/foobar HTTP/1.1 + > Host: localhost:7878 + > User-Agent: curl/7.54.0 + > Accept: */* + > + < HTTP/1.1 200 OK + < x-request-id: 8449a2ed-2b00-4fbf-98af-63e17d65a345 + < content-type: text/plain + < content-length: 39 + < date: Sat, 23 May 2020 09:00:40 GMT + < + Got 4 parts: + heads + shoulders + knees + * Connection #0 to host localhost left intact + toes + + curl -vvv http://localhost:7878/multi/heads/shoulders/foobar/knees/toes + * Trying 127.0.0.1... + * TCP_NODELAY set + * Connected to localhost (127.0.0.1) port 7878 (#0) + > GET /multi/heads/shoulders/foobar/knees/toes HTTP/1.1 + > Host: localhost:7878 + > User-Agent: curl/7.54.0 + > Accept: */* + > + < HTTP/1.1 200 OK + < x-request-id: 4cbcf782-9dbb-4fcd-b12e-55661d3309a4 + < content-type: text/plain + < content-length: 72 + < date: Sat, 23 May 2020 09:09:51 GMT + < + Got 2 parts for top: + heads + shoulders + + Got 2 parts for bottom: + knees + * Connection #0 to host localhost left intact + toes ``` ## License diff --git a/examples/path/globs/src/main.rs b/examples/path/globs/src/main.rs index a526cc516..5c347044a 100644 --- a/examples/path/globs/src/main.rs +++ b/examples/path/globs/src/main.rs @@ -10,12 +10,22 @@ use gotham::state::{FromState, State}; #[derive(Deserialize, StateData, StaticResponseExtender)] struct PathExtractor { - // If there is exactly one * in the route, and it is the last path segment, this will be a Vec - // containing each path segment as a separate String, with no /s. + // This will be a Vec containing each path segment as a separate String, with no '/'s. #[serde(rename = "*")] parts: Vec, } +#[derive(Deserialize, StateData, StaticResponseExtender)] +struct NamedPathExtractor { + parts: Vec, +} + +#[derive(Deserialize, StateData, StaticResponseExtender)] +struct MultiGlobExtractor { + top: Vec, + bottom: Vec, +} + fn parts_handler(state: State) -> (State, String) { let res = { let path = PathExtractor::borrow_from(&state); @@ -37,13 +47,79 @@ fn parts_handler(state: State) -> (State, String) { (state, res) } +fn named_parts_handler(state: State) -> (State, String) { + let res = { + let path = NamedPathExtractor::borrow_from(&state); + + let mut response_string = format!( + "Got {} part{}:", + path.parts.len(), + if path.parts.len() == 1 { "" } else { "s" } + ); + + for part in path.parts.iter() { + response_string.push_str("\n"); + response_string.push_str(&part); + } + + response_string + }; + + (state, res) +} + +fn multi_parts_handler(state: State) -> (State, String) { + let res = { + let path = MultiGlobExtractor::borrow_from(&state); + + let mut top = format!( + "Got {} part{} for top:", + path.top.len(), + if path.top.len() == 1 { "" } else { "s" } + ); + + for part in path.top.iter() { + top.push_str("\n"); + top.push_str(&part); + } + + let mut bottom = format!( + "Got {} part{} for bottom:", + path.bottom.len(), + if path.bottom.len() == 1 { "" } else { "s" } + ); + + for part in path.bottom.iter() { + bottom.push_str("\n"); + bottom.push_str(&part); + } + + vec![top, bottom].join("\n\n") + }; + + (state, res) +} + fn router() -> Router { build_simple_router(|route| { route - // The last path segment is allowed to be a *, and it will match one or more path segments. + // A path segment is allowed to be a *, and it will match one or more path segments. .get("/parts/*") .with_path_extractor::() .to(parts_handler); + + route + // You can provide a param name for this glob segment. + // It doesn't need to be the last segment + .get("/middle/*parts/foobar") + .with_path_extractor::() + .to(named_parts_handler); + + route + // You can even have multiple glob segments + .get("/multi/*top/foobar/*bottom") + .with_path_extractor::() + .to(multi_parts_handler); }) } @@ -115,4 +191,40 @@ mod tests { &b"Got 4 parts:\nhead\nshoulders\nknees\ntoes"[..] ); } + + #[test] + fn extracts_named_multiple_components_from_middle() { + let test_server = TestServer::new(router()).unwrap(); + let response = test_server + .client() + .get("http://localhost/middle/head/shoulders/knees/toes/foobar") + .perform() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.read_body().unwrap(); + assert_eq!( + &body[..], + &b"Got 4 parts:\nhead\nshoulders\nknees\ntoes"[..] + ); + } + + #[test] + fn extracts_multiple_multiple_components() { + let test_server = TestServer::new(router()).unwrap(); + let response = test_server + .client() + .get("http://localhost/multi/head/shoulders/foobar/knees/toes") + .perform() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.read_body().unwrap(); + assert_eq!( + &body[..], + &b"Got 2 parts for top:\nhead\nshoulders\n\nGot 2 parts for bottom:\nknees\ntoes"[..] + ); + } } diff --git a/examples/sessions/custom_data_type/Cargo.toml b/examples/sessions/custom_data_type/Cargo.toml index 8ef283063..562b967df 100644 --- a/examples/sessions/custom_data_type/Cargo.toml +++ b/examples/sessions/custom_data_type/Cargo.toml @@ -14,4 +14,4 @@ mime = "0.3" serde = "1" serde_derive = "1" time = "0.1" -cookie = "0.13" +cookie = "0.14" diff --git a/examples/sessions/introduction/Cargo.toml b/examples/sessions/introduction/Cargo.toml index d0c3b6938..c996d1524 100644 --- a/examples/sessions/introduction/Cargo.toml +++ b/examples/sessions/introduction/Cargo.toml @@ -10,4 +10,4 @@ edition = "2018" gotham = { path = "../../../gotham" } mime = "0.3" -cookie = "0.13" +cookie = "0.14" diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index 1124c7151..f3775413b 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -9,7 +9,8 @@ edition = "2018" [dependencies] gotham = { path = "../../gotham" } futures = "0.3.1" -tokio-tungstenite = "0.9" -pretty_env_logger = "0.3" +tokio-tungstenite = "0.10" +tokio = "0.2" +pretty_env_logger = "0.4" sha1 = "*" base64 = "*" diff --git a/examples/websocket/src/index.html b/examples/websocket/src/index.html new file mode 100644 index 000000000..c86656de8 --- /dev/null +++ b/examples/websocket/src/index.html @@ -0,0 +1,27 @@ + + +

Websocket Echo Server

+
+ + +
+ + diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 5e56413f3..01e1134fb 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -20,7 +20,7 @@ fn handler(mut state: State) -> (State, Response) { if ws::requested(&headers) { let (response, ws) = match ws::accept(&headers, body) { Ok(res) => res, - Err(_) => return bad_request(state), + Err(_) => return (state, bad_request()), }; let req_id = request_id(&state).to_owned(); @@ -28,7 +28,7 @@ fn handler(mut state: State) -> (State, Response) { .map_err(|err| eprintln!("websocket init error: {}", err)) .and_then(move |ws| connected(req_id, ws)); - hyper::rt::spawn(ws); + tokio::spawn(ws); (state, response) } else { @@ -36,60 +36,172 @@ fn handler(mut state: State) -> (State, Response) { } } -fn connected(req_id: String, stream: S) -> impl Future> +async fn connected(req_id: String, stream: S) -> Result<(), ()> where - S: Stream - + Sink, + S: Stream> + Sink, { - let (sink, stream) = stream.split(); + let (mut sink, mut stream) = stream.split(); println!("Client {} connected", req_id); - sink.send_all(stream.map({ - let req_id = req_id.clone(); - move |msg| { - println!("{}: {:?}", req_id, msg); - msg + while let Some(message) = stream + .next() + .await + .transpose() + .map_err(|error| println!("Websocket receive error: {}", error))? + { + println!("{}: {:?}", req_id, message); + match sink.send(message).await { + Ok(()) => (), + // this error indicates a successfully closed connection + Err(ws::Error::ConnectionClosed) => break, + Err(error) => { + println!("Websocket send error: {}", error); + return Err(()); + } } - })) - .map_err(|err| println!("Websocket error: {}", err)) - .map(move |_| println!("Client {} disconnected", req_id)) + } + + println!("Client {} disconnected", req_id); + Ok(()) } -fn bad_request(state: State) -> (State, Response) { - let response = Response::builder() +fn bad_request() -> Response { + Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::empty()) - .unwrap(); - (state, response) + .unwrap() } -const INDEX_HTML: &str = r#" - - -

Websocket Echo Server

-
- - -
- - -"#; + + #[test] + fn server_should_echo_websocket_messages() { + let server = create_test_server(); + let client = server.client(); + + let mut request = client.get("ws://127.0.0.1:10000"); + let headers = request.headers_mut(); + headers.insert(UPGRADE, HeaderValue::from_static("websocket")); + headers.insert(SEC_WEBSOCKET_KEY, HeaderValue::from_static("QmF0bWFu")); + + let mut response = client + .perform(request) + .expect("Failed to request websocket upgrade"); + + let connection_header = response + .headers() + .get(CONNECTION) + .expect("Missing connection header"); + assert_eq!(connection_header.as_bytes(), "upgrade".as_bytes()); + let upgrade_header = response + .headers() + .get(UPGRADE) + .expect("Missing upgrade header"); + assert_eq!(upgrade_header.as_bytes(), "websocket".as_bytes()); + let websocket_accept_header = response + .headers() + .get(SEC_WEBSOCKET_ACCEPT) + .expect("Missing websocket accept header"); + assert_eq!( + websocket_accept_header.as_bytes(), + "hRHWRk+NDTj5O2GjSexJZg8ImzI=".as_bytes() + ); + + // This will be used to swap out the body from the TestResponse because it only implements `DerefMut` but not `Into` + let mut body = Body::empty(); + std::mem::swap(&mut body, response.deref_mut().body_mut()); + + server + .run_future(async move { + let upgraded = body + .on_upgrade() + .await + .expect("Failed to upgrade client websocket."); + let mut websocket_stream = + WebSocketStream::from_raw_socket(upgraded, Role::Client, None).await; + + let message = Message::Text("Hello".to_string()); + websocket_stream + .send(message.clone()) + .await + .expect("Failed to send text message."); + + let response = websocket_stream + .next() + .await + .expect("Socket was closed") + .expect("Failed to receive response"); + assert_eq!(message, response); + + websocket_stream + .send(Message::Close(None)) + .await + .expect("Failed to send close message"); + + Ok::<(), DummyError>(()) + }) + .unwrap(); + } + + #[test] + fn should_respond_with_bad_request_if_the_request_is_bad() { + let server = create_test_server(); + let client = server.client(); + + let mut request = client.get("ws://127.0.0.1:10000"); + let headers = request.headers_mut(); + headers.insert(UPGRADE, HeaderValue::from_static("websocket")); + + let response = request.perform().expect("Failed to perform request."); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + let body = response.read_body().expect("Failed to read response body"); + assert!(body.is_empty()); + } + + #[derive(Debug)] + struct DummyError {} + + impl Display for DummyError { + fn fmt(&self, _formatter: &mut Formatter) -> std::fmt::Result { + Ok(()) + } + } + + impl Error for DummyError {} +} diff --git a/examples/websocket/src/ws.rs b/examples/websocket/src/ws.rs index 56c6402c1..1b29e5b98 100644 --- a/examples/websocket/src/ws.rs +++ b/examples/websocket/src/ws.rs @@ -1,7 +1,9 @@ use base64; use futures::prelude::*; -use gotham::hyper::header::{HeaderValue, CONNECTION, UPGRADE}; -use gotham::hyper::{upgrade::Upgraded, Body, HeaderMap, Response, StatusCode}; +use gotham::hyper::header::{ + HeaderValue, CONNECTION, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, UPGRADE, +}; +use gotham::hyper::{self, upgrade::Upgraded, Body, HeaderMap, Response, StatusCode}; use sha1::Sha1; use tokio_tungstenite::{tungstenite, WebSocketStream}; @@ -9,8 +11,6 @@ pub use tungstenite::protocol::{Message, Role}; pub use tungstenite::Error; const PROTO_WEBSOCKET: &str = "websocket"; -const SEC_WEBSOCKET_KEY: &str = "Sec-WebSocket-Key"; -const SEC_WEBSOCKET_ACCEPT: &str = "Sec-WebSocket-Accept"; /// Check if a WebSocket upgrade was requested. pub fn requested(headers: &HeaderMap) -> bool { @@ -32,9 +32,9 @@ pub fn accept( (), > { let res = response(headers)?; - let ws = body - .on_upgrade() - .map(|upgraded| WebSocketStream::from_raw_socket(upgraded, Role::Server, None)); + let ws = body.on_upgrade().and_then(|upgraded| { + WebSocketStream::from_raw_socket(upgraded, Role::Server, None).map(Ok) + }); Ok((res, ws)) } @@ -58,3 +58,15 @@ fn accept_key(key: &[u8]) -> String { sha1.update(WS_GUID); base64::encode(&sha1.digest().bytes()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_accept_key_from_rfc6455() { + // From https://tools.ietf.org/html/rfc6455#section-1.2 + let key = accept_key("dGhlIHNhbXBsZSBub25jZQ==".as_bytes()); + assert_eq!(key, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="); + } +} diff --git a/gotham/Cargo.toml b/gotham/Cargo.toml index 010e2a9ed..f6966518e 100644 --- a/gotham/Cargo.toml +++ b/gotham/Cargo.toml @@ -25,13 +25,13 @@ hyper = "0.13.1" serde = "1.0" serde_derive = "1.0" bincode = "1.0" -mime = "0.3" +mime = "0.3.15" mime_guess = "2.0.1" futures = "0.3.1" tokio = { version = "0.2.6", features = ["full"] } bytes = "0.5" mio = "0.7" -borrow-bag = "1.0" +borrow-bag = { path = "../misc/borrow_bag", version = "1.0" } percent-encoding = "2.1" pin-project = "0.4.2" uuid = { version = "0.7", features = ["v4"] } @@ -42,7 +42,7 @@ rand_chacha = "0.1" linked-hash-map = "0.5" num_cpus = "1.8" regex = "1.0" -cookie = "0.13" +cookie = "0.14" http = "0.2" httpdate = "0.3" itertools = "0.9.0" diff --git a/gotham/src/handler/mod.rs b/gotham/src/handler/mod.rs index db0ee349b..b3d967a4f 100644 --- a/gotham/src/handler/mod.rs +++ b/gotham/src/handler/mod.rs @@ -24,12 +24,14 @@ pub mod assets; pub use self::error::{HandlerError, IntoHandlerError}; +/// A type alias for the results returned by async fns that can be passed to to_async. +pub type HandlerResult = std::result::Result<(State, Response), (State, HandlerError)>; + /// A type alias for the trait objects returned by `HandlerService`. /// /// When the `Future` resolves to an error, the `(State, HandlerError)` value is used to generate /// an appropriate HTTP error response. -pub type HandlerFuture = - dyn Future), (State, HandlerError)>> + Send; +pub type HandlerFuture = dyn Future + Send; /// A `Handler` is an asynchronous function, taking a `State` value which represents the request /// and related runtime state, and returns a future which resolves to a response. diff --git a/gotham/src/lib.rs b/gotham/src/lib.rs index d0f0fd7f7..ee7a11769 100644 --- a/gotham/src/lib.rs +++ b/gotham/src/lib.rs @@ -129,6 +129,7 @@ where accepted_protocol .serve_connection(socket, service) + .with_upgrades() .map_err(|_| ()) .await?; diff --git a/gotham/src/router/builder/draw.rs b/gotham/src/router/builder/draw.rs index 133ad4e5d..974ddc5e4 100644 --- a/gotham/src/router/builder/draw.rs +++ b/gotham/src/router/builder/draw.rs @@ -901,6 +901,7 @@ where } } Some('*') if segment.len() == 1 => (segment, SegmentType::Glob), + Some('*') => (&segment[1..], SegmentType::Glob), Some('\\') => (&segment[1..], SegmentType::Static), _ => (segment, SegmentType::Static), }; diff --git a/gotham/src/router/builder/single.rs b/gotham/src/router/builder/single.rs index a1f80c340..b241bd2af 100644 --- a/gotham/src/router/builder/single.rs +++ b/gotham/src/router/builder/single.rs @@ -4,7 +4,7 @@ use std::panic::RefUnwindSafe; use crate::extractor::{PathExtractor, QueryStringExtractor}; use crate::handler::assets::{DirHandler, FileHandler, FileOptions, FilePathExtractor}; -use crate::handler::{Handler, NewHandler}; +use crate::handler::{Handler, HandlerResult, NewHandler}; use crate::pipeline::chain::PipelineHandleChain; use crate::router::builder::{ ExtendRouteMatcher, ReplacePathExtractor, ReplaceQueryStringExtractor, SingleRouteBuilder, @@ -12,6 +12,9 @@ use crate::router::builder::{ use crate::router::route::dispatch::DispatcherImpl; use crate::router::route::matcher::RouteMatcher; use crate::router::route::{Delegation, Extractors, RouteImpl}; +use crate::state::State; +use core::future::Future; +use futures::FutureExt; /// Describes the API for defining a single route, after determining which request paths will be /// dispatched here. The API here uses chained function calls to build and add the route into the @@ -106,6 +109,54 @@ pub trait DefineSingleRoute { where H: Handler + RefUnwindSafe + Copy + Send + Sync + 'static; + /// Similar to `to`, but accepts an `async fn` + /// + /// # Examples + /// + /// ```rust + /// # extern crate gotham; + /// # extern crate hyper; + /// # + /// # use hyper::{Body, Response, StatusCode}; + /// # use gotham::handler::HandlerResult; + /// # use gotham::state::State; + /// # use gotham::router::Router; + /// # use gotham::router::builder::*; + /// # use gotham::pipeline::new_pipeline; + /// # use gotham::pipeline::single::*; + /// # use gotham::middleware::session::NewSessionMiddleware; + /// # use gotham::test::TestServer; + /// # + /// async fn my_handler(state: State) -> HandlerResult { + /// // Handler implementation elided. + /// # Ok((state, Response::builder().status(StatusCode::ACCEPTED).body(Body::empty()).unwrap())) + /// } + /// # + /// # fn router() -> Router { + /// # let (chain, pipelines) = single_pipeline( + /// # new_pipeline().add(NewSessionMiddleware::default()).build() + /// # ); + /// + /// build_router(chain, pipelines, |route| { + /// route.get("/request/path").to_async(my_handler); + /// }) + /// # + /// # } + /// # + /// # fn main() { + /// # let test_server = TestServer::new(router()).unwrap(); + /// # let response = test_server.client() + /// # .get("https://example.com/request/path") + /// # .perform() + /// # .unwrap(); + /// # assert_eq!(response.status(), StatusCode::ACCEPTED); + /// # } + /// ``` + fn to_async(self, handler: H) + where + Self: Sized, + H: (FnOnce(State) -> Fut) + RefUnwindSafe + Copy + Send + Sync + 'static, + Fut: Future + Send + 'static; /// Directs the route to the given `NewHandler`. This gives more control over how `Handler` /// values are constructed. /// @@ -471,6 +522,15 @@ where self.to_new_handler(move || Ok(handler)) } + fn to_async(self, handler: H) + where + Self: Sized, + H: (FnOnce(State) -> Fut) + RefUnwindSafe + Copy + Send + Sync + 'static, + Fut: Future + Send + 'static, + { + self.to_new_handler(move || Ok(move |s: State| handler(s).boxed())) + } + fn to_new_handler(self, new_handler: NH) where NH: NewHandler + 'static, diff --git a/middleware/diesel/Cargo.toml b/middleware/diesel/Cargo.toml index 658738a76..c5b5746ff 100644 --- a/middleware/diesel/Cargo.toml +++ b/middleware/diesel/Cargo.toml @@ -22,5 +22,5 @@ log = "0.4" [dev-dependencies] diesel = { version = "1", features = ["sqlite"] } -mime = "0.3" +mime = "0.3.15" tokio = { version = "0.2.6", features = ["full"] }