diff options
Diffstat (limited to 'third_party/rust/warp')
86 files changed, 14746 insertions, 0 deletions
diff --git a/third_party/rust/warp/.cargo-checksum.json b/third_party/rust/warp/.cargo-checksum.json new file mode 100644 index 0000000000..337ad21d34 --- /dev/null +++ b/third_party/rust/warp/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"5b878821dd387e90121a16ab87d873947ef8c962a06cd22493a59b2f491b4f19","Cargo.lock":"66fed07c933163cd9a29b9008873825646d1ee9d883d8430d2b8e173b0628bae","Cargo.toml":"95dd6d85a0c3135bbc7f2205344ad58c375438905d129a2ba3d3151a68776816","LICENSE":"6dfa8def8b810ab8043bdacf09b4166b9c4651f93b4a19399ddc4c52055b1069","README.md":"305f2f26da1c75f64d29c86685a08c2442de435557bd1480614de53f02803ce3","examples/README.md":"1330dfe1aae7e4237eb8abaa25da8920ed560daefca27450e6d4f3d700377537","examples/autoreload.rs":"4ec938ed185066dac92293fb30f225c5741d8a03ef7d084cfed9038cc33266e7","examples/body.rs":"57eb99b3a59c33474c5d5c7c22964bea66c663f525c755924ed75eca8fba4aa1","examples/dir.rs":"3e1bd9abb6e317bd64433dd3440636187ec258dc8a72f5e56ee0926269256f95","examples/dir/another.html":"f58abf6f1a7b233b58b537b981b10be2dc64be3ac66f139230bed1f80f9f5277","examples/dir/index.html":"52f5b947733c9d2b7900fa8e613c123455bdbf7a25aca8e30aba56f75a837e65","examples/dyn_reply.rs":"2b85c26246322e34ba1ddce317f83e3a0fdaff295c7730f97e6c961475e93ebd","examples/file.rs":"a155c1fbeedaecf8e926a97689b2ad45f711d20f87e4c29ff84a4b1dacaeedd5","examples/futures.rs":"e80901b15731d4555d7d4290f5779e7361497900661431288ac23c28925269d0","examples/handlebars_template.rs":"f0acb19640e2a2894e08740ff5d3e12a9bf0b93d88a9fb313ef2b1749d59969b","examples/headers.rs":"651cd6425219d1d09cfb4f1114fefc2dff4435070b90f7c1d1696c95c96913b5","examples/hello.rs":"9ca9f197e2365c21729bbe74fe07590c615aaf87f07aece51b5640c97a453196","examples/rejections.rs":"ec09f48dd164427eb02cad28f9688321a24dd7c323ddcb918a58bef371f98529","examples/returning.rs":"e19e8df9ec34d7eeb60f6053e5b1f51043159dccb7f5e7148b69597eff2061ea","examples/routing.rs":"0511a80ad1874d9fda302479fb50a861d5a546da19e1f06a3befff4819fe56e1","examples/sse.rs":"4adae8335a9d3757bf4b9b4477358df299c339e958ae427ab853dc2cadbe155c","examples/sse_chat.rs":"31b33b517f72f3f34232ee39781e23ae6d4a97ac90a289a8a1eb3f91d7da0841","examples/tls.rs":"450d3e7e7da94d29a9c8e48dc0775f27931886eafd02e137cbfb3e7924edc698","examples/tls/cert.pem":"6b974e5654edca3664916613c9f932a64990cff3f604df5369275c35f64d36a2","examples/tls/key.rsa":"ffe615f53e98682bc470ae6fa964ce34a383b4509ae7bb9a7ee2ece1b1bdc7e3","examples/todos.rs":"0bc7575d7acc942635f1b7df75751ff9d2dc1f6cc73fd632036f773ce859b8be","examples/unix_socket.rs":"89128846ae31a7feef00d092edfd989dffba4647caa78f43f750510a5a5ff9bf","examples/websockets.rs":"a3677b311813dc68e7d1c1ad91779b348942cedd47db4b13a4158cdf54ce4411","examples/websockets_chat.rs":"50b3f6cc0036a83d67361889179f6f8948d2f6a80aa1ab2972d5e89f6d275941","src/error.rs":"221d8d1180ca31570416ef9593483a27fd343029fc16eaed842131671c16a754","src/filter/and.rs":"aed85d45ac92ad4e80c767340bfed436a106e80e82f6230074abfa578c60c93f","src/filter/and_then.rs":"56146227d0cb2c7d519b056a4dcb4dbb8d7245ec9fd3b0e789fa940dadf35b98","src/filter/boxed.rs":"160536a43c411110d2420a9215d6e55bd4d94323570e013ba4ba168103a8c716","src/filter/map.rs":"df0d0a6012eaef124bb7f1b21ec7a4507c582438c51c88cec8759538c61fcd86","src/filter/map_err.rs":"1bdec8f691482a531a1b85c9eaf96fddc86f076c14f5f8540fea95cadfac92eb","src/filter/mod.rs":"4bfb4110190919f260eb58a4eea6c4ec54365a2b95eef02630a44ea4b8eea17b","src/filter/or.rs":"642f1a936c3af5429bb6f15aedc505906622dd7cf2a6782cee72962cf7045db6","src/filter/or_else.rs":"8d736ff444af09846e797f0523ffbc904e8ae4466e1be16f28ab5eacac0286b8","src/filter/recover.rs":"6d65299154a7305057b70e01addce04f5afa8e563c7f655de6f310bfaeaa20af","src/filter/service.rs":"78583f2f028025dabda0636d6a59a095faad9a3d85e32fe0defe0a5d9b8b4a99","src/filter/unify.rs":"8f56fa9fe85dd68ed84b8070705f50d3f53de74e6f0dfec4fa74e1458c2a31e0","src/filter/untuple_one.rs":"696d475fd1e3f2c50b0be519196462ff7648953d5aed8cae057bc81790479f3d","src/filter/wrap.rs":"44d02002e06f9661fd5d2fa56eccd11129dcf68f7a1795f2e0e94c7219c5e873","src/filters/addr.rs":"be634906b34e26c35dc4a62b382fc34aa4a363778e77a085a0dc6be2f7efac99","src/filters/any.rs":"2c993a9866cd5e545f6dcddf27d3a5394a3484eff6c4a839af4e50f90b43b95f","src/filters/body.rs":"f1ccea8b6c4ca3db27b1cdcc85ded2d5ba8482d952738efb64b293f9dadecde6","src/filters/cookie.rs":"f785fca4d0e26af4c0807dbe440f42f7cbba4f09c87e760f1911c3f5cf83b927","src/filters/cors.rs":"14c9567cd2412d3215aaa9f342737e484e50913978e5eebc3cf96f86d719e815","src/filters/ext.rs":"0dfe1218f06c723663a649c2a0e36fe32be02191e1ef7aaeecdceede967520ff","src/filters/fs.rs":"0328510f8bd5d9b8cc044564a1e2c40c35237191d1e0cce377854b7194566f9f","src/filters/header.rs":"29f4a9944f28bbe83c9941de9da76dd3d6bbf25e3c5a199e88aeb3bbee061188","src/filters/log.rs":"1571e74b9463164e1bb93a49189fd0487eecc6d94235f9e944fc62b5821c7971","src/filters/method.rs":"b075fbb566b9a8daa62a26da55fff9aa70beb94d3a1492b5edac0c007e9b8a79","src/filters/mod.rs":"5e348ee50811c4981886fa90d7f8e9e53ab08103819289a6f00a59961a6889e8","src/filters/multipart.rs":"a08f8a1621524ca027d10fc72d12310133245222864fcf94cea6aa9aaf127b7d","src/filters/path.rs":"420ae23e448ccbab560f10eba118dfbe56f0b8b1d3554a0cdec89b1fc07f6af6","src/filters/query.rs":"d98b03128dfce822844d750870b8c1cb75a4edc8947313182031c6139db8f973","src/filters/reply.rs":"6102b63e142dfe2c55bdd37f1b3e9d25852a1ed7f1ec2ec4fec91f478cf672a4","src/filters/sse.rs":"bce025464b7eacd2ec4dd45351ff722581479dd950d4225cab3727f79120a2af","src/filters/ws.rs":"2c97875909a6f8fd9f750c18fc8122d77e15cf65aa5438a44255daa3ddecb7eb","src/generic.rs":"66be231b0cc92c41379f81d23343fd971941e3588f833f537ae5c97e445828e0","src/lib.rs":"80f3fe3560b11419f060aad118ef5f2e74aace4fede1e4392e78a20675e385e3","src/redirect.rs":"190ab1011c2771fc405a32047399cfce90b1864baaaa08d4df4f047dd9814c7b","src/reject.rs":"78da91ec2aa93c2293aced66ba921a160f0cc65da7e13fdc3b7394ea10365088","src/reply.rs":"771009494bbf2c16b7ccb09a754e4617ed71ee8168d8566fd8d49bd9ebac14f7","src/route.rs":"717c035d6e6ea98bc111524e7ed97fb3041110b9db76f4954b802be224c14cc1","src/server.rs":"bc185bb47f3f07c5859d6b2568f2fa6fb24a47e639bdbf29d25ce0a3385af29f","src/service.rs":"4564ec95e98a2314f73df24582ca8f6ec96cc8eda90fb5a5d1d83a9d5c841b86","src/test.rs":"f87cd4a248ffb85a4c3f03f04b70512508fe7e1a6ab4f638f3a0e6f4608412a5","src/tls.rs":"5e9a15960750583732eb45e8af941bdd2fb412d70fa00c4f27d1c6d005336c23","src/transport.rs":"338211e665e46ffd32e161292dd0dd89d9a5d2f814a0641273c710be1b3e8fdf","tests/body.rs":"371d58394da468aab9f2412e73b44de615a517c3fc94986d25d6a246ab461e6d","tests/cookie.rs":"3fddd2d109da20ceb9d6c389facda7bbf4d066d9a32f639c4dad9679590dcb93","tests/cors.rs":"07798bf7f30f73f19fae2b9cf7b9f94228d490bdc52767dc3413863f635c6af6","tests/ext.rs":"93d6527288f71ee20b63f6a47f616f055735373b0f203f0863f27c2b65fd8991","tests/filter.rs":"76c05031f1e0d6271ff2ac7eb596b4c97ffd0fe93d4d49af4783cc4d862dae25","tests/fs.rs":"d7ca82ec2f074e67844d7013960cf49ddeb201fe8a0b023e6d53028702809bbc","tests/header.rs":"78d7b4fd80025694cde65010cf04e4cc23ad4ac91fff3cb527542c278e6cfe4b","tests/method.rs":"6ae1f188b06b07822bbd97f671886259ee8e7008313449ec04604c8f396cf59b","tests/multipart.rs":"62cb1ded7cc1805c925162b9cc8598a7ec85e9d7ae36df5cac367110b4e951ac","tests/path.rs":"f6e0537ef7558b89768688e0c7978bed615edd2a40f7137e83ea148de0d1dd2e","tests/query.rs":"045ecd7b22afb73bc210058ab863c60189ce0af5dadc3b7999e4e527c1420163","tests/redirect.rs":"25e0c18fcc7bf1c0393708e0efc10f4508093d1b355371a66b9a59e2eb51ed13","tests/reply_with.rs":"35fdfe9653ffab0776fe9fb65e231f9ea647c9f391b17794010adbcbd5009e65","tests/ws.rs":"e130c3f95c930b5761c675b7ebf352c7f9aaf62aaeca97237cb5285921c6c71a"},"package":"54cd1e2b3eb3539284d88b76a9afcf5e20f2ef2fab74db5b21a1c30d7d945e82"}
\ No newline at end of file diff --git a/third_party/rust/warp/CHANGELOG.md b/third_party/rust/warp/CHANGELOG.md new file mode 100644 index 0000000000..64d4c8818e --- /dev/null +++ b/third_party/rust/warp/CHANGELOG.md @@ -0,0 +1,250 @@ +### v0.2.2 (March 3, 2020) + +- **Features**: + - Implement `Reply` for all `Box<T>` where `T: Reply`. + - Add `name` methods to `MissingHeader`, `InvalidHeader`, and `MissingCookie` rejections. + - Add `warp::ext::optional()` filter that optionally retrieves an extension from the request. +- **Fixes**: + - Fix the sending of pings when a user sends a `ws::Message::ping()`. + +### v0.2.1 (January 23, 2020) + +- **Features**: + - Add `close` and `close_with` constructors to `warp::ws::Message`. +- **Fixes**: + - Fix `warp::fs` filters using a very small read buffer. + +## v0.2.0 (January 16, 2020) + +- **Features**: + - Update to `std::future`, adding `async`/`await` support! + - Add `warp::service()` to convert a `Filter` into a `tower::Service`. + - Implement `Reply` for `Box<dyn Reply>`. +- **Changes**: + - Refactored Rejection system (#311). + - Change `path!` macro to assume a `path::end()` by default, with explicit `/ ..` to allow building a prefix (#359). + - Change `warp::path(str)` to accept any `AsRef<str>` argument. + - Rename "2"-suffixed filters and types (`get2` to `get`, `ws2` to `ws`, etc). + - `Filter::{or, or_else, recover}` now require `Self::Error=Rejection`. This helps catch filters that didn't make sense (like `warp::any().or(warp::get())`). + - Change several `warp::body` filters (#345). + - Change `warp::cors()` to return a `warp::cors::Builder` which still implements `Wrap`, but can also `build` a cheaper-to-clone wrapper. + - Change `warp::multipart` stream API to allow for errors when streaming. + - Change `warp::sse` to no longer return a `Filter`, adds `warp::sse::reply` to do what `Sse::reply` did. + - Change `Server::tls()` to return a TLS server builder (#340). + - Change internal `warp::never::Never` usage with `std::convert::Infallible`. + - Remove `warp::ext::set()` function (#222). + - Remove deprecated `warp::cookie::optional_value()`. + + +### v0.1.20 (September 17, 2019) + +- **Features**: + - Implement `Clone` for the `warp::cors` filter. + - Add `into_bytes` method for `warp::ws::Message`. + +### v0.1.19 (August 16, 2019) + +- **Features**: + - Make `warp::multipart` and `wrap::ws` support optional, though enabled by default. +- **Fixes**: + - Fix `warp::fs::dir` filter to reject paths containing backslashes. + +### v0.1.18 (July 25, 2019) + +- **Features**: + - Add `warp::multipart` support. + +### v0.1.17 (July 8, 2019) + +- **Features**: + - Export all built-in Rejection causes in the `warp::reject` module. + - Add `Server::try_bind` as fallible bind methods. + +### v0.1.16 (June 11, 2019) + +- **Features**: + - Unseal the `Reply` trait: custom types can now implement `Reply`. + - Add `warp::sse::keep_alive()` replacement for `warp::sse::keep()` which allows customizing keep-alive behavior. + - Add `warp::log::Info::host()` accessor. +- **Fixes**: + - Fix `warp::fs` filters from sending some headers for `304` responses. + +### v0.1.15 (April 2, 2019) + +- **Features**: + - Add more accessors to `warp::log::Info` type for building custom log formats. + - Implement `Reply` for `Cow<'static, str>`. + +### v0.1.14 (March 19, 2019) + +- **Features**: + - Add `warp::header::optional` filter. + +### v0.1.13 (February 13, 2019) + +- **Features**: + - Implement `Reply` for `Vec<u8>` and `&'static [u8]`. + - Set `content-type` header automatically for string and bytes replies. + - Add `expose_headers` to `warp::cors` filter. + +### v0.1.12 (January 29, 2019) + +- **Features**: + - Implement `PartialEq`, `Eq`, and `Clone` for `warp::ws::Message`. +- **Fixes**: + - Fix panic when incoming request URI may not have a path (such as `CONNECT` requests). + +### v0.1.11 (January 14, 2019) + +- **Features**: + - Add `warp::sse` filters for handling Server-Sent-Events. + - Add `allow_headers` to `warp::cors` filter. +- **Fixes**: + - Fix TLS handshake to close the connection if handshake fails. + +### v0.1.10 (December 17, 2018) + +- **Features**: + - Add optional TLS support. Enable the `tls` feature, and then use `Server::tls`. + - Add `warp::cors` filter for CORS support. + - Add `warp::addr::remote` to access the remote address of a request. + - Add `warp::log::custom` to support customizing of access logging. + - Add `warp::test::ws` to improve testing Websocket filters. + +### v0.1.9 (October 30, 2018) + +- **Features**: + - Add `warp::ext::get` and `warp::ext::set` to set request extensions. + - Add `Filter::untuple_one` to unroll nested tuple layers from extractions. + - Add `Ws2::max_send_queue` configuration method. + - Add `ws::Message::is_ping` method, and yield pings to user code. +- **Fixes**: + - Fix panic in debug mode when receiving a websocket ping. + +### v0.1.8 (October 25, 2018) + +- **Features**: + - Improved flexibility of `Rejection` system. + + The `Rejection` type can now nest and combine arbitrary rejections, + so it is no longer bound to a small set of meanings. The ranking of + status codes is still used to determine which rejection gets priority. + + A different priority can be implemented by handling rejections with + a `Filter::recover`, and searching for causes in order via + `Rejection::find_cause`. + - Adds `warp::reject::custom()` to create a `Rejection` with + any `Into<Box<std::error::Error>>`. These rejections should be + handled with an eventual `Filter::recover`. Any unhandled + custom rejections are considered a server error. + - Deprecates `Rejection::with`. Use custom rejections instead. + - Deprecates `Rejection::into_cause`, as it can no longer work. Always + returns `Err(Rejection)`. + - Deprecates `Rejection::json`, since the format needed is too generic. + The `errors.rs` example shows how to send custom JSON when recovering + from rejections. + - Deprecates `warp::reject()`, since it current signals a `400 Bad + Request`, but in newer versions, it will signal `404 Not Found`. + It's deprecated simply to warn that the semantics are changing, + but the function won't actually go away. + - Deprecates `reject::bad_request()`, `reject::forbidden()`, and + `reject::server_error()`. Uses custom rejections instead. + - Renamed `warp::path::index` to `warp::path::end`. + + +### v0.1.7 (October 15, 2018) + +- **Features**: + - Export the types returned from the `warp::body::stream()` filter, `BodyStream` and `StreamBuf`. + - Deprecated `Rejection::into_cause`, since an upcoming Rejection refactor will make it impossible to support. + +- **Fixes**: + - Fix websocket filters to do a case-insensitive match of the `Connection` header. + +### v0.1.6 (October 5, 2018) + +- **Features**: + - Add Conditional and Range request support for `warp::fs` filters. + - Relaxed bounds on `Rejection::with` to no longer need to be `Sized`. + - Add `warp::path::peek()` which gets the unmatched tail without adjusting the currently matched path. + +### v0.1.5 (October 3, 2018) + +- **Features**: + - Serve `index.html` automatically with `warp::fs::dir` filter. + - Include `last-modified` header with `warp::fs` filters. + - Add `warp::redirect` to easily reply with redirections. + - Add `warp::reply::{with_status, with_header}` to wrap `impl Reply`s directly with a new status code or header. + - Add support for running a warp `Server` with a custom source of incoming connections. + - `Server::run_incoming` to have the runtime started automatically. + - `Server::serve_incoming` to get a future to run on existing runtime. + - These can be used to support Unix Domain Sockets, TLS, and other transports. + - Add `Rejection::into_cause()` to retrieve the original error of a rejection back. + - Add `Rejection::json()` to convert a rejection into a JSON response. + +- **Fixes** + - Internal errors in warp that result in rendering a `500 Internal Server Error` are now also logged at the `error` level. + + +### v0.1.4 (September 25, 2018) + +- **Features**: + - Add `warp::reply::with::headers(HeaderMap)` filter wrapper. + - Add `warp::cookie::optional()` to get an optional cookie value. + - Add `warp::path::full()` to be able to extract the full request path without affecting route matching. + - Add graceful shutdown support to the `Server`. + - Allow empty query strings to be treated as for `warp::query()`. + +### v0.1.3 (August 28, 2018) + +- **Features**: + - Add `warp::reject::forbidden()` to represent `403 Forbidden` responses. + - Add `Rejection::with(cause)` to customize rejection messages. +- **Fixes**: + - Fix `warp::body::form` to allow charsets in the `content-type` header. + +### v0.1.2 (August 14, 2018) + +- **Features**: + - Implemented `Reply` for `Response<impl Into<hyper::Body>`, allowing streaming response bodies. + - Add `warp::body::stream()` filter to access the request body as an `impl Stream`. + - Add `warp::ws2()` as a more flexible websocket filter. + - This allows passing other extracted values to the upgrade callback, such as a value from a header or path. + - Deprecates `warp::ws()`, and `ws2()` will become `ws()` in 0.2. + - Add `warp::get2()`, `warp::post2()`, `warp::put2()`, and `warp::delete2()` as more standard method filters that are used via chaining instead of nesting. + - `get()`, `post()`, `put()`, and `delete()` are deprecated, and the new versions will become them in 0.2. + - Add `Filter::unify()` for when a filter returns `Either<T, T>`, converting the `Either` into the inner `T`, regardless of which variant it was. + - This requires that both sides of the `Either` be the same type. + - This can be useful when extracting a value that might be present in different places of the request. + + ```rust + // Allow `MyId` to be a path parameter or a header... + let id = warp::path::param::<MyId>() + .or(warp::header::<MyId>()) + .unify(); + + // A way of providing default values... + let dnt = warp::header::<bool>("dnt") + .or(warp::any().map(|| true)) + .unify(); + ``` + - Add `content-type` header automatically to replies from `file` and `dir` filters based on file extension. + - Add `warp::head()`, `warp::options()`, and `warp::patch()` as new Method filters. + - Try to use OS blocksize in `warp::fs` filters. +- **Fixes**: + - Chaining filters that try to consume the request body will log that the body is already consumed, and return a `500 Internal Server Error` rejection. + +### v0.1.1 (August 7, 2018) + +- **Features**: + - Add `warp::query::raw()` filter to get query as a `String`. + - Add `Filter::recover()` to ease customizing of rejected responses. + - Add `warp::header::headers_clone()` filter to get a clone of request's `HeaderMap`. + - Add `warp::path::tail()` filter to get remaining "tail" of the request path. +- **Fixes**: + - URL decode path segments in `warp::fs` filters. + + +## v0.1.0 (August 1, 2018) + +- Intial release. diff --git a/third_party/rust/warp/Cargo.lock b/third_party/rust/warp/Cargo.lock new file mode 100644 index 0000000000..c35f495174 --- /dev/null +++ b/third_party/rust/warp/Cargo.lock @@ -0,0 +1,1633 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bumpalo" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bytes" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "c2-chacha" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cc" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dtoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "env_logger" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fnv" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-channel" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-executor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-io" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-sink" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-task" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-util" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "h2" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "handlebars" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pest_derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "headers" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "headers-core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hermit-abi" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "http" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "h2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "indexmap" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "input_buffer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "js-sys" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wasm-bindgen 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "listenfd" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mime_guess" +version = "1.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mio" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "multipart" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "buf_redux 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 1.8.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "twoway 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "net2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-trie 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pest_generator 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pest_generator" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pest_meta 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pin-project" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pin-project-internal 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pin-utils" +version = "0.1.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pretty_env_logger" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro-nested" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "regex" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "remove_dir_all" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ring" +version = "0.16.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustls" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.11 (registry+https://github.com/rust-lang/crates.io-index)", + "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.16.11 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_urlencoded" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "smallvec" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-macros 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-macros" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-rustls" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "tungstenite 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-util" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tower-service" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "try-lock" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tungstenite" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "input_buffer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typenum" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ucd-trie" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "untrusted" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "urlencoding" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "utf-8" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "uuid" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "warp" +version = "0.2.2" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "handlebars 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "headers 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)", + "listenfd 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "multipart 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pretty_env_logger 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-rustls 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tungstenite 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wasm-bindgen" +version = "0.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bumpalo 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro-support 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-backend 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "web-sys" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "js-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "webpki" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.16.11 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum aho-corasick 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d5e63fd144e18ba274ae7095c0197a870a7b9468abc801dd62f190d80817d2ec" +"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +"checksum buf_redux 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +"checksum bumpalo 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742" +"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +"checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" +"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" +"checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" +"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +"checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" +"checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5c329ae8753502fb44ae4fc2b622fa2a94652c41e795143765ba0927f92ab780" +"checksum futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" +"checksum futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" +"checksum futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba" +"checksum futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" +"checksum futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" +"checksum futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" +"checksum futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" +"checksum futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" +"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +"checksum h2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9d5c295d1c0c68e4e42003d75f908f5e16a1edd1cbe0b0d02e4dc2006a384f47" +"checksum handlebars 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ba758d094d31274eb49d15da6f326b96bf3185239a6359bf684f3d5321148900" +"checksum headers 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a72b4bd7cbbf0c22190e82f02517f456a6b9be24c25a6827b5802e478b8c2d70" +"checksum headers-core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +"checksum hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" +"checksum http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" +"checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +"checksum hyper 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fa1c527bbc634be72aa7ba31e4e4def9bbb020f5416916279b7c705cd838893e" +"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" +"checksum input_buffer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" +"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" +"checksum js-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)" = "1cb931d43e71f560c81badb0191596562bafad2be06a3f9025b845c847c60df5" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" +"checksum listenfd 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "492158e732f2e2de81c592f0a2427e57e12cd3d59877378fe7af624b6bbe0ca1" +"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +"checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +"checksum mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +"checksum mime_guess 1.8.8 (registry+https://github.com/rust-lang/crates.io-index)" = "216929a5ee4dd316b1702eedf5e74548c123d370f47841ceaac38ca154690ca3" +"checksum mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +"checksum mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" +"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum multipart 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "136eed74cadb9edd2651ffba732b19a450316b680e4f48d6c79e905799e19d01" +"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" +"checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +"checksum pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +"checksum pest_derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +"checksum pest_generator 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "27e5277315f6b4f27e0e6744feb5d5ba1891e7164871033d3c8344c6783b349a" +"checksum pest_meta 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +"checksum phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +"checksum phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +"checksum phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +"checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +"checksum pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c" +"checksum pin-project-internal 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" +"checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" +"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" +"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" +"checksum pretty_env_logger 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "717ee476b1690853d222af4634056d830b5197ffd747726a9a1eee6da9f49074" +"checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" +"checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" +"checksum proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" +"checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" +"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +"checksum regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" +"checksum regex-syntax 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "1132f845907680735a84409c3bebc64d1364a5683ffbce899550cd09d5eaefc1" +"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +"checksum ring 0.16.11 (registry+https://github.com/rust-lang/crates.io-index)" = "741ba1704ae21999c00942f9f5944f801e977f54302af346b596287599ad1862" +"checksum rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" +"checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +"checksum scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +"checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +"checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +"checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" +"checksum serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" +"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" +"checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +"checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" +"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +"checksum smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" +"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +"checksum syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" +"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "0fa5e81d6bc4e67fe889d5783bd2a128ab2e0cfa487e0be16b6a8d177b101616" +"checksum tokio-macros 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +"checksum tokio-rustls 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "141afec0978abae6573065a48882c6bae44c5cc61db9b511ac4abf6a09bfd9cc" +"checksum tokio-tungstenite 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b8fe88007ebc363512449868d7da4389c9400072a3f666f212c7280082882a" +"checksum tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" +"checksum tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" +"checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +"checksum tungstenite 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cfea31758bf674f990918962e8e5f07071a3161bd7c4138ed23e416e1ac4264e" +"checksum twoway 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +"checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" +"checksum ucd-trie 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8f00ed7be0c1ff1e24f46c3d2af4859f7e863672ba3a6e92e7cff702bf9f06c2" +"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +"checksum unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60369ef7a31de49bcb3f6ca728d4ba7300d9a1658f94c727d4cab8c8d9f4aece" +"checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +"checksum urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3df3561629a8bb4c57e5a2e4c43348d9e29c7c29d9b1c4c1f47166deca8f37ed" +"checksum utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +"checksum uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e1436e58182935dcd9ce0add9ea0b558e8a87befe01c1a301e6020aeb0876363" +"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" +"checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +"checksum wasm-bindgen 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "3557c397ab5a8e347d434782bcd31fc1483d927a6826804cec05cc792ee2519d" +"checksum wasm-bindgen-backend 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "e0da9c9a19850d3af6df1cb9574970b566d617ecfaf36eb0b706b6f3ef9bd2f8" +"checksum wasm-bindgen-macro 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "0f6fde1d36e75a714b5fe0cffbb78978f222ea6baebb726af13c78869fdb4205" +"checksum wasm-bindgen-macro-support 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "25bda4168030a6412ea8a047e27238cadf56f0e53516e1e83fec0a8b7c786f6d" +"checksum wasm-bindgen-shared 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "fc9f36ad51f25b0219a3d4d13b90eb44cd075dff8b6280cca015775d7acaddd8" +"checksum web-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)" = "721c6263e2c66fd44501cc5efbfa2b7dfa775d13e4ea38c46299646ed1f9c70a" +"checksum webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1f50e1972865d6b1adb54167d1c8ed48606004c2c9d0ea5f1eeb34d95e863ef" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" diff --git a/third_party/rust/warp/Cargo.toml b/third_party/rust/warp/Cargo.toml new file mode 100644 index 0000000000..aac7e0b207 --- /dev/null +++ b/third_party/rust/warp/Cargo.toml @@ -0,0 +1,140 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +edition = "2018" +name = "warp" +version = "0.2.2" +authors = ["Sean McArthur <sean@seanmonstar.com>"] +autoexamples = true +autotests = true +description = "serve the web at warp speeds" +documentation = "https://docs.rs/warp" +readme = "README.md" +keywords = ["warp", "server", "http", "hyper"] +categories = ["web-programming::http-server"] +license = "MIT" +repository = "https://github.com/seanmonstar/warp" +[package.metadata.docs.rs] +features = ["tls"] +[profile.bench] +codegen-units = 1 +incremental = false + +[profile.release] +codegen-units = 1 +incremental = false + +[[example]] +name = "unix_socket" +required-features = ["tokio/uds"] + +[[example]] +name = "websockets" +required-features = ["websocket"] + +[[example]] +name = "websockets_chat" +required-features = ["websocket"] + +[[test]] +name = "multipart" +required-features = ["multipart"] + +[[test]] +name = "ws" +required-features = ["websocket"] +[dependencies.bytes] +version = "0.5" + +[dependencies.futures] +version = "0.3" +features = ["alloc"] +default-features = false + +[dependencies.headers] +version = "0.3" + +[dependencies.http] +version = "0.2" + +[dependencies.hyper] +version = "0.13" + +[dependencies.log] +version = "0.4" + +[dependencies.mime] +version = "0.3" + +[dependencies.mime_guess] +version = "2.0.0" + +[dependencies.multipart] +version = "0.16" +features = ["server"] +optional = true +default-features = false + +[dependencies.pin-project] +version = "0.4.5" + +[dependencies.scoped-tls] +version = "1.0" + +[dependencies.serde] +version = "1.0" + +[dependencies.serde_json] +version = "1.0" + +[dependencies.serde_urlencoded] +version = "0.6" + +[dependencies.tokio] +version = "0.2" +features = ["blocking", "fs", "stream", "sync", "time"] + +[dependencies.tokio-rustls] +version = "0.12.2" +optional = true + +[dependencies.tokio-tungstenite] +version = "0.10" +optional = true +default-features = false + +[dependencies.tower-service] +version = "0.3" + +[dependencies.urlencoding] +version = "1.0.0" +[dev-dependencies.handlebars] +version = "3.0.0" + +[dev-dependencies.listenfd] +version = "0.3" + +[dev-dependencies.pretty_env_logger] +version = "0.3" + +[dev-dependencies.serde_derive] +version = "1.0" + +[dev-dependencies.tokio] +version = "0.2" +features = ["macros"] + +[features] +default = ["multipart", "websocket"] +tls = ["tokio-rustls"] +websocket = ["tokio-tungstenite"] diff --git a/third_party/rust/warp/LICENSE b/third_party/rust/warp/LICENSE new file mode 100644 index 0000000000..2c77834221 --- /dev/null +++ b/third_party/rust/warp/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2018 Sean McArthur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/third_party/rust/warp/README.md b/third_party/rust/warp/README.md new file mode 100644 index 0000000000..dd19ba1d81 --- /dev/null +++ b/third_party/rust/warp/README.md @@ -0,0 +1,58 @@ +# warp + +[![GHA Build Status](https://github.com/seanmonstar/warp/workflows/CI/badge.svg)](https://github.com/seanmonstar/warp/actions?query=workflow%3ACI) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +[![crates.io](https://img.shields.io/crates/v/warp.svg)](https://crates.io/crates/warp) +[![Released API docs](https://docs.rs/warp/badge.svg)](https://docs.rs/warp) + +A super-easy, composable, web server framework for warp speeds. + +The fundamental building block of `warp` is the `Filter`: they can be combined +and composed to express rich requirements on requests. + +Thanks to its `Filter` system, warp provides these out of the box: + +* Path routing and parameter extraction +* Header requirements and extraction +* Query string deserialization +* JSON and Form bodies +* Multipart form data +* Static Files and Directories +* Websockets +* Access logging + +Since it builds on top of [hyper](https://hyper.rs), you automatically get: + +- HTTP/1 +- HTTP/2 +- Asynchronous +- One of the fastest HTTP implementations +- Tested and **correct** + +## Example + +Add warp and Tokio to your dependencies: + +```toml +tokio = { version = "0.2", features = ["macros"] } +warp = "0.2" +``` + +And then get started in your `main.rs`: + +```rust +use warp::Filter; + +#[tokio::main] +async fn main() { + // GET /hello/warp => 200 OK with body "Hello, warp!" + let hello = warp::path!("hello" / String) + .map(|name| format!("Hello, {}!", name)); + + warp::serve(hello) + .run(([127, 0, 0, 1], 3030)) + .await; +} +``` + +For more information you can check the [docs](https://docs.rs/warp) or the [examples](https://github.com/seanmonstar/warp/tree/master/examples). diff --git a/third_party/rust/warp/examples/README.md b/third_party/rust/warp/examples/README.md new file mode 100644 index 0000000000..3f5ecb95eb --- /dev/null +++ b/third_party/rust/warp/examples/README.md @@ -0,0 +1,41 @@ +# Examples + +Welcome to the examples! These show off `warp`'s functionality and explain how to use it. + +## Getting Started + +- [`hello.rs`](./hello.rs) - Just a basic "Hello World" API +- [`routing.rs`](./routing.rs) - Builds up a more complex set of routes and shows how to combine filters +- [`body.rs`](./body.rs) - What's a good API without parsing data from the request body? +- [`headers.rs`](./headers.rs) - Parsing data from the request headers +- [`rejections.rs`](./rejections.rs) - Your APIs are obviously perfect, but for silly others who call them incorrectly you'll want to define errors for them +- [`futures.rs`](./futures.rs) - Wait, wait! ... Or how to integrate futures into filters +- [`todos.rs`](./todos.rs) - Putting this all together with a proper app + +## Further Use Cases + +### Serving HTML and Other Files + +- [`file.rs`](./file.rs) - Serving static files +- [`dir.rs`](./dir.rs) - Or a whole directory of files +- [`handlebars_template.rs`](./handlebars_template.rs) - Using Handlebars to fill in an HTML template + +### Websockets + +Hooray! `warp` also includes built-in support for WebSockets + +- [`websockets.rs`](./websockets.rs) - Basic handling of a WebSocket upgrade +- [`websockets_chat.rs`](./websockets_chat.rs) - Full WebSocket app + +### Server-Side Events + +- [`sse.rs`](./sse.rs) - Basic Server-Side Event +- [`sse_chat.rs`](./sse_chat.rs) - Full SSE app + +### TLS + +- [`tls.rs`](./tls.rs) - can i haz security? + +### Autoreloading + +- [`autoreload.rs`](./autoreload.rs) - Change some code and watch the server reload automatically! diff --git a/third_party/rust/warp/examples/autoreload.rs b/third_party/rust/warp/examples/autoreload.rs new file mode 100644 index 0000000000..4aabfdb6a2 --- /dev/null +++ b/third_party/rust/warp/examples/autoreload.rs @@ -0,0 +1,38 @@ +#![deny(warnings)] +use hyper::server::Server; +use listenfd::ListenFd; +use std::convert::Infallible; +use warp::Filter; + +extern crate listenfd; +/// You'll need to install `systemfd` and `cargo-watch`: +/// ``` +/// cargo install systemfd cargo-watch +/// ``` +/// And run with: +/// ``` +/// systemfd --no-pid -s http::3030 -- cargo watch -x 'run --example autoreload' +/// ``` +#[tokio::main] +async fn main() { + // Match any request and return hello world! + let routes = warp::any().map(|| "Hello, World!"); + + // hyper let's us build a server from a TcpListener (which will be + // useful shortly). Thus, we'll need to convert our `warp::Filter` into + // a `hyper::service::MakeService` for use with a `hyper::server::Server`. + let svc = warp::service(routes); + let make_svc = hyper::service::make_service_fn(|_: _| async move { Ok::<_, Infallible>(svc) }); + + let mut listenfd = ListenFd::from_env(); + // if listenfd doesn't take a TcpListener (i.e. we're not running via + // the command above), we fall back to explicitly binding to a given + // host:port. + let server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() { + Server::from_tcp(l).unwrap() + } else { + Server::bind(&([127, 0, 0, 1], 3030).into()) + }; + + server.serve(make_svc).await.unwrap(); +} diff --git a/third_party/rust/warp/examples/body.rs b/third_party/rust/warp/examples/body.rs new file mode 100644 index 0000000000..174d928ab8 --- /dev/null +++ b/third_party/rust/warp/examples/body.rs @@ -0,0 +1,30 @@ +#![deny(warnings)] + +use serde_derive::{Deserialize, Serialize}; + +use warp::Filter; + +#[derive(Deserialize, Serialize)] +struct Employee { + name: String, + rate: u32, +} + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + // POST /employees/:rate {"name":"Sean","rate":2} + let promote = warp::post() + .and(warp::path("employees")) + .and(warp::path::param::<u32>()) + // Only accept bodies smaller than 16kb... + .and(warp::body::content_length_limit(1024 * 16)) + .and(warp::body::json()) + .map(|rate, mut employee: Employee| { + employee.rate = rate; + warp::reply::json(&employee) + }); + + warp::serve(promote).run(([127, 0, 0, 1], 3030)).await +} diff --git a/third_party/rust/warp/examples/dir.rs b/third_party/rust/warp/examples/dir.rs new file mode 100644 index 0000000000..30261a220e --- /dev/null +++ b/third_party/rust/warp/examples/dir.rs @@ -0,0 +1,10 @@ +#![deny(warnings)] + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + warp::serve(warp::fs::dir("examples/dir")) + .run(([127, 0, 0, 1], 3030)) + .await; +} diff --git a/third_party/rust/warp/examples/dir/another.html b/third_party/rust/warp/examples/dir/another.html new file mode 100644 index 0000000000..941c9a5937 --- /dev/null +++ b/third_party/rust/warp/examples/dir/another.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <title>dir/another.html</title> + </head> + <body> + <h1>Welcome to Another Page</h1> + <a href="/">back</a> + </body> +</html>
\ No newline at end of file diff --git a/third_party/rust/warp/examples/dir/index.html b/third_party/rust/warp/examples/dir/index.html new file mode 100644 index 0000000000..cb86323446 --- /dev/null +++ b/third_party/rust/warp/examples/dir/index.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <title>dir/index.html</title> + </head> + <body> + <h1>Welcome to Dir</h1> + <a href="/another.html">another page</a> + </body> +</html>
\ No newline at end of file diff --git a/third_party/rust/warp/examples/dyn_reply.rs b/third_party/rust/warp/examples/dyn_reply.rs new file mode 100644 index 0000000000..4f59cf8ba9 --- /dev/null +++ b/third_party/rust/warp/examples/dyn_reply.rs @@ -0,0 +1,17 @@ +#![deny(warnings)] +use warp::{http::StatusCode, Filter}; + +async fn dyn_reply(word: String) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + if &word == "hello" { + Ok(Box::new("world")) + } else { + Ok(Box::new(StatusCode::BAD_REQUEST)) + } +} + +#[tokio::main] +async fn main() { + let routes = warp::path::param().and_then(dyn_reply); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/file.rs b/third_party/rust/warp/examples/file.rs new file mode 100644 index 0000000000..a0cf2afa45 --- /dev/null +++ b/third_party/rust/warp/examples/file.rs @@ -0,0 +1,21 @@ +#![deny(warnings)] + +use warp::Filter; + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + let readme = warp::get() + .and(warp::path::end()) + .and(warp::fs::file("./README.md")); + + // dir already requires GET... + let examples = warp::path("ex").and(warp::fs::dir("./examples/")); + + // GET / => README.md + // GET /ex/... => ./examples/.. + let routes = readme.or(examples); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/futures.rs b/third_party/rust/warp/examples/futures.rs new file mode 100644 index 0000000000..0134280934 --- /dev/null +++ b/third_party/rust/warp/examples/futures.rs @@ -0,0 +1,37 @@ +#![deny(warnings)] + +use std::convert::Infallible; +use std::str::FromStr; +use std::time::Duration; +use warp::Filter; + +#[tokio::main] +async fn main() { + // Match `/:Seconds`... + let routes = warp::path::param() + // and_then create a `Future` that will simply wait N seconds... + .and_then(sleepy); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} + +async fn sleepy(Seconds(seconds): Seconds) -> Result<impl warp::Reply, Infallible> { + tokio::time::delay_for(Duration::from_secs(seconds)).await; + Ok(format!("I waited {} seconds!", seconds)) +} + +/// A newtype to enforce our maximum allowed seconds. +struct Seconds(u64); + +impl FromStr for Seconds { + type Err = (); + fn from_str(src: &str) -> Result<Self, Self::Err> { + src.parse::<u64>().map_err(|_| ()).and_then(|num| { + if num <= 5 { + Ok(Seconds(num)) + } else { + Err(()) + } + }) + } +} diff --git a/third_party/rust/warp/examples/handlebars_template.rs b/third_party/rust/warp/examples/handlebars_template.rs new file mode 100644 index 0000000000..3740a32c34 --- /dev/null +++ b/third_party/rust/warp/examples/handlebars_template.rs @@ -0,0 +1,58 @@ +#![deny(warnings)] +use std::sync::Arc; + +use handlebars::Handlebars; +use serde::Serialize; +use serde_json::json; +use warp::Filter; + +struct WithTemplate<T: Serialize> { + name: &'static str, + value: T, +} + +fn render<T>(template: WithTemplate<T>, hbs: Arc<Handlebars>) -> impl warp::Reply +where + T: Serialize, +{ + let render = hbs + .render(template.name, &template.value) + .unwrap_or_else(|err| err.to_string()); + warp::reply::html(render) +} + +#[tokio::main] +async fn main() { + let template = "<!DOCTYPE html> + <html> + <head> + <title>Warp Handlebars template example</title> + </head> + <body> + <h1>Hello {{user}}!</h1> + </body> + </html>"; + + let mut hb = Handlebars::new(); + // register the template + hb.register_template_string("template.html", template) + .unwrap(); + + // Turn Handlebars instance into a Filter so we can combine it + // easily with others... + let hb = Arc::new(hb); + + // Create a reusable closure to render template + let handlebars = move |with_template| render(with_template, hb.clone()); + + //GET / + let route = warp::get() + .and(warp::path::end()) + .map(|| WithTemplate { + name: "template.html", + value: json!({"user" : "Warp"}), + }) + .map(handlebars); + + warp::serve(route).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/headers.rs b/third_party/rust/warp/examples/headers.rs new file mode 100644 index 0000000000..2b3dca7b50 --- /dev/null +++ b/third_party/rust/warp/examples/headers.rs @@ -0,0 +1,27 @@ +#![deny(warnings)] +use std::net::SocketAddr; +use warp::Filter; + +/// Create a server that requires header conditions: +/// +/// - `Host` is a `SocketAddr` +/// - `Accept` is exactly `*/*` +/// +/// If these conditions don't match, a 404 is returned. +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + // For this example, we assume no DNS was used, + // so the Host header should be an address. + let host = warp::header::<SocketAddr>("host"); + + // Match when we get `accept: */*` exactly. + let accept_stars = warp::header::exact("accept", "*/*"); + + let routes = host + .and(accept_stars) + .map(|addr| format!("accepting stars on {}", addr)); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/hello.rs b/third_party/rust/warp/examples/hello.rs new file mode 100644 index 0000000000..27aa2e51c8 --- /dev/null +++ b/third_party/rust/warp/examples/hello.rs @@ -0,0 +1,10 @@ +#![deny(warnings)] +use warp::Filter; + +#[tokio::main] +async fn main() { + // Match any request and return hello world! + let routes = warp::any().map(|| "Hello, World!"); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/rejections.rs b/third_party/rust/warp/examples/rejections.rs new file mode 100644 index 0000000000..286b7772b6 --- /dev/null +++ b/third_party/rust/warp/examples/rejections.rs @@ -0,0 +1,90 @@ +#![deny(warnings)] + +use std::convert::Infallible; +use std::num::NonZeroU16; + +use serde_derive::Serialize; +use warp::http::StatusCode; +use warp::{reject, Filter, Rejection, Reply}; + +/// Rejections represent cases where a filter should not continue processing +/// the request, but a different filter *could* process it. +#[tokio::main] +async fn main() { + let math = warp::path!("math" / u16) + .and(div_by()) + .map(|num: u16, denom: NonZeroU16| { + warp::reply::json(&Math { + op: format!("{} / {}", num, denom), + output: num / denom.get(), + }) + }); + + let routes = warp::get().and(math).recover(handle_rejection); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} + +/// Extract a denominator from a "div-by" header, or reject with DivideByZero. +fn div_by() -> impl Filter<Extract = (NonZeroU16,), Error = Rejection> + Copy { + warp::header::<u16>("div-by").and_then(|n: u16| async move { + if let Some(denom) = NonZeroU16::new(n) { + Ok(denom) + } else { + Err(reject::custom(DivideByZero)) + } + }) +} + +#[derive(Debug)] +struct DivideByZero; + +impl reject::Reject for DivideByZero {} + +// JSON replies + +/// A successful math operation. +#[derive(Serialize)] +struct Math { + op: String, + output: u16, +} + +/// An API error serializable to JSON. +#[derive(Serialize)] +struct ErrorMessage { + code: u16, + message: String, +} + +// This function receives a `Rejection` and tries to return a custom +// value, otherwise simply passes the rejection along. +async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> { + let code; + let message; + + if err.is_not_found() { + code = StatusCode::NOT_FOUND; + message = "NOT_FOUND"; + } else if let Some(DivideByZero) = err.find() { + code = StatusCode::BAD_REQUEST; + message = "DIVIDE_BY_ZERO"; + } else if let Some(_) = err.find::<warp::reject::MethodNotAllowed>() { + // We can handle a specific error, here METHOD_NOT_ALLOWED, + // and render it however we want + code = StatusCode::METHOD_NOT_ALLOWED; + message = "METHOD_NOT_ALLOWED"; + } else { + // We should have expected this... Just log and say its a 500 + eprintln!("unhandled rejection: {:?}", err); + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "UNHANDLED_REJECTION"; + } + + let json = warp::reply::json(&ErrorMessage { + code: code.as_u16(), + message: message.into(), + }); + + Ok(warp::reply::with_status(json, code)) +} diff --git a/third_party/rust/warp/examples/returning.rs b/third_party/rust/warp/examples/returning.rs new file mode 100644 index 0000000000..f4f61e60fc --- /dev/null +++ b/third_party/rust/warp/examples/returning.rs @@ -0,0 +1,20 @@ +use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; + +// Option 1: BoxedFilter +// Note that this may be useful for shortening compile times when you are composing many filters. +// Boxing the filters will use dynamic dispatch and speed up compilation while +// making it slightly slower at runtime. +pub fn assets_filter() -> BoxedFilter<(impl Reply,)> { + warp::path("assets").and(warp::fs::dir("./assets")).boxed() +} + +// Option 2: impl Filter + Clone +pub fn index_filter() -> impl Filter<Extract = (&'static str,), Error = Rejection> + Clone { + warp::path::end().map(|| "Index page") +} + +#[tokio::main] +async fn main() { + let routes = index_filter().or(assets_filter()); + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/routing.rs b/third_party/rust/warp/examples/routing.rs new file mode 100644 index 0000000000..6d1caa5bfe --- /dev/null +++ b/third_party/rust/warp/examples/routing.rs @@ -0,0 +1,92 @@ +#![deny(warnings)] + +use warp::Filter; + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + // We'll start simple, and gradually show how you combine these powers + // into super powers! + + // GET /hi + let hi = warp::path("hi").map(|| "Hello, World!"); + + // How about multiple segments? First, we could use the `path!` macro: + // + // GET /hello/from/warp + let hello_from_warp = warp::path!("hello" / "from" / "warp").map(|| "Hello from warp!"); + + // Fine, but how do I handle parameters in paths? + // + // GET /sum/:u32/:u32 + let sum = warp::path!("sum" / u32 / u32).map(|a, b| format!("{} + {} = {}", a, b, a + b)); + + // Any type that implements FromStr can be used, and in any order: + // + // GET /:u16/times/:u16 + let times = + warp::path!(u16 / "times" / u16).map(|a, b| format!("{} times {} = {}", a, b, a * b)); + + // Oh shoot, those math routes should be mounted at a different path, + // is that possible? Yep. + // + // GET /math/sum/:u32/:u32 + // GET /math/:u16/times/:u16 + let math = warp::path("math"); + let _sum = math.and(sum); + let _times = math.and(times); + + // What! And? What's that do? + // + // It combines the filters in a sort of "this and then that" order. In + // fact, it's exactly what the `path!` macro has been doing internally. + // + // GET /bye/:string + let bye = warp::path("bye") + .and(warp::path::param()) + .map(|name: String| format!("Good bye, {}!", name)); + + // Ah, can filters do things besides `and`? + // + // Why, yes they can! They can also `or`! As you might expect, `or` creates + // a "this or else that" chain of filters. If the first doesn't succeed, + // then it tries the other. + // + // So, those `math` routes could have been mounted all as one, with `or`. + // + // GET /math/sum/:u32/:u32 + // GET /math/:u16/times/:u16 + let math = warp::path("math").and(sum.or(times)); + + // We can use the end() filter to match a shorter path + let help = warp::path("math") + // Careful! Omitting the following line would make this filter match + // requests to /math/sum/:u32/:u32 and /math/:u16/times/:u16 + .and(warp::path::end()) + .map(|| "This is the Math API. Try calling /math/sum/:u32/:u32 or /math/:u16/times/:u16"); + let math = help.or(math); + + // Let's let people know that the `sum` and `times` routes are under `math`. + let sum = sum.map(|output| format!("(This route has moved to /math/sum/:u16/:u16) {}", output)); + let times = + times.map(|output| format!("(This route has moved to /math/:u16/times/:u16) {}", output)); + + // It turns out, using `or` is how you combine everything together into + // a single API. (We also actually haven't been enforcing the that the + // method is GET, so we'll do that too!) + // + // GET /hi + // GET /hello/from/warp + // GET /bye/:string + // GET /math/sum/:u32/:u32 + // GET /math/:u16/times/:u16 + + let routes = warp::get().and(hi.or(hello_from_warp).or(bye).or(math).or(sum).or(times)); + + // Note that composing filters for many routes may increase compile times (because it uses a lot of generics). + // If you wish to use dynamic dispatch instead and speed up compile times while + // making it slightly slower at runtime, you can use Filter::boxed(). + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/sse.rs b/third_party/rust/warp/examples/sse.rs new file mode 100644 index 0000000000..9c883a7c9a --- /dev/null +++ b/third_party/rust/warp/examples/sse.rs @@ -0,0 +1,28 @@ +use futures::StreamExt; +use std::convert::Infallible; +use std::time::Duration; +use tokio::time::interval; +use warp::{sse::ServerSentEvent, Filter}; + +// create server-sent event +fn sse_counter(counter: u64) -> Result<impl ServerSentEvent, Infallible> { + Ok(warp::sse::data(counter)) +} + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + let routes = warp::path("ticks").and(warp::get()).map(|| { + let mut counter: u64 = 0; + // create server event source + let event_stream = interval(Duration::from_secs(1)).map(move |_| { + counter += 1; + sse_counter(counter) + }); + // reply using server-sent events + warp::sse::reply(event_stream) + }); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/sse_chat.rs b/third_party/rust/warp/examples/sse_chat.rs new file mode 100644 index 0000000000..693a87f552 --- /dev/null +++ b/third_party/rust/warp/examples/sse_chat.rs @@ -0,0 +1,164 @@ +use futures::{Stream, StreamExt}; +use std::collections::HashMap; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, +}; +use tokio::sync::mpsc; +use warp::{sse::ServerSentEvent, Filter}; + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + // Keep track of all connected users, key is usize, value + // is an event stream sender. + let users = Arc::new(Mutex::new(HashMap::new())); + // Turn our "state" into a new Filter... + let users = warp::any().map(move || users.clone()); + + // POST /chat -> send message + let chat_send = warp::path("chat") + .and(warp::post()) + .and(warp::path::param::<usize>()) + .and(warp::body::content_length_limit(500)) + .and( + warp::body::bytes().and_then(|body: bytes::Bytes| async move { + std::str::from_utf8(&body) + .map(String::from) + .map_err(|_e| warp::reject::custom(NotUtf8)) + }), + ) + .and(users.clone()) + .map(|my_id, msg, users| { + user_message(my_id, msg, &users); + warp::reply() + }); + + // GET /chat -> messages stream + let chat_recv = warp::path("chat").and(warp::get()).and(users).map(|users| { + // reply using server-sent events + let stream = user_connected(users); + warp::sse::reply(warp::sse::keep_alive().stream(stream)) + }); + + // GET / -> index html + let index = warp::path::end().map(|| { + warp::http::Response::builder() + .header("content-type", "text/html; charset=utf-8") + .body(INDEX_HTML) + }); + + let routes = index.or(chat_recv).or(chat_send); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} + +/// Our global unique user id counter. +static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); + +/// Message variants. +#[derive(Debug)] +enum Message { + UserId(usize), + Reply(String), +} + +#[derive(Debug)] +struct NotUtf8; +impl warp::reject::Reject for NotUtf8 {} + +/// Our state of currently connected users. +/// +/// - Key is their id +/// - Value is a sender of `Message` +type Users = Arc<Mutex<HashMap<usize, mpsc::UnboundedSender<Message>>>>; + +fn user_connected( + users: Users, +) -> impl Stream<Item = Result<impl ServerSentEvent + Send + 'static, warp::Error>> + Send + 'static +{ + // Use a counter to assign a new unique ID for this user. + let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); + + eprintln!("new chat user: {}", my_id); + + // Use an unbounded channel to handle buffering and flushing of messages + // to the event source... + let (tx, rx) = mpsc::unbounded_channel(); + + tx.send(Message::UserId(my_id)) + // rx is right above, so this cannot fail + .unwrap(); + + // Save the sender in our list of connected users. + users.lock().unwrap().insert(my_id, tx); + + // Convert messages into Server-Sent Events and return resulting stream. + rx.map(|msg| match msg { + Message::UserId(my_id) => Ok((warp::sse::event("user"), warp::sse::data(my_id)).into_a()), + Message::Reply(reply) => Ok(warp::sse::data(reply).into_b()), + }) +} + +fn user_message(my_id: usize, msg: String, users: &Users) { + let new_msg = format!("<User#{}>: {}", my_id, msg); + + // New message from this user, send it to everyone else (except same uid)... + // + // We use `retain` instead of a for loop so that we can reap any user that + // appears to have disconnected. + users.lock().unwrap().retain(|uid, tx| { + if my_id == *uid { + // don't send to same user, but do retain + true + } else { + // If not `is_ok`, the SSE stream is gone, and so don't retain + tx.send(Message::Reply(new_msg.clone())).is_ok() + } + }); +} + +static INDEX_HTML: &str = r#" +<!DOCTYPE html> +<html> + <head> + <title>Warp Chat</title> + </head> + <body> + <h1>warp chat</h1> + <div id="chat"> + <p><em>Connecting...</em></p> + </div> + <input type="text" id="text" /> + <button type="button" id="send">Send</button> + <script type="text/javascript"> + var uri = 'http://' + location.host + '/chat'; + var sse = new EventSource(uri); + function message(data) { + var line = document.createElement('p'); + line.innerText = data; + chat.appendChild(line); + } + sse.onopen = function() { + chat.innerHTML = "<p><em>Connected!</em></p>"; + } + var user_id; + sse.addEventListener("user", function(msg) { + user_id = msg.data; + }); + sse.onmessage = function(msg) { + message(msg.data); + }; + send.onclick = function() { + var msg = text.value; + var xhr = new XMLHttpRequest(); + xhr.open("POST", uri + '/' + user_id, true); + xhr.send(msg); + text.value = ''; + message('<You>: ' + msg); + }; + </script> + </body> +</html> +"#; diff --git a/third_party/rust/warp/examples/tls.rs b/third_party/rust/warp/examples/tls.rs new file mode 100644 index 0000000000..9000263842 --- /dev/null +++ b/third_party/rust/warp/examples/tls.rs @@ -0,0 +1,24 @@ +#![deny(warnings)] + +// Don't copy this `cfg`, it's only needed because this file is within +// the warp repository. +#[cfg(feature = "tls")] +#[tokio::main] +async fn main() { + use warp::Filter; + + // Match any request and return hello world! + let routes = warp::any().map(|| "Hello, World!"); + + warp::serve(routes) + .tls() + .cert_path("examples/tls/cert.pem") + .key_path("examples/tls/key.rsa") + .run(([127, 0, 0, 1], 3030)) + .await; +} + +#[cfg(not(feature = "tls"))] +fn main() { + eprintln!("Requires the `tls` feature."); +} diff --git a/third_party/rust/warp/examples/tls/cert.pem b/third_party/rust/warp/examples/tls/cert.pem new file mode 100644 index 0000000000..03af12ff81 --- /dev/null +++ b/third_party/rust/warp/examples/tls/cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEADCCAmigAwIBAgICAcgwDQYJKoZIhvcNAQELBQAwLDEqMCgGA1UEAwwhcG9u +eXRvd24gUlNBIGxldmVsIDIgaW50ZXJtZWRpYXRlMB4XDTE2MDgxMzE2MDcwNFoX +DTIyMDIwMzE2MDcwNFowGTEXMBUGA1UEAwwOdGVzdHNlcnZlci5jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpVhh1/FNP2qvWenbZSghari/UThwe +dynfnHG7gc3JmygkEdErWBO/CHzHgsx7biVE5b8sZYNEDKFojyoPHGWK2bQM/FTy +niJCgNCLdn6hUqqxLAml3cxGW77hAWu94THDGB1qFe+eFiAUnDmob8gNZtAzT6Ky +b/JGJdrEU0wj+Rd7wUb4kpLInNH/Jc+oz2ii2AjNbGOZXnRz7h7Kv3sO9vABByYe +LcCj3qnhejHMqVhbAT1MD6zQ2+YKBjE52MsQKU/xhUpu9KkUyLh0cxkh3zrFiKh4 +Vuvtc+n7aeOv2jJmOl1dr0XLlSHBlmoKqH6dCTSbddQLmlK7dms8vE01AgMBAAGj +gb4wgbswDAYDVR0TAQH/BAIwADALBgNVHQ8EBAMCBsAwHQYDVR0OBBYEFMeUzGYV +bXwJNQVbY1+A8YXYZY8pMEIGA1UdIwQ7MDmAFJvEsUi7+D8vp8xcWvnEdVBGkpoW +oR6kHDAaMRgwFgYDVQQDDA9wb255dG93biBSU0EgQ0GCAXswOwYDVR0RBDQwMoIO +dGVzdHNlcnZlci5jb22CFXNlY29uZC50ZXN0c2VydmVyLmNvbYIJbG9jYWxob3N0 +MA0GCSqGSIb3DQEBCwUAA4IBgQBsk5ivAaRAcNgjc7LEiWXFkMg703AqDDNx7kB1 +RDgLalLvrjOfOp2jsDfST7N1tKLBSQ9bMw9X4Jve+j7XXRUthcwuoYTeeo+Cy0/T +1Q78ctoX74E2nB958zwmtRykGrgE/6JAJDwGcgpY9kBPycGxTlCN926uGxHsDwVs +98cL6ZXptMLTR6T2XP36dAJZuOICSqmCSbFR8knc/gjUO36rXTxhwci8iDbmEVaf +BHpgBXGU5+SQ+QM++v6bHGf4LNQC5NZ4e4xvGax8ioYu/BRsB/T3Lx+RlItz4zdU +XuxCNcm3nhQV2ZHquRdbSdoyIxV5kJXel4wCmOhWIq7A2OBKdu5fQzIAzzLi65EN +RPAKsKB4h7hGgvciZQ7dsMrlGw0DLdJ6UrFyiR5Io7dXYT/+JP91lP5xsl6Lhg9O +FgALt7GSYRm2cZdgi9pO9rRr83Br1VjQT1vHz6yoZMXSqc4A2zcN2a2ZVq//rHvc +FZygs8miAhWPzqnpmgTj1cPiU1M= +-----END CERTIFICATE----- diff --git a/third_party/rust/warp/examples/tls/key.rsa b/third_party/rust/warp/examples/tls/key.rsa new file mode 100644 index 0000000000..b13bf5d07f --- /dev/null +++ b/third_party/rust/warp/examples/tls/key.rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAqVYYdfxTT9qr1np22UoIWq4v1E4cHncp35xxu4HNyZsoJBHR +K1gTvwh8x4LMe24lROW/LGWDRAyhaI8qDxxlitm0DPxU8p4iQoDQi3Z+oVKqsSwJ +pd3MRlu+4QFrveExwxgdahXvnhYgFJw5qG/IDWbQM0+ism/yRiXaxFNMI/kXe8FG ++JKSyJzR/yXPqM9ootgIzWxjmV50c+4eyr97DvbwAQcmHi3Ao96p4XoxzKlYWwE9 +TA+s0NvmCgYxOdjLEClP8YVKbvSpFMi4dHMZId86xYioeFbr7XPp+2njr9oyZjpd +Xa9Fy5UhwZZqCqh+nQk0m3XUC5pSu3ZrPLxNNQIDAQABAoIBAFKtZJgGsK6md4vq +kyiYSufrcBLaaEQ/rkQtYCJKyC0NAlZKFLRy9oEpJbNLm4cQSkYPXn3Qunx5Jj2k +2MYz+SgIDy7f7KHgr52Ew020dzNQ52JFvBgt6NTZaqL1TKOS1fcJSSNIvouTBerK +NCSXHzfb4P+MfEVe/w1c4ilE+kH9SzdEo2jK/sRbzHIY8TX0JbmQ4SCLLayr22YG +usIxtIYcWt3MMP/G2luRnYzzBCje5MXdpAhlHLi4TB6x4h5PmBKYc57uOVNngKLd +YyrQKcszW4Nx5v0a4HG3A5EtUXNCco1+5asXOg2lYphQYVh2R+1wgu5WiDjDVu+6 +EYgjFSkCgYEA0NBk6FDoxE/4L/4iJ4zIhu9BptN8Je/uS5c6wRejNC/VqQyw7SHb +hRFNrXPvq5Y+2bI/DxtdzZLKAMXOMjDjj0XEgfOIn2aveOo3uE7zf1i+njxwQhPu +uSYA9AlBZiKGr2PCYSDPnViHOspVJjxRuAgyWM1Qf+CTC0D95aj0oz8CgYEAz5n4 +Cb3/WfUHxMJLljJ7PlVmlQpF5Hk3AOR9+vtqTtdxRjuxW6DH2uAHBDdC3OgppUN4 +CFj55kzc2HUuiHtmPtx8mK6G+otT7Lww+nLSFL4PvZ6CYxqcio5MPnoYd+pCxrXY +JFo2W7e4FkBOxb5PF5So5plg+d0z/QiA7aFP1osCgYEAtgi1rwC5qkm8prn4tFm6 +hkcVCIXc+IWNS0Bu693bXKdGr7RsmIynff1zpf4ntYGpEMaeymClCY0ppDrMYlzU +RBYiFNdlBvDRj6s/H+FTzHRk2DT/99rAhY9nzVY0OQFoQIXK8jlURGrkmI/CYy66 +XqBmo5t4zcHM7kaeEBOWEKkCgYAYnO6VaRtPNQfYwhhoFFAcUc+5t+AVeHGW/4AY +M5qlAlIBu64JaQSI5KqwS0T4H+ZgG6Gti68FKPO+DhaYQ9kZdtam23pRVhd7J8y+ +xMI3h1kiaBqZWVxZ6QkNFzizbui/2mtn0/JB6YQ/zxwHwcpqx0tHG8Qtm5ZAV7PB +eLCYhQKBgQDALJxU/6hMTdytEU5CLOBSMby45YD/RrfQrl2gl/vA0etPrto4RkVq +UrkDO/9W4mZORClN3knxEFSTlYi8YOboxdlynpFfhcs82wFChs+Ydp1eEsVHAqtu +T+uzn0sroycBiBfVB949LExnzGDFUkhG0i2c2InarQYLTsIyHCIDEA== +-----END RSA PRIVATE KEY----- diff --git a/third_party/rust/warp/examples/todos.rs b/third_party/rust/warp/examples/todos.rs new file mode 100644 index 0000000000..d32db3a245 --- /dev/null +++ b/third_party/rust/warp/examples/todos.rs @@ -0,0 +1,295 @@ +#![deny(warnings)] + +use std::env; +use warp::Filter; + +/// Provides a RESTful web server managing some Todos. +/// +/// API will be: +/// +/// - `GET /todos`: return a JSON list of Todos. +/// - `POST /todos`: create a new Todo. +/// - `PUT /todos/:id`: update a specific Todo. +/// - `DELETE /todos/:id`: delete a specific Todo. +#[tokio::main] +async fn main() { + if env::var_os("RUST_LOG").is_none() { + // Set `RUST_LOG=todos=debug` to see debug logs, + // this only shows access logs. + env::set_var("RUST_LOG", "todos=info"); + } + pretty_env_logger::init(); + + let db = models::blank_db(); + + let api = filters::todos(db); + + // View access logs by setting `RUST_LOG=todos`. + let routes = api.with(warp::log("todos")); + // Start up the server... + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} + +mod filters { + use super::handlers; + use super::models::{Db, ListOptions, Todo}; + use warp::Filter; + + /// The 4 TODOs filters combined. + pub fn todos( + db: Db, + ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + todos_list(db.clone()) + .or(todos_create(db.clone())) + .or(todos_update(db.clone())) + .or(todos_delete(db)) + } + + /// GET /todos?offset=3&limit=5 + pub fn todos_list( + db: Db, + ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("todos") + .and(warp::get()) + .and(warp::query::<ListOptions>()) + .and(with_db(db)) + .and_then(handlers::list_todos) + } + + /// POST /todos with JSON body + pub fn todos_create( + db: Db, + ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("todos") + .and(warp::post()) + .and(json_body()) + .and(with_db(db)) + .and_then(handlers::create_todo) + } + + /// PUT /todos/:id with JSON body + pub fn todos_update( + db: Db, + ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("todos" / u64) + .and(warp::put()) + .and(json_body()) + .and(with_db(db)) + .and_then(handlers::update_todo) + } + + /// DELETE /todos/:id + pub fn todos_delete( + db: Db, + ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + // We'll make one of our endpoints admin-only to show how authentication filters are used + let admin_only = warp::header::exact("authorization", "Bearer admin"); + + warp::path!("todos" / u64) + // It is important to put the auth check _after_ the path filters. + // If we put the auth check before, the request `PUT /todos/invalid-string` + // would try this filter and reject because the authorization header doesn't match, + // rather because the param is wrong for that other path. + .and(admin_only) + .and(warp::delete()) + .and(with_db(db)) + .and_then(handlers::delete_todo) + } + + fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = std::convert::Infallible> + Clone { + warp::any().map(move || db.clone()) + } + + fn json_body() -> impl Filter<Extract = (Todo,), Error = warp::Rejection> + Clone { + // When accepting a body, we want a JSON body + // (and to reject huge payloads)... + warp::body::content_length_limit(1024 * 16).and(warp::body::json()) + } +} + +/// These are our API handlers, the ends of each filter chain. +/// Notice how thanks to using `Filter::and`, we can define a function +/// with the exact arguments we'd expect from each filter in the chain. +/// No tuples are needed, it's auto flattened for the functions. +mod handlers { + use super::models::{Db, ListOptions, Todo}; + use std::convert::Infallible; + use warp::http::StatusCode; + + pub async fn list_todos(opts: ListOptions, db: Db) -> Result<impl warp::Reply, Infallible> { + // Just return a JSON array of todos, applying the limit and offset. + let todos = db.lock().await; + let todos: Vec<Todo> = todos + .clone() + .into_iter() + .skip(opts.offset.unwrap_or(0)) + .take(opts.limit.unwrap_or(std::usize::MAX)) + .collect(); + Ok(warp::reply::json(&todos)) + } + + pub async fn create_todo(create: Todo, db: Db) -> Result<impl warp::Reply, Infallible> { + log::debug!("create_todo: {:?}", create); + + let mut vec = db.lock().await; + + for todo in vec.iter() { + if todo.id == create.id { + log::debug!(" -> id already exists: {}", create.id); + // Todo with id already exists, return `400 BadRequest`. + return Ok(StatusCode::BAD_REQUEST); + } + } + + // No existing Todo with id, so insert and return `201 Created`. + vec.push(create); + + Ok(StatusCode::CREATED) + } + + pub async fn update_todo( + id: u64, + update: Todo, + db: Db, + ) -> Result<impl warp::Reply, Infallible> { + log::debug!("update_todo: id={}, todo={:?}", id, update); + let mut vec = db.lock().await; + + // Look for the specified Todo... + for todo in vec.iter_mut() { + if todo.id == id { + *todo = update; + return Ok(StatusCode::OK); + } + } + + log::debug!(" -> todo id not found!"); + + // If the for loop didn't return OK, then the ID doesn't exist... + Ok(StatusCode::NOT_FOUND) + } + + pub async fn delete_todo(id: u64, db: Db) -> Result<impl warp::Reply, Infallible> { + log::debug!("delete_todo: id={}", id); + + let mut vec = db.lock().await; + + let len = vec.len(); + vec.retain(|todo| { + // Retain all Todos that aren't this id... + // In other words, remove all that *are* this id... + todo.id != id + }); + + // If the vec is smaller, we found and deleted a Todo! + let deleted = vec.len() != len; + + if deleted { + // respond with a `204 No Content`, which means successful, + // yet no body expected... + Ok(StatusCode::NO_CONTENT) + } else { + log::debug!(" -> todo id not found!"); + Ok(StatusCode::NOT_FOUND) + } + } +} + +mod models { + use serde_derive::{Deserialize, Serialize}; + use std::sync::Arc; + use tokio::sync::Mutex; + + /// So we don't have to tackle how different database work, we'll just use + /// a simple in-memory DB, a vector synchronized by a mutex. + pub type Db = Arc<Mutex<Vec<Todo>>>; + + pub fn blank_db() -> Db { + Arc::new(Mutex::new(Vec::new())) + } + + #[derive(Debug, Deserialize, Serialize, Clone)] + pub struct Todo { + pub id: u64, + pub text: String, + pub completed: bool, + } + + // The query parameters for list_todos. + #[derive(Debug, Deserialize)] + pub struct ListOptions { + pub offset: Option<usize>, + pub limit: Option<usize>, + } +} + +#[cfg(test)] +mod tests { + use warp::http::StatusCode; + use warp::test::request; + + use super::{ + filters, + models::{self, Todo}, + }; + + #[tokio::test] + async fn test_post() { + let db = models::blank_db(); + let api = filters::todos(db); + + let resp = request() + .method("POST") + .path("/todos") + .json(&Todo { + id: 1, + text: "test 1".into(), + completed: false, + }) + .reply(&api) + .await; + + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[tokio::test] + async fn test_post_conflict() { + let db = models::blank_db(); + db.lock().await.push(todo1()); + let api = filters::todos(db); + + let resp = request() + .method("POST") + .path("/todos") + .json(&todo1()) + .reply(&api) + .await; + + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn test_put_unknown() { + let _ = pretty_env_logger::try_init(); + let db = models::blank_db(); + let api = filters::todos(db); + + let resp = request() + .method("PUT") + .path("/todos/1") + .header("authorization", "Bearer admin") + .json(&todo1()) + .reply(&api) + .await; + + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + fn todo1() -> Todo { + Todo { + id: 1, + text: "test 1".into(), + completed: false, + } + } +} diff --git a/third_party/rust/warp/examples/unix_socket.rs b/third_party/rust/warp/examples/unix_socket.rs new file mode 100644 index 0000000000..951a28782e --- /dev/null +++ b/third_party/rust/warp/examples/unix_socket.rs @@ -0,0 +1,14 @@ +#![deny(warnings)] + +use tokio::net::UnixListener; + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + let mut listener = UnixListener::bind("/tmp/warp.sock").unwrap(); + let incoming = listener.incoming(); + warp::serve(warp::fs::dir("examples/dir")) + .run_incoming(incoming) + .await; +} diff --git a/third_party/rust/warp/examples/websockets.rs b/third_party/rust/warp/examples/websockets.rs new file mode 100644 index 0000000000..6387041f06 --- /dev/null +++ b/third_party/rust/warp/examples/websockets.rs @@ -0,0 +1,27 @@ +#![deny(warnings)] + +use futures::{FutureExt, StreamExt}; +use warp::Filter; + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + let routes = warp::path("echo") + // The `ws()` filter will prepare the Websocket handshake. + .and(warp::ws()) + .map(|ws: warp::ws::Ws| { + // And then our closure will be called when it completes... + ws.on_upgrade(|websocket| { + // Just echo all messages back... + let (tx, rx) = websocket.split(); + rx.forward(tx).map(|result| { + if let Err(e) = result { + eprintln!("websocket error: {:?}", e); + } + }) + }) + }); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/websockets_chat.rs b/third_party/rust/warp/examples/websockets_chat.rs new file mode 100644 index 0000000000..7f4e1d5513 --- /dev/null +++ b/third_party/rust/warp/examples/websockets_chat.rs @@ -0,0 +1,168 @@ +// #![deny(warnings)] +use std::collections::HashMap; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +use futures::{FutureExt, StreamExt}; +use tokio::sync::{mpsc, Mutex}; +use warp::ws::{Message, WebSocket}; +use warp::Filter; + +/// Our global unique user id counter. +static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); + +/// Our state of currently connected users. +/// +/// - Key is their id +/// - Value is a sender of `warp::ws::Message` +type Users = Arc<Mutex<HashMap<usize, mpsc::UnboundedSender<Result<Message, warp::Error>>>>>; + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + // Keep track of all connected users, key is usize, value + // is a websocket sender. + let users = Arc::new(Mutex::new(HashMap::new())); + // Turn our "state" into a new Filter... + let users = warp::any().map(move || users.clone()); + + // GET /chat -> websocket upgrade + let chat = warp::path("chat") + // The `ws()` filter will prepare Websocket handshake... + .and(warp::ws()) + .and(users) + .map(|ws: warp::ws::Ws, users| { + // This will call our function if the handshake succeeds. + ws.on_upgrade(move |socket| user_connected(socket, users)) + }); + + // GET / -> index html + let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML)); + + let routes = index.or(chat); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} + +async fn user_connected(ws: WebSocket, users: Users) { + // Use a counter to assign a new unique ID for this user. + let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); + + eprintln!("new chat user: {}", my_id); + + // Split the socket into a sender and receive of messages. + let (user_ws_tx, mut user_ws_rx) = ws.split(); + + // Use an unbounded channel to handle buffering and flushing of messages + // to the websocket... + let (tx, rx) = mpsc::unbounded_channel(); + tokio::task::spawn(rx.forward(user_ws_tx).map(|result| { + if let Err(e) = result { + eprintln!("websocket send error: {}", e); + } + })); + + // Save the sender in our list of connected users. + users.lock().await.insert(my_id, tx); + + // Return a `Future` that is basically a state machine managing + // this specific user's connection. + + // Make an extra clone to give to our disconnection handler... + let users2 = users.clone(); + + // Every time the user sends a message, broadcast it to + // all other users... + while let Some(result) = user_ws_rx.next().await { + let msg = match result { + Ok(msg) => msg, + Err(e) => { + eprintln!("websocket error(uid={}): {}", my_id, e); + break; + } + }; + user_message(my_id, msg, &users).await; + } + + // user_ws_rx stream will keep processing as long as the user stays + // connected. Once they disconnect, then... + user_disconnected(my_id, &users2).await; +} + +async fn user_message(my_id: usize, msg: Message, users: &Users) { + // Skip any non-Text messages... + let msg = if let Ok(s) = msg.to_str() { + s + } else { + return; + }; + + let new_msg = format!("<User#{}>: {}", my_id, msg); + + // New message from this user, send it to everyone else (except same uid)... + // + // We use `retain` instead of a for loop so that we can reap any user that + // appears to have disconnected. + for (&uid, tx) in users.lock().await.iter_mut() { + if my_id != uid { + if let Err(_disconnected) = tx.send(Ok(Message::text(new_msg.clone()))) { + // The tx is disconnected, our `user_disconnected` code + // should be happening in another task, nothing more to + // do here. + } + } + } +} + +async fn user_disconnected(my_id: usize, users: &Users) { + eprintln!("good bye user: {}", my_id); + + // Stream closed up, so remove from the user list + users.lock().await.remove(&my_id); +} + +static INDEX_HTML: &str = r#" +<!DOCTYPE html> +<html> + <head> + <title>Warp Chat</title> + </head> + <body> + <h1>warp chat</h1> + <div id="chat"> + <p><em>Connecting...</em></p> + </div> + <input type="text" id="text" /> + <button type="button" id="send">Send</button> + <script type="text/javascript"> + var uri = 'ws://' + location.host + '/chat'; + var ws = new WebSocket(uri); + + function message(data) { + var line = document.createElement('p'); + line.innerText = data; + chat.appendChild(line); + } + + ws.onopen = function() { + chat.innerHTML = "<p><em>Connected!</em></p>"; + } + + ws.onmessage = function(msg) { + message(msg.data); + }; + + send.onclick = function() { + var msg = text.value; + ws.send(msg); + text.value = ''; + + message('<You>: ' + msg); + }; + </script> + </body> +</html> +"#; diff --git a/third_party/rust/warp/src/error.rs b/third_party/rust/warp/src/error.rs new file mode 100644 index 0000000000..b17b8bc482 --- /dev/null +++ b/third_party/rust/warp/src/error.rs @@ -0,0 +1,69 @@ +use std::convert::Infallible; +use std::error::Error as StdError; +use std::fmt; + +type BoxError = Box<dyn std::error::Error + Send + Sync>; + +/// Errors that can happen inside warp. +pub struct Error { + inner: BoxError, +} + +impl Error { + pub(crate) fn new<E: Into<BoxError>>(err: E) -> Error { + Error { inner: err.into() } + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Skip showing worthless `Error { .. }` wrapper. + fmt::Debug::fmt(&self.inner, f) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.inner, f) + } +} + +impl StdError for Error {} + +impl From<Infallible> for Error { + fn from(infallible: Infallible) -> Error { + match infallible {} + } +} + +#[test] +fn error_size_of() { + assert_eq!( + ::std::mem::size_of::<Error>(), + ::std::mem::size_of::<usize>() * 2 + ); +} + +macro_rules! unit_error { + ( + $(#[$docs:meta])* + $pub:vis $typ:ident: $display:literal + ) => ( + $(#[$docs])* + $pub struct $typ { _p: (), } + + impl ::std::fmt::Debug for $typ { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_struct(stringify!($typ)).finish() + } + } + + impl ::std::fmt::Display for $typ { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.write_str($display) + } + } + + impl ::std::error::Error for $typ {} + ) +} diff --git a/third_party/rust/warp/src/filter/and.rs b/third_party/rust/warp/src/filter/and.rs new file mode 100644 index 0000000000..96973ad865 --- /dev/null +++ b/third_party/rust/warp/src/filter/and.rs @@ -0,0 +1,89 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::ready; +use pin_project::{pin_project, project}; + +use super::{Combine, Filter, FilterBase, HList, Internal, Tuple}; +use crate::reject::CombineRejection; + +#[derive(Clone, Copy, Debug)] +pub struct And<T, U> { + pub(super) first: T, + pub(super) second: U, +} + +impl<T, U> FilterBase for And<T, U> +where + T: Filter, + T::Extract: Send, + U: Filter + Clone + Send, + <T::Extract as Tuple>::HList: Combine<<U::Extract as Tuple>::HList> + Send, + <<<T::Extract as Tuple>::HList as Combine<<U::Extract as Tuple>::HList>>::Output as HList>::Tuple: Send, + U::Error: CombineRejection<T::Error>, +{ + type Extract = <<<T::Extract as Tuple>::HList as Combine<<U::Extract as Tuple>::HList>>::Output as HList>::Tuple; + type Error = <U::Error as CombineRejection<T::Error>>::One; + type Future = AndFuture<T, U>; + + fn filter(&self, _: Internal) -> Self::Future { + AndFuture { + state: State::First(self.first.filter(Internal), self.second.clone()), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct AndFuture<T: Filter, U: Filter> { + #[pin] + state: State<T, U>, +} + +#[pin_project] +enum State<T: Filter, U: Filter> { + First(#[pin] T::Future, U), + Second(Option<T::Extract>, #[pin] U::Future), + Done, +} + +impl<T, U> Future for AndFuture<T, U> +where + T: Filter, + U: Filter, + <T::Extract as Tuple>::HList: Combine<<U::Extract as Tuple>::HList> + Send, + U::Error: CombineRejection<T::Error>, +{ + type Output = Result< + <<<T::Extract as Tuple>::HList as Combine<<U::Extract as Tuple>::HList>>::Output as HList>::Tuple, + <U::Error as CombineRejection<T::Error>>::One>; + + #[project] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + loop { + let pin = self.as_mut().project(); + #[project] + let (ex1, fut2) = match pin.state.project() { + State::First(first, second) => match ready!(first.poll(cx)) { + Ok(first) => (first, second.filter(Internal)), + Err(err) => return Poll::Ready(Err(From::from(err))), + }, + State::Second(ex1, second) => { + let ex2 = match ready!(second.poll(cx)) { + Ok(second) => second, + Err(err) => return Poll::Ready(Err(From::from(err))), + }; + let ex3 = ex1.take().unwrap().hlist().combine(ex2.hlist()).flatten(); + self.set(AndFuture { state: State::Done }); + return Poll::Ready(Ok(ex3)); + } + State::Done => panic!("polled after complete"), + }; + + self.set(AndFuture { + state: State::Second(Some(ex1), fut2), + }); + } + } +} diff --git a/third_party/rust/warp/src/filter/and_then.rs b/third_party/rust/warp/src/filter/and_then.rs new file mode 100644 index 0000000000..535053c17e --- /dev/null +++ b/third_party/rust/warp/src/filter/and_then.rs @@ -0,0 +1,99 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::{ready, TryFuture}; +use pin_project::{pin_project, project}; + +use super::{Filter, FilterBase, Func, Internal}; +use crate::reject::CombineRejection; + +#[derive(Clone, Copy, Debug)] +pub struct AndThen<T, F> { + pub(super) filter: T, + pub(super) callback: F, +} + +impl<T, F> FilterBase for AndThen<T, F> +where + T: Filter, + F: Func<T::Extract> + Clone + Send, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: CombineRejection<T::Error>, +{ + type Extract = (<F::Output as TryFuture>::Ok,); + type Error = <<F::Output as TryFuture>::Error as CombineRejection<T::Error>>::One; + type Future = AndThenFuture<T, F>; + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + AndThenFuture { + state: State::First(self.filter.filter(Internal), self.callback.clone()), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct AndThenFuture<T: Filter, F> +where + T: Filter, + F: Func<T::Extract>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: CombineRejection<T::Error>, +{ + #[pin] + state: State<T, F>, +} + +#[pin_project] +enum State<T, F> +where + T: Filter, + F: Func<T::Extract>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: CombineRejection<T::Error>, +{ + First(#[pin] T::Future, F), + Second(#[pin] F::Output), + Done, +} + +impl<T, F> Future for AndThenFuture<T, F> +where + T: Filter, + F: Func<T::Extract>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: CombineRejection<T::Error>, +{ + type Output = Result< + (<F::Output as TryFuture>::Ok,), + <<F::Output as TryFuture>::Error as CombineRejection<T::Error>>::One, + >; + + #[project] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + loop { + let pin = self.as_mut().project(); + #[project] + let (ex1, second) = match pin.state.project() { + State::First(first, second) => match ready!(first.try_poll(cx)) { + Ok(first) => (first, second), + Err(err) => return Poll::Ready(Err(From::from(err))), + }, + State::Second(second) => { + let ex3 = match ready!(second.try_poll(cx)) { + Ok(item) => Ok((item,)), + Err(err) => Err(From::from(err)), + }; + self.set(AndThenFuture { state: State::Done }); + return Poll::Ready(ex3); + } + State::Done => panic!("polled after complete"), + }; + let fut2 = second.call(ex1); + self.set(AndThenFuture { + state: State::Second(fut2), + }); + } + } +} diff --git a/third_party/rust/warp/src/filter/boxed.rs b/third_party/rust/warp/src/filter/boxed.rs new file mode 100644 index 0000000000..322262cf60 --- /dev/null +++ b/third_party/rust/warp/src/filter/boxed.rs @@ -0,0 +1,100 @@ +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use futures::TryFutureExt; + +use super::{Filter, FilterBase, Internal, Tuple}; +use crate::reject::Rejection; + +/// A type representing a boxed `Filter` trait object. +/// +/// The filter inside is a dynamic trait object. The purpose of this type is +/// to ease returning `Filter`s from other functions. +/// +/// To create one, call `Filter::boxed` on any filter. +/// +/// # Examples +/// +/// ``` +/// use warp::{Filter, filters::BoxedFilter, Reply}; +/// +/// pub fn assets_filter() -> BoxedFilter<(impl Reply,)> { +/// warp::path("assets") +/// .and(warp::fs::dir("./assets")) +/// .boxed() +/// } +/// ``` +/// +pub struct BoxedFilter<T: Tuple> { + filter: Arc< + dyn Filter< + Extract = T, + Error = Rejection, + Future = Pin<Box<dyn Future<Output = Result<T, Rejection>> + Send>>, + > + Send + + Sync, + >, +} + +impl<T: Tuple + Send> BoxedFilter<T> { + pub(super) fn new<F>(filter: F) -> BoxedFilter<T> + where + F: Filter<Extract = T> + Send + Sync + 'static, + F::Error: Into<Rejection>, + { + BoxedFilter { + filter: Arc::new(BoxingFilter { + filter: filter.map_err(super::Internal, Into::into), + }), + } + } +} + +impl<T: Tuple> Clone for BoxedFilter<T> { + fn clone(&self) -> BoxedFilter<T> { + BoxedFilter { + filter: self.filter.clone(), + } + } +} + +impl<T: Tuple> fmt::Debug for BoxedFilter<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("BoxedFilter").finish() + } +} + +fn _assert_send() { + fn _assert<T: Send>() {} + _assert::<BoxedFilter<()>>(); +} + +impl<T: Tuple + Send> FilterBase for BoxedFilter<T> { + type Extract = T; + type Error = Rejection; + type Future = Pin<Box<dyn Future<Output = Result<T, Rejection>> + Send>>; + + fn filter(&self, _: Internal) -> Self::Future { + self.filter.filter(Internal) + } +} + +struct BoxingFilter<F> { + filter: F, +} + +impl<F> FilterBase for BoxingFilter<F> +where + F: Filter, + F::Future: Send + 'static, +{ + type Extract = F::Extract; + type Error = F::Error; + type Future = Pin<Box<dyn Future<Output = Result<Self::Extract, Self::Error>> + Send>>; + + fn filter(&self, _: Internal) -> Self::Future { + Box::pin(self.filter.filter(Internal).into_future()) + } +} diff --git a/third_party/rust/warp/src/filter/map.rs b/third_party/rust/warp/src/filter/map.rs new file mode 100644 index 0000000000..4bacba9e28 --- /dev/null +++ b/third_party/rust/warp/src/filter/map.rs @@ -0,0 +1,59 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::{ready, TryFuture}; +use pin_project::pin_project; + +use super::{Filter, FilterBase, Func, Internal}; + +#[derive(Clone, Copy, Debug)] +pub struct Map<T, F> { + pub(super) filter: T, + pub(super) callback: F, +} + +impl<T, F> FilterBase for Map<T, F> +where + T: Filter, + F: Func<T::Extract> + Clone + Send, +{ + type Extract = (F::Output,); + type Error = T::Error; + type Future = MapFuture<T, F>; + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + MapFuture { + extract: self.filter.filter(Internal), + callback: self.callback.clone(), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct MapFuture<T: Filter, F> { + #[pin] + extract: T::Future, + callback: F, +} + +impl<T, F> Future for MapFuture<T, F> +where + T: Filter, + F: Func<T::Extract>, +{ + type Output = Result<(F::Output,), T::Error>; + + #[inline] + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + let pin = self.project(); + match ready!(pin.extract.try_poll(cx)) { + Ok(ex) => { + let ex = (pin.callback.call(ex),); + Poll::Ready(Ok(ex)) + } + Err(err) => Poll::Ready(Err(err)), + } + } +} diff --git a/third_party/rust/warp/src/filter/map_err.rs b/third_party/rust/warp/src/filter/map_err.rs new file mode 100644 index 0000000000..3d3e6c3047 --- /dev/null +++ b/third_party/rust/warp/src/filter/map_err.rs @@ -0,0 +1,58 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::TryFuture; +use pin_project::pin_project; + +use super::{Filter, FilterBase, Internal}; +use crate::reject::IsReject; + +#[derive(Clone, Copy, Debug)] +pub struct MapErr<T, F> { + pub(super) filter: T, + pub(super) callback: F, +} + +impl<T, F, E> FilterBase for MapErr<T, F> +where + T: Filter, + F: Fn(T::Error) -> E + Clone + Send, + E: IsReject, +{ + type Extract = T::Extract; + type Error = E; + type Future = MapErrFuture<T, F>; + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + MapErrFuture { + extract: self.filter.filter(Internal), + callback: self.callback.clone(), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct MapErrFuture<T: Filter, F> { + #[pin] + extract: T::Future, + callback: F, +} + +impl<T, F, E> Future for MapErrFuture<T, F> +where + T: Filter, + F: Fn(T::Error) -> E, +{ + type Output = Result<T::Extract, E>; + + #[inline] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + self.as_mut() + .project() + .extract + .try_poll(cx) + .map_err(|err| (self.callback)(err)) + } +} diff --git a/third_party/rust/warp/src/filter/mod.rs b/third_party/rust/warp/src/filter/mod.rs new file mode 100644 index 0000000000..f4818d9607 --- /dev/null +++ b/third_party/rust/warp/src/filter/mod.rs @@ -0,0 +1,462 @@ +mod and; +mod and_then; +mod boxed; +mod map; +mod map_err; +mod or; +mod or_else; +mod recover; +pub(crate) mod service; +mod unify; +mod untuple_one; +mod wrap; + +use std::future::Future; +use std::pin::Pin; + +use futures::{future, TryFuture, TryFutureExt}; + +pub(crate) use crate::generic::{one, Combine, Either, Func, HList, One, Tuple}; +use crate::reject::{CombineRejection, IsReject, Rejection}; +use crate::route::{self, Route}; + +pub(crate) use self::and::And; +use self::and_then::AndThen; +pub use self::boxed::BoxedFilter; +pub(crate) use self::map::Map; +pub(crate) use self::map_err::MapErr; +pub(crate) use self::or::Or; +use self::or_else::OrElse; +use self::recover::Recover; +use self::unify::Unify; +use self::untuple_one::UntupleOne; +pub(crate) use self::wrap::{Wrap, WrapSealed}; + +// A crate-private base trait, allowing the actual `filter` method to change +// signatures without it being a breaking change. +pub trait FilterBase { + type Extract: Tuple; // + Send; + type Error: IsReject; + type Future: Future<Output = Result<Self::Extract, Self::Error>> + Send; + + fn filter(&self, internal: Internal) -> Self::Future; + + fn map_err<F, E>(self, _internal: Internal, fun: F) -> MapErr<Self, F> + where + Self: Sized, + F: Fn(Self::Error) -> E + Clone, + E: ::std::fmt::Debug + Send, + { + MapErr { + filter: self, + callback: fun, + } + } +} + +// A crate-private argument to prevent users from calling methods on +// the `FilterBase` trait. +// +// For instance, this innocent user code could otherwise call `filter`: +// +// ``` +// async fn with_filter<F: Filter>(f: F) -> Result<F::Extract, F::Error> { +// f.filter().await +// } +// ``` +#[allow(missing_debug_implementations)] +pub struct Internal; + +/// Composable request filters. +/// +/// A `Filter` can optionally extract some data from a request, combine +/// it with others, mutate it, and return back some value as a reply. The +/// power of `Filter`s come from being able to isolate small subsets, and then +/// chain and reuse them in various parts of your app. +/// +/// # Extracting Tuples +/// +/// You may notice that several of these filters extract some tuple, often +/// times a tuple of just 1 item! Why? +/// +/// If a filter extracts a `(String,)`, that simply means that it +/// extracts a `String`. If you were to `map` the filter, the argument type +/// would be exactly that, just a `String`. +/// +/// What is it? It's just some type magic that allows for automatic combining +/// and flattening of tuples. Without it, combining two filters together with +/// `and`, where one extracted `()`, and another `String`, would mean the +/// `map` would be given a single argument of `((), String,)`, which is just +/// no fun. +pub trait Filter: FilterBase { + /// Composes a new `Filter` that requires both this and the other to filter a request. + /// + /// Additionally, this will join together the extracted values of both + /// filters, so that `map` and `and_then` receive them as separate arguments. + /// + /// If a `Filter` extracts nothing (so, `()`), combining with any other + /// filter will simply discard the `()`. If a `Filter` extracts one or + /// more items, combining will mean it extracts the values of itself + /// combined with the other. + /// + /// # Example + /// + /// ``` + /// use warp::Filter; + /// + /// // Match `/hello/:name`... + /// warp::path("hello") + /// .and(warp::path::param::<String>()); + /// ``` + fn and<F>(self, other: F) -> And<Self, F> + where + Self: Sized, + <Self::Extract as Tuple>::HList: Combine<<F::Extract as Tuple>::HList>, + F: Filter + Clone, + F::Error: CombineRejection<Self::Error>, + { + And { + first: self, + second: other, + } + } + + /// Composes a new `Filter` of either this or the other filter. + /// + /// # Example + /// + /// ``` + /// use std::net::SocketAddr; + /// use warp::Filter; + /// + /// // Match either `/:u32` or `/:socketaddr` + /// warp::path::param::<u32>() + /// .or(warp::path::param::<SocketAddr>()); + /// ``` + fn or<F>(self, other: F) -> Or<Self, F> + where + Self: Filter<Error = Rejection> + Sized, + F: Filter, + F::Error: CombineRejection<Self::Error>, + { + Or { + first: self, + second: other, + } + } + + /// Composes this `Filter` with a function receiving the extracted value. + /// + /// + /// # Example + /// + /// ``` + /// use warp::Filter; + /// + /// // Map `/:id` + /// warp::path::param().map(|id: u64| { + /// format!("Hello #{}", id) + /// }); + /// ``` + /// + /// # `Func` + /// + /// The generic `Func` trait is implemented for any function that receives + /// the same arguments as this `Filter` extracts. In practice, this + /// shouldn't ever bother you, and simply makes things feel more natural. + /// + /// For example, if three `Filter`s were combined together, suppose one + /// extracts nothing (so `()`), and the other two extract two integers, + /// a function that accepts exactly two integer arguments is allowed. + /// Specifically, any `Fn(u32, u32)`. + /// + /// Without `Product` and `Func`, this would be a lot messier. First of + /// all, the `()`s couldn't be discarded, and the tuples would be nested. + /// So, instead, you'd need to pass an `Fn(((), (u32, u32)))`. That's just + /// a single argument. Bleck! + /// + /// Even worse, the tuples would shuffle the types around depending on + /// the exact invocation of `and`s. So, `unit.and(int).and(int)` would + /// result in a different extracted type from `unit.and(int.and(int)`, + /// or from `int.and(unit).and(int)`. If you changed around the order + /// of filters, while still having them be semantically equivalent, you'd + /// need to update all your `map`s as well. + /// + /// `Product`, `HList`, and `Func` do all the heavy work so that none of + /// this is a bother to you. What's more, the types are enforced at + /// compile-time, and tuple flattening is optimized away to nothing by + /// LLVM. + fn map<F>(self, fun: F) -> Map<Self, F> + where + Self: Sized, + F: Func<Self::Extract> + Clone, + { + Map { + filter: self, + callback: fun, + } + } + + /// Composes this `Filter` with a function receiving the extracted value. + /// + /// The function should return some `TryFuture` type. + /// + /// The `Error` type of the return `Future` needs be a `Rejection`, which + /// means most futures will need to have their error mapped into one. + /// + /// # Example + /// + /// ``` + /// use warp::Filter; + /// + /// // Validate after `/:id` + /// warp::path::param().and_then(|id: u64| async move { + /// if id != 0 { + /// Ok(format!("Hello #{}", id)) + /// } else { + /// Err(warp::reject::not_found()) + /// } + /// }); + /// ``` + fn and_then<F>(self, fun: F) -> AndThen<Self, F> + where + Self: Sized, + F: Func<Self::Extract> + Clone, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: CombineRejection<Self::Error>, + { + AndThen { + filter: self, + callback: fun, + } + } + + /// Compose this `Filter` with a function receiving an error. + /// + /// The function should return some `TryFuture` type yielding the + /// same item and error types. + fn or_else<F>(self, fun: F) -> OrElse<Self, F> + where + Self: Filter<Error = Rejection> + Sized, + F: Func<Rejection>, + F::Output: TryFuture<Ok = Self::Extract> + Send, + <F::Output as TryFuture>::Error: IsReject, + { + OrElse { + filter: self, + callback: fun, + } + } + + /// Compose this `Filter` with a function receiving an error and + /// returning a *new* type, instead of the *same* type. + /// + /// This is useful for "customizing" rejections into new response types. + /// See also the [rejections example][ex]. + /// + /// [ex]: https://github.com/seanmonstar/warp/blob/master/examples/rejections.rs + fn recover<F>(self, fun: F) -> Recover<Self, F> + where + Self: Filter<Error = Rejection> + Sized, + F: Func<Rejection>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: IsReject, + { + Recover { + filter: self, + callback: fun, + } + } + + /// Unifies the extracted value of `Filter`s composed with `or`. + /// + /// When a `Filter` extracts some `Either<T, T>`, where both sides + /// are the same type, this combinator can be used to grab the + /// inner value, regardless of which side of `Either` it was. This + /// is useful for values that could be extracted from multiple parts + /// of a request, and the exact place isn't important. + /// + /// # Example + /// + /// ```rust + /// use std::net::SocketAddr; + /// use warp::Filter; + /// + /// let client_ip = warp::header("x-real-ip") + /// .or(warp::header("x-forwarded-for")) + /// .unify() + /// .map(|ip: SocketAddr| { + /// // Get the IP from either header, + /// // and unify into the inner type. + /// }); + /// ``` + fn unify<T>(self) -> Unify<Self> + where + Self: Filter<Extract = (Either<T, T>,)> + Sized, + T: Tuple, + { + Unify { filter: self } + } + + /// Convenience method to remove one layer of tupling. + /// + /// This is useful for when things like `map` don't return a new value, + /// but just `()`, since warp will wrap it up into a `((),)`. + /// + /// # Example + /// + /// ``` + /// use warp::Filter; + /// + /// let route = warp::path::param() + /// .map(|num: u64| { + /// println!("just logging: {}", num); + /// // returning "nothing" + /// }) + /// .untuple_one() + /// .map(|| { + /// println!("the ((),) was removed"); + /// warp::reply() + /// }); + /// ``` + /// + /// ``` + /// use warp::Filter; + /// + /// let route = warp::any() + /// .map(|| { + /// // wanting to return a tuple + /// (true, 33) + /// }) + /// .untuple_one() + /// .map(|is_enabled: bool, count: i32| { + /// println!("untupled: ({}, {})", is_enabled, count); + /// }); + /// ``` + fn untuple_one<T>(self) -> UntupleOne<Self> + where + Self: Filter<Extract = (T,)> + Sized, + T: Tuple, + { + UntupleOne { filter: self } + } + + /// Wraps the current filter with some wrapper. + /// + /// The wrapper may do some preparation work before starting this filter, + /// and may do post-processing after the filter completes. + /// + /// # Example + /// + /// ``` + /// use warp::Filter; + /// + /// let route = warp::any() + /// .map(warp::reply); + /// + /// // Wrap the route with a log wrapper. + /// let route = route.with(warp::log("example")); + /// ``` + fn with<W>(self, wrapper: W) -> W::Wrapped + where + Self: Sized, + W: Wrap<Self>, + { + wrapper.wrap(self) + } + + /// Boxes this filter into a trait object, making it easier to name the type. + /// + /// # Example + /// + /// ``` + /// use warp::Filter; + /// + /// fn impl_reply() -> warp::filters::BoxedFilter<(impl warp::Reply,)> { + /// warp::any() + /// .map(warp::reply) + /// .boxed() + /// } + /// + /// fn named_i32() -> warp::filters::BoxedFilter<(i32,)> { + /// warp::path::param::<i32>() + /// .boxed() + /// } + /// + /// fn named_and() -> warp::filters::BoxedFilter<(i32, String)> { + /// warp::path::param::<i32>() + /// .and(warp::header::<String>("host")) + /// .boxed() + /// } + /// ``` + fn boxed(self) -> BoxedFilter<Self::Extract> + where + Self: Sized + Send + Sync + 'static, + Self::Extract: Send, + Self::Error: Into<Rejection>, + { + BoxedFilter::new(self) + } +} + +impl<T: FilterBase> Filter for T {} + +pub trait FilterClone: Filter + Clone {} + +impl<T: Filter + Clone> FilterClone for T {} + +fn _assert_object_safe() { + fn _assert(_f: &dyn Filter<Extract = (), Error = (), Future = future::Ready<()>>) {} +} + +// ===== FilterFn ===== + +pub(crate) fn filter_fn<F, U>(func: F) -> FilterFn<F> +where + F: Fn(&mut Route) -> U, + U: TryFuture, + U::Ok: Tuple, + U::Error: IsReject, +{ + FilterFn { func } +} + +pub(crate) fn filter_fn_one<F, U>( + func: F, +) -> FilterFn<impl Fn(&mut Route) -> future::MapOk<U, fn(U::Ok) -> (U::Ok,)> + Copy> +where + F: Fn(&mut Route) -> U + Copy, + U: TryFuture, + U::Error: IsReject, +{ + filter_fn(move |route| func(route).map_ok(tup_one as _)) +} + +fn tup_one<T>(item: T) -> (T,) { + (item,) +} + +#[derive(Copy, Clone)] +#[allow(missing_debug_implementations)] +pub(crate) struct FilterFn<F> { + // TODO: could include a `debug_str: &'static str` to be used in Debug impl + func: F, +} + +impl<F, U> FilterBase for FilterFn<F> +where + F: Fn(&mut Route) -> U, + U: TryFuture + Send + 'static, + U::Ok: Tuple + Send, + U::Error: IsReject, +{ + type Extract = U::Ok; + type Error = U::Error; + type Future = + Pin<Box<dyn Future<Output = Result<Self::Extract, Self::Error>> + Send + 'static>>; + + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + Box::pin(route::with(|route| (self.func)(route)).into_future()) + } +} diff --git a/third_party/rust/warp/src/filter/or.rs b/third_party/rust/warp/src/filter/or.rs new file mode 100644 index 0000000000..b091282206 --- /dev/null +++ b/third_party/rust/warp/src/filter/or.rs @@ -0,0 +1,112 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::{ready, TryFuture}; +use pin_project::{pin_project, project}; + +use super::{Filter, FilterBase, Internal}; +use crate::generic::Either; +use crate::reject::CombineRejection; +use crate::route; + +type Combined<E1, E2> = <E1 as CombineRejection<E2>>::Combined; + +#[derive(Clone, Copy, Debug)] +pub struct Or<T, U> { + pub(super) first: T, + pub(super) second: U, +} + +impl<T, U> FilterBase for Or<T, U> +where + T: Filter, + U: Filter + Clone + Send, + U::Error: CombineRejection<T::Error>, +{ + type Extract = (Either<T::Extract, U::Extract>,); + //type Error = <U::Error as CombineRejection<T::Error>>::Combined; + type Error = Combined<U::Error, T::Error>; + type Future = EitherFuture<T, U>; + + fn filter(&self, _: Internal) -> Self::Future { + let idx = route::with(|route| route.matched_path_index()); + EitherFuture { + state: State::First(self.first.filter(Internal), self.second.clone()), + original_path_index: PathIndex(idx), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct EitherFuture<T: Filter, U: Filter> { + #[pin] + state: State<T, U>, + original_path_index: PathIndex, +} + +#[pin_project] +enum State<T: Filter, U: Filter> { + First(#[pin] T::Future, U), + Second(Option<T::Error>, #[pin] U::Future), + Done, +} + +#[derive(Copy, Clone)] +struct PathIndex(usize); + +impl PathIndex { + fn reset_path(&self) { + route::with(|route| route.reset_matched_path_index(self.0)); + } +} + +impl<T, U> Future for EitherFuture<T, U> +where + T: Filter, + U: Filter, + U::Error: CombineRejection<T::Error>, +{ + type Output = Result<(Either<T::Extract, U::Extract>,), Combined<U::Error, T::Error>>; + + #[project] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + loop { + let pin = self.as_mut().project(); + #[project] + let (err1, fut2) = match pin.state.project() { + State::First(first, second) => match ready!(first.try_poll(cx)) { + Ok(ex1) => { + return Poll::Ready(Ok((Either::A(ex1),))); + } + Err(e) => { + pin.original_path_index.reset_path(); + (e, second.filter(Internal)) + } + }, + State::Second(err1, second) => { + let ex2 = match ready!(second.try_poll(cx)) { + Ok(ex2) => Ok((Either::B(ex2),)), + Err(e) => { + pin.original_path_index.reset_path(); + let err1 = err1.take().expect("polled after complete"); + Err(e.combine(err1)) + } + }; + self.set(EitherFuture { + state: State::Done, + ..*self + }); + return Poll::Ready(ex2); + } + State::Done => panic!("polled after complete"), + }; + + self.set(EitherFuture { + state: State::Second(Some(err1), fut2), + ..*self + }); + } + } +} diff --git a/third_party/rust/warp/src/filter/or_else.rs b/third_party/rust/warp/src/filter/or_else.rs new file mode 100644 index 0000000000..da43ea0eac --- /dev/null +++ b/third_party/rust/warp/src/filter/or_else.rs @@ -0,0 +1,109 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::{ready, TryFuture}; +use pin_project::{pin_project, project}; + +use super::{Filter, FilterBase, Func, Internal}; +use crate::reject::IsReject; +use crate::route; + +#[derive(Clone, Copy, Debug)] +pub struct OrElse<T, F> { + pub(super) filter: T, + pub(super) callback: F, +} + +impl<T, F> FilterBase for OrElse<T, F> +where + T: Filter, + F: Func<T::Error> + Clone + Send, + F::Output: TryFuture<Ok = T::Extract> + Send, + <F::Output as TryFuture>::Error: IsReject, +{ + type Extract = <F::Output as TryFuture>::Ok; + type Error = <F::Output as TryFuture>::Error; + type Future = OrElseFuture<T, F>; + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + let idx = route::with(|route| route.matched_path_index()); + OrElseFuture { + state: State::First(self.filter.filter(Internal), self.callback.clone()), + original_path_index: PathIndex(idx), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct OrElseFuture<T: Filter, F> +where + T: Filter, + F: Func<T::Error>, + F::Output: TryFuture<Ok = T::Extract> + Send, +{ + #[pin] + state: State<T, F>, + original_path_index: PathIndex, +} + +#[pin_project] +enum State<T, F> +where + T: Filter, + F: Func<T::Error>, + F::Output: TryFuture<Ok = T::Extract> + Send, +{ + First(#[pin] T::Future, F), + Second(#[pin] F::Output), + Done, +} + +#[derive(Copy, Clone)] +struct PathIndex(usize); + +impl PathIndex { + fn reset_path(&self) { + route::with(|route| route.reset_matched_path_index(self.0)); + } +} + +impl<T, F> Future for OrElseFuture<T, F> +where + T: Filter, + F: Func<T::Error>, + F::Output: TryFuture<Ok = T::Extract> + Send, +{ + type Output = Result<<F::Output as TryFuture>::Ok, <F::Output as TryFuture>::Error>; + + #[project] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + loop { + let pin = self.as_mut().project(); + #[project] + let (err, second) = match pin.state.project() { + State::First(first, second) => match ready!(first.try_poll(cx)) { + Ok(ex) => return Poll::Ready(Ok(ex)), + Err(err) => (err, second), + }, + State::Second(second) => { + let ex2 = ready!(second.try_poll(cx)); + self.set(OrElseFuture { + state: State::Done, + ..*self + }); + return Poll::Ready(ex2); + } + State::Done => panic!("polled after complete"), + }; + + pin.original_path_index.reset_path(); + let fut2 = second.call(err); + self.set(OrElseFuture { + state: State::Second(fut2), + ..*self + }); + } + } +} diff --git a/third_party/rust/warp/src/filter/recover.rs b/third_party/rust/warp/src/filter/recover.rs new file mode 100644 index 0000000000..7f9108105d --- /dev/null +++ b/third_party/rust/warp/src/filter/recover.rs @@ -0,0 +1,119 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::{ready, TryFuture}; +use pin_project::{pin_project, project}; + +use super::{Filter, FilterBase, Func, Internal}; +use crate::generic::Either; +use crate::reject::IsReject; +use crate::route; + +#[derive(Clone, Copy, Debug)] +pub struct Recover<T, F> { + pub(super) filter: T, + pub(super) callback: F, +} + +impl<T, F> FilterBase for Recover<T, F> +where + T: Filter, + F: Func<T::Error> + Clone + Send, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: IsReject, +{ + type Extract = (Either<T::Extract, (<F::Output as TryFuture>::Ok,)>,); + type Error = <F::Output as TryFuture>::Error; + type Future = RecoverFuture<T, F>; + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + let idx = route::with(|route| route.matched_path_index()); + RecoverFuture { + state: State::First(self.filter.filter(Internal), self.callback.clone()), + original_path_index: PathIndex(idx), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct RecoverFuture<T: Filter, F> +where + T: Filter, + F: Func<T::Error>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: IsReject, +{ + #[pin] + state: State<T, F>, + original_path_index: PathIndex, +} + +#[pin_project] +enum State<T, F> +where + T: Filter, + F: Func<T::Error>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: IsReject, +{ + First(#[pin] T::Future, F), + Second(#[pin] F::Output), + Done, +} + +#[derive(Copy, Clone)] +struct PathIndex(usize); + +impl PathIndex { + fn reset_path(&self) { + route::with(|route| route.reset_matched_path_index(self.0)); + } +} + +impl<T, F> Future for RecoverFuture<T, F> +where + T: Filter, + F: Func<T::Error>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: IsReject, +{ + type Output = Result< + (Either<T::Extract, (<F::Output as TryFuture>::Ok,)>,), + <F::Output as TryFuture>::Error, + >; + + #[project] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + loop { + let pin = self.as_mut().project(); + #[project] + let (err, second) = match pin.state.project() { + State::First(first, second) => match ready!(first.try_poll(cx)) { + Ok(ex) => return Poll::Ready(Ok((Either::A(ex),))), + Err(err) => (err, second), + }, + State::Second(second) => { + let ex2 = match ready!(second.try_poll(cx)) { + Ok(ex2) => Ok((Either::B((ex2,)),)), + Err(e) => Err(e), + }; + self.set(RecoverFuture { + state: State::Done, + ..*self + }); + return Poll::Ready(ex2); + } + State::Done => panic!("polled after complete"), + }; + + pin.original_path_index.reset_path(); + let fut2 = second.call(err); + self.set(RecoverFuture { + state: State::Second(fut2), + ..*self + }); + } + } +} diff --git a/third_party/rust/warp/src/filter/service.rs b/third_party/rust/warp/src/filter/service.rs new file mode 100644 index 0000000000..c2dea4ee17 --- /dev/null +++ b/third_party/rust/warp/src/filter/service.rs @@ -0,0 +1,137 @@ +use std::convert::Infallible; +use std::future::Future; +use std::net::SocketAddr; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::future::TryFuture; +use hyper::service::Service; +use pin_project::pin_project; + +use crate::reject::IsReject; +use crate::reply::{Reply, Response}; +use crate::route::{self, Route}; +use crate::{Filter, Request}; + +/// Convert a `Filter` into a `Service`. +/// +/// Filters are normally what APIs are built on in warp. However, it can be +/// useful to convert a `Filter` into a [`Service`][Service], such as if +/// further customizing a `hyper::Service`, or if wanting to make use of +/// the greater [Tower][tower] set of middleware. +/// +/// # Example +/// +/// Running a `warp::Filter` on a regular `hyper::Server`: +/// +/// ``` +/// # async fn run() -> Result<(), Box<dyn std::error::Error>> { +/// use std::convert::Infallible; +/// use warp::Filter; +/// +/// // Our Filter... +/// let route = warp::any().map(|| "Hello From Warp!"); +/// +/// // Convert it into a `Service`... +/// let svc = warp::service(route); +/// +/// // Typical hyper setup... +/// let make_svc = hyper::service::make_service_fn(move |_| async move { +/// Ok::<_, Infallible>(svc) +/// }); +/// +/// hyper::Server::bind(&([127, 0, 0, 1], 3030).into()) +/// .serve(make_svc) +/// .await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// [Service]: https://docs.rs/hyper/0.13.*/hyper/service/trait.Service.html +/// [tower]: https://docs.rs/tower +pub fn service<F>(filter: F) -> FilteredService<F> +where + F: Filter, + <F::Future as TryFuture>::Ok: Reply, + <F::Future as TryFuture>::Error: IsReject, +{ + FilteredService { filter } +} + +#[derive(Copy, Clone, Debug)] +pub struct FilteredService<F> { + filter: F, +} + +impl<F> FilteredService<F> +where + F: Filter, + <F::Future as TryFuture>::Ok: Reply, + <F::Future as TryFuture>::Error: IsReject, +{ + #[inline] + pub(crate) fn call_with_addr( + &self, + req: Request, + remote_addr: Option<SocketAddr>, + ) -> FilteredFuture<F::Future> { + debug_assert!(!route::is_set(), "nested route::set calls"); + + let route = Route::new(req, remote_addr); + let fut = route::set(&route, || self.filter.filter(super::Internal)); + FilteredFuture { future: fut, route } + } +} + +impl<F> Service<Request> for FilteredService<F> +where + F: Filter, + <F::Future as TryFuture>::Ok: Reply, + <F::Future as TryFuture>::Error: IsReject, +{ + type Response = Response; + type Error = Infallible; + type Future = FilteredFuture<F::Future>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, req: Request) -> Self::Future { + self.call_with_addr(req, None) + } +} + +#[pin_project] +#[derive(Debug)] +pub struct FilteredFuture<F> { + #[pin] + future: F, + route: ::std::cell::RefCell<Route>, +} + +impl<F> Future for FilteredFuture<F> +where + F: TryFuture, + F::Ok: Reply, + F::Error: IsReject, +{ + type Output = Result<Response, Infallible>; + + #[inline] + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + debug_assert!(!route::is_set(), "nested route::set calls"); + + let pin = self.project(); + let fut = pin.future; + match route::set(&pin.route, || fut.try_poll(cx)) { + Poll::Ready(Ok(ok)) => Poll::Ready(Ok(ok.into_response())), + Poll::Pending => Poll::Pending, + Poll::Ready(Err(err)) => { + log::debug!("rejected: {:?}", err); + Poll::Ready(Ok(err.into_response())) + } + } + } +} diff --git a/third_party/rust/warp/src/filter/unify.rs b/third_party/rust/warp/src/filter/unify.rs new file mode 100644 index 0000000000..cdd1361080 --- /dev/null +++ b/third_party/rust/warp/src/filter/unify.rs @@ -0,0 +1,53 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::{ready, TryFuture}; +use pin_project::pin_project; + +use super::{Either, Filter, FilterBase, Internal, Tuple}; + +#[derive(Clone, Copy, Debug)] +pub struct Unify<F> { + pub(super) filter: F, +} + +impl<F, T> FilterBase for Unify<F> +where + F: Filter<Extract = (Either<T, T>,)>, + T: Tuple, +{ + type Extract = T; + type Error = F::Error; + type Future = UnifyFuture<F::Future>; + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + UnifyFuture { + inner: self.filter.filter(Internal), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct UnifyFuture<F> { + #[pin] + inner: F, +} + +impl<F, T> Future for UnifyFuture<F> +where + F: TryFuture<Ok = (Either<T, T>,)>, +{ + type Output = Result<T, F::Error>; + + #[inline] + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + let unified = match ready!(self.project().inner.try_poll(cx)) { + Ok((Either::A(a),)) => Ok(a), + Ok((Either::B(b),)) => Ok(b), + Err(err) => Err(err), + }; + Poll::Ready(unified) + } +} diff --git a/third_party/rust/warp/src/filter/untuple_one.rs b/third_party/rust/warp/src/filter/untuple_one.rs new file mode 100644 index 0000000000..07876736d9 --- /dev/null +++ b/third_party/rust/warp/src/filter/untuple_one.rs @@ -0,0 +1,52 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::{ready, TryFuture}; +use pin_project::pin_project; + +use super::{Filter, FilterBase, Internal, Tuple}; + +#[derive(Clone, Copy, Debug)] +pub struct UntupleOne<F> { + pub(super) filter: F, +} + +impl<F, T> FilterBase for UntupleOne<F> +where + F: Filter<Extract = (T,)>, + T: Tuple, +{ + type Extract = T; + type Error = F::Error; + type Future = UntupleOneFuture<F>; + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + UntupleOneFuture { + extract: self.filter.filter(Internal), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct UntupleOneFuture<F: Filter> { + #[pin] + extract: F::Future, +} + +impl<F, T> Future for UntupleOneFuture<F> +where + F: Filter<Extract = (T,)>, + T: Tuple, +{ + type Output = Result<T, F::Error>; + + #[inline] + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + match ready!(self.project().extract.try_poll(cx)) { + Ok((t,)) => Poll::Ready(Ok(t)), + Err(err) => Poll::Ready(Err(err)), + } + } +} diff --git a/third_party/rust/warp/src/filter/wrap.rs b/third_party/rust/warp/src/filter/wrap.rs new file mode 100644 index 0000000000..2226286e25 --- /dev/null +++ b/third_party/rust/warp/src/filter/wrap.rs @@ -0,0 +1,27 @@ +use super::Filter; + +pub trait WrapSealed<F: Filter> { + type Wrapped: Filter; + + fn wrap(&self, filter: F) -> Self::Wrapped; +} + +impl<'a, T, F> WrapSealed<F> for &'a T +where + T: WrapSealed<F>, + F: Filter, +{ + type Wrapped = T::Wrapped; + fn wrap(&self, filter: F) -> Self::Wrapped { + (*self).wrap(filter) + } +} + +pub trait Wrap<F: Filter>: WrapSealed<F> {} + +impl<T, F> Wrap<F> for T +where + T: WrapSealed<F>, + F: Filter, +{ +} diff --git a/third_party/rust/warp/src/filters/addr.rs b/third_party/rust/warp/src/filters/addr.rs new file mode 100644 index 0000000000..8d2ab0aa18 --- /dev/null +++ b/third_party/rust/warp/src/filters/addr.rs @@ -0,0 +1,26 @@ +//! Socket Address filters. + +use std::convert::Infallible; +use std::net::SocketAddr; + +use crate::filter::{filter_fn_one, Filter}; + +/// Creates a `Filter` to get the remote address of the connection. +/// +/// If the underlying transport doesn't use socket addresses, this will yield +/// `None`. +/// +/// # Example +/// +/// ``` +/// use std::net::SocketAddr; +/// use warp::Filter; +/// +/// let route = warp::addr::remote() +/// .map(|addr: Option<SocketAddr>| { +/// println!("remote address = {:?}", addr); +/// }); +/// ``` +pub fn remote() -> impl Filter<Extract = (Option<SocketAddr>,), Error = Infallible> + Copy { + filter_fn_one(|route| futures::future::ok(route.remote_addr())) +} diff --git a/third_party/rust/warp/src/filters/any.rs b/third_party/rust/warp/src/filters/any.rs new file mode 100644 index 0000000000..65db6661d5 --- /dev/null +++ b/third_party/rust/warp/src/filters/any.rs @@ -0,0 +1,76 @@ +//! A filter that matches any route. +use std::convert::Infallible; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use crate::filter::{Filter, FilterBase, Internal}; + +/// A filter that matches any route. +/// +/// This can be a useful building block to build new filters from, +/// since [`Filter`](crate::Filter) is otherwise a sealed trait. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::any() +/// .map(|| { +/// "I always return this string!" +/// }); +/// ``` +/// +/// This could allow creating a single `impl Filter` returning a specific +/// reply, that can then be used as the end of several different filter +/// chains. +/// +/// Another use case is turning some clone-able resource into a `Filter`, +/// thus allowing to easily `and` it together with others. +/// +/// ``` +/// use std::sync::Arc; +/// use warp::Filter; +/// +/// let state = Arc::new(vec![33, 41]); +/// let with_state = warp::any().map(move || state.clone()); +/// +/// // Now we could `and` with any other filter: +/// +/// let route = warp::path::param() +/// .and(with_state) +/// .map(|param_id: u32, db: Arc<Vec<u32>>| { +/// db.contains(¶m_id) +/// }); +/// ``` +pub fn any() -> impl Filter<Extract = (), Error = Infallible> + Copy { + Any +} + +#[derive(Copy, Clone)] +#[allow(missing_debug_implementations)] +struct Any; + +impl FilterBase for Any { + type Extract = (); + type Error = Infallible; + type Future = AnyFut; + + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + AnyFut + } +} + +#[allow(missing_debug_implementations)] +struct AnyFut; + +impl Future for AnyFut { + type Output = Result<(), Infallible>; + + #[inline] + fn poll(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Self::Output> { + Poll::Ready(Ok(())) + } +} diff --git a/third_party/rust/warp/src/filters/body.rs b/third_party/rust/warp/src/filters/body.rs new file mode 100644 index 0000000000..771ddce48f --- /dev/null +++ b/third_party/rust/warp/src/filters/body.rs @@ -0,0 +1,341 @@ +//! Body filters +//! +//! Filters that extract a body for a route. + +use std::error::Error as StdError; +use std::fmt; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use bytes::{buf::BufExt, Buf, Bytes}; +use futures::{future, ready, Stream, TryFutureExt}; +use headers::ContentLength; +use http::header::CONTENT_TYPE; +use hyper::Body; +use mime; +use serde::de::DeserializeOwned; +use serde_json; +use serde_urlencoded; + +use crate::filter::{filter_fn, filter_fn_one, Filter, FilterBase}; +use crate::reject::{self, Rejection}; + +type BoxError = Box<dyn StdError + Send + Sync>; + +// Extracts the `Body` Stream from the route. +// +// Does not consume any of it. +pub(crate) fn body() -> impl Filter<Extract = (Body,), Error = Rejection> + Copy { + filter_fn_one(|route| { + future::ready(route.take_body().ok_or_else(|| { + log::error!("request body already taken in previous filter"); + reject::known(BodyConsumedMultipleTimes { _p: () }) + })) + }) +} + +/// Require a `content-length` header to have a value no greater than some limit. +/// +/// Rejects if `content-length` header is missing, is invalid, or has a number +/// larger than the limit provided. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // Limit the upload to 4kb... +/// let upload = warp::body::content_length_limit(4096) +/// .and(warp::body::aggregate()); +/// ``` +pub fn content_length_limit(limit: u64) -> impl Filter<Extract = (), Error = Rejection> + Copy { + crate::filters::header::header2() + .map_err(crate::filter::Internal, |_| { + log::debug!("content-length missing"); + reject::length_required() + }) + .and_then(move |ContentLength(length)| { + if length <= limit { + future::ok(()) + } else { + log::debug!("content-length: {} is over limit {}", length, limit); + future::err(reject::payload_too_large()) + } + }) + .untuple_one() +} + +/// Create a `Filter` that extracts the request body as a `futures::Stream`. +/// +/// If other filters have already extracted the body, this filter will reject +/// with a `500 Internal Server Error`. +/// +/// # Warning +/// +/// This does not have a default size limit, it would be wise to use one to +/// prevent a overly large request from using too much memory. +pub fn stream( +) -> impl Filter<Extract = (impl Stream<Item = Result<impl Buf, crate::Error>>,), Error = Rejection> + Copy +{ + body().map(|body: Body| BodyStream { body }) +} + +/// Returns a `Filter` that matches any request and extracts a `Future` of a +/// concatenated body. +/// +/// The contents of the body will be flattened into a single contiguous +/// `Bytes`, which may require memory copies. If you don't require a +/// contiguous buffer, using `aggregate` can be give better performance. +/// +/// # Warning +/// +/// This does not have a default size limit, it would be wise to use one to +/// prevent a overly large request from using too much memory. +/// +/// # Example +/// +/// ``` +/// use warp::{Buf, Filter}; +/// +/// let route = warp::body::content_length_limit(1024 * 32) +/// .and(warp::body::bytes()) +/// .map(|bytes: bytes::Bytes| { +/// println!("bytes = {:?}", bytes); +/// }); +/// ``` +pub fn bytes() -> impl Filter<Extract = (Bytes,), Error = Rejection> + Copy { + body().and_then(|body: hyper::Body| { + hyper::body::to_bytes(body).map_err(|err| { + log::debug!("to_bytes error: {}", err); + reject::known(BodyReadError(err)) + }) + }) +} + +/// Returns a `Filter` that matches any request and extracts a `Future` of an +/// aggregated body. +/// +/// The `Buf` may contain multiple, non-contiguous buffers. This can be more +/// performant (by reducing copies) when receiving large bodies. +/// +/// # Warning +/// +/// This does not have a default size limit, it would be wise to use one to +/// prevent a overly large request from using too much memory. +/// +/// # Example +/// +/// ``` +/// use warp::{Buf, Filter}; +/// +/// fn full_body(mut body: impl Buf) { +/// // It could have several non-contiguous slices of memory... +/// while body.has_remaining() { +/// println!("slice = {:?}", body.bytes()); +/// let cnt = body.bytes().len(); +/// body.advance(cnt); +/// } +/// } +/// +/// let route = warp::body::content_length_limit(1024 * 32) +/// .and(warp::body::aggregate()) +/// .map(full_body); +/// ``` +pub fn aggregate() -> impl Filter<Extract = (impl Buf,), Error = Rejection> + Copy { + body().and_then(|body: ::hyper::Body| { + hyper::body::aggregate(body).map_err(|err| { + log::debug!("aggregate error: {}", err); + reject::known(BodyReadError(err)) + }) + }) +} + +/// Returns a `Filter` that matches any request and extracts a `Future` of a +/// JSON-decoded body. +/// +/// # Warning +/// +/// This does not have a default size limit, it would be wise to use one to +/// prevent a overly large request from using too much memory. +/// +/// # Example +/// +/// ``` +/// use std::collections::HashMap; +/// use warp::Filter; +/// +/// let route = warp::body::content_length_limit(1024 * 32) +/// .and(warp::body::json()) +/// .map(|simple_map: HashMap<String, String>| { +/// "Got a JSON body!" +/// }); +/// ``` +pub fn json<T: DeserializeOwned + Send>() -> impl Filter<Extract = (T,), Error = Rejection> + Copy { + is_content_type::<Json>() + .and(aggregate()) + .and_then(|buf| async move { + Json::decode(buf).map_err(|err| { + log::debug!("request json body error: {}", err); + reject::known(BodyDeserializeError { cause: err }) + }) + }) +} + +/// Returns a `Filter` that matches any request and extracts a +/// `Future` of a form encoded body. +/// +/// # Note +/// +/// This filter is for the simpler `application/x-www-form-urlencoded` format, +/// not `multipart/form-data`. +/// +/// # Warning +/// +/// This does not have a default size limit, it would be wise to use one to +/// prevent a overly large request from using too much memory. +/// +/// +/// ``` +/// use std::collections::HashMap; +/// use warp::Filter; +/// +/// let route = warp::body::content_length_limit(1024 * 32) +/// .and(warp::body::form()) +/// .map(|simple_map: HashMap<String, String>| { +/// "Got a urlencoded body!" +/// }); +/// ``` +pub fn form<T: DeserializeOwned + Send>() -> impl Filter<Extract = (T,), Error = Rejection> + Copy { + is_content_type::<Form>() + .and(aggregate()) + .and_then(|buf| async move { + Form::decode(buf).map_err(|err| { + log::debug!("request form body error: {}", err); + reject::known(BodyDeserializeError { cause: err }) + }) + }) +} + +// ===== Decoders ===== + +trait Decode { + const MIME: (mime::Name<'static>, mime::Name<'static>); + const WITH_NO_CONTENT_TYPE: bool; + + fn decode<B: Buf, T: DeserializeOwned>(buf: B) -> Result<T, BoxError>; +} + +struct Json; + +impl Decode for Json { + const MIME: (mime::Name<'static>, mime::Name<'static>) = (mime::APPLICATION, mime::JSON); + const WITH_NO_CONTENT_TYPE: bool = true; + + fn decode<B: Buf, T: DeserializeOwned>(buf: B) -> Result<T, BoxError> { + serde_json::from_reader(buf.reader()).map_err(Into::into) + } +} + +struct Form; + +impl Decode for Form { + const MIME: (mime::Name<'static>, mime::Name<'static>) = + (mime::APPLICATION, mime::WWW_FORM_URLENCODED); + const WITH_NO_CONTENT_TYPE: bool = true; + + fn decode<B: Buf, T: DeserializeOwned>(buf: B) -> Result<T, BoxError> { + serde_urlencoded::from_reader(buf.reader()).map_err(Into::into) + } +} + +// Require the `content-type` header to be this type (or, if there's no `content-type` +// header at all, optimistically hope it's the right type). +fn is_content_type<D: Decode>() -> impl Filter<Extract = (), Error = Rejection> + Copy { + filter_fn(move |route| { + let (type_, subtype) = D::MIME; + if let Some(value) = route.headers().get(CONTENT_TYPE) { + log::trace!("is_content_type {}/{}? {:?}", type_, subtype, value); + let ct = value + .to_str() + .ok() + .and_then(|s| s.parse::<mime::Mime>().ok()); + if let Some(ct) = ct { + if ct.type_() == type_ && ct.subtype() == subtype { + future::ok(()) + } else { + log::debug!( + "content-type {:?} doesn't match {}/{}", + value, + type_, + subtype + ); + future::err(reject::unsupported_media_type()) + } + } else { + log::debug!("content-type {:?} couldn't be parsed", value); + future::err(reject::unsupported_media_type()) + } + } else if D::WITH_NO_CONTENT_TYPE { + // Optimistically assume its correct! + log::trace!("no content-type header, assuming {}/{}", type_, subtype); + future::ok(()) + } else { + log::debug!("no content-type found"); + future::err(reject::unsupported_media_type()) + } + }) +} + +// ===== BodyStream ===== + +struct BodyStream { + body: Body, +} + +impl Stream for BodyStream { + type Item = Result<Bytes, crate::Error>; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> { + let opt_item = ready!(Pin::new(&mut self.get_mut().body).poll_next(cx)); + + match opt_item { + None => Poll::Ready(None), + Some(item) => { + let stream_buf = item.map_err(crate::Error::new); + + Poll::Ready(Some(stream_buf)) + } + } + } +} + +// ===== Rejections ===== + +/// An error used in rejections when deserializing a request body fails. +#[derive(Debug)] +pub struct BodyDeserializeError { + cause: BoxError, +} + +impl fmt::Display for BodyDeserializeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Request body deserialize error: {}", self.cause) + } +} + +impl StdError for BodyDeserializeError {} + +#[derive(Debug)] +pub(crate) struct BodyReadError(::hyper::Error); + +impl ::std::fmt::Display for BodyReadError { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Request body read error: {}", self.0) + } +} + +impl StdError for BodyReadError {} + +unit_error! { + pub(crate) BodyConsumedMultipleTimes: "Request body consumed multiple times" +} diff --git a/third_party/rust/warp/src/filters/cookie.rs b/third_party/rust/warp/src/filters/cookie.rs new file mode 100644 index 0000000000..650914536f --- /dev/null +++ b/third_party/rust/warp/src/filters/cookie.rs @@ -0,0 +1,33 @@ +//! Cookie Filters + +use futures::future; +use headers::Cookie; + +use super::header; +use crate::filter::{Filter, One}; +use crate::reject::Rejection; +use std::convert::Infallible; + +/// Creates a `Filter` that requires a cookie by name. +/// +/// If found, extracts the value of the cookie, otherwise rejects. +pub fn cookie(name: &'static str) -> impl Filter<Extract = One<String>, Error = Rejection> + Copy { + header::header2().and_then(move |cookie: Cookie| { + let cookie = cookie + .get(name) + .map(String::from) + .ok_or_else(|| crate::reject::missing_cookie(name)); + future::ready(cookie) + }) +} + +/// Creates a `Filter` that looks for an optional cookie by name. +/// +/// If found, extracts the value of the cookie, otherwise continues +/// the request, extracting `None`. +pub fn optional( + name: &'static str, +) -> impl Filter<Extract = One<Option<String>>, Error = Infallible> + Copy { + header::optional2() + .map(move |opt: Option<Cookie>| opt.and_then(|cookie| cookie.get(name).map(String::from))) +} diff --git a/third_party/rust/warp/src/filters/cors.rs b/third_party/rust/warp/src/filters/cors.rs new file mode 100644 index 0000000000..d6e2a7d646 --- /dev/null +++ b/third_party/rust/warp/src/filters/cors.rs @@ -0,0 +1,619 @@ +//! CORS Filters + +use std::collections::HashSet; +use std::convert::TryFrom; +use std::error::Error as StdError; +use std::sync::Arc; + +use headers::{ + AccessControlAllowHeaders, AccessControlAllowMethods, AccessControlExposeHeaders, HeaderMapExt, +}; +use http::{ + self, + header::{self, HeaderName, HeaderValue}, +}; + +use crate::filter::{Filter, WrapSealed}; +use crate::reject::{CombineRejection, Rejection}; +use crate::reply::Reply; + +use self::internal::{CorsFilter, IntoOrigin, Seconds}; + +/// Create a wrapping filter that exposes [CORS][] behavior for a wrapped +/// filter. +/// +/// [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let cors = warp::cors() +/// .allow_origin("https://hyper.rs") +/// .allow_methods(vec!["GET", "POST", "DELETE"]); +/// +/// let route = warp::any() +/// .map(warp::reply) +/// .with(cors); +/// ``` +/// If you want to allow any route: +/// ``` +/// use warp::Filter; +/// let cors = warp::cors() +/// .allow_any_origin(); +/// ``` +/// You can find more usage examples [here](https://github.com/seanmonstar/warp/blob/7fa54eaecd0fe12687137372791ff22fc7995766/tests/cors.rs). +pub fn cors() -> Builder { + Builder { + credentials: false, + allowed_headers: HashSet::new(), + exposed_headers: HashSet::new(), + max_age: None, + methods: HashSet::new(), + origins: None, + } +} + +/// A wrapping filter constructed via `warp::cors()`. +#[derive(Clone, Debug)] +pub struct Cors { + config: Arc<Configured>, +} + +/// A constructed via `warp::cors()`. +#[derive(Clone, Debug)] +pub struct Builder { + credentials: bool, + allowed_headers: HashSet<HeaderName>, + exposed_headers: HashSet<HeaderName>, + max_age: Option<u64>, + methods: HashSet<http::Method>, + origins: Option<HashSet<HeaderValue>>, +} + +impl Builder { + /// Sets whether to add the `Access-Control-Allow-Credentials` header. + pub fn allow_credentials(mut self, allow: bool) -> Self { + self.credentials = allow; + self + } + + /// Adds a method to the existing list of allowed request methods. + /// + /// # Panics + /// + /// Panics if the provided argument is not a valid `http::Method`. + pub fn allow_method<M>(mut self, method: M) -> Self + where + http::Method: TryFrom<M>, + { + let method = match TryFrom::try_from(method) { + Ok(m) => m, + Err(_) => panic!("illegal Method"), + }; + self.methods.insert(method); + self + } + + /// Adds multiple methods to the existing list of allowed request methods. + /// + /// # Panics + /// + /// Panics if the provided argument is not a valid `http::Method`. + pub fn allow_methods<I>(mut self, methods: I) -> Self + where + I: IntoIterator, + http::Method: TryFrom<I::Item>, + { + let iter = methods.into_iter().map(|m| match TryFrom::try_from(m) { + Ok(m) => m, + Err(_) => panic!("illegal Method"), + }); + self.methods.extend(iter); + self + } + + /// Adds a header to the list of allowed request headers. + /// + /// # Panics + /// + /// Panics if the provided argument is not a valid `http::header::HeaderName`. + pub fn allow_header<H>(mut self, header: H) -> Self + where + HeaderName: TryFrom<H>, + { + let header = match TryFrom::try_from(header) { + Ok(m) => m, + Err(_) => panic!("illegal Header"), + }; + self.allowed_headers.insert(header); + self + } + + /// Adds multiple headers to the list of allowed request headers. + /// + /// # Panics + /// + /// Panics if any of the headers are not a valid `http::header::HeaderName`. + pub fn allow_headers<I>(mut self, headers: I) -> Self + where + I: IntoIterator, + HeaderName: TryFrom<I::Item>, + { + let iter = headers.into_iter().map(|h| match TryFrom::try_from(h) { + Ok(h) => h, + Err(_) => panic!("illegal Header"), + }); + self.allowed_headers.extend(iter); + self + } + + /// Adds a header to the list of exposed headers. + /// + /// # Panics + /// + /// Panics if the provided argument is not a valid `http::header::HeaderName`. + pub fn expose_header<H>(mut self, header: H) -> Self + where + HeaderName: TryFrom<H>, + { + let header = match TryFrom::try_from(header) { + Ok(m) => m, + Err(_) => panic!("illegal Header"), + }; + self.exposed_headers.insert(header); + self + } + + /// Adds multiple headers to the list of exposed headers. + /// + /// # Panics + /// + /// Panics if any of the headers are not a valid `http::header::HeaderName`. + pub fn expose_headers<I>(mut self, headers: I) -> Self + where + I: IntoIterator, + HeaderName: TryFrom<I::Item>, + { + let iter = headers.into_iter().map(|h| match TryFrom::try_from(h) { + Ok(h) => h, + Err(_) => panic!("illegal Header"), + }); + self.exposed_headers.extend(iter); + self + } + + /// Sets that *any* `Origin` header is allowed. + /// + /// # Warning + /// + /// This can allow websites you didn't instead to access this resource, + /// it is usually better to set an explicit list. + pub fn allow_any_origin(mut self) -> Self { + self.origins = None; + self + } + + /// Add an origin to the existing list of allowed `Origin`s. + /// + /// # Panics + /// + /// Panics if the provided argument is not a valid `Origin`. + pub fn allow_origin(self, origin: impl IntoOrigin) -> Self { + self.allow_origins(Some(origin)) + } + + /// Add multiple origins to the existing list of allowed `Origin`s. + /// + /// # Panics + /// + /// Panics if the provided argument is not a valid `Origin`. + pub fn allow_origins<I>(mut self, origins: I) -> Self + where + I: IntoIterator, + I::Item: IntoOrigin, + { + let iter = origins + .into_iter() + .map(IntoOrigin::into_origin) + .map(|origin| { + origin + .to_string() + .parse() + .expect("Origin is always a valid HeaderValue") + }); + + self.origins.get_or_insert_with(HashSet::new).extend(iter); + + self + } + + /// Sets the `Access-Control-Max-Age` header. + /// + /// # Example + /// + /// + /// ``` + /// use std::time::Duration; + /// use warp::Filter; + /// + /// let cors = warp::cors() + /// .max_age(30) // 30u32 seconds + /// .max_age(Duration::from_secs(30)); // or a Duration + /// ``` + pub fn max_age(mut self, seconds: impl Seconds) -> Self { + self.max_age = Some(seconds.seconds()); + self + } + + /// Builds the `Cors` wrapper from the configured settings. + /// + /// This step isn't *required*, as the `Builder` itself can be passed + /// to `Filter::with`. This just allows constructing once, thus not needing + /// to pay the cost of "building" every time. + pub fn build(self) -> Cors { + let expose_headers_header = if self.exposed_headers.is_empty() { + None + } else { + Some(self.exposed_headers.iter().cloned().collect()) + }; + let allowed_headers_header = self.allowed_headers.iter().cloned().collect(); + let methods_header = self.methods.iter().cloned().collect(); + + let config = Arc::new(Configured { + cors: self, + allowed_headers_header, + expose_headers_header, + methods_header, + }); + + Cors { config } + } +} + +impl<F> WrapSealed<F> for Builder +where + F: Filter + Clone + Send + Sync + 'static, + F::Extract: Reply, + F::Error: CombineRejection<Rejection>, + <F::Error as CombineRejection<Rejection>>::One: CombineRejection<Rejection>, +{ + type Wrapped = CorsFilter<F>; + + fn wrap(&self, inner: F) -> Self::Wrapped { + let Cors { config } = self.clone().build(); + + CorsFilter { config, inner } + } +} + +impl<F> WrapSealed<F> for Cors +where + F: Filter + Clone + Send + Sync + 'static, + F::Extract: Reply, + F::Error: CombineRejection<Rejection>, + <F::Error as CombineRejection<Rejection>>::One: CombineRejection<Rejection>, +{ + type Wrapped = CorsFilter<F>; + + fn wrap(&self, inner: F) -> Self::Wrapped { + let config = self.config.clone(); + + CorsFilter { config, inner } + } +} + +/// An error used to reject requests that are forbidden by a `cors` filter. +pub struct CorsForbidden { + kind: Forbidden, +} + +#[derive(Debug)] +enum Forbidden { + OriginNotAllowed, + MethodNotAllowed, + HeaderNotAllowed, +} + +impl ::std::fmt::Debug for CorsForbidden { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_tuple("CorsForbidden").field(&self.kind).finish() + } +} + +impl ::std::fmt::Display for CorsForbidden { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + let detail = match self.kind { + Forbidden::OriginNotAllowed => "origin not allowed", + Forbidden::MethodNotAllowed => "request-method not allowed", + Forbidden::HeaderNotAllowed => "header not allowed", + }; + write!(f, "CORS request forbidden: {}", detail) + } +} + +impl StdError for CorsForbidden {} + +#[derive(Clone, Debug)] +struct Configured { + cors: Builder, + allowed_headers_header: AccessControlAllowHeaders, + expose_headers_header: Option<AccessControlExposeHeaders>, + methods_header: AccessControlAllowMethods, +} + +enum Validated { + Preflight(HeaderValue), + Simple(HeaderValue), + NotCors, +} + +impl Configured { + fn check_request( + &self, + method: &http::Method, + headers: &http::HeaderMap, + ) -> Result<Validated, Forbidden> { + match (headers.get(header::ORIGIN), method) { + (Some(origin), &http::Method::OPTIONS) => { + // OPTIONS requests are preflight CORS requests... + + if !self.is_origin_allowed(origin) { + return Err(Forbidden::OriginNotAllowed); + } + + if let Some(req_method) = headers.get(header::ACCESS_CONTROL_REQUEST_METHOD) { + if !self.is_method_allowed(req_method) { + return Err(Forbidden::MethodNotAllowed); + } + } else { + log::trace!("preflight request missing access-control-request-method header"); + return Err(Forbidden::MethodNotAllowed); + } + + if let Some(req_headers) = headers.get(header::ACCESS_CONTROL_REQUEST_HEADERS) { + let headers = req_headers + .to_str() + .map_err(|_| Forbidden::HeaderNotAllowed)?; + for header in headers.split(',') { + if !self.is_header_allowed(header) { + return Err(Forbidden::HeaderNotAllowed); + } + } + } + + Ok(Validated::Preflight(origin.clone())) + } + (Some(origin), _) => { + // Any other method, simply check for a valid origin... + + log::trace!("origin header: {:?}", origin); + if self.is_origin_allowed(origin) { + Ok(Validated::Simple(origin.clone())) + } else { + Err(Forbidden::OriginNotAllowed) + } + } + (None, _) => { + // No `ORIGIN` header means this isn't CORS! + Ok(Validated::NotCors) + } + } + } + + fn is_method_allowed(&self, header: &HeaderValue) -> bool { + http::Method::from_bytes(header.as_bytes()) + .map(|method| self.cors.methods.contains(&method)) + .unwrap_or(false) + } + + fn is_header_allowed(&self, header: &str) -> bool { + HeaderName::from_bytes(header.as_bytes()) + .map(|header| self.cors.allowed_headers.contains(&header)) + .unwrap_or(false) + } + + fn is_origin_allowed(&self, origin: &HeaderValue) -> bool { + if let Some(ref allowed) = self.cors.origins { + allowed.contains(origin) + } else { + true + } + } + + fn append_preflight_headers(&self, headers: &mut http::HeaderMap) { + self.append_common_headers(headers); + + headers.typed_insert(self.allowed_headers_header.clone()); + headers.typed_insert(self.methods_header.clone()); + + if let Some(max_age) = self.cors.max_age { + headers.insert(header::ACCESS_CONTROL_MAX_AGE, max_age.into()); + } + } + + fn append_common_headers(&self, headers: &mut http::HeaderMap) { + if self.cors.credentials { + headers.insert( + header::ACCESS_CONTROL_ALLOW_CREDENTIALS, + HeaderValue::from_static("true"), + ); + } + if let Some(expose_headers_header) = &self.expose_headers_header { + headers.typed_insert(expose_headers_header.clone()) + } + } +} + +mod internal { + use std::future::Future; + use std::pin::Pin; + use std::sync::Arc; + use std::task::{Context, Poll}; + + use futures::{future, ready, TryFuture}; + use headers::Origin; + use http::header; + use pin_project::pin_project; + + use super::{Configured, CorsForbidden, Validated}; + use crate::filter::{Filter, FilterBase, Internal, One}; + use crate::generic::Either; + use crate::reject::{CombineRejection, Rejection}; + use crate::route; + + #[derive(Clone, Debug)] + pub struct CorsFilter<F> { + pub(super) config: Arc<Configured>, + pub(super) inner: F, + } + + impl<F> FilterBase for CorsFilter<F> + where + F: Filter, + F::Extract: Send, + F::Future: Future, + F::Error: CombineRejection<Rejection>, + { + type Extract = + One<Either<One<Preflight>, One<Either<One<Wrapped<F::Extract>>, F::Extract>>>>; + type Error = <F::Error as CombineRejection<Rejection>>::One; + type Future = future::Either< + future::Ready<Result<Self::Extract, Self::Error>>, + WrappedFuture<F::Future>, + >; + + fn filter(&self, _: Internal) -> Self::Future { + let validated = + route::with(|route| self.config.check_request(route.method(), route.headers())); + + match validated { + Ok(Validated::Preflight(origin)) => { + let preflight = Preflight { + config: self.config.clone(), + origin, + }; + future::Either::Left(future::ok((Either::A((preflight,)),))) + } + Ok(Validated::Simple(origin)) => future::Either::Right(WrappedFuture { + inner: self.inner.filter(Internal), + wrapped: Some((self.config.clone(), origin)), + }), + Ok(Validated::NotCors) => future::Either::Right(WrappedFuture { + inner: self.inner.filter(Internal), + wrapped: None, + }), + Err(err) => { + let rejection = crate::reject::known(CorsForbidden { kind: err }); + future::Either::Left(future::err(rejection.into())) + } + } + } + } + + #[derive(Debug)] + pub struct Preflight { + config: Arc<Configured>, + origin: header::HeaderValue, + } + + impl crate::reply::Reply for Preflight { + fn into_response(self) -> crate::reply::Response { + let mut res = crate::reply::Response::default(); + self.config.append_preflight_headers(res.headers_mut()); + res.headers_mut() + .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, self.origin); + res + } + } + + #[derive(Debug)] + pub struct Wrapped<R> { + config: Arc<Configured>, + inner: R, + origin: header::HeaderValue, + } + + impl<R> crate::reply::Reply for Wrapped<R> + where + R: crate::reply::Reply, + { + fn into_response(self) -> crate::reply::Response { + let mut res = self.inner.into_response(); + self.config.append_common_headers(res.headers_mut()); + res.headers_mut() + .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, self.origin); + res + } + } + + #[pin_project] + #[derive(Debug)] + pub struct WrappedFuture<F> { + #[pin] + inner: F, + wrapped: Option<(Arc<Configured>, header::HeaderValue)>, + } + + impl<F> Future for WrappedFuture<F> + where + F: TryFuture, + F::Error: CombineRejection<Rejection>, + { + type Output = Result< + One<Either<One<Preflight>, One<Either<One<Wrapped<F::Ok>>, F::Ok>>>>, + <F::Error as CombineRejection<Rejection>>::One, + >; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + let pin = self.project(); + match ready!(pin.inner.try_poll(cx)) { + Ok(inner) => { + let item = if let Some((config, origin)) = pin.wrapped.take() { + (Either::A((Wrapped { + config, + inner, + origin, + },)),) + } else { + (Either::B(inner),) + }; + let item = (Either::B(item),); + Poll::Ready(Ok(item)) + } + Err(err) => Poll::Ready(Err(err.into())), + } + } + } + + pub trait Seconds { + fn seconds(self) -> u64; + } + + impl Seconds for u32 { + fn seconds(self) -> u64 { + self.into() + } + } + + impl Seconds for ::std::time::Duration { + fn seconds(self) -> u64 { + self.as_secs() + } + } + + pub trait IntoOrigin { + fn into_origin(self) -> Origin; + } + + impl<'a> IntoOrigin for &'a str { + fn into_origin(self) -> Origin { + let mut parts = self.splitn(2, "://"); + let scheme = parts.next().expect("missing scheme"); + let rest = parts.next().expect("missing scheme"); + + Origin::try_from_parts(scheme, rest, None).expect("invalid Origin") + } + } +} diff --git a/third_party/rust/warp/src/filters/ext.rs b/third_party/rust/warp/src/filters/ext.rs new file mode 100644 index 0000000000..483a6db918 --- /dev/null +++ b/third_party/rust/warp/src/filters/ext.rs @@ -0,0 +1,36 @@ +//! Request Extensions + +use std::convert::Infallible; + +use futures::future; + +use crate::filter::{filter_fn_one, Filter}; +use crate::reject::{self, Rejection}; + +/// Get a previously set extension of the current route. +/// +/// If the extension doesn't exist, this rejects with a `MissingExtension`. +pub fn get<T: Clone + Send + Sync + 'static>( +) -> impl Filter<Extract = (T,), Error = Rejection> + Copy { + filter_fn_one(|route| { + let route = route + .extensions() + .get::<T>() + .cloned() + .ok_or_else(|| reject::known(MissingExtension { _p: () })); + future::ready(route) + }) +} + +/// Get a previously set extension of the current route. +/// +/// If the extension doesn't exist, it yields `None`. +pub fn optional<T: Clone + Send + Sync + 'static>( +) -> impl Filter<Extract = (Option<T>,), Error = Infallible> + Copy { + filter_fn_one(|route| future::ok(route.extensions().get::<T>().cloned())) +} + +unit_error! { + /// An error used to reject if `get` cannot find the extension. + pub MissingExtension: "Missing request extension" +} diff --git a/third_party/rust/warp/src/filters/fs.rs b/third_party/rust/warp/src/filters/fs.rs new file mode 100644 index 0000000000..c79725c68a --- /dev/null +++ b/third_party/rust/warp/src/filters/fs.rs @@ -0,0 +1,501 @@ +//! File System Filters + +use std::cmp; +use std::convert::Infallible; +use std::fs::Metadata; +use std::future::Future; +use std::io; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::Poll; + +use bytes::{Bytes, BytesMut}; +use futures::future::Either; +use futures::{future, ready, stream, FutureExt, Stream, StreamExt, TryFutureExt}; +use headers::{ + AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt, IfModifiedSince, IfRange, + IfUnmodifiedSince, LastModified, Range, +}; +use http::StatusCode; +use hyper::Body; +use mime_guess; +use tokio::fs::File as TkFile; +use tokio::io::AsyncRead; +use urlencoding::decode; + +use crate::filter::{Filter, FilterClone, One}; +use crate::reject::{self, Rejection}; +use crate::reply::{Reply, Response}; + +/// Creates a `Filter` that serves a File at the `path`. +/// +/// Does not filter out based on any information of the request. Always serves +/// the file at the exact `path` provided. Thus, this can be used to serve a +/// single file with `GET`s, but could also be used in combination with other +/// filters, such as after validating in `POST` request, wanting to return a +/// specific file as the body. +/// +/// For serving a directory, see [dir](dir). +/// +/// # Example +/// +/// ``` +/// // Always serves this file from the file system. +/// let route = warp::fs::file("/www/static/app.js"); +/// ``` +pub fn file(path: impl Into<PathBuf>) -> impl FilterClone<Extract = One<File>, Error = Rejection> { + let path = Arc::new(path.into()); + crate::any() + .map(move || { + log::trace!("file: {:?}", path); + ArcPath(path.clone()) + }) + .and(conditionals()) + .and_then(|path, conditionals| file_reply(path, conditionals)) +} + +/// Creates a `Filter` that serves a directory at the base `path` joined +/// by the request path. +/// +/// This can be used to serve "static files" from a directory. By far the most +/// common pattern of serving static files is for `GET` requests, so this +/// filter automatically includes a `GET` check. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // Matches requests that start with `/static`, +/// // and then uses the rest of that path to lookup +/// // and serve a file from `/www/static`. +/// let route = warp::path("static") +/// .and(warp::fs::dir("/www/static")); +/// +/// // For example: +/// // - `GET /static/app.js` would serve the file `/www/static/app.js` +/// // - `GET /static/css/app.css` would serve the file `/www/static/css/app.css` +/// ``` +pub fn dir(path: impl Into<PathBuf>) -> impl FilterClone<Extract = One<File>, Error = Rejection> { + let base = Arc::new(path.into()); + crate::get() + .and(path_from_tail(base)) + .and(conditionals()) + .and_then(file_reply) +} + +fn path_from_tail( + base: Arc<PathBuf>, +) -> impl FilterClone<Extract = One<ArcPath>, Error = Rejection> { + crate::path::tail().and_then(move |tail: crate::path::Tail| { + future::ready(sanitize_path(base.as_ref(), tail.as_str())).and_then(|mut buf| async { + let is_dir = tokio::fs::metadata(buf.clone()) + .await + .map(|m| m.is_dir()) + .unwrap_or(false); + + if is_dir { + log::debug!("dir: appending index.html to directory path"); + buf.push("index.html"); + } + log::trace!("dir: {:?}", buf); + Ok(ArcPath(Arc::new(buf))) + }) + }) +} + +fn sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, Rejection> { + let mut buf = PathBuf::from(base.as_ref()); + let p = match decode(tail) { + Ok(p) => p, + Err(err) => { + log::debug!("dir: failed to decode route={:?}: {:?}", tail, err); + // FromUrlEncodingError doesn't implement StdError + return Err(reject::not_found()); + } + }; + log::trace!("dir? base={:?}, route={:?}", base.as_ref(), p); + for seg in p.split('/') { + if seg.starts_with("..") { + log::warn!("dir: rejecting segment starting with '..'"); + return Err(reject::not_found()); + } else if seg.contains('\\') { + log::warn!("dir: rejecting segment containing with backslash (\\)"); + return Err(reject::not_found()); + } else { + buf.push(seg); + } + } + Ok(buf) +} + +#[derive(Debug)] +struct Conditionals { + if_modified_since: Option<IfModifiedSince>, + if_unmodified_since: Option<IfUnmodifiedSince>, + if_range: Option<IfRange>, + range: Option<Range>, +} + +enum Cond { + NoBody(Response), + WithBody(Option<Range>), +} + +impl Conditionals { + fn check(self, last_modified: Option<LastModified>) -> Cond { + if let Some(since) = self.if_unmodified_since { + let precondition = last_modified + .map(|time| since.precondition_passes(time.into())) + .unwrap_or(false); + + log::trace!( + "if-unmodified-since? {:?} vs {:?} = {}", + since, + last_modified, + precondition + ); + if !precondition { + let mut res = Response::new(Body::empty()); + *res.status_mut() = StatusCode::PRECONDITION_FAILED; + return Cond::NoBody(res); + } + } + + if let Some(since) = self.if_modified_since { + log::trace!( + "if-modified-since? header = {:?}, file = {:?}", + since, + last_modified + ); + let unmodified = last_modified + .map(|time| !since.is_modified(time.into())) + // no last_modified means its always modified + .unwrap_or(false); + if unmodified { + let mut res = Response::new(Body::empty()); + *res.status_mut() = StatusCode::NOT_MODIFIED; + return Cond::NoBody(res); + } + } + + if let Some(if_range) = self.if_range { + log::trace!("if-range? {:?} vs {:?}", if_range, last_modified); + let can_range = !if_range.is_modified(None, last_modified.as_ref()); + + if !can_range { + return Cond::WithBody(None); + } + } + + Cond::WithBody(self.range) + } +} + +fn conditionals() -> impl Filter<Extract = One<Conditionals>, Error = Infallible> + Copy { + crate::header::optional2() + .and(crate::header::optional2()) + .and(crate::header::optional2()) + .and(crate::header::optional2()) + .map( + |if_modified_since, if_unmodified_since, if_range, range| Conditionals { + if_modified_since, + if_unmodified_since, + if_range, + range, + }, + ) +} + +/// A file response. +#[derive(Debug)] +pub struct File { + resp: Response, +} + +// Silly wrapper since Arc<PathBuf> doesn't implement AsRef<Path> ;_; +#[derive(Clone, Debug)] +struct ArcPath(Arc<PathBuf>); + +impl AsRef<Path> for ArcPath { + fn as_ref(&self) -> &Path { + (*self.0).as_ref() + } +} + +impl Reply for File { + fn into_response(self) -> Response { + self.resp + } +} + +fn file_reply( + path: ArcPath, + conditionals: Conditionals, +) -> impl Future<Output = Result<File, Rejection>> + Send { + TkFile::open(path.clone()).then(move |res| match res { + Ok(f) => Either::Left(file_conditional(f, path, conditionals)), + Err(err) => { + let rej = match err.kind() { + io::ErrorKind::NotFound => { + log::debug!("file not found: {:?}", path.as_ref().display()); + reject::not_found() + } + io::ErrorKind::PermissionDenied => { + log::warn!("file permission denied: {:?}", path.as_ref().display()); + reject::known(FilePermissionError { _p: () }) + } + _ => { + log::error!( + "file open error (path={:?}): {} ", + path.as_ref().display(), + err + ); + reject::known(FileOpenError { _p: () }) + } + }; + Either::Right(future::err(rej)) + } + }) +} + +async fn file_metadata(f: TkFile) -> Result<(TkFile, Metadata), Rejection> { + match f.metadata().await { + Ok(meta) => Ok((f, meta)), + Err(err) => { + log::debug!("file metadata error: {}", err); + Err(reject::not_found()) + } + } +} + +fn file_conditional( + f: TkFile, + path: ArcPath, + conditionals: Conditionals, +) -> impl Future<Output = Result<File, Rejection>> + Send { + file_metadata(f).map_ok(move |(file, meta)| { + let mut len = meta.len(); + let modified = meta.modified().ok().map(LastModified::from); + + let resp = match conditionals.check(modified) { + Cond::NoBody(resp) => resp, + Cond::WithBody(range) => { + bytes_range(range, len) + .map(|(start, end)| { + let sub_len = end - start; + let buf_size = optimal_buf_size(&meta); + let stream = file_stream(file, buf_size, (start, end)); + let body = Body::wrap_stream(stream); + + let mut resp = Response::new(body); + + if sub_len != len { + *resp.status_mut() = StatusCode::PARTIAL_CONTENT; + resp.headers_mut().typed_insert( + ContentRange::bytes(start..end, len).expect("valid ContentRange"), + ); + + len = sub_len; + } + + let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream(); + + resp.headers_mut().typed_insert(ContentLength(len)); + resp.headers_mut().typed_insert(ContentType::from(mime)); + resp.headers_mut().typed_insert(AcceptRanges::bytes()); + + if let Some(last_modified) = modified { + resp.headers_mut().typed_insert(last_modified); + } + + resp + }) + .unwrap_or_else(|BadRange| { + // bad byte range + let mut resp = Response::new(Body::empty()); + *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; + resp.headers_mut() + .typed_insert(ContentRange::unsatisfied_bytes(len)); + resp + }) + } + }; + + File { resp } + }) +} + +struct BadRange; + +fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRange> { + use std::ops::Bound; + + let range = if let Some(range) = range { + range + } else { + return Ok((0, max_len)); + }; + + let ret = range + .iter() + .map(|(start, end)| { + let start = match start { + Bound::Unbounded => 0, + Bound::Included(s) => s, + Bound::Excluded(s) => s + 1, + }; + + let end = match end { + Bound::Unbounded => max_len, + Bound::Included(s) => s + 1, + Bound::Excluded(s) => s, + }; + + if start < end && end <= max_len { + Ok((start, end)) + } else { + log::trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len); + Err(BadRange) + } + }) + .next() + .unwrap_or(Ok((0, max_len))); + ret +} + +fn file_stream( + mut file: TkFile, + buf_size: usize, + (start, end): (u64, u64), +) -> impl Stream<Item = Result<Bytes, io::Error>> + Send { + use std::io::SeekFrom; + + let seek = async move { + if start != 0 { + file.seek(SeekFrom::Start(start)).await?; + } + Ok(file) + }; + + seek.into_stream() + .map(move |result| { + let mut buf = BytesMut::new(); + let mut len = end - start; + let mut f = match result { + Ok(f) => f, + Err(f) => return Either::Left(stream::once(future::err(f))), + }; + + Either::Right(stream::poll_fn(move |cx| { + if len == 0 { + return Poll::Ready(None); + } + reserve_at_least(&mut buf, buf_size); + + let n = match ready!(Pin::new(&mut f).poll_read_buf(cx, &mut buf)) { + Ok(n) => n as u64, + Err(err) => { + log::debug!("file read error: {}", err); + return Poll::Ready(Some(Err(err))); + } + }; + + if n == 0 { + log::debug!("file read found EOF before expected length"); + return Poll::Ready(None); + } + + let mut chunk = buf.split().freeze(); + if n > len { + chunk = chunk.split_to(len as usize); + len = 0; + } else { + len -= n; + } + + Poll::Ready(Some(Ok(chunk))) + })) + }) + .flatten() +} + +fn reserve_at_least(buf: &mut BytesMut, cap: usize) { + if buf.capacity() - buf.len() < cap { + buf.reserve(cap); + } +} + +const DEFAULT_READ_BUF_SIZE: usize = 8_192; + +fn optimal_buf_size(metadata: &Metadata) -> usize { + let block_size = get_block_size(metadata); + + // If file length is smaller than block size, don't waste space + // reserving a bigger-than-needed buffer. + cmp::min(block_size as u64, metadata.len()) as usize +} + +#[cfg(unix)] +fn get_block_size(metadata: &Metadata) -> usize { + use std::os::unix::fs::MetadataExt; + //TODO: blksize() returns u64, should handle bad cast... + //(really, a block size bigger than 4gb?) + + // Use device blocksize unless it's really small. + cmp::max(metadata.blksize() as usize, DEFAULT_READ_BUF_SIZE) +} + +#[cfg(not(unix))] +fn get_block_size(_metadata: &Metadata) -> usize { + DEFAULT_READ_BUF_SIZE +} + +// ===== Rejections ===== + +unit_error! { + pub(crate) FileOpenError: "file open error" +} + +unit_error! { + pub(crate) FilePermissionError: "file perimission error" +} + +#[cfg(test)] +mod tests { + use super::sanitize_path; + use bytes::BytesMut; + + #[test] + fn test_sanitize_path() { + let base = "/var/www"; + + fn p(s: &str) -> &::std::path::Path { + s.as_ref() + } + + assert_eq!( + sanitize_path(base, "/foo.html").unwrap(), + p("/var/www/foo.html") + ); + + // bad paths + sanitize_path(base, "/../foo.html").expect_err("dot dot"); + + sanitize_path(base, "/C:\\/foo.html").expect_err("C:\\"); + } + + #[test] + fn test_reserve_at_least() { + let mut buf = BytesMut::new(); + let cap = 8_192; + + assert_eq!(buf.len(), 0); + assert_eq!(buf.capacity(), 0); + + super::reserve_at_least(&mut buf, cap); + assert_eq!(buf.len(), 0); + assert_eq!(buf.capacity(), cap); + } +} diff --git a/third_party/rust/warp/src/filters/header.rs b/third_party/rust/warp/src/filters/header.rs new file mode 100644 index 0000000000..5e79c25db8 --- /dev/null +++ b/third_party/rust/warp/src/filters/header.rs @@ -0,0 +1,203 @@ +//! Header Filters +//! +//! These filters are used to interact with the Request HTTP headers. Some +//! of them, like `exact` and `exact_ignore_case`, are just predicates, +//! they don't extract any values. The `header` filter allows parsing +//! a type from any header. +use std::convert::Infallible; +use std::str::FromStr; + +use futures::future; +use headers::{Header, HeaderMapExt}; +use http::HeaderMap; + +use crate::filter::{filter_fn, filter_fn_one, Filter, One}; +use crate::reject::{self, Rejection}; + +/// Create a `Filter` that tries to parse the specified header. +/// +/// This `Filter` will look for a header with supplied name, and try to +/// parse to a `T`, otherwise rejects the request. +/// +/// # Example +/// +/// ``` +/// use std::net::SocketAddr; +/// +/// // Parse `content-length: 100` as a `u64` +/// let content_length = warp::header::<u64>("content-length"); +/// +/// // Parse `host: 127.0.0.1:8080` as a `SocketAddr +/// let local_host = warp::header::<SocketAddr>("host"); +/// +/// // Parse `foo: bar` into a `String` +/// let foo = warp::header::<String>("foo"); +/// ``` +pub fn header<T: FromStr + Send + 'static>( + name: &'static str, +) -> impl Filter<Extract = One<T>, Error = Rejection> + Copy { + filter_fn_one(move |route| { + log::trace!("header({:?})", name); + let route = route + .headers() + .get(name) + .ok_or_else(|| reject::missing_header(name)) + .and_then(|value| value.to_str().map_err(|_| reject::invalid_header(name))) + .and_then(|s| T::from_str(s).map_err(|_| reject::invalid_header(name))); + future::ready(route) + }) +} + +pub(crate) fn header2<T: Header + Send + 'static>( +) -> impl Filter<Extract = One<T>, Error = Rejection> + Copy { + filter_fn_one(move |route| { + log::trace!("header2({:?})", T::name()); + let route = route + .headers() + .typed_get() + .ok_or_else(|| reject::invalid_header(T::name().as_str())); + future::ready(route) + }) +} + +/// Create a `Filter` that tries to parse the specified header, if it exists. +/// +/// If the header does not exist, it yields `None`. Otherwise, it will try to +/// parse as a `T`, and if it fails, a invalid header rejection is return. If +/// successful, the filter yields `Some(T)`. +/// +/// # Example +/// +/// ``` +/// // Grab the `authorization` header if it exists. +/// let opt_auth = warp::header::optional::<String>("authorization"); +/// ``` +pub fn optional<T>( + name: &'static str, +) -> impl Filter<Extract = One<Option<T>>, Error = Rejection> + Copy +where + T: FromStr + Send + 'static, +{ + filter_fn_one(move |route| { + log::trace!("optional({:?})", name); + let result = route.headers().get(name).map(|value| { + value + .to_str() + .map_err(|_| reject::invalid_header(name))? + .parse::<T>() + .map_err(|_| reject::invalid_header(name)) + }); + + match result { + Some(Ok(t)) => future::ok(Some(t)), + Some(Err(e)) => future::err(e), + None => future::ok(None), + } + }) +} + +pub(crate) fn optional2<T>() -> impl Filter<Extract = One<Option<T>>, Error = Infallible> + Copy +where + T: Header + Send + 'static, +{ + filter_fn_one(move |route| future::ready(Ok(route.headers().typed_get()))) +} + +/* TODO +pub fn exact2<T>(header: T) -> impl FilterClone<Extract=(), Error=Rejection> +where + T: Header + PartialEq + Clone + Send, +{ + filter_fn(move |route| { + log::trace!("exact2({:?})", T::NAME); + route.headers() + .typed_get::<T>() + .and_then(|val| if val == header { + Some(()) + } else { + None + }) + .ok_or_else(|| reject::bad_request()) + }) +} +*/ + +/// Create a `Filter` that requires a header to match the value exactly. +/// +/// This `Filter` will look for a header with supplied name and the exact +/// value, otherwise rejects the request. +/// +/// # Example +/// +/// ``` +/// // Require `dnt: 1` header to be set. +/// let must_dnt = warp::header::exact("dnt", "1"); +/// ``` +pub fn exact( + name: &'static str, + value: &'static str, +) -> impl Filter<Extract = (), Error = Rejection> + Copy { + filter_fn(move |route| { + log::trace!("exact?({:?}, {:?})", name, value); + let route = route + .headers() + .get(name) + .ok_or_else(|| reject::missing_header(name)) + .and_then(|val| { + if val == value { + Ok(()) + } else { + Err(reject::invalid_header(name)) + } + }); + future::ready(route) + }) +} + +/// Create a `Filter` that requires a header to match the value exactly. +/// +/// This `Filter` will look for a header with supplied name and the exact +/// value, ignoring ASCII case, otherwise rejects the request. +/// +/// # Example +/// +/// ``` +/// // Require `connection: keep-alive` header to be set. +/// let keep_alive = warp::header::exact("connection", "keep-alive"); +/// ``` +pub fn exact_ignore_case( + name: &'static str, + value: &'static str, +) -> impl Filter<Extract = (), Error = Rejection> + Copy { + filter_fn(move |route| { + log::trace!("exact_ignore_case({:?}, {:?})", name, value); + let route = route + .headers() + .get(name) + .ok_or_else(|| reject::missing_header(name)) + .and_then(|val| { + if val.as_bytes().eq_ignore_ascii_case(value.as_bytes()) { + Ok(()) + } else { + Err(reject::invalid_header(name)) + } + }); + future::ready(route) + }) +} + +/// Create a `Filter` that returns a clone of the request's `HeaderMap`. +/// +/// # Example +/// +/// ``` +/// use warp::{Filter, http::HeaderMap}; +/// +/// let headers = warp::header::headers_cloned() +/// .map(|headers: HeaderMap| { +/// format!("header count: {}", headers.len()) +/// }); +/// ``` +pub fn headers_cloned() -> impl Filter<Extract = One<HeaderMap>, Error = Infallible> + Copy { + filter_fn_one(|route| future::ok(route.headers().clone())) +} diff --git a/third_party/rust/warp/src/filters/log.rs b/third_party/rust/warp/src/filters/log.rs new file mode 100644 index 0000000000..c3ff7c3b26 --- /dev/null +++ b/third_party/rust/warp/src/filters/log.rs @@ -0,0 +1,275 @@ +//! Logger Filters + +use std::fmt; +use std::net::SocketAddr; +use std::time::{Duration, Instant}; + +use http::{self, header, StatusCode}; + +use crate::filter::{Filter, WrapSealed}; +use crate::reject::IsReject; +use crate::reply::Reply; +use crate::route::Route; + +use self::internal::WithLog; + +/// Create a wrapping filter with the specified `name` as the `target`. +/// +/// This uses the default access logging format, and log records produced +/// will have their `target` set to `name`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // If using something like `pretty_env_logger`, +/// // view logs by setting `RUST_LOG=example::api`. +/// let log = warp::log("example::api"); +/// let route = warp::any() +/// .map(warp::reply) +/// .with(log); +/// ``` +pub fn log(name: &'static str) -> Log<impl Fn(Info) + Copy> { + let func = move |info: Info| { + // TODO? + // - response content length? + log::info!( + target: name, + "{} \"{} {} {:?}\" {} \"{}\" \"{}\" {:?}", + OptFmt(info.route.remote_addr()), + info.method(), + info.path(), + info.route.version(), + info.status().as_u16(), + OptFmt(info.referer()), + OptFmt(info.user_agent()), + info.elapsed(), + ); + }; + Log { func } +} + +/// Create a wrapping filter that receives `warp::log::Info`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let log = warp::log::custom(|info| { +/// // Use a log macro, or slog, or println, or whatever! +/// eprintln!( +/// "{} {} {}", +/// info.method(), +/// info.path(), +/// info.status(), +/// ); +/// }); +/// let route = warp::any() +/// .map(warp::reply) +/// .with(log); +/// ``` +pub fn custom<F>(func: F) -> Log<F> +where + F: Fn(Info), +{ + Log { func } +} + +/// Decorates a [`Filter`](crate::Filter) to log requests and responses. +#[derive(Clone, Copy, Debug)] +pub struct Log<F> { + func: F, +} + +/// Information about the request/response that can be used to prepare log lines. +#[allow(missing_debug_implementations)] +pub struct Info<'a> { + route: &'a Route, + start: Instant, + status: StatusCode, +} + +impl<FN, F> WrapSealed<F> for Log<FN> +where + FN: Fn(Info) + Clone + Send, + F: Filter + Clone + Send, + F::Extract: Reply, + F::Error: IsReject, +{ + type Wrapped = WithLog<FN, F>; + + fn wrap(&self, filter: F) -> Self::Wrapped { + WithLog { + filter, + log: self.clone(), + } + } +} + +impl<'a> Info<'a> { + /// View the remote `SocketAddr` of the request. + pub fn remote_addr(&self) -> Option<SocketAddr> { + self.route.remote_addr() + } + + /// View the `http::Method` of the request. + pub fn method(&self) -> &http::Method { + self.route.method() + } + + /// View the URI path of the request. + pub fn path(&self) -> &str { + self.route.full_path() + } + + /// View the `http::Version` of the request. + pub fn version(&self) -> http::Version { + self.route.version() + } + + /// View the `http::StatusCode` of the response. + pub fn status(&self) -> http::StatusCode { + self.status + } + + /// View the referer of the request. + pub fn referer(&self) -> Option<&str> { + self.route + .headers() + .get(header::REFERER) + .and_then(|v| v.to_str().ok()) + } + + /// View the user agent of the request. + pub fn user_agent(&self) -> Option<&str> { + self.route + .headers() + .get(header::USER_AGENT) + .and_then(|v| v.to_str().ok()) + } + + /// View the `Duration` that elapsed for the request. + pub fn elapsed(&self) -> Duration { + tokio::time::Instant::now().into_std() - self.start + } + + /// View the host of the request + pub fn host(&self) -> Option<&str> { + self.route + .headers() + .get(header::HOST) + .and_then(|v| v.to_str().ok()) + } +} + +struct OptFmt<T>(Option<T>); + +impl<T: fmt::Display> fmt::Display for OptFmt<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref t) = self.0 { + fmt::Display::fmt(t, f) + } else { + f.write_str("-") + } + } +} + +mod internal { + use std::future::Future; + use std::pin::Pin; + use std::task::{Context, Poll}; + use std::time::Instant; + + use futures::{ready, TryFuture}; + use pin_project::pin_project; + + use super::{Info, Log}; + use crate::filter::{Filter, FilterBase, Internal}; + use crate::reject::IsReject; + use crate::reply::{Reply, Response}; + use crate::route; + + #[allow(missing_debug_implementations)] + pub struct Logged(pub(super) Response); + + impl Reply for Logged { + #[inline] + fn into_response(self) -> Response { + self.0 + } + } + + #[allow(missing_debug_implementations)] + #[derive(Clone, Copy)] + pub struct WithLog<FN, F> { + pub(super) filter: F, + pub(super) log: Log<FN>, + } + + impl<FN, F> FilterBase for WithLog<FN, F> + where + FN: Fn(Info) + Clone + Send, + F: Filter + Clone + Send, + F::Extract: Reply, + F::Error: IsReject, + { + type Extract = (Logged,); + type Error = F::Error; + type Future = WithLogFuture<FN, F::Future>; + + fn filter(&self, _: Internal) -> Self::Future { + let started = tokio::time::Instant::now().into_std(); + WithLogFuture { + log: self.log.clone(), + future: self.filter.filter(Internal), + started, + } + } + } + + #[allow(missing_debug_implementations)] + #[pin_project] + pub struct WithLogFuture<FN, F> { + log: Log<FN>, + #[pin] + future: F, + started: Instant, + } + + impl<FN, F> Future for WithLogFuture<FN, F> + where + FN: Fn(Info), + F: TryFuture, + F::Ok: Reply, + F::Error: IsReject, + { + type Output = Result<(Logged,), F::Error>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + let pin = self.as_mut().project(); + let (result, status) = match ready!(pin.future.try_poll(cx)) { + Ok(reply) => { + let resp = reply.into_response(); + let status = resp.status(); + (Poll::Ready(Ok((Logged(resp),))), status) + } + Err(reject) => { + let status = reject.status(); + (Poll::Ready(Err(reject)), status) + } + }; + + route::with(|route| { + (self.log.func)(Info { + route, + start: self.started, + status, + }); + }); + + result + } + } +} diff --git a/third_party/rust/warp/src/filters/method.rs b/third_party/rust/warp/src/filters/method.rs new file mode 100644 index 0000000000..ce5b767119 --- /dev/null +++ b/third_party/rust/warp/src/filters/method.rs @@ -0,0 +1,150 @@ +//! HTTP Method filters. +//! +//! The filters deal with the HTTP Method part of a request. Several here will +//! match the request `Method`, and if not matched, will reject the request +//! with a `405 Method Not Allowed`. +//! +//! There is also [`warp::method()`](method), which never rejects +//! a request, and just extracts the method to be used in your filter chains. +use futures::future; +use http::Method; + +use crate::filter::{filter_fn, filter_fn_one, Filter, One}; +use crate::reject::Rejection; +use std::convert::Infallible; + +/// Create a `Filter` that requires the request method to be `GET`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let get_only = warp::get().map(warp::reply); +/// ``` +pub fn get() -> impl Filter<Extract = (), Error = Rejection> + Copy { + method_is(|| &Method::GET) +} + +/// Create a `Filter` that requires the request method to be `POST`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let post_only = warp::post().map(warp::reply); +/// ``` +pub fn post() -> impl Filter<Extract = (), Error = Rejection> + Copy { + method_is(|| &Method::POST) +} + +/// Create a `Filter` that requires the request method to be `PUT`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let put_only = warp::put().map(warp::reply); +/// ``` +pub fn put() -> impl Filter<Extract = (), Error = Rejection> + Copy { + method_is(|| &Method::PUT) +} + +/// Create a `Filter` that requires the request method to be `DELETE`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let delete_only = warp::delete().map(warp::reply); +/// ``` +pub fn delete() -> impl Filter<Extract = (), Error = Rejection> + Copy { + method_is(|| &Method::DELETE) +} + +/// Create a `Filter` that requires the request method to be `HEAD`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let head_only = warp::head().map(warp::reply); +/// ``` +pub fn head() -> impl Filter<Extract = (), Error = Rejection> + Copy { + method_is(|| &Method::HEAD) +} + +/// Create a `Filter` that requires the request method to be `OPTIONS`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let options_only = warp::options().map(warp::reply); +/// ``` +pub fn options() -> impl Filter<Extract = (), Error = Rejection> + Copy { + method_is(|| &Method::OPTIONS) +} + +/// Create a `Filter` that requires the request method to be `PATCH`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let patch_only = warp::patch().map(warp::reply); +/// ``` +pub fn patch() -> impl Filter<Extract = (), Error = Rejection> + Copy { + method_is(|| &Method::PATCH) +} + +/// Extract the `Method` from the request. +/// +/// This never rejects a request. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::method() +/// .map(|method| { +/// format!("You sent a {} request!", method) +/// }); +/// ``` +pub fn method() -> impl Filter<Extract = One<Method>, Error = Infallible> + Copy { + filter_fn_one(|route| future::ok::<_, Infallible>(route.method().clone())) +} + +// NOTE: This takes a static function instead of `&'static Method` directly +// so that the `impl Filter` can be zero-sized. Moving it around should be +// cheaper than holding a single static pointer (which would make it 1 word). +fn method_is<F>(func: F) -> impl Filter<Extract = (), Error = Rejection> + Copy +where + F: Fn() -> &'static Method + Copy, +{ + filter_fn(move |route| { + let method = func(); + log::trace!("method::{:?}?: {:?}", method, route.method()); + if route.method() == method { + future::ok(()) + } else { + future::err(crate::reject::method_not_allowed()) + } + }) +} + +#[cfg(test)] +mod tests { + #[test] + fn method_size_of() { + // See comment on `method_is` function. + assert_eq!(std::mem::size_of_val(&super::get()), 0,); + } +} diff --git a/third_party/rust/warp/src/filters/mod.rs b/third_party/rust/warp/src/filters/mod.rs new file mode 100644 index 0000000000..9225601d3c --- /dev/null +++ b/third_party/rust/warp/src/filters/mod.rs @@ -0,0 +1,25 @@ +//! Built-in Filters +//! +//! This module mostly serves as documentation to group together the list of +//! built-in filters. Most of these are available at more convenient paths. + +pub mod addr; +pub mod any; +pub mod body; +pub mod cookie; +pub mod cors; +pub mod ext; +pub mod fs; +pub mod header; +pub mod log; +pub mod method; +#[cfg(feature = "multipart")] +pub mod multipart; +pub mod path; +pub mod query; +pub mod reply; +pub mod sse; +#[cfg(feature = "websocket")] +pub mod ws; + +pub use crate::filter::BoxedFilter; diff --git a/third_party/rust/warp/src/filters/multipart.rs b/third_party/rust/warp/src/filters/multipart.rs new file mode 100644 index 0000000000..2fbc4247a5 --- /dev/null +++ b/third_party/rust/warp/src/filters/multipart.rs @@ -0,0 +1,190 @@ +//! Multipart body filters +//! +//! Filters that extract a multipart body for a route. + +use std::fmt; +use std::future::Future; +use std::io::{Cursor, Read}; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use bytes::{Buf, Bytes}; +use futures::{future, Stream}; +use headers::ContentType; +use mime::Mime; +use multipart::server::Multipart; + +use crate::filter::{Filter, FilterBase, Internal}; +use crate::reject::{self, Rejection}; + +// If not otherwise configured, default to 2MB. +const DEFAULT_FORM_DATA_MAX_LENGTH: u64 = 1024 * 1024 * 2; + +/// A `Filter` to extract a `multipart/form-data` body from a request. +/// +/// Create with the `warp::multipart::form()` function. +#[derive(Debug, Clone)] +pub struct FormOptions { + max_length: u64, +} + +/// A `Stream` of multipart/form-data `Part`s. +/// +/// Extracted with a `warp::multipart::form` filter. +pub struct FormData { + inner: Multipart<Cursor<::bytes::Bytes>>, +} + +/// A single "part" of a multipart/form-data body. +/// +/// Yielded from the `FormData` stream. +pub struct Part { + name: String, + filename: Option<String>, + content_type: Option<String>, + data: Option<Vec<u8>>, +} + +/// Create a `Filter` to extact a `multipart/form-data` body from a request. +/// +/// The extracted `FormData` type is a `Stream` of `Part`s, and each `Part` +/// in turn is a `Stream` of bytes. +pub fn form() -> FormOptions { + FormOptions { + max_length: DEFAULT_FORM_DATA_MAX_LENGTH, + } +} + +// ===== impl Form ===== + +impl FormOptions { + /// Set the maximum byte length allowed for this body. + /// + /// Defaults to 2MB. + pub fn max_length(mut self, max: u64) -> Self { + self.max_length = max; + self + } +} + +type FormFut = Pin<Box<dyn Future<Output = Result<(FormData,), Rejection>> + Send>>; + +impl FilterBase for FormOptions { + type Extract = (FormData,); + type Error = Rejection; + type Future = FormFut; + + fn filter(&self, _: Internal) -> Self::Future { + let boundary = super::header::header2::<ContentType>().and_then(|ct| { + let mime = Mime::from(ct); + let mime = mime + .get_param("boundary") + .map(|v| v.to_string()) + .ok_or_else(|| reject::invalid_header("content-type")); + future::ready(mime) + }); + + let filt = super::body::content_length_limit(self.max_length) + .and(boundary) + .and(super::body::bytes()) + .map(|boundary, body| FormData { + inner: Multipart::with_body(Cursor::new(body), boundary), + }); + + let fut = filt.filter(Internal); + + Box::pin(fut) + } +} + +// ===== impl FormData ===== + +impl fmt::Debug for FormData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("FormData").finish() + } +} + +impl Stream for FormData { + type Item = Result<Part, crate::Error>; + + fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> { + match (*self).inner.read_entry() { + Ok(Some(mut field)) => { + let mut data = Vec::new(); + field + .data + .read_to_end(&mut data) + .map_err(crate::Error::new)?; + Poll::Ready(Some(Ok(Part { + name: field.headers.name.to_string(), + filename: field.headers.filename, + content_type: field.headers.content_type.map(|m| m.to_string()), + data: Some(data), + }))) + } + Ok(None) => Poll::Ready(None), + Err(e) => Poll::Ready(Some(Err(crate::Error::new(e)))), + } + } +} + +// ===== impl Part ===== + +impl Part { + /// Get the name of this part. + pub fn name(&self) -> &str { + &self.name + } + + /// Get the filename of this part, if present. + pub fn filename(&self) -> Option<&str> { + self.filename.as_ref().map(|s| &**s) + } + + /// Get the content-type of this part, if present. + pub fn content_type(&self) -> Option<&str> { + self.content_type.as_ref().map(|s| &**s) + } + + /// Asynchronously get some of the data for this `Part`. + pub async fn data(&mut self) -> Option<Result<impl Buf, crate::Error>> { + self.take_data() + } + + /// Convert this `Part` into a `Stream` of `Buf`s. + pub fn stream(self) -> impl Stream<Item = Result<impl Buf, crate::Error>> { + PartStream(self) + } + + fn take_data(&mut self) -> Option<Result<Bytes, crate::Error>> { + self.data.take().map(|vec| Ok(vec.into())) + } +} + +impl fmt::Debug for Part { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut builder = f.debug_struct("Part"); + builder.field("name", &self.name); + + if let Some(ref filename) = self.filename { + builder.field("filename", filename); + } + + if let Some(ref mime) = self.content_type { + builder.field("content_type", mime); + } + + builder.finish() + } +} + +struct PartStream(Part); + +impl Stream for PartStream { + type Item = Result<Bytes, crate::Error>; + + fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> { + Poll::Ready(self.0.take_data()) + } +} diff --git a/third_party/rust/warp/src/filters/path.rs b/third_party/rust/warp/src/filters/path.rs new file mode 100644 index 0000000000..bfaa33d82f --- /dev/null +++ b/third_party/rust/warp/src/filters/path.rs @@ -0,0 +1,649 @@ +//! Path Filters +//! +//! The filters here work on the "path" of requests. +//! +//! - [`path`](./fn.path.html) matches a specific segment, like `/foo`. +//! - [`param`](./fn.param.html) tries to parse a segment into a type, like `/:u16`. +//! - [`end`](./fn.end.html) matches when the path end is found. +//! - [`path!`](../../macro.path.html) eases combining multiple `path` and `param` filters. +//! +//! # Routing +//! +//! Routing in warp is simple yet powerful. +//! +//! First up, matching a single segment: +//! +//! ``` +//! use warp::Filter; +//! +//! // GET /hi +//! let hi = warp::path("hi").map(|| { +//! "Hello, World!" +//! }); +//! ``` +//! +//! How about multiple segments? It's easiest with the `path!` macro: +//! +//! ``` +//! # use warp::Filter; +//! // GET /hello/from/warp +//! let hello_from_warp = warp::path!("hello" / "from" / "warp").map(|| { +//! "Hello from warp!" +//! }); +//! ``` +//! +//! Neat! But how do I handle **parameters** in paths? +//! +//! ``` +//! # use warp::Filter; +//! // GET /sum/:u32/:u32 +//! let sum = warp::path!("sum" / u32 / u32).map(|a, b| { +//! format!("{} + {} = {}", a, b, a + b) +//! }); +//! ``` +//! +//! In fact, any type that implements `FromStr` can be used, in any order: +//! +//! ``` +//! # use warp::Filter; +//! // GET /:u16/times/:u16 +//! let times = warp::path!(u16 / "times" / u16).map(|a, b| { +//! format!("{} times {} = {}", a, b, a * b) +//! }); +//! ``` +//! +//! Oh shoot, those math routes should be **mounted** at a different path, +//! is that possible? Yep! +//! +//! ``` +//! # use warp::Filter; +//! # let sum = warp::any().map(warp::reply); +//! # let times = sum.clone(); +//! // GET /math/sum/:u32/:u32 +//! // GET /math/:u16/times/:u16 +//! let math = warp::path("math"); +//! let math_sum = math.and(sum); +//! let math_times = math.and(times); +//! ``` +//! +//! What! `and`? What's that do? +//! +//! It combines the filters in a sort of "this and then that" order. In fact, +//! it's exactly what the `path!` macro has been doing internally. +//! +//! ``` +//! # use warp::Filter; +//! // GET /bye/:string +//! let bye = warp::path("bye") +//! .and(warp::path::param()) +//! .map(|name: String| { +//! format!("Good bye, {}!", name) +//! }); +//! ``` +//! +//! Ah, so, can filters do things besides `and`? +//! +//! Why, yes they can! They can also `or`! As you might expect, `or` creates a +//! "this or else that" chain of filters. If the first doesn't succeed, then +//! it tries the other. +//! +//! So, those `math` routes could have been **mounted** all as one, with `or`. +//! +//! +//! ``` +//! # use warp::Filter; +//! # let sum = warp::path("sum"); +//! # let times = warp::path("times"); +//! // GET /math/sum/:u32/:u32 +//! // GET /math/:u16/times/:u16 +//! let math = warp::path("math") +//! .and(sum.or(times)); +//! ``` +//! +//! It turns out, using `or` is how you combine everything together into a +//! single API. +//! +//! ``` +//! # use warp::Filter; +//! # let hi = warp::path("hi"); +//! # let hello_from_warp = hi.clone(); +//! # let bye = hi.clone(); +//! # let math = hi.clone(); +//! // GET /hi +//! // GET /hello/from/warp +//! // GET /bye/:string +//! // GET /math/sum/:u32/:u32 +//! // GET /math/:u16/times/:u16 +//! let routes = hi +//! .or(hello_from_warp) +//! .or(bye) +//! .or(math); +//! ``` +//! +//! Note that you will generally want path filters to come **before** other filters +//! like `body` or `headers`. If a different type of filter comes first, a request +//! with an invalid body for route `/right-path-wrong-body` may try matching against `/wrong-path` +//! and return the error from `/wrong-path` instead of the correct body-related error. + +use std::convert::Infallible; +use std::fmt; +use std::str::FromStr; + +use futures::future; +use http::uri::PathAndQuery; + +use self::internal::Opaque; +use crate::filter::{filter_fn, one, Filter, FilterBase, Internal, One, Tuple}; +use crate::reject::{self, Rejection}; +use crate::route::{self, Route}; + +/// Create an exact match path segment `Filter`. +/// +/// This will try to match exactly to the current request path segment. +/// +/// # Note +/// +/// - [`end()`](./fn.end.html) should be used to match the end of a path to avoid having +/// filters for shorter paths like `/math` unintentionally match a longer +/// path such as `/math/sum` +/// - Path-related filters should generally come **before** other types of filters, such +/// as those checking headers or body types. Including those other filters before +/// the path checks may result in strange errors being returned because a given request +/// does not match the parameters for a completely separate route. +/// +/// # Panics +/// +/// Exact path filters cannot be empty, or contain slashes. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // Matches '/hello' +/// let hello = warp::path("hello") +/// .map(|| "Hello, World!"); +/// ``` +pub fn path<P>(p: P) -> Exact<Opaque<P>> +where + P: AsRef<str>, +{ + let s = p.as_ref(); + assert!(!s.is_empty(), "exact path segments should not be empty"); + assert!( + !s.contains('/'), + "exact path segments should not contain a slash: {:?}", + s + ); + + Exact(Opaque(p)) + /* + segment(move |seg| { + log::trace!("{:?}?: {:?}", p, seg); + if seg == p { + Ok(()) + } else { + Err(reject::not_found()) + } + }) + */ +} + +/// A `Filter` matching an exact path segment. +/// +/// Constructed from `path()` or `path!()`. +#[allow(missing_debug_implementations)] +#[derive(Clone, Copy)] +pub struct Exact<P>(P); + +impl<P> FilterBase for Exact<P> +where + P: AsRef<str>, +{ + type Extract = (); + type Error = Rejection; + type Future = future::Ready<Result<Self::Extract, Self::Error>>; + + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + route::with(|route| { + let p = self.0.as_ref(); + future::ready(with_segment(route, |seg| { + log::trace!("{:?}?: {:?}", p, seg); + + if seg == p { + Ok(()) + } else { + Err(reject::not_found()) + } + })) + }) + } +} + +/// Matches the end of a route. +/// +/// Note that _not_ including `end()` may result in shorter paths like +/// `/math` unintentionally matching `/math/sum`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // Matches '/' +/// let hello = warp::path::end() +/// .map(|| "Hello, World!"); +/// ``` +pub fn end() -> impl Filter<Extract = (), Error = Rejection> + Copy { + filter_fn(move |route| { + if route.path().is_empty() { + future::ok(()) + } else { + future::err(reject::not_found()) + } + }) +} + +/// Extract a parameter from a path segment. +/// +/// This will try to parse a value from the current request path +/// segment, and if successful, the value is returned as the `Filter`'s +/// "extracted" value. +/// +/// If the value could not be parsed, rejects with a `404 Not Found`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::path::param() +/// .map(|id: u32| { +/// format!("You asked for /{}", id) +/// }); +/// ``` +pub fn param<T: FromStr + Send + 'static>( +) -> impl Filter<Extract = One<T>, Error = Rejection> + Copy { + filter_segment(|seg| { + log::trace!("param?: {:?}", seg); + if seg.is_empty() { + return Err(reject::not_found()); + } + T::from_str(seg).map(one).map_err(|_| reject::not_found()) + }) +} + +/// Extract the unmatched tail of the path. +/// +/// This will return a `Tail`, which allows access to the rest of the path +/// that previous filters have not already matched. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::path("foo") +/// .and(warp::path::tail()) +/// .map(|tail| { +/// // GET /foo/bar/baz would return "bar/baz". +/// format!("The tail after foo is {:?}", tail) +/// }); +/// ``` +pub fn tail() -> impl Filter<Extract = One<Tail>, Error = Infallible> + Copy { + filter_fn(move |route| { + let path = path_and_query(&route); + let idx = route.matched_path_index(); + + // Giving the user the full tail means we assume the full path + // has been matched now. + let end = path.path().len() - idx; + route.set_unmatched_path(end); + + future::ok(one(Tail { + path, + start_index: idx, + })) + }) +} + +/// Represents that tail part of a request path, returned by the `tail()` filter. +pub struct Tail { + path: PathAndQuery, + start_index: usize, +} + +impl Tail { + /// Get the `&str` representation of the remaining path. + pub fn as_str(&self) -> &str { + &self.path.path()[self.start_index..] + } +} + +impl fmt::Debug for Tail { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} + +/// Peek at the unmatched tail of the path, without affecting the matched path. +/// +/// This will return a `Peek`, which allows access to the rest of the path +/// that previous filters have not already matched. This differs from `tail` +/// in that `peek` will **not** set the entire path as matched. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::path("foo") +/// .and(warp::path::peek()) +/// .map(|peek| { +/// // GET /foo/bar/baz would return "bar/baz". +/// format!("The path after foo is {:?}", peek) +/// }); +/// ``` +pub fn peek() -> impl Filter<Extract = One<Peek>, Error = Infallible> + Copy { + filter_fn(move |route| { + let path = path_and_query(&route); + let idx = route.matched_path_index(); + + future::ok(one(Peek { + path, + start_index: idx, + })) + }) +} + +/// Represents that tail part of a request path, returned by the `tail()` filter. +pub struct Peek { + path: PathAndQuery, + start_index: usize, +} + +impl Peek { + /// Get the `&str` representation of the remaining path. + pub fn as_str(&self) -> &str { + &self.path.path()[self.start_index..] + } + + /// Get an iterator over the segments of the peeked path. + pub fn segments(&self) -> impl Iterator<Item = &str> { + self.as_str().split('/').filter(|seg| !seg.is_empty()) + } +} + +impl fmt::Debug for Peek { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} + +/// Returns the full request path, irrespective of other filters. +/// +/// This will return a `FullPath`, which can be stringified to return the +/// full path of the request. +/// +/// This is more useful in generic pre/post-processing filters, and should +/// probably not be used for request matching/routing. +/// +/// # Example +/// +/// ``` +/// use warp::{Filter, path::FullPath}; +/// use std::{collections::HashMap, sync::{Arc, Mutex}}; +/// +/// let counts = Arc::new(Mutex::new(HashMap::new())); +/// let access_counter = warp::path::full() +/// .map(move |path: FullPath| { +/// let mut counts = counts.lock().unwrap(); +/// +/// *counts.entry(path.as_str().to_string()) +/// .and_modify(|c| *c += 1) +/// .or_insert(0) +/// }); +/// +/// let route = warp::path("foo") +/// .and(warp::path("bar")) +/// .and(access_counter) +/// .map(|count| { +/// format!("This is the {}th visit to this URL!", count) +/// }); +/// ``` +pub fn full() -> impl Filter<Extract = One<FullPath>, Error = Infallible> + Copy { + filter_fn(move |route| future::ok(one(FullPath(path_and_query(&route))))) +} + +/// Represents the full request path, returned by the `full()` filter. +pub struct FullPath(PathAndQuery); + +impl FullPath { + /// Get the `&str` representation of the request path. + pub fn as_str(&self) -> &str { + &self.0.path() + } +} + +impl fmt::Debug for FullPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} + +fn filter_segment<F, U>(func: F) -> impl Filter<Extract = U, Error = Rejection> + Copy +where + F: Fn(&str) -> Result<U, Rejection> + Copy, + U: Tuple + Send + 'static, +{ + filter_fn(move |route| future::ready(with_segment(route, func))) +} + +fn with_segment<F, U>(route: &mut Route, func: F) -> Result<U, Rejection> +where + F: Fn(&str) -> Result<U, Rejection>, +{ + let seg = segment(route); + let ret = func(seg); + if ret.is_ok() { + let idx = seg.len(); + route.set_unmatched_path(idx); + } + ret +} + +fn segment(route: &Route) -> &str { + route + .path() + .splitn(2, '/') + .next() + .expect("split always has at least 1") +} + +fn path_and_query(route: &Route) -> PathAndQuery { + route + .uri() + .path_and_query() + .expect("server URIs should always have path_and_query") + .clone() +} + +/// Convenient way to chain multiple path filters together. +/// +/// Any number of either type identifiers or string expressions can be passed, +/// each separated by a forward slash (`/`). Strings will be used to match +/// path segments exactly, and type identifiers are used just like +/// [`param`](filters::path::param) filters. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // Match `/sum/:a/:b` +/// let route = warp::path!("sum" / u32 / u32) +/// .map(|a, b| { +/// format!("{} + {} = {}", a, b, a + b) +/// }); +/// ``` +/// +/// The equivalent filter chain without using the `path!` macro looks this: +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::path("sum") +/// .and(warp::path::param::<u32>()) +/// .and(warp::path::param::<u32>()) +/// .and(warp::path::end()) +/// .map(|a, b| { +/// format!("{} + {} = {}", a, b, a + b) +/// }); +/// ``` +/// +/// # Path Prefixes +/// +/// The `path!` macro automatically assumes the path should include an `end()` +/// filter. To build up a path filter *prefix*, such that the `end()` isn't +/// included, use the `/ ..` syntax. +/// +/// +/// ``` +/// use warp::Filter; +/// +/// let prefix = warp::path!("math" / "sum" / ..); +/// +/// let sum = warp::path!(u32 / u32) +/// .map(|a, b| { +/// format!("{} + {} = {}", a, b, a + b) +/// }); +/// +/// let help = warp::path::end() +/// .map(|| "This API returns the sum of two u32's"); +/// +/// let api = prefix.and(sum.or(help)); +/// ``` +#[macro_export] +macro_rules! path { + ($($pieces:tt)*) => ({ + $crate::__internal_path!(@start $($pieces)*) + }); +} + +#[doc(hidden)] +#[macro_export] +// not public API +macro_rules! __internal_path { + (@start ..) => ({ + compile_error!("'..' cannot be the only segment") + }); + (@start $first:tt $(/ $tail:tt)*) => ({ + $crate::__internal_path!(@munch $crate::any(); [$first] [$(/ $tail)*]) + }); + + (@munch $sum:expr; [$cur:tt] [/ $next:tt $(/ $tail:tt)*]) => ({ + $crate::__internal_path!(@munch $crate::Filter::and($sum, $crate::__internal_path!(@segment $cur)); [$next] [$(/ $tail)*]) + }); + (@munch $sum:expr; [$cur:tt] []) => ({ + $crate::__internal_path!(@last $sum; $cur) + }); + + (@last $sum:expr; ..) => ( + $sum + ); + (@last $sum:expr; $end:tt) => ( + $crate::Filter::and( + $crate::Filter::and($sum, $crate::__internal_path!(@segment $end)), + $crate::path::end() + ) + ); + + (@segment ..) => ( + compile_error!("'..' must be the last segment") + ); + (@segment $param:ty) => ( + $crate::path::param::<$param>() + ); + // Constructs a unique ZST so the &'static str pointer doesn't need to + // be carried around. + (@segment $s:literal) => ({ + #[derive(Clone, Copy)] + struct __StaticPath; + impl ::std::convert::AsRef<str> for __StaticPath { + fn as_ref(&self) -> &str { + static S: &str = $s; + S + } + } + $crate::path(__StaticPath) + }); +} + +// path! compile fail tests + +/// ```compile_fail +/// warp::path!("foo" / .. / "bar"); +/// ``` +/// +/// ```compile_fail +/// warp::path!(.. / "bar"); +/// ``` +/// +/// ```compile_fail +/// warp::path!("foo" ..); +/// ``` +/// +/// ```compile_fail +/// warp::path!("foo" / .. /); +/// ``` +/// +/// ```compile_fail +/// warp::path!(..); +/// ``` +fn _path_macro_compile_fail() {} + +mod internal { + // Used to prevent users from naming this type. + // + // For instance, `Exact<Opaque<String>>` means a user cannot depend + // on it being `Exact<String>`. + #[allow(missing_debug_implementations)] + #[derive(Clone, Copy)] + pub struct Opaque<T>(pub(super) T); + + impl<T: AsRef<str>> AsRef<str> for Opaque<T> { + #[inline] + fn as_ref(&self) -> &str { + self.0.as_ref() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_path_exact_size() { + use std::mem::{size_of, size_of_val}; + + assert_eq!( + size_of_val(&path("hello")), + size_of::<&str>(), + "exact(&str) is size of &str" + ); + + assert_eq!( + size_of_val(&path(String::from("world"))), + size_of::<String>(), + "exact(String) is size of String" + ); + + assert_eq!( + size_of_val(&path!("zst")), + size_of::<()>(), + "path!(&str) is ZST" + ); + } +} diff --git a/third_party/rust/warp/src/filters/query.rs b/third_party/rust/warp/src/filters/query.rs new file mode 100644 index 0000000000..be72243267 --- /dev/null +++ b/third_party/rust/warp/src/filters/query.rs @@ -0,0 +1,39 @@ +//! Query Filters + +use futures::future; +use serde::de::DeserializeOwned; +use serde_urlencoded; + +use crate::filter::{filter_fn_one, Filter, One}; +use crate::reject::{self, Rejection}; + +/// Creates a `Filter` that decodes query parameters to the type `T`. +/// +/// If cannot decode into a `T`, the request is rejected with a `400 Bad Request`. +pub fn query<T: DeserializeOwned + Send + 'static>( +) -> impl Filter<Extract = One<T>, Error = Rejection> + Copy { + filter_fn_one(|route| { + let query_string = route.query().unwrap_or_else(|| { + log::debug!("route was called without a query string, defaulting to empty"); + "" + }); + + let query_encoded = serde_urlencoded::from_str(query_string).map_err(|e| { + log::debug!("failed to decode query string '{}': {:?}", query_string, e); + reject::invalid_query() + }); + future::ready(query_encoded) + }) +} + +/// Creates a `Filter` that returns the raw query string as type String. +pub fn raw() -> impl Filter<Extract = One<String>, Error = Rejection> + Copy { + filter_fn_one(|route| { + let route = route + .query() + .map(|q| q.to_owned()) + .map(Ok) + .unwrap_or_else(|| Err(reject::invalid_query())); + future::ready(route) + }) +} diff --git a/third_party/rust/warp/src/filters/reply.rs b/third_party/rust/warp/src/filters/reply.rs new file mode 100644 index 0000000000..c4c37bbd80 --- /dev/null +++ b/third_party/rust/warp/src/filters/reply.rs @@ -0,0 +1,257 @@ +//! Reply Filters +//! +//! These "filters" behave a little differently than the rest. Instead of +//! being used directly on requests, these filters "wrap" other filters. +//! +//! +//! ## Wrapping a `Filter` (`with`) +//! +//! ``` +//! use warp::Filter; +//! +//! let with_server = warp::reply::with::header("server", "warp"); +//! +//! let route = warp::any() +//! .map(warp::reply) +//! .with(with_server); +//! ``` +//! +//! Wrapping allows adding in conditional logic *before* the request enters +//! the inner filter (though the `with::header` wrapper does not). + +use std::convert::TryFrom; +use std::sync::Arc; + +use http::header::{HeaderMap, HeaderName, HeaderValue}; + +use self::sealed::{WithDefaultHeader_, WithHeader_, WithHeaders_}; +use crate::filter::{Filter, Map, WrapSealed}; +use crate::reply::Reply; + +/// Wrap a [`Filter`](crate::Filter) that adds a header to the reply. +/// +/// # Note +/// +/// This **only** adds a header if the underlying filter is successful, and +/// returns a [`Reply`](Reply). If the underlying filter was rejected, the +/// header is not added. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // Always set `foo: bar` header. +/// let route = warp::any() +/// .map(warp::reply) +/// .with(warp::reply::with::header("foo", "bar")); +/// ``` +pub fn header<K, V>(name: K, value: V) -> WithHeader +where + HeaderName: TryFrom<K>, + <HeaderName as TryFrom<K>>::Error: Into<http::Error>, + HeaderValue: TryFrom<V>, + <HeaderValue as TryFrom<V>>::Error: Into<http::Error>, +{ + let (name, value) = assert_name_and_value(name, value); + WithHeader { name, value } +} + +/// Wrap a [`Filter`](crate::Filter) that adds multiple headers to the reply. +/// +/// # Note +/// +/// This **only** adds a header if the underlying filter is successful, and +/// returns a [`Reply`](Reply). If the underlying filter was rejected, the +/// header is not added. +/// +/// # Example +/// +/// ``` +/// use warp::http::header::{HeaderMap, HeaderValue}; +/// use warp::Filter; +/// +/// let mut headers = HeaderMap::new(); +/// headers.insert("server", HeaderValue::from_static("wee/0")); +/// headers.insert("foo", HeaderValue::from_static("bar")); +/// +/// // Always set `server: wee/0` and `foo: bar` headers. +/// let route = warp::any() +/// .map(warp::reply) +/// .with(warp::reply::with::headers(headers)); +/// ``` +pub fn headers(headers: HeaderMap) -> WithHeaders { + WithHeaders { + headers: Arc::new(headers), + } +} + +// pub fn headers? + +/// Wrap a [`Filter`](crate::Filter) that adds a header to the reply, if they +/// aren't already set. +/// +/// # Note +/// +/// This **only** adds a header if the underlying filter is successful, and +/// returns a [`Reply`](Reply). If the underlying filter was rejected, the +/// header is not added. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // Set `server: warp` if not already set. +/// let route = warp::any() +/// .map(warp::reply) +/// .with(warp::reply::with::default_header("server", "warp")); +/// ``` +pub fn default_header<K, V>(name: K, value: V) -> WithDefaultHeader +where + HeaderName: TryFrom<K>, + <HeaderName as TryFrom<K>>::Error: Into<http::Error>, + HeaderValue: TryFrom<V>, + <HeaderValue as TryFrom<V>>::Error: Into<http::Error>, +{ + let (name, value) = assert_name_and_value(name, value); + WithDefaultHeader { name, value } +} + +/// Wrap a `Filter` to always set a header. +#[derive(Clone, Debug)] +pub struct WithHeader { + name: HeaderName, + value: HeaderValue, +} + +impl<F, R> WrapSealed<F> for WithHeader +where + F: Filter<Extract = (R,)>, + R: Reply, +{ + type Wrapped = Map<F, WithHeader_>; + + fn wrap(&self, filter: F) -> Self::Wrapped { + let with = WithHeader_ { with: self.clone() }; + filter.map(with) + } +} + +/// Wrap a `Filter` to always set multiple headers. +#[derive(Clone, Debug)] +pub struct WithHeaders { + headers: Arc<HeaderMap>, +} + +impl<F, R> WrapSealed<F> for WithHeaders +where + F: Filter<Extract = (R,)>, + R: Reply, +{ + type Wrapped = Map<F, WithHeaders_>; + + fn wrap(&self, filter: F) -> Self::Wrapped { + let with = WithHeaders_ { with: self.clone() }; + filter.map(with) + } +} + +/// Wrap a `Filter` to set a header if it is not already set. +#[derive(Clone, Debug)] +pub struct WithDefaultHeader { + name: HeaderName, + value: HeaderValue, +} + +impl<F, R> WrapSealed<F> for WithDefaultHeader +where + F: Filter<Extract = (R,)>, + R: Reply, +{ + type Wrapped = Map<F, WithDefaultHeader_>; + + fn wrap(&self, filter: F) -> Self::Wrapped { + let with = WithDefaultHeader_ { with: self.clone() }; + filter.map(with) + } +} + +fn assert_name_and_value<K, V>(name: K, value: V) -> (HeaderName, HeaderValue) +where + HeaderName: TryFrom<K>, + <HeaderName as TryFrom<K>>::Error: Into<http::Error>, + HeaderValue: TryFrom<V>, + <HeaderValue as TryFrom<V>>::Error: Into<http::Error>, +{ + let name = <HeaderName as TryFrom<K>>::try_from(name) + .map_err(Into::into) + .unwrap_or_else(|_| panic!("invalid header name")); + + let value = <HeaderValue as TryFrom<V>>::try_from(value) + .map_err(Into::into) + .unwrap_or_else(|_| panic!("invalid header value")); + + (name, value) +} + +mod sealed { + use super::{WithDefaultHeader, WithHeader, WithHeaders}; + use crate::generic::{Func, One}; + use crate::reply::{Reply, Reply_}; + + #[derive(Clone)] + #[allow(missing_debug_implementations)] + pub struct WithHeader_ { + pub(super) with: WithHeader, + } + + impl<R: Reply> Func<One<R>> for WithHeader_ { + type Output = Reply_; + + fn call(&self, args: One<R>) -> Self::Output { + let mut resp = args.0.into_response(); + // Use "insert" to replace any set header... + resp.headers_mut() + .insert(&self.with.name, self.with.value.clone()); + Reply_(resp) + } + } + + #[derive(Clone)] + #[allow(missing_debug_implementations)] + pub struct WithHeaders_ { + pub(super) with: WithHeaders, + } + + impl<R: Reply> Func<One<R>> for WithHeaders_ { + type Output = Reply_; + + fn call(&self, args: One<R>) -> Self::Output { + let mut resp = args.0.into_response(); + for (name, value) in &*self.with.headers { + resp.headers_mut().insert(name, value.clone()); + } + Reply_(resp) + } + } + + #[derive(Clone)] + #[allow(missing_debug_implementations)] + pub struct WithDefaultHeader_ { + pub(super) with: WithDefaultHeader, + } + + impl<R: Reply> Func<One<R>> for WithDefaultHeader_ { + type Output = Reply_; + + fn call(&self, args: One<R>) -> Self::Output { + let mut resp = args.0.into_response(); + resp.headers_mut() + .entry(&self.with.name) + .or_insert_with(|| self.with.value.clone()); + + Reply_(resp) + } + } +} diff --git a/third_party/rust/warp/src/filters/sse.rs b/third_party/rust/warp/src/filters/sse.rs new file mode 100644 index 0000000000..6235339fb1 --- /dev/null +++ b/third_party/rust/warp/src/filters/sse.rs @@ -0,0 +1,715 @@ +//! Server-Sent Events (SSE) +//! +//! # Example +//! +//! ``` +//! +//! use std::time::Duration; +//! use std::convert::Infallible; +//! use warp::{Filter, sse::ServerSentEvent}; +//! use futures::{stream::iter, Stream}; +//! +//! fn sse_events() -> impl Stream<Item = Result<impl ServerSentEvent, Infallible>> { +//! iter(vec![ +//! Ok(warp::sse::data("unnamed event").into_a()), +//! Ok(( +//! warp::sse::event("chat"), +//! warp::sse::data("chat message"), +//! ).into_a().into_b()), +//! Ok(( +//! warp::sse::id(13), +//! warp::sse::event("chat"), +//! warp::sse::data("other chat message\nwith next line"), +//! warp::sse::retry(Duration::from_millis(5000)), +//! ).into_b().into_b()), +//! ]) +//! } +//! +//! let app = warp::path("push-notifications") +//! .and(warp::get()) +//! .map(|| { +//! warp::sse::reply(warp::sse::keep_alive().stream(sse_events())) +//! }); +//! ``` +//! +//! Each field already is event which can be sent to client. +//! The events with multiple fields can be created by combining fields using tuples. +//! +//! See also the [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) API, +//! which specifies the expected behavior of Server Sent Events. +//! + +use std::borrow::Cow; +use std::error::Error as StdError; +use std::fmt::{self, Display, Formatter, Write}; +use std::future::Future; +use std::pin::Pin; +use std::str::FromStr; +use std::task::{Context, Poll}; +use std::time::Duration; + +use futures::{future, Stream, TryStream, TryStreamExt}; +use http::header::{HeaderValue, CACHE_CONTROL, CONTENT_TYPE}; +use hyper::Body; +use pin_project::pin_project; +use serde::Serialize; +use serde_json; +use tokio::time::{self, Delay}; + +use self::sealed::{ + BoxedServerSentEvent, EitherServerSentEvent, SseError, SseField, SseFormat, SseWrapper, +}; +use super::header; +use crate::filter::One; +use crate::reply::Response; +use crate::{Filter, Rejection, Reply}; + +/// Server-sent event message +pub trait ServerSentEvent: SseFormat + Sized + Send + Sync + 'static { + /// Convert to either A + fn into_a<B>(self) -> EitherServerSentEvent<Self, B> { + EitherServerSentEvent::A(self) + } + + /// Convert to either B + fn into_b<A>(self) -> EitherServerSentEvent<A, Self> { + EitherServerSentEvent::B(self) + } + + /// Convert to boxed + fn boxed(self) -> BoxedServerSentEvent { + BoxedServerSentEvent(Box::new(self)) + } +} + +impl<T: SseFormat + Send + Sync + 'static> ServerSentEvent for T {} + +#[allow(missing_debug_implementations)] +struct SseComment<T>(T); + +/// Comment field (":<comment-text>") +pub fn comment<T>(comment: T) -> impl ServerSentEvent +where + T: Display + Send + Sync + 'static, +{ + SseComment(comment) +} + +impl<T: Display> SseFormat for SseComment<T> { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + if let SseField::Comment = k { + k.fmt(f)?; + self.0.fmt(f)?; + f.write_char('\n')?; + } + Ok(()) + } +} + +#[allow(missing_debug_implementations)] +struct SseEvent<T>(T); + +/// Event name field ("event:<event-name>") +pub fn event<T>(event: T) -> impl ServerSentEvent +where + T: Display + Send + Sync + 'static, +{ + SseEvent(event) +} + +impl<T: Display> SseFormat for SseEvent<T> { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + if let SseField::Event = k { + k.fmt(f)?; + self.0.fmt(f)?; + f.write_char('\n')?; + } + Ok(()) + } +} + +#[allow(missing_debug_implementations)] +struct SseId<T>(T); + +/// Identifier field ("id:<identifier>") +pub fn id<T>(id: T) -> impl ServerSentEvent +where + T: Display + Send + Sync + 'static, +{ + SseId(id) +} + +impl<T: Display> SseFormat for SseId<T> { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + if let SseField::Id = k { + k.fmt(f)?; + self.0.fmt(f)?; + f.write_char('\n')?; + } + Ok(()) + } +} + +#[allow(missing_debug_implementations)] +struct SseRetry(Duration); + +/// Retry timeout field ("retry:<timeout>") +pub fn retry(time: Duration) -> impl ServerSentEvent { + SseRetry(time) +} + +impl SseFormat for SseRetry { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + if let SseField::Retry = k { + k.fmt(f)?; + + let secs = self.0.as_secs(); + let millis = self.0.subsec_millis(); + + if secs > 0 { + // format seconds + secs.fmt(f)?; + + // pad milliseconds + if millis < 10 { + f.write_str("00")?; + } else if millis < 100 { + f.write_char('0')?; + } + } + + // format milliseconds + millis.fmt(f)?; + + f.write_char('\n')?; + } + Ok(()) + } +} + +#[allow(missing_debug_implementations)] +struct SseData<T>(T); + +/// Data field(s) ("data:<content>") +/// +/// The multiline content will be transferred +/// using sequential data fields, one per line. +pub fn data<T>(data: T) -> impl ServerSentEvent +where + T: Display + Send + Sync + 'static, +{ + SseData(data) +} + +impl<T: Display> SseFormat for SseData<T> { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + if let SseField::Data = k { + for line in self.0.to_string().split('\n') { + k.fmt(f)?; + line.fmt(f)?; + f.write_char('\n')?; + } + } + Ok(()) + } +} + +#[allow(missing_debug_implementations)] +struct SseJson<T>(T); + +/// Data field with JSON content ("data:<json-content>") +pub fn json<T>(data: T) -> impl ServerSentEvent +where + T: Serialize + Send + Sync + 'static, +{ + SseJson(data) +} + +impl<T: Serialize> SseFormat for SseJson<T> { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + if let SseField::Data = k { + k.fmt(f)?; + serde_json::to_string(&self.0) + .map_err(|error| { + log::error!("sse::json error {}", error); + fmt::Error + }) + .and_then(|data| data.fmt(f))?; + f.write_char('\n')?; + } + Ok(()) + } +} + +macro_rules! tuple_fmt { + (($($t:ident),+) => ($($i:tt),+)) => { + impl<$($t),+> SseFormat for ($($t),+) + where + $($t: SseFormat,)+ + { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + $(self.$i.fmt_field(f, k)?;)+ + Ok(()) + } + } + }; +} + +tuple_fmt!((A, B) => (0, 1)); +tuple_fmt!((A, B, C) => (0, 1, 2)); +tuple_fmt!((A, B, C, D) => (0, 1, 2, 3)); +tuple_fmt!((A, B, C, D, E) => (0, 1, 2, 3, 4)); +tuple_fmt!((A, B, C, D, E, F) => (0, 1, 2, 3, 4, 5)); +tuple_fmt!((A, B, C, D, E, F, G) => (0, 1, 2, 3, 4, 5, 6)); +tuple_fmt!((A, B, C, D, E, F, G, H) => (0, 1, 2, 3, 4, 5, 6, 7)); + +/// Gets the optional last event id from request. +/// Typically this identifier represented as number or string. +/// +/// ``` +/// let app = warp::sse::last_event_id::<u32>(); +/// +/// // The identifier is present +/// async { +/// assert_eq!( +/// warp::test::request() +/// .header("Last-Event-ID", "12") +/// .filter(&app) +/// .await +/// .unwrap(), +/// Some(12) +/// ); +/// +/// // The identifier is missing +/// assert_eq!( +/// warp::test::request() +/// .filter(&app) +/// .await +/// .unwrap(), +/// None +/// ); +/// +/// // The identifier is not a valid +/// assert!( +/// warp::test::request() +/// .header("Last-Event-ID", "abc") +/// .filter(&app) +/// .await +/// .is_err(), +/// ); +///}; +/// ``` +pub fn last_event_id<T>() -> impl Filter<Extract = One<Option<T>>, Error = Rejection> + Copy +where + T: FromStr + Send + Sync + 'static, +{ + header::optional("last-event-id") +} + +/// Server-sent events reply +/// +/// This function converts stream of server events into a `Reply` with: +/// +/// - Status of `200 OK` +/// - Header `content-type: text/event-stream` +/// - Header `cache-control: no-cache`. +/// +/// # Example +/// +/// ``` +/// +/// use std::time::Duration; +/// use futures::Stream; +/// use futures::stream::iter; +/// use std::convert::Infallible; +/// use warp::{Filter, sse::ServerSentEvent}; +/// use serde_derive::Serialize; +/// +/// #[derive(Serialize)] +/// struct Msg { +/// from: u32, +/// text: String, +/// } +/// +/// fn event_stream() -> impl Stream<Item = Result<impl ServerSentEvent, Infallible>> { +/// iter(vec![ +/// // Unnamed event with data only +/// Ok(warp::sse::data("payload").boxed()), +/// // Named event with ID and retry timeout +/// Ok(( +/// warp::sse::data("other message\nwith next line"), +/// warp::sse::event("chat"), +/// warp::sse::id(1), +/// warp::sse::retry(Duration::from_millis(15000)) +/// ).boxed()), +/// // Event with JSON data +/// Ok(( +/// warp::sse::id(2), +/// warp::sse::json(Msg { +/// from: 2, +/// text: "hello".into(), +/// }), +/// ).boxed()), +/// ]) +/// } +/// +/// async { +/// let app = warp::path("sse").and(warp::get()).map(|| { +/// warp::sse::reply(event_stream()) +/// }); +/// +/// let res = warp::test::request() +/// .method("GET") +/// .header("Connection", "Keep-Alive") +/// .path("/sse") +/// .reply(&app) +/// .await +/// .into_body(); +/// +/// assert_eq!( +/// res, +/// r#"data:payload +/// +/// event:chat +/// data:other message +/// data:with next line +/// id:1 +/// retry:15000 +/// +/// data:{"from":2,"text":"hello"} +/// id:2 +/// +/// "# +/// ); +/// }; +/// ``` +pub fn reply<S>(event_stream: S) -> impl Reply +where + S: TryStream + Send + Sync + 'static, + S::Ok: ServerSentEvent, + S::Error: StdError + Send + Sync + 'static, +{ + SseReply { event_stream } +} + +#[allow(missing_debug_implementations)] +struct SseReply<S> { + event_stream: S, +} + +impl<S> Reply for SseReply<S> +where + S: TryStream + Send + Sync + 'static, + S::Ok: ServerSentEvent, + S::Error: StdError + Send + Sync + 'static, +{ + #[inline] + fn into_response(self) -> Response { + let body_stream = self + .event_stream + .map_err(|error| { + // FIXME: error logging + log::error!("sse stream error: {}", error); + SseError + }) + .into_stream() + .and_then(|event| future::ready(SseWrapper::format(&event))); + + let mut res = Response::new(Body::wrap_stream(body_stream)); + // Set appropriate content type + res.headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("text/event-stream")); + // Disable response body caching + res.headers_mut() + .insert(CACHE_CONTROL, HeaderValue::from_static("no-cache")); + res + } +} + +/// Configure the interval between keep-alive messages, the content +/// of each message, and the associated stream. +#[derive(Debug)] +pub struct KeepAlive { + comment_text: Cow<'static, str>, + max_interval: Duration, +} + +impl KeepAlive { + /// Customize the interval between keep-alive messages. + /// + /// Default is 15 seconds. + pub fn interval(mut self, time: Duration) -> Self { + self.max_interval = time; + self + } + + /// Customize the text of the keep-alive message. + /// + /// Default is an empty comment. + pub fn text(mut self, text: impl Into<Cow<'static, str>>) -> Self { + self.comment_text = text.into(); + self + } + + /// Wrap an event stream with keep-alive functionality. + /// + /// See [`keep_alive`](keep_alive) for more. + pub fn stream<S>( + self, + event_stream: S, + ) -> impl TryStream< + Ok = impl ServerSentEvent + Send + 'static, + Error = impl StdError + Send + Sync + 'static, + > + Send + + 'static + where + S: TryStream + Send + 'static, + S::Ok: ServerSentEvent + Send, + S::Error: StdError + Send + Sync + 'static, + { + let alive_timer = time::delay_for(self.max_interval); + SseKeepAlive { + event_stream, + comment_text: self.comment_text, + max_interval: self.max_interval, + alive_timer, + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +struct SseKeepAlive<S> { + #[pin] + event_stream: S, + comment_text: Cow<'static, str>, + max_interval: Duration, + alive_timer: Delay, +} + +#[doc(hidden)] +#[deprecated(note = "use warp::see:keep_alive() instead")] +pub fn keep<S>( + event_stream: S, + keep_interval: impl Into<Option<Duration>>, +) -> impl TryStream< + Ok = impl ServerSentEvent + Send + 'static, + Error = impl StdError + Send + Sync + 'static, +> + Send + + 'static +where + S: TryStream + Send + 'static, + S::Ok: ServerSentEvent + Send, + S::Error: StdError + Send + Sync + 'static, +{ + let max_interval = keep_interval + .into() + .unwrap_or_else(|| Duration::from_secs(15)); + let alive_timer = time::delay_for(max_interval); + SseKeepAlive { + event_stream, + comment_text: Cow::Borrowed(""), + max_interval, + alive_timer, + } +} + +/// Keeps event source connection alive when no events sent over a some time. +/// +/// Some proxy servers may drop HTTP connection after a some timeout of inactivity. +/// This function helps to prevent such behavior by sending comment events every +/// `keep_interval` of inactivity. +/// +/// By default the comment is `:` (an empty comment) and the time interval between +/// events is 15 seconds. Both may be customized using the builder pattern +/// as shown below. +/// +/// ``` +/// use std::time::Duration; +/// use std::convert::Infallible; +/// use futures::StreamExt; +/// use tokio::time::interval; +/// use warp::{Filter, Stream, sse::ServerSentEvent}; +/// +/// // create server-sent event +/// fn sse_counter(counter: u64) -> Result<impl ServerSentEvent, Infallible> { +/// Ok(warp::sse::data(counter)) +/// } +/// +/// fn main() { +/// let routes = warp::path("ticks") +/// .and(warp::get()) +/// .map(|| { +/// let mut counter: u64 = 0; +/// let event_stream = interval(Duration::from_secs(15)).map(move |_| { +/// counter += 1; +/// sse_counter(counter) +/// }); +/// // reply using server-sent events +/// let stream = warp::sse::keep_alive() +/// .interval(Duration::from_secs(5)) +/// .text("thump".to_string()) +/// .stream(event_stream); +/// warp::sse::reply(stream) +/// }); +/// } +/// ``` +/// +/// See [notes](https://www.w3.org/TR/2009/WD-eventsource-20090421/#notes). +pub fn keep_alive() -> KeepAlive { + KeepAlive { + comment_text: Cow::Borrowed(""), + max_interval: Duration::from_secs(15), + } +} + +impl<S> Stream for SseKeepAlive<S> +where + S: TryStream + Send + 'static, + S::Ok: ServerSentEvent, + S::Error: StdError + Send + Sync + 'static, +{ + type Item = Result<EitherServerSentEvent<S::Ok, SseComment<Cow<'static, str>>>, SseError>; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> { + let mut pin = self.project(); + match pin.event_stream.try_poll_next(cx) { + Poll::Pending => match Pin::new(&mut pin.alive_timer).poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(_) => { + // restart timer + pin.alive_timer + .reset(tokio::time::Instant::now() + *pin.max_interval); + let comment_str = pin.comment_text.clone(); + Poll::Ready(Some(Ok(EitherServerSentEvent::B(SseComment(comment_str))))) + } + }, + Poll::Ready(Some(Ok(event))) => { + // restart timer + pin.alive_timer + .reset(tokio::time::Instant::now() + *pin.max_interval); + Poll::Ready(Some(Ok(EitherServerSentEvent::A(event)))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Ready(Some(Err(error))) => { + log::error!("sse::keep error: {}", error); + Poll::Ready(Some(Err(SseError))) + } + } + } +} + +mod sealed { + use super::*; + + /// SSE error type + #[derive(Debug)] + pub struct SseError; + + impl Display for SseError { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "sse error") + } + } + + impl StdError for SseError {} + + impl Display for SseField { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + use self::SseField::*; + f.write_str(match self { + Event => "event:", + Id => "id:", + Data => "data:", + Retry => "retry:", + Comment => ":", + }) + } + } + + /// SSE field kind + #[allow(missing_debug_implementations)] + pub enum SseField { + /// Event name field + Event, + /// Event id field + Id, + /// Event data field + Data, + /// Retry timeout field + Retry, + /// Comment field + Comment, + } + + /// SSE formatter trait + pub trait SseFormat { + /// format message field + fn fmt_field(&self, _f: &mut Formatter, _key: &SseField) -> fmt::Result { + Ok(()) + } + } + + /// SSE wrapper to help formatting messages + #[allow(missing_debug_implementations)] + pub struct SseWrapper<'a, T: 'a>(&'a T); + + impl<'a, T> SseWrapper<'a, T> + where + T: SseFormat + 'a, + { + pub fn format(event: &'a T) -> Result<String, SseError> { + let mut buf = String::new(); + buf.write_fmt(format_args!("{}", SseWrapper(event))) + .map_err(|_| SseError)?; + buf.shrink_to_fit(); + Ok(buf) + } + } + + impl<'a, T> Display for SseWrapper<'a, T> + where + T: SseFormat, + { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt_field(f, &SseField::Comment)?; + // The event name usually transferred before the other fields. + self.0.fmt_field(f, &SseField::Event)?; + // It is important that the data will be transferred before + // the identifier to prevent possible losing events when + // resuming connection. + self.0.fmt_field(f, &SseField::Data)?; + self.0.fmt_field(f, &SseField::Id)?; + self.0.fmt_field(f, &SseField::Retry)?; + f.write_char('\n') + } + } + + #[allow(missing_debug_implementations)] + pub struct BoxedServerSentEvent(pub(super) Box<dyn SseFormat + Send + Sync>); + + impl SseFormat for BoxedServerSentEvent { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + self.0.fmt_field(f, k) + } + } + + #[allow(missing_debug_implementations)] + pub enum EitherServerSentEvent<A, B> { + A(A), + B(B), + } + + impl<A, B> SseFormat for EitherServerSentEvent<A, B> + where + A: SseFormat, + B: SseFormat, + { + fn fmt_field(&self, f: &mut Formatter, k: &SseField) -> fmt::Result { + match self { + EitherServerSentEvent::A(a) => a.fmt_field(f, k), + EitherServerSentEvent::B(b) => b.fmt_field(f, k), + } + } + } +} diff --git a/third_party/rust/warp/src/filters/ws.rs b/third_party/rust/warp/src/filters/ws.rs new file mode 100644 index 0000000000..2dd8c8d9d0 --- /dev/null +++ b/third_party/rust/warp/src/filters/ws.rs @@ -0,0 +1,365 @@ +//! Websockets Filters + +use std::borrow::Cow; +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use super::{body, header}; +use crate::filter::{Filter, One}; +use crate::reject::Rejection; +use crate::reply::{Reply, Response}; +use futures::{future, ready, FutureExt, Sink, Stream, TryFutureExt}; +use headers::{Connection, HeaderMapExt, SecWebsocketAccept, SecWebsocketKey, Upgrade}; +use http; +use tokio_tungstenite::{ + tungstenite::protocol::{self, WebSocketConfig}, + WebSocketStream, +}; + +/// Creates a Websocket Filter. +/// +/// The yielded `Ws` is used to finish the websocket upgrade. +/// +/// # Note +/// +/// This filter combines multiple filters internally, so you don't need them: +/// +/// - Method must be `GET` +/// - Header `connection` must be `upgrade` +/// - Header `upgrade` must be `websocket` +/// - Header `sec-websocket-version` must be `13` +/// - Header `sec-websocket-key` must be set. +/// +/// If the filters are met, yields a `Ws`. Calling `Ws::on_upgrade` will +/// return a reply with: +/// +/// - Status of `101 Switching Protocols` +/// - Header `connection: upgrade` +/// - Header `upgrade: websocket` +/// - Header `sec-websocket-accept` with the hash value of the received key. +pub fn ws() -> impl Filter<Extract = One<Ws>, Error = Rejection> + Copy { + let connection_has_upgrade = header::header2() + .and_then(|conn: ::headers::Connection| { + if conn.contains("upgrade") { + future::ok(()) + } else { + future::err(crate::reject::known(MissingConnectionUpgrade)) + } + }) + .untuple_one(); + + crate::get() + .and(connection_has_upgrade) + .and(header::exact_ignore_case("upgrade", "websocket")) + .and(header::exact("sec-websocket-version", "13")) + //.and(header::exact2(Upgrade::websocket())) + //.and(header::exact2(SecWebsocketVersion::V13)) + .and(header::header2::<SecWebsocketKey>()) + .and(body::body()) + .map(move |key: SecWebsocketKey, body: ::hyper::Body| Ws { + body, + config: None, + key, + }) +} + +/// Extracted by the [`ws`](ws) filter, and used to finish an upgrade. +pub struct Ws { + body: ::hyper::Body, + config: Option<WebSocketConfig>, + key: SecWebsocketKey, +} + +impl Ws { + /// Finish the upgrade, passing a function to handle the `WebSocket`. + /// + /// The passed function must return a `Future`. + pub fn on_upgrade<F, U>(self, func: F) -> impl Reply + where + F: FnOnce(WebSocket) -> U + Send + 'static, + U: Future<Output = ()> + Send + 'static, + { + WsReply { + ws: self, + on_upgrade: func, + } + } + + // config + + /// Set the size of the internal message send queue. + pub fn max_send_queue(mut self, max: usize) -> Self { + self.config + .get_or_insert_with(WebSocketConfig::default) + .max_send_queue = Some(max); + self + } + + /// Set the maximum message size (defaults to 64 megabytes) + pub fn max_message_size(mut self, max: usize) -> Self { + self.config + .get_or_insert_with(WebSocketConfig::default) + .max_message_size = Some(max); + self + } +} + +impl fmt::Debug for Ws { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Ws").finish() + } +} + +#[allow(missing_debug_implementations)] +struct WsReply<F> { + ws: Ws, + on_upgrade: F, +} + +impl<F, U> Reply for WsReply<F> +where + F: FnOnce(WebSocket) -> U + Send + 'static, + U: Future<Output = ()> + Send + 'static, +{ + fn into_response(self) -> Response { + let on_upgrade = self.on_upgrade; + let config = self.ws.config; + let fut = self + .ws + .body + .on_upgrade() + .and_then(move |upgraded| { + log::trace!("websocket upgrade complete"); + WebSocket::from_raw_socket(upgraded, protocol::Role::Server, config).map(Ok) + }) + .and_then(move |socket| on_upgrade(socket).map(Ok)) + .map(|result| { + if let Err(err) = result { + log::debug!("ws upgrade error: {}", err); + } + }); + ::tokio::task::spawn(fut); + + let mut res = http::Response::default(); + + *res.status_mut() = http::StatusCode::SWITCHING_PROTOCOLS; + + res.headers_mut().typed_insert(Connection::upgrade()); + res.headers_mut().typed_insert(Upgrade::websocket()); + res.headers_mut() + .typed_insert(SecWebsocketAccept::from(self.ws.key)); + + res + } +} + +/// A websocket `Stream` and `Sink`, provided to `ws` filters. +pub struct WebSocket { + inner: WebSocketStream<hyper::upgrade::Upgraded>, +} + +impl WebSocket { + pub(crate) async fn from_raw_socket( + upgraded: hyper::upgrade::Upgraded, + role: protocol::Role, + config: Option<protocol::WebSocketConfig>, + ) -> Self { + WebSocketStream::from_raw_socket(upgraded, role, config) + .map(|inner| WebSocket { inner }) + .await + } + + /// Gracefully close this websocket. + pub async fn close(mut self) -> Result<(), crate::Error> { + future::poll_fn(|cx| Pin::new(&mut self).poll_close(cx)).await + } +} + +impl Stream for WebSocket { + type Item = Result<Message, crate::Error>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> { + match ready!(Pin::new(&mut self.inner).poll_next(cx)) { + Some(Ok(item)) => Poll::Ready(Some(Ok(Message { inner: item }))), + Some(Err(e)) => { + log::debug!("websocket poll error: {}", e); + Poll::Ready(Some(Err(crate::Error::new(e)))) + } + None => { + log::trace!("websocket closed"); + Poll::Ready(None) + } + } + } +} + +impl Sink<Message> for WebSocket { + type Error = crate::Error; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + match ready!(Pin::new(&mut self.inner).poll_ready(cx)) { + Ok(()) => Poll::Ready(Ok(())), + Err(e) => Poll::Ready(Err(crate::Error::new(e))), + } + } + + fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + match Pin::new(&mut self.inner).start_send(item.inner) { + Ok(()) => Ok(()), + Err(e) => { + log::debug!("websocket start_send error: {}", e); + Err(crate::Error::new(e)) + } + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> { + match ready!(Pin::new(&mut self.inner).poll_flush(cx)) { + Ok(()) => Poll::Ready(Ok(())), + Err(e) => Poll::Ready(Err(crate::Error::new(e))), + } + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> { + match ready!(Pin::new(&mut self.inner).poll_close(cx)) { + Ok(()) => Poll::Ready(Ok(())), + Err(err) => { + log::debug!("websocket close error: {}", err); + Poll::Ready(Err(crate::Error::new(err))) + } + } + } +} + +impl fmt::Debug for WebSocket { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("WebSocket").finish() + } +} + +/// A WebSocket message. +/// +/// Only repesents Text and Binary messages. +/// +/// This will likely become a `non-exhaustive` enum in the future, once that +/// language feature has stabilized. +#[derive(Eq, PartialEq, Clone)] +pub struct Message { + inner: protocol::Message, +} + +impl Message { + /// Construct a new Text `Message`. + pub fn text<S: Into<String>>(s: S) -> Message { + Message { + inner: protocol::Message::text(s), + } + } + + /// Construct a new Binary `Message`. + pub fn binary<V: Into<Vec<u8>>>(v: V) -> Message { + Message { + inner: protocol::Message::binary(v), + } + } + + /// Construct a new Ping `Message`. + pub fn ping<V: Into<Vec<u8>>>(v: V) -> Message { + Message { + inner: protocol::Message::Ping(v.into()), + } + } + + /// Construct the default Close `Message`. + pub fn close() -> Message { + Message { + inner: protocol::Message::Close(None), + } + } + + /// Construct a Close `Message` with a code and reason. + pub fn close_with(code: impl Into<u16>, reason: impl Into<Cow<'static, str>>) -> Message { + Message { + inner: protocol::Message::Close(Some(protocol::frame::CloseFrame { + code: protocol::frame::coding::CloseCode::from(code.into()), + reason: reason.into(), + })), + } + } + + /// Returns true if this message is a Text message. + pub fn is_text(&self) -> bool { + self.inner.is_text() + } + + /// Returns true if this message is a Binary message. + pub fn is_binary(&self) -> bool { + self.inner.is_binary() + } + + /// Returns true if this message a is a Close message. + pub fn is_close(&self) -> bool { + self.inner.is_close() + } + + /// Returns true if this message is a Ping message. + pub fn is_ping(&self) -> bool { + self.inner.is_ping() + } + + /// Returns true if this message is a Pong message. + pub fn is_pong(&self) -> bool { + self.inner.is_pong() + } + + /// Try to get a reference to the string text, if this is a Text message. + pub fn to_str(&self) -> Result<&str, ()> { + match self.inner { + protocol::Message::Text(ref s) => Ok(s), + _ => Err(()), + } + } + + /// Return the bytes of this message, if the message can contain data. + pub fn as_bytes(&self) -> &[u8] { + match self.inner { + protocol::Message::Text(ref s) => s.as_bytes(), + protocol::Message::Binary(ref v) => v, + protocol::Message::Ping(ref v) => v, + protocol::Message::Pong(ref v) => v, + protocol::Message::Close(_) => &[], + } + } + + /// Destructure this message into binary data. + pub fn into_bytes(self) -> Vec<u8> { + self.inner.into_data() + } +} + +impl fmt::Debug for Message { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +impl Into<Vec<u8>> for Message { + fn into(self) -> Vec<u8> { + self.into_bytes() + } +} + +// ===== Rejections ===== + +#[derive(Debug)] +pub(crate) struct MissingConnectionUpgrade; + +impl ::std::fmt::Display for MissingConnectionUpgrade { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Connection header did not include 'upgrade'") + } +} + +impl ::std::error::Error for MissingConnectionUpgrade {} diff --git a/third_party/rust/warp/src/generic.rs b/third_party/rust/warp/src/generic.rs new file mode 100644 index 0000000000..1626a81409 --- /dev/null +++ b/third_party/rust/warp/src/generic.rs @@ -0,0 +1,237 @@ +#[derive(Debug)] +pub struct Product<H, T: HList>(pub(crate) H, pub(crate) T); + +pub type One<T> = (T,); + +#[inline] +pub(crate) fn one<T>(val: T) -> One<T> { + (val,) +} + +#[derive(Debug)] +pub enum Either<T, U> { + A(T), + B(U), +} + +// Converts Product (and ()) into tuples. +pub trait HList: Sized { + type Tuple: Tuple<HList = Self>; + + fn flatten(self) -> Self::Tuple; +} + +// Typeclass that tuples can be converted into a Product (or unit ()). +pub trait Tuple: Sized { + type HList: HList<Tuple = Self>; + + fn hlist(self) -> Self::HList; +} + +// Combines Product together. +pub trait Combine<T: HList> { + type Output: HList; + + fn combine(self, other: T) -> Self::Output; +} + +pub trait Func<Args> { + type Output; + + fn call(&self, args: Args) -> Self::Output; +} + +// ===== impl Combine ===== + +impl<T: HList> Combine<T> for () { + type Output = T; + #[inline] + fn combine(self, other: T) -> Self::Output { + other + } +} + +impl<H, T: HList, U: HList> Combine<U> for Product<H, T> +where + T: Combine<U>, + Product<H, <T as Combine<U>>::Output>: HList, +{ + type Output = Product<H, <T as Combine<U>>::Output>; + + #[inline] + fn combine(self, other: U) -> Self::Output { + Product(self.0, self.1.combine(other)) + } +} + +impl HList for () { + type Tuple = (); + #[inline] + fn flatten(self) -> Self::Tuple {} +} + +impl Tuple for () { + type HList = (); + + #[inline] + fn hlist(self) -> Self::HList {} +} + +impl<F, R> Func<()> for F +where + F: Fn() -> R, +{ + type Output = R; + + #[inline] + fn call(&self, _args: ()) -> Self::Output { + (*self)() + } +} + +impl<F, R> Func<crate::Rejection> for F +where + F: Fn(crate::Rejection) -> R, +{ + type Output = R; + + #[inline] + fn call(&self, arg: crate::Rejection) -> Self::Output { + (*self)(arg) + } +} + +macro_rules! product { + ($H:expr) => { Product($H, ()) }; + ($H:expr, $($T:expr),*) => { Product($H, product!($($T),*)) }; +} + +macro_rules! Product { + ($H:ty) => { Product<$H, ()> }; + ($H:ty, $($T:ty),*) => { Product<$H, Product!($($T),*)> }; +} + +macro_rules! product_pat { + ($H:pat) => { Product($H, ()) }; + ($H:pat, $($T:pat),*) => { Product($H, product_pat!($($T),*)) }; +} + +macro_rules! generics { + ($type:ident) => { + impl<$type> HList for Product!($type) { + type Tuple = ($type,); + + #[inline] + fn flatten(self) -> Self::Tuple { + (self.0,) + } + } + + impl<$type> Tuple for ($type,) { + type HList = Product!($type); + #[inline] + fn hlist(self) -> Self::HList { + product!(self.0) + } + } + + impl<F, R, $type> Func<Product!($type)> for F + where + F: Fn($type) -> R, + { + type Output = R; + + #[inline] + fn call(&self, args: Product!($type)) -> Self::Output { + (*self)(args.0) + } + + } + + impl<F, R, $type> Func<($type,)> for F + where + F: Fn($type) -> R, + { + type Output = R; + + #[inline] + fn call(&self, args: ($type,)) -> Self::Output { + (*self)(args.0) + } + } + + }; + + ($type1:ident, $( $type:ident ),*) => { + generics!($( $type ),*); + + impl<$type1, $( $type ),*> HList for Product!($type1, $($type),*) { + type Tuple = ($type1, $( $type ),*); + + #[inline] + fn flatten(self) -> Self::Tuple { + #[allow(non_snake_case)] + let product_pat!($type1, $( $type ),*) = self; + ($type1, $( $type ),*) + } + } + + impl<$type1, $( $type ),*> Tuple for ($type1, $($type),*) { + type HList = Product!($type1, $( $type ),*); + + #[inline] + fn hlist(self) -> Self::HList { + #[allow(non_snake_case)] + let ($type1, $( $type ),*) = self; + product!($type1, $( $type ),*) + } + } + + impl<F, R, $type1, $( $type ),*> Func<Product!($type1, $($type),*)> for F + where + F: Fn($type1, $( $type ),*) -> R, + { + type Output = R; + + #[inline] + fn call(&self, args: Product!($type1, $($type),*)) -> Self::Output { + #[allow(non_snake_case)] + let product_pat!($type1, $( $type ),*) = args; + (*self)($type1, $( $type ),*) + } + } + + impl<F, R, $type1, $( $type ),*> Func<($type1, $($type),*)> for F + where + F: Fn($type1, $( $type ),*) -> R, + { + type Output = R; + + #[inline] + fn call(&self, args: ($type1, $($type),*)) -> Self::Output { + #[allow(non_snake_case)] + let ($type1, $( $type ),*) = args; + (*self)($type1, $( $type ),*) + } + } + }; +} + +generics! { + T1, + T2, + T3, + T4, + T5, + T6, + T7, + T8, + T9, + T10, + T11, + T12, + T13, + T14, + T15, + T16 +} diff --git a/third_party/rust/warp/src/lib.rs b/third_party/rust/warp/src/lib.rs new file mode 100644 index 0000000000..241ce84432 --- /dev/null +++ b/third_party/rust/warp/src/lib.rs @@ -0,0 +1,170 @@ +#![doc(html_root_url = "https://docs.rs/warp/0.2.2")] +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![cfg_attr(test, deny(warnings))] + +//! # warp +//! +//! warp is a super-easy, composable, web server framework for warp speeds. +//! +//! Thanks to its [`Filter`][Filter] system, warp provides these out of the box: +//! +//! - Path routing and parameter extraction +//! - Header requirements and extraction +//! - Query string deserialization +//! - JSON and Form bodies +//! - Multipart form data +//! - Static Files and Directories +//! - Websockets +//! - Access logging +//! - Etc +//! +//! Since it builds on top of [hyper](https://hyper.rs), you automatically get: +//! +//! - HTTP/1 +//! - HTTP/2 +//! - Asynchronous +//! - One of the fastest HTTP implementations +//! - Tested and **correct** +//! +//! ## Filters +//! +//! The main concept in warp is the [`Filter`][Filter], which allows composition +//! to describe various endpoints in your web service. Besides this powerful +//! trait, warp comes with several built in [filters](filters/index.html), which +//! can be combined for your specific needs. +//! +//! As a small example, consider an endpoint that has path and header requirements: +//! +//! ``` +//! use warp::Filter; +//! +//! let hi = warp::path("hello") +//! .and(warp::path::param()) +//! .and(warp::header("user-agent")) +//! .map(|param: String, agent: String| { +//! format!("Hello {}, whose agent is {}", param, agent) +//! }); +//! ``` +//! +//! This example composes several [`Filter`s][Filter] together using `and`: +//! +//! - A path prefix of "hello" +//! - A path parameter of a `String` +//! - The `user-agent` header parsed as a `String` +//! +//! These specific filters will [`reject`][reject] requests that don't match +//! their requirements. +//! +//! This ends up matching requests like: +//! +//! ```notrust +//! GET /hello/sean HTTP/1.1 +//! Host: hyper.rs +//! User-Agent: reqwest/v0.8.6 +//! +//! ``` +//! And it returns a response similar to this: +//! +//! ```notrust +//! HTTP/1.1 200 OK +//! Content-Length: 41 +//! Date: ... +//! +//! Hello sean, whose agent is reqwest/v0.8.6 +//! ``` +//! +//! Take a look at the full list of [`filters`](filters/index.html) to see what +//! you can build. +//! +//! ## Testing +//! +//! Testing your web services easily is extremely important, and warp provides +//! a [`test`](test) module to help send mocked requests through your service. +//! +//! [Filter]: trait.Filter.html +//! [reject]: reject/index.html + +#[macro_use] +mod error; +mod filter; +pub mod filters; +mod generic; +pub mod redirect; +pub mod reject; +pub mod reply; +mod route; +mod server; +mod service; +pub mod test; +#[cfg(feature = "tls")] +mod tls; +mod transport; + +pub use self::error::Error; +pub use self::filter::Filter; +// This otherwise shows a big dump of re-exports in the doc homepage, +// with zero context, so just hide it from the docs. Doc examples +// on each can show that a convenient import exists. +#[cfg(feature = "multipart")] +#[doc(hidden)] +pub use self::filters::multipart; +#[cfg(feature = "websocket")] +#[doc(hidden)] +pub use self::filters::ws; +#[doc(hidden)] +pub use self::filters::{ + addr, + // any() function + any::any, + body, + cookie, + // cookie() function + cookie::cookie, + cors, + // cors() function + cors::cors, + ext, + fs, + header, + // header() function + header::header, + log, + // log() function + log::log, + method::{delete, get, head, method, options, patch, post, put}, + path, + // path() function and macro + path::path, + query, + // query() function + query::query, + sse, +}; +// ws() function +#[cfg(feature = "websocket")] +#[doc(hidden)] +pub use self::filters::ws::ws; +#[doc(hidden)] +pub use self::redirect::redirect; +#[doc(hidden)] +#[allow(deprecated)] +pub use self::reject::{reject, Rejection}; +#[doc(hidden)] +pub use self::reply::{reply, Reply}; +#[cfg(feature = "tls")] +pub use self::server::TlsServer; +pub use self::server::{serve, Server}; +pub use self::service::service; +#[doc(hidden)] +pub use http; +#[doc(hidden)] +pub use hyper; + +#[doc(hidden)] +pub use bytes::Buf; +#[doc(hidden)] +pub use futures::{Future, Sink, Stream}; +#[doc(hidden)] + +pub(crate) type Request = http::Request<hyper::Body>; diff --git a/third_party/rust/warp/src/redirect.rs b/third_party/rust/warp/src/redirect.rs new file mode 100644 index 0000000000..1459cef7dc --- /dev/null +++ b/third_party/rust/warp/src/redirect.rs @@ -0,0 +1,70 @@ +//! Redirect requests to a new location. +//! +//! The types in this module are helpers that implement [`Reply`](Reply), and easy +//! to use in order to setup redirects. + +use http::{header, StatusCode}; + +use self::sealed::AsLocation; +use crate::reply::{self, Reply}; + +/// A simple `301` redirect to a different location. +/// +/// # Example +/// +/// ``` +/// use warp::{http::Uri, Filter}; +/// +/// let route = warp::path("v1") +/// .map(|| { +/// warp::redirect(Uri::from_static("/v2")) +/// }); +/// ``` +pub fn redirect(uri: impl AsLocation) -> impl Reply { + reply::with_header( + StatusCode::MOVED_PERMANENTLY, + header::LOCATION, + uri.header_value(), + ) +} + +/// A simple `307` temporary redirect to a different location. +/// +/// # Example +/// +/// ``` +/// use warp::{http::Uri, Filter}; +/// +/// let route = warp::path("v1") +/// .map(|| { +/// warp::redirect::temporary(Uri::from_static("/v2")) +/// }); +/// ``` +pub fn temporary(uri: impl AsLocation) -> impl Reply { + reply::with_header( + StatusCode::TEMPORARY_REDIRECT, + header::LOCATION, + uri.header_value(), + ) +} + +mod sealed { + use bytes::Bytes; + use http::{header::HeaderValue, Uri}; + + // These sealed traits are to allow adding possibly new impls so other + // arguments could be accepted, like maybe just `warp::redirect("/v2")`. + pub trait AsLocation: Sealed {} + pub trait Sealed { + fn header_value(self) -> HeaderValue; + } + + impl AsLocation for Uri {} + + impl Sealed for Uri { + fn header_value(self) -> HeaderValue { + let bytes = Bytes::from(self.to_string()); + HeaderValue::from_maybe_shared(bytes).expect("Uri is a valid HeaderValue") + } + } +} diff --git a/third_party/rust/warp/src/reject.rs b/third_party/rust/warp/src/reject.rs new file mode 100644 index 0000000000..8fd28badda --- /dev/null +++ b/third_party/rust/warp/src/reject.rs @@ -0,0 +1,810 @@ +//! Rejections +//! +//! Part of the power of the [`Filter`](../trait.Filter.html) system is being able to +//! reject a request from a filter chain. This allows for filters to be +//! combined with `or`, so that if one side of the chain finds that a request +//! doesn't fulfill its requirements, the other side can try to process +//! the request. +//! +//! Many of the built-in [`filters`](../filters) will automatically reject +//! the request with an appropriate rejection. However, you can also build +//! new custom [`Filter`](../trait.Filter.html)s and still want other routes to be +//! matchable in the case a predicate doesn't hold. +//! +//! # Example +//! +//! ``` +//! use warp::Filter; +//! +//! // Filter on `/:id`, but reject with 404 if the `id` is `0`. +//! let route = warp::path::param() +//! .and_then(|id: u32| async move { +//! if id == 0 { +//! Err(warp::reject::not_found()) +//! } else { +//! Ok("something since id is valid") +//! } +//! }); +//! ``` + +use std::any::Any; +use std::convert::Infallible; +use std::error::Error as StdError; +use std::fmt; + +use http::{ + self, + header::{HeaderValue, CONTENT_TYPE}, + StatusCode, +}; +use hyper::Body; + +pub(crate) use self::sealed::{CombineRejection, IsReject}; + +/// Rejects a request with `404 Not Found`. +#[inline] +pub fn reject() -> Rejection { + not_found() +} + +/// Rejects a request with `404 Not Found`. +#[inline] +pub fn not_found() -> Rejection { + Rejection { + reason: Reason::NotFound, + } +} + +// 400 Bad Request +#[inline] +pub(crate) fn invalid_query() -> Rejection { + known(InvalidQuery { _p: () }) +} + +// 400 Bad Request +#[inline] +pub(crate) fn missing_header(name: &'static str) -> Rejection { + known(MissingHeader { name }) +} + +// 400 Bad Request +#[inline] +pub(crate) fn invalid_header(name: &'static str) -> Rejection { + known(InvalidHeader { name }) +} + +// 400 Bad Request +#[inline] +pub(crate) fn missing_cookie(name: &'static str) -> Rejection { + known(MissingCookie { name }) +} + +// 405 Method Not Allowed +#[inline] +pub(crate) fn method_not_allowed() -> Rejection { + known(MethodNotAllowed { _p: () }) +} + +// 411 Length Required +#[inline] +pub(crate) fn length_required() -> Rejection { + known(LengthRequired { _p: () }) +} + +// 413 Payload Too Large +#[inline] +pub(crate) fn payload_too_large() -> Rejection { + known(PayloadTooLarge { _p: () }) +} + +// 415 Unsupported Media Type +// +// Used by the body filters if the request payload content-type doesn't match +// what can be deserialized. +#[inline] +pub(crate) fn unsupported_media_type() -> Rejection { + known(UnsupportedMediaType { _p: () }) +} + +/// Rejects a request with a custom cause. +/// +/// A [`recover`][] filter should convert this `Rejection` into a `Reply`, +/// or else this will be returned as a `500 Internal Server Error`. +/// +/// [`recover`]: ../trait.Filter.html#method.recover +pub fn custom<T: Reject>(err: T) -> Rejection { + Rejection::custom(Box::new(err)) +} + +/// Protect against re-rejecting a rejection. +/// +/// ```compile_fail +/// fn with(r: warp::Rejection) { +/// let _wat = warp::reject::custom(r); +/// } +/// ``` +fn __reject_custom_compilefail() {} + +/// A marker trait to ensure proper types are used for custom rejections. +/// +/// # Example +/// +/// ``` +/// use warp::{Filter, reject::Reject}; +/// +/// #[derive(Debug)] +/// struct RateLimited; +/// +/// impl Reject for RateLimited {} +/// +/// let route = warp::any().and_then(|| async { +/// Err::<(), _>(warp::reject::custom(RateLimited)) +/// }); +/// ``` +// Require `Sized` for now to prevent passing a `Box<dyn Reject>`, since we +// would be double-boxing it, and the downcasting wouldn't work as expected. +pub trait Reject: fmt::Debug + Sized + Send + Sync + 'static {} + +trait Cause: fmt::Debug + Send + Sync + 'static { + fn as_any(&self) -> &dyn Any; +} + +impl<T> Cause for T +where + T: fmt::Debug + Send + Sync + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } +} + +impl dyn Cause { + fn downcast_ref<T: Any>(&self) -> Option<&T> { + self.as_any().downcast_ref::<T>() + } +} + +pub(crate) fn known<T: Into<Known>>(err: T) -> Rejection { + Rejection::known(err.into()) +} + +/// Rejection of a request by a [`Filter`](crate::Filter). +/// +/// See the [`reject`](module@crate::reject) documentation for more. +pub struct Rejection { + reason: Reason, +} + +enum Reason { + NotFound, + Other(Box<Rejections>), +} + +enum Rejections { + Known(Known), + Custom(Box<dyn Cause>), + Combined(Box<Rejections>, Box<Rejections>), +} + +macro_rules! enum_known { + ($($(#[$attr:meta])* $var:ident($ty:path),)+) => ( + pub(crate) enum Known { + $( + $(#[$attr])* + $var($ty), + )+ + } + + impl Known { + fn inner_as_any(&self) -> &dyn Any { + match *self { + $( + $(#[$attr])* + Known::$var(ref t) => t, + )+ + } + } + } + + impl fmt::Debug for Known { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + $( + $(#[$attr])* + Known::$var(ref t) => t.fmt(f), + )+ + } + } + } + + impl fmt::Display for Known { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + $( + $(#[$attr])* + Known::$var(ref t) => t.fmt(f), + )+ + } + } + } + + $( + #[doc(hidden)] + $(#[$attr])* + impl From<$ty> for Known { + fn from(ty: $ty) -> Known { + Known::$var(ty) + } + } + )+ + ); +} + +enum_known! { + MethodNotAllowed(MethodNotAllowed), + InvalidHeader(InvalidHeader), + MissingHeader(MissingHeader), + MissingCookie(MissingCookie), + InvalidQuery(InvalidQuery), + LengthRequired(LengthRequired), + PayloadTooLarge(PayloadTooLarge), + UnsupportedMediaType(UnsupportedMediaType), + FileOpenError(crate::fs::FileOpenError), + FilePermissionError(crate::fs::FilePermissionError), + BodyReadError(crate::body::BodyReadError), + BodyDeserializeError(crate::body::BodyDeserializeError), + CorsForbidden(crate::cors::CorsForbidden), + #[cfg(feature = "websocket")] + MissingConnectionUpgrade(crate::ws::MissingConnectionUpgrade), + MissingExtension(crate::ext::MissingExtension), + BodyConsumedMultipleTimes(crate::body::BodyConsumedMultipleTimes), +} + +impl Rejection { + fn known(known: Known) -> Self { + Rejection { + reason: Reason::Other(Box::new(Rejections::Known(known))), + } + } + + fn custom(other: Box<dyn Cause>) -> Self { + Rejection { + reason: Reason::Other(Box::new(Rejections::Custom(other))), + } + } + + /// Searches this `Rejection` for a specific cause. + /// + /// A `Rejection` will accumulate causes over a `Filter` chain. This method + /// can search through them and return the first cause of this type. + /// + /// # Example + /// + /// ``` + /// #[derive(Debug)] + /// struct Nope; + /// + /// impl warp::reject::Reject for Nope {} + /// + /// let reject = warp::reject::custom(Nope); + /// + /// if let Some(nope) = reject.find::<Nope>() { + /// println!("found it: {:?}", nope); + /// } + /// ``` + pub fn find<T: 'static>(&self) -> Option<&T> { + if let Reason::Other(ref rejections) = self.reason { + return rejections.find(); + } + None + } + + /// Returns true if this Rejection was made via `warp::reject::not_found`. + /// + /// # Example + /// + /// ``` + /// let rejection = warp::reject(); + /// + /// assert!(rejection.is_not_found()); + /// ``` + pub fn is_not_found(&self) -> bool { + if let Reason::NotFound = self.reason { + true + } else { + false + } + } +} + +impl From<Infallible> for Rejection { + #[inline] + fn from(infallible: Infallible) -> Rejection { + match infallible {} + } +} + +impl IsReject for Infallible { + fn status(&self) -> StatusCode { + match *self {} + } + + fn into_response(&self) -> crate::reply::Response { + match *self {} + } +} + +impl IsReject for Rejection { + fn status(&self) -> StatusCode { + match self.reason { + Reason::NotFound => StatusCode::NOT_FOUND, + Reason::Other(ref other) => other.status(), + } + } + + fn into_response(&self) -> crate::reply::Response { + match self.reason { + Reason::NotFound => { + let mut res = http::Response::default(); + *res.status_mut() = StatusCode::NOT_FOUND; + res + } + Reason::Other(ref other) => other.into_response(), + } + } +} + +impl fmt::Debug for Rejection { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Rejection").field(&self.reason).finish() + } +} + +impl fmt::Debug for Reason { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Reason::NotFound => f.write_str("NotFound"), + Reason::Other(ref other) => match **other { + Rejections::Known(ref e) => fmt::Debug::fmt(e, f), + Rejections::Custom(ref e) => fmt::Debug::fmt(e, f), + Rejections::Combined(ref a, ref b) => { + let mut list = f.debug_list(); + a.debug_list(&mut list); + b.debug_list(&mut list); + list.finish() + } + }, + } + } +} + +// ===== Rejections ===== + +impl Rejections { + fn status(&self) -> StatusCode { + match *self { + Rejections::Known(ref k) => match *k { + Known::MethodNotAllowed(_) => StatusCode::METHOD_NOT_ALLOWED, + Known::InvalidHeader(_) + | Known::MissingHeader(_) + | Known::MissingCookie(_) + | Known::InvalidQuery(_) + | Known::BodyReadError(_) + | Known::BodyDeserializeError(_) => StatusCode::BAD_REQUEST, + #[cfg(feature = "websocket")] + Known::MissingConnectionUpgrade(_) => StatusCode::BAD_REQUEST, + Known::LengthRequired(_) => StatusCode::LENGTH_REQUIRED, + Known::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE, + Known::UnsupportedMediaType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE, + Known::FilePermissionError(_) | Known::CorsForbidden(_) => StatusCode::FORBIDDEN, + Known::FileOpenError(_) + | Known::MissingExtension(_) + | Known::BodyConsumedMultipleTimes(_) => StatusCode::INTERNAL_SERVER_ERROR, + }, + Rejections::Custom(..) => StatusCode::INTERNAL_SERVER_ERROR, + Rejections::Combined(ref a, ref b) => preferred(a, b).status(), + } + } + + fn into_response(&self) -> crate::reply::Response { + match *self { + Rejections::Known(ref e) => { + let mut res = http::Response::new(Body::from(e.to_string())); + *res.status_mut() = self.status(); + res.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=utf-8"), + ); + res + } + Rejections::Custom(ref e) => { + log::error!( + "unhandled custom rejection, returning 500 response: {:?}", + e + ); + let body = format!("Unhandled rejection: {:?}", e); + let mut res = http::Response::new(Body::from(body)); + *res.status_mut() = self.status(); + res.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=utf-8"), + ); + res + } + Rejections::Combined(ref a, ref b) => preferred(a, b).into_response(), + } + } + + fn find<T: 'static>(&self) -> Option<&T> { + match *self { + Rejections::Known(ref e) => e.inner_as_any().downcast_ref(), + Rejections::Custom(ref e) => e.downcast_ref(), + Rejections::Combined(ref a, ref b) => a.find().or_else(|| b.find()), + } + } + + fn debug_list(&self, f: &mut fmt::DebugList<'_, '_>) { + match *self { + Rejections::Known(ref e) => { + f.entry(e); + } + Rejections::Custom(ref e) => { + f.entry(e); + } + Rejections::Combined(ref a, ref b) => { + a.debug_list(f); + b.debug_list(f); + } + } + } +} + +fn preferred<'a>(a: &'a Rejections, b: &'a Rejections) -> &'a Rejections { + // Compare status codes, with this priority: + // - NOT_FOUND is lowest + // - METHOD_NOT_ALLOWED is second + // - if one status code is greater than the other + // - otherwise, prefer A... + match (a.status(), b.status()) { + (_, StatusCode::NOT_FOUND) => a, + (StatusCode::NOT_FOUND, _) => b, + (_, StatusCode::METHOD_NOT_ALLOWED) => a, + (StatusCode::METHOD_NOT_ALLOWED, _) => b, + (sa, sb) if sa < sb => b, + _ => a, + } +} + +unit_error! { + /// Invalid query + pub InvalidQuery: "Invalid query string" +} + +unit_error! { + /// HTTP method not allowed + pub MethodNotAllowed: "HTTP method not allowed" +} + +unit_error! { + /// A content-length header is required + pub LengthRequired: "A content-length header is required" +} + +unit_error! { + /// The request payload is too large + pub PayloadTooLarge: "The request payload is too large" +} + +unit_error! { + /// The request's content-type is not supported + pub UnsupportedMediaType: "The request's content-type is not supported" +} + +/// Missing request header +#[derive(Debug)] +pub struct MissingHeader { + name: &'static str, +} + +impl MissingHeader { + /// Retrieve the name of the header that was missing + pub fn name(&self) -> &str { + self.name + } +} + +impl ::std::fmt::Display for MissingHeader { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Missing request header {:?}", self.name) + } +} + +impl StdError for MissingHeader {} + +/// Invalid request header +#[derive(Debug)] +pub struct InvalidHeader { + name: &'static str, +} + +impl InvalidHeader { + /// Retrieve the name of the header that was invalid + pub fn name(&self) -> &str { + self.name + } +} + +impl ::std::fmt::Display for InvalidHeader { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Invalid request header {:?}", self.name) + } +} + +impl StdError for InvalidHeader {} + +/// Missing cookie +#[derive(Debug)] +pub struct MissingCookie { + name: &'static str, +} + +impl MissingCookie { + /// Retrieve the name of the cookie that was missing + pub fn name(&self) -> &str { + self.name + } +} + +impl ::std::fmt::Display for MissingCookie { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Missing request cookie {:?}", self.name) + } +} + +impl StdError for MissingCookie {} + +mod sealed { + use super::{Reason, Rejection, Rejections}; + use http::StatusCode; + use std::convert::Infallible; + use std::fmt; + + // This sealed trait exists to allow Filters to return either `Rejection` + // or `!`. There are no other types that make sense, and so it is sealed. + pub trait IsReject: fmt::Debug + Send + Sync { + fn status(&self) -> StatusCode; + fn into_response(&self) -> crate::reply::Response; + } + + fn _assert_object_safe() { + fn _assert(_: &dyn IsReject) {} + } + + // This weird trait is to allow optimizations of propagating when a + // rejection can *never* happen (currently with the `Never` type, + // eventually to be replaced with `!`). + // + // Using this trait means the `Never` gets propagated to chained filters, + // allowing LLVM to eliminate more code paths. Without it, such as just + // requiring that `Rejection::from(Never)` were used in those filters, + // would mean that links later in the chain may assume a rejection *could* + // happen, and no longer eliminate those branches. + pub trait CombineRejection<E>: Send + Sized { + /// The type that should be returned when only 1 of the two + /// "rejections" occurs. + /// + /// # For example: + /// + /// `warp::any().and(warp::path("foo"))` has the following steps: + /// + /// 1. Since this is `and`, only **one** of the rejections will occur, + /// and as soon as it does, it will be returned. + /// 2. `warp::any()` rejects with `Never`. So, it will never return `Never`. + /// 3. `warp::path()` rejects with `Rejection`. It may return `Rejection`. + /// + /// Thus, if the above filter rejects, it will definitely be `Rejection`. + type One: IsReject + From<Self> + From<E> + Into<Rejection>; + + /// The type that should be returned when both rejections occur, + /// and need to be combined. + type Combined: IsReject; + + fn combine(self, other: E) -> Self::Combined; + } + + impl CombineRejection<Rejection> for Rejection { + type One = Rejection; + type Combined = Rejection; + + fn combine(self, other: Rejection) -> Self::Combined { + let reason = match (self.reason, other.reason) { + (Reason::Other(left), Reason::Other(right)) => { + Reason::Other(Box::new(Rejections::Combined(left, right))) + } + (Reason::Other(other), Reason::NotFound) + | (Reason::NotFound, Reason::Other(other)) => { + // ignore the NotFound + Reason::Other(other) + } + (Reason::NotFound, Reason::NotFound) => Reason::NotFound, + }; + + Rejection { reason } + } + } + + impl CombineRejection<Infallible> for Rejection { + type One = Rejection; + type Combined = Infallible; + + fn combine(self, other: Infallible) -> Self::Combined { + match other {} + } + } + + impl CombineRejection<Rejection> for Infallible { + type One = Rejection; + type Combined = Infallible; + + fn combine(self, _: Rejection) -> Self::Combined { + match self {} + } + } + + impl CombineRejection<Infallible> for Infallible { + type One = Infallible; + type Combined = Infallible; + + fn combine(self, _: Infallible) -> Self::Combined { + match self {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use http::StatusCode; + + #[derive(Debug, PartialEq)] + struct Left; + + #[derive(Debug, PartialEq)] + struct Right; + + impl Reject for Left {} + impl Reject for Right {} + + #[test] + fn rejection_status() { + assert_eq!(not_found().status(), StatusCode::NOT_FOUND); + assert_eq!( + method_not_allowed().status(), + StatusCode::METHOD_NOT_ALLOWED + ); + assert_eq!(length_required().status(), StatusCode::LENGTH_REQUIRED); + assert_eq!(payload_too_large().status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + unsupported_media_type().status(), + StatusCode::UNSUPPORTED_MEDIA_TYPE + ); + assert_eq!(custom(Left).status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + #[tokio::test] + async fn combine_rejection_causes_with_some_left_and_none_right() { + let left = custom(Left); + let right = not_found(); + let reject = left.combine(right); + let resp = reject.into_response(); + + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + response_body_string(resp).await, + "Unhandled rejection: Left" + ) + } + + #[tokio::test] + async fn combine_rejection_causes_with_none_left_and_some_right() { + let left = not_found(); + let right = custom(Right); + let reject = left.combine(right); + let resp = reject.into_response(); + + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + response_body_string(resp).await, + "Unhandled rejection: Right" + ) + } + + #[tokio::test] + async fn unhandled_customs() { + let reject = not_found().combine(custom(Right)); + + let resp = reject.into_response(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + response_body_string(resp).await, + "Unhandled rejection: Right" + ); + + // There's no real way to determine which is worse, since both are a 500, + // so pick the first one. + let reject = custom(Left).combine(custom(Right)); + + let resp = reject.into_response(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + response_body_string(resp).await, + "Unhandled rejection: Left" + ); + + // With many rejections, custom still is top priority. + let reject = not_found() + .combine(not_found()) + .combine(not_found()) + .combine(custom(Right)) + .combine(not_found()); + + let resp = reject.into_response(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + response_body_string(resp).await, + "Unhandled rejection: Right" + ); + } + + async fn response_body_string(resp: crate::reply::Response) -> String { + let (_, body) = resp.into_parts(); + let body_bytes = hyper::body::to_bytes(body).await.expect("failed concat"); + String::from_utf8_lossy(&body_bytes).to_string() + } + + #[test] + fn find_cause() { + let rej = custom(Left); + + assert_eq!(rej.find::<Left>(), Some(&Left)); + + let rej = rej.combine(method_not_allowed()); + + assert_eq!(rej.find::<Left>(), Some(&Left)); + assert!(rej.find::<MethodNotAllowed>().is_some(), "MethodNotAllowed"); + } + + #[test] + fn size_of_rejection() { + assert_eq!( + ::std::mem::size_of::<Rejection>(), + ::std::mem::size_of::<usize>(), + ); + } + + #[derive(Debug)] + struct X(u32); + impl Reject for X {} + + fn combine_n<F, R>(n: u32, new_reject: F) -> Rejection + where + F: Fn(u32) -> R, + R: Reject, + { + let mut rej = not_found(); + + for i in 0..n { + rej = rej.combine(custom(new_reject(i))); + } + + rej + } + + #[test] + fn test_debug() { + let rej = combine_n(3, X); + + let s = format!("{:?}", rej); + assert_eq!(s, "Rejection([X(0), X(1), X(2)])"); + } +} diff --git a/third_party/rust/warp/src/reply.rs b/third_party/rust/warp/src/reply.rs new file mode 100644 index 0000000000..657e6e8220 --- /dev/null +++ b/third_party/rust/warp/src/reply.rs @@ -0,0 +1,581 @@ +//! Reply to requests. +//! +//! A [`Reply`](./trait.Reply.html) is a type that can be converted into an HTTP +//! response to be sent to the client. These are typically the successful +//! counterpart to a [rejection](../reject). +//! +//! The functions in this module are helpers for quickly creating a reply. +//! Besides them, you can return a type that implements [`Reply`](./trait.Reply.html). This +//! could be any of the following: +//! +//! - [`http::Response<impl Into<hyper::Body>`](https://docs.rs/http) +//! - `String` +//! - `&'static str` +//! - `http::StatusCode` +//! +//! # Example +//! +//! ``` +//! use warp::{Filter, http::Response}; +//! +//! // Returns an empty `200 OK` response. +//! let empty_200 = warp::any().map(warp::reply); +//! +//! // Returns a `200 OK` response with custom header and body. +//! let custom = warp::any().map(|| { +//! Response::builder() +//! .header("my-custom-header", "some-value") +//! .body("and a custom body") +//! }); +//! +//! // GET requests return the empty 200, POST return the custom. +//! let routes = warp::get().and(empty_200) +//! .or(warp::post().and(custom)); +//! ``` + +use std::borrow::Cow; +use std::convert::TryFrom; +use std::error::Error as StdError; +use std::fmt; + +use crate::generic::{Either, One}; +use http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; +use http::StatusCode; +use hyper::Body; +use serde::Serialize; +use serde_json; + +// This re-export just looks weird in docs... +pub(crate) use self::sealed::Reply_; +use self::sealed::{BoxedReply, Internal}; +#[doc(hidden)] +pub use crate::filters::reply as with; + +/// Response type into which types implementing the `Reply` trait are convertable. +pub type Response = ::http::Response<Body>; + +/// Returns an empty `Reply` with status code `200 OK`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // GET /just-ok returns an empty `200 OK`. +/// let route = warp::path("just-ok") +/// .map(|| { +/// println!("got a /just-ok request!"); +/// warp::reply() +/// }); +/// ``` +#[inline] +pub fn reply() -> impl Reply { + StatusCode::OK +} + +/// Convert the value into a `Reply` with the value encoded as JSON. +/// +/// The passed value must implement [`Serialize`][ser]. Many +/// collections do, and custom domain types can have `Serialize` derived. +/// +/// [ser]: https://serde.rs +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// // GET /ids returns a `200 OK` with a JSON array of ids: +/// // `[1, 3, 7, 13]` +/// let route = warp::path("ids") +/// .map(|| { +/// let our_ids = vec![1, 3, 7, 13]; +/// warp::reply::json(&our_ids) +/// }); +/// ``` +/// +/// # Note +/// +/// If a type fails to be serialized into JSON, the error is logged at the +/// `error` level, and the returned `impl Reply` will be an empty +/// `500 Internal Server Error` response. +pub fn json<T>(val: &T) -> Json +where + T: Serialize, +{ + Json { + inner: serde_json::to_vec(val).map_err(|err| { + log::error!("reply::json error: {}", err); + }), + } +} + +/// A JSON formatted reply. +#[allow(missing_debug_implementations)] +pub struct Json { + inner: Result<Vec<u8>, ()>, +} + +impl Reply for Json { + #[inline] + fn into_response(self) -> Response { + match self.inner { + Ok(body) => { + let mut res = Response::new(body.into()); + res.headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + res + } + Err(()) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } + } +} + +#[derive(Debug)] +pub(crate) struct ReplyJsonError; + +impl fmt::Display for ReplyJsonError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("warp::reply::json() failed") + } +} + +impl StdError for ReplyJsonError {} + +/// Reply with a body and `content-type` set to `text/html; charset=utf-8`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let body = r#" +/// <html> +/// <head> +/// <title>HTML with warp!</title> +/// </head> +/// <body> +/// <h1>warp + HTML = :heart:</h1> +/// </body> +/// </html> +/// "#; +/// +/// let route = warp::any() +/// .map(|| { +/// warp::reply::html(body) +/// }); +/// ``` +pub fn html<T>(body: T) -> impl Reply +where + Body: From<T>, + T: Send, +{ + Html { body } +} + +#[allow(missing_debug_implementations)] +struct Html<T> { + body: T, +} + +impl<T> Reply for Html<T> +where + Body: From<T>, + T: Send, +{ + #[inline] + fn into_response(self) -> Response { + let mut res = Response::new(Body::from(self.body)); + res.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_static("text/html; charset=utf-8"), + ); + res + } +} + +/// Types that can be converted into a `Response`. +/// +/// This trait is implemented for the following: +/// +/// - `http::StatusCode` +/// - `http::Response<impl Into<hyper::Body>>` +/// - `String` +/// - `&'static str` +/// +/// # Example +/// +/// ```rust +/// use warp::{Filter, http::Response}; +/// +/// struct Message { +/// msg: String +/// } +/// +/// impl warp::Reply for Message { +/// fn into_response(self) -> warp::reply::Response { +/// Response::new(format!("message: {}", self.msg).into()) +/// } +/// } +/// +/// fn handler() -> Message { +/// Message { msg: "Hello".to_string() } +/// } +/// +/// let route = warp::any().map(handler); +/// ``` +pub trait Reply: BoxedReply + Send { + /// Converts the given value into a [`Response`]. + /// + /// [`Response`]: type.Response.html + fn into_response(self) -> Response; + + /* + TODO: Currently unsure about having trait methods here, as it + requires returning an exact type, which I'd rather not commit to. + Additionally, it doesn't work great with `Box<Reply>`. + + A possible alternative is to have wrappers, like + + - `WithStatus<R: Reply>(StatusCode, R)` + + + /// Change the status code of this `Reply`. + fn with_status(self, status: StatusCode) -> Reply_ + where + Self: Sized, + { + let mut res = self.into_response(); + *res.status_mut() = status; + Reply_(res) + } + + /// Add a header to this `Reply`. + /// + /// # Example + /// + /// ```rust + /// use warp::Reply; + /// + /// let reply = warp::reply() + /// .with_header("x-foo", "bar"); + /// ``` + fn with_header<K, V>(self, name: K, value: V) -> Reply_ + where + Self: Sized, + HeaderName: TryFrom<K>, + HeaderValue: TryFrom<V>, + { + match <HeaderName as TryFrom<K>>::try_from(name) { + Ok(name) => match <HeaderValue as TryFrom<V>>::try_from(value) { + Ok(value) => { + let mut res = self.into_response(); + res.headers_mut().append(name, value); + Reply_(res) + }, + Err(err) => { + log::error!("with_header value error: {}", err.into()); + Reply_(::reject::server_error() + .into_response()) + } + }, + Err(err) => { + log::error!("with_header name error: {}", err.into()); + Reply_(::reject::server_error() + .into_response()) + } + } + } + */ +} + +impl<T: Reply + ?Sized> Reply for Box<T> { + fn into_response(self) -> Response { + self.boxed_into_response(Internal) + } +} + +fn _assert_object_safe() { + fn _assert(_: &dyn Reply) {} +} + +/// Wrap an `impl Reply` to change its `StatusCode`. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::any() +/// .map(warp::reply) +/// .map(|reply| { +/// warp::reply::with_status(reply, warp::http::StatusCode::CREATED) +/// }); +/// ``` +pub fn with_status<T: Reply>(reply: T, status: StatusCode) -> WithStatus<T> { + WithStatus { reply, status } +} + +/// Wrap an `impl Reply` to change its `StatusCode`. +/// +/// Returned by `warp::reply::with_status`. +#[derive(Debug)] +pub struct WithStatus<T> { + reply: T, + status: StatusCode, +} + +impl<T: Reply> Reply for WithStatus<T> { + fn into_response(self) -> Response { + let mut res = self.reply.into_response(); + *res.status_mut() = self.status; + res + } +} + +/// Wrap an `impl Reply` to add a header when rendering. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::any() +/// .map(warp::reply) +/// .map(|reply| { +/// warp::reply::with_header(reply, "server", "warp") +/// }); +/// ``` +pub fn with_header<T: Reply, K, V>(reply: T, name: K, value: V) -> WithHeader<T> +where + HeaderName: TryFrom<K>, + <HeaderName as TryFrom<K>>::Error: Into<http::Error>, + HeaderValue: TryFrom<V>, + <HeaderValue as TryFrom<V>>::Error: Into<http::Error>, +{ + let header = match <HeaderName as TryFrom<K>>::try_from(name) { + Ok(name) => match <HeaderValue as TryFrom<V>>::try_from(value) { + Ok(value) => Some((name, value)), + Err(err) => { + log::error!("with_header value error: {}", err.into()); + None + } + }, + Err(err) => { + log::error!("with_header name error: {}", err.into()); + None + } + }; + + WithHeader { header, reply } +} + +/// Wraps an `impl Reply` and adds a header when rendering. +/// +/// Returned by `warp::reply::with_header`. +#[derive(Debug)] +pub struct WithHeader<T> { + header: Option<(HeaderName, HeaderValue)>, + reply: T, +} + +impl<T: Reply> Reply for WithHeader<T> { + fn into_response(self) -> Response { + let mut res = self.reply.into_response(); + if let Some((name, value)) = self.header { + res.headers_mut().insert(name, value); + } + res + } +} + +impl<T: Send> Reply for ::http::Response<T> +where + Body: From<T>, +{ + #[inline] + fn into_response(self) -> Response { + self.map(Body::from) + } +} + +impl Reply for ::http::StatusCode { + #[inline] + fn into_response(self) -> Response { + let mut res = Response::default(); + *res.status_mut() = self; + res + } +} + +impl<T> Reply for Result<T, ::http::Error> +where + T: Reply + Send, +{ + #[inline] + fn into_response(self) -> Response { + match self { + Ok(t) => t.into_response(), + Err(e) => { + log::error!("reply error: {:?}", e); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } + } +} + +fn text_plain<T: Into<Body>>(body: T) -> Response { + let mut response = ::http::Response::new(body.into()); + response.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=utf-8"), + ); + response +} + +impl Reply for String { + #[inline] + fn into_response(self) -> Response { + text_plain(self) + } +} + +impl Reply for Vec<u8> { + #[inline] + fn into_response(self) -> Response { + ::http::Response::builder() + .header( + CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + ) + .body(Body::from(self)) + .unwrap() + } +} + +impl Reply for &'static str { + #[inline] + fn into_response(self) -> Response { + text_plain(self) + } +} + +impl Reply for Cow<'static, str> { + #[inline] + fn into_response(self) -> Response { + match self { + Cow::Borrowed(s) => s.into_response(), + Cow::Owned(s) => s.into_response(), + } + } +} + +impl Reply for &'static [u8] { + #[inline] + fn into_response(self) -> Response { + ::http::Response::builder() + .header( + CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + ) + .body(Body::from(self)) + .unwrap() + } +} + +impl<T, U> Reply for Either<T, U> +where + T: Reply, + U: Reply, +{ + #[inline] + fn into_response(self) -> Response { + match self { + Either::A(a) => a.into_response(), + Either::B(b) => b.into_response(), + } + } +} + +impl<T> Reply for One<T> +where + T: Reply, +{ + #[inline] + fn into_response(self) -> Response { + self.0.into_response() + } +} + +impl Reply for std::convert::Infallible { + #[inline(always)] + fn into_response(self) -> Response { + match self {} + } +} + +mod sealed { + use super::{Reply, Response}; + + // An opaque type to return `impl Reply` from trait methods. + #[allow(missing_debug_implementations)] + pub struct Reply_(pub(crate) Response); + + impl Reply for Reply_ { + #[inline] + fn into_response(self) -> Response { + self.0 + } + } + + #[allow(missing_debug_implementations)] + pub struct Internal; + + // Implemented for all types that implement `Reply`. + // + // A user doesn't need to worry about this, it's just trait + // hackery to get `Box<dyn Reply>` working. + pub trait BoxedReply { + fn boxed_into_response(self: Box<Self>, internal: Internal) -> Response; + } + + impl<T: Reply> BoxedReply for T { + fn boxed_into_response(self: Box<Self>, _: Internal) -> Response { + (*self).into_response() + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + #[test] + fn json_serde_error() { + // a HashMap<Vec, _> cannot be serialized to JSON + let mut map = HashMap::new(); + map.insert(vec![1, 2], 45); + + let res = json(&map).into_response(); + assert_eq!(res.status(), 500); + } + + #[test] + fn response_builder_error() { + let res = ::http::Response::builder() + .status(1337) + .body("woops") + .into_response(); + + assert_eq!(res.status(), 500); + } + + #[test] + fn boxed_reply() { + let r: Box<dyn Reply> = Box::new(reply()); + let resp = r.into_response(); + assert_eq!(resp.status(), 200); + } +} diff --git a/third_party/rust/warp/src/route.rs b/third_party/rust/warp/src/route.rs new file mode 100644 index 0000000000..8eed7d79ff --- /dev/null +++ b/third_party/rust/warp/src/route.rs @@ -0,0 +1,142 @@ +use scoped_tls::scoped_thread_local; +use std::cell::RefCell; +use std::mem; +use std::net::SocketAddr; + +use http; +use hyper::Body; + +use crate::Request; + +scoped_thread_local!(static ROUTE: RefCell<Route>); + +pub(crate) fn set<F, U>(r: &RefCell<Route>, func: F) -> U +where + F: FnOnce() -> U, +{ + ROUTE.set(r, func) +} + +pub(crate) fn is_set() -> bool { + ROUTE.is_set() +} + +pub(crate) fn with<F, R>(func: F) -> R +where + F: FnOnce(&mut Route) -> R, +{ + ROUTE.with(move |route| func(&mut *route.borrow_mut())) +} + +#[derive(Debug)] +pub(crate) struct Route { + body: BodyState, + remote_addr: Option<SocketAddr>, + req: Request, + segments_index: usize, +} + +#[derive(Debug)] +enum BodyState { + Ready, + Taken, +} + +impl Route { + pub(crate) fn new(req: Request, remote_addr: Option<SocketAddr>) -> RefCell<Route> { + let segments_index = if req.uri().path().starts_with('/') { + // Skip the beginning slash. + 1 + } else { + 0 + }; + + RefCell::new(Route { + body: BodyState::Ready, + remote_addr, + req, + segments_index, + }) + } + + pub(crate) fn method(&self) -> &http::Method { + self.req.method() + } + + pub(crate) fn headers(&self) -> &http::HeaderMap { + self.req.headers() + } + + pub(crate) fn version(&self) -> http::Version { + self.req.version() + } + + pub(crate) fn extensions(&self) -> &http::Extensions { + self.req.extensions() + } + + /* + pub(crate) fn extensions_mut(&mut self) -> &mut http::Extensions { + self.req.extensions_mut() + } + */ + + pub(crate) fn uri(&self) -> &http::Uri { + self.req.uri() + } + + pub(crate) fn path(&self) -> &str { + &self.req.uri().path()[self.segments_index..] + } + + pub(crate) fn full_path(&self) -> &str { + self.req.uri().path() + } + + pub(crate) fn set_unmatched_path(&mut self, index: usize) { + let index = self.segments_index + index; + + let path = self.req.uri().path(); + + if path.len() == index { + self.segments_index = index; + } else { + debug_assert_eq!(path.as_bytes()[index], b'/'); + + self.segments_index = index + 1; + } + } + + pub(crate) fn query(&self) -> Option<&str> { + self.req.uri().query() + } + + pub(crate) fn matched_path_index(&self) -> usize { + self.segments_index + } + + pub(crate) fn reset_matched_path_index(&mut self, index: usize) { + debug_assert!( + index <= self.segments_index, + "reset_match_path_index should not be bigger: current={}, arg={}", + self.segments_index, + index, + ); + self.segments_index = index; + } + + pub(crate) fn remote_addr(&self) -> Option<SocketAddr> { + self.remote_addr + } + + pub(crate) fn take_body(&mut self) -> Option<Body> { + match self.body { + BodyState::Ready => { + let body = mem::replace(self.req.body_mut(), Body::empty()); + self.body = BodyState::Taken; + Some(body) + } + BodyState::Taken => None, + } + } +} diff --git a/third_party/rust/warp/src/server.rs b/third_party/rust/warp/src/server.rs new file mode 100644 index 0000000000..3736fca969 --- /dev/null +++ b/third_party/rust/warp/src/server.rs @@ -0,0 +1,458 @@ +#[cfg(feature = "tls")] +use crate::tls::TlsConfigBuilder; +use std::convert::Infallible; +use std::error::Error as StdError; +use std::future::Future; +use std::net::SocketAddr; +#[cfg(feature = "tls")] +use std::path::Path; + +use futures::{future, FutureExt, TryFuture, TryStream, TryStreamExt}; +use hyper::server::conn::AddrIncoming; +use hyper::service::{make_service_fn, service_fn}; +use hyper::Server as HyperServer; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::filter::Filter; +use crate::reject::IsReject; +use crate::reply::Reply; +use crate::transport::Transport; + +/// Create a `Server` with the provided `Filter`. +pub fn serve<F>(filter: F) -> Server<F> +where + F: Filter + Clone + Send + Sync + 'static, + F::Extract: Reply, + F::Error: IsReject, +{ + Server { + pipeline: false, + filter, + } +} + +/// A Warp Server ready to filter requests. +#[derive(Debug)] +pub struct Server<F> { + pipeline: bool, + filter: F, +} + +/// A Warp Server ready to filter requests over TLS. +/// +/// *This type requires the `"tls"` feature.* +#[cfg(feature = "tls")] +pub struct TlsServer<F> { + server: Server<F>, + tls: TlsConfigBuilder, +} + +// Getting all various generic bounds to make this a re-usable method is +// very complicated, so instead this is just a macro. +macro_rules! into_service { + ($into:expr) => {{ + let inner = crate::service($into); + make_service_fn(move |transport| { + let inner = inner.clone(); + let remote_addr = Transport::remote_addr(transport); + future::ok::<_, Infallible>(service_fn(move |req| { + inner.call_with_addr(req, remote_addr) + })) + }) + }}; +} + +macro_rules! addr_incoming { + ($addr:expr) => {{ + let mut incoming = AddrIncoming::bind($addr)?; + incoming.set_nodelay(true); + let addr = incoming.local_addr(); + (addr, incoming) + }}; +} + +macro_rules! bind_inner { + ($this:ident, $addr:expr) => {{ + let service = into_service!($this.filter); + let (addr, incoming) = addr_incoming!($addr); + let srv = HyperServer::builder(incoming) + .http1_pipeline_flush($this.pipeline) + .serve(service); + Ok::<_, hyper::Error>((addr, srv)) + }}; + + (tls: $this:ident, $addr:expr) => {{ + let service = into_service!($this.server.filter); + let (addr, incoming) = addr_incoming!($addr); + let tls = $this.tls.build()?; + let srv = HyperServer::builder(crate::tls::TlsAcceptor::new(tls, incoming)) + .http1_pipeline_flush($this.server.pipeline) + .serve(service); + Ok::<_, Box<dyn std::error::Error + Send + Sync>>((addr, srv)) + }}; +} + +macro_rules! bind { + ($this:ident, $addr:expr) => {{ + let addr = $addr.into(); + (|addr| bind_inner!($this, addr))(&addr).unwrap_or_else(|e| { + panic!("error binding to {}: {}", addr, e); + }) + }}; + + (tls: $this:ident, $addr:expr) => {{ + let addr = $addr.into(); + (|addr| bind_inner!(tls: $this, addr))(&addr).unwrap_or_else(|e| { + panic!("error binding to {}: {}", addr, e); + }) + }}; +} + +macro_rules! try_bind { + ($this:ident, $addr:expr) => {{ + (|addr| bind_inner!($this, addr))($addr) + }}; + + (tls: $this:ident, $addr:expr) => {{ + (|addr| bind_inner!(tls: $this, addr))($addr) + }}; +} + +// ===== impl Server ===== + +impl<F> Server<F> +where + F: Filter + Clone + Send + Sync + 'static, + <F::Future as TryFuture>::Ok: Reply, + <F::Future as TryFuture>::Error: IsReject, +{ + /// Run this `Server` forever on the current thread. + pub async fn run(self, addr: impl Into<SocketAddr> + 'static) { + let (addr, fut) = self.bind_ephemeral(addr); + + log::info!("listening on http://{}", addr); + + fut.await; + } + + /// Run this `Server` forever on the current thread with a specific stream + /// of incoming connections. + /// + /// This can be used for Unix Domain Sockets, or TLS, etc. + pub async fn run_incoming<I>(self, incoming: I) + where + I: TryStream + Send, + I::Ok: AsyncRead + AsyncWrite + Send + 'static + Unpin, + I::Error: Into<Box<dyn StdError + Send + Sync>>, + { + self.run_incoming2(incoming.map_ok(crate::transport::LiftIo).into_stream()) + .await; + } + + async fn run_incoming2<I>(self, incoming: I) + where + I: TryStream + Send, + I::Ok: Transport + Send + 'static + Unpin, + I::Error: Into<Box<dyn StdError + Send + Sync>>, + { + let fut = self.serve_incoming2(incoming); + + log::info!("listening with custom incoming"); + + fut.await; + } + + /// Bind to a socket address, returning a `Future` that can be + /// executed on any runtime. + /// + /// # Panics + /// + /// Panics if we are unable to bind to the provided address. + pub fn bind(self, addr: impl Into<SocketAddr> + 'static) -> impl Future<Output = ()> + 'static { + let (_, fut) = self.bind_ephemeral(addr); + fut + } + + /// Bind to a socket address, returning a `Future` that can be + /// executed on any runtime. + /// + /// In case we are unable to bind to the specified address, resolves to an + /// error and logs the reason. + pub async fn try_bind(self, addr: impl Into<SocketAddr> + 'static) { + let addr = addr.into(); + let srv = match try_bind!(self, &addr) { + Ok((_, srv)) => srv, + Err(err) => { + log::error!("error binding to {}: {}", addr, err); + return; + } + }; + + srv.map(|result| { + if let Err(err) = result { + log::error!("server error: {}", err) + } + }) + .await; + } + + /// Bind to a possibly ephemeral socket address. + /// + /// Returns the bound address and a `Future` that can be executed on + /// any runtime. + /// + /// # Panics + /// + /// Panics if we are unable to bind to the provided address. + pub fn bind_ephemeral( + self, + addr: impl Into<SocketAddr> + 'static, + ) -> (SocketAddr, impl Future<Output = ()> + 'static) { + let (addr, srv) = bind!(self, addr); + let srv = srv.map(|result| { + if let Err(err) = result { + log::error!("server error: {}", err) + } + }); + + (addr, srv) + } + + /// Tried to bind a possibly ephemeral socket address. + /// + /// Returns a `Result` which fails in case we are unable to bind with the + /// underlying error. + /// + /// Returns the bound address and a `Future` that can be executed on + /// any runtime. + pub fn try_bind_ephemeral( + self, + addr: impl Into<SocketAddr> + 'static, + ) -> Result<(SocketAddr, impl Future<Output = ()> + 'static), crate::Error> { + let addr = addr.into(); + let (addr, srv) = try_bind!(self, &addr).map_err(crate::Error::new)?; + let srv = srv.map(|result| { + if let Err(err) = result { + log::error!("server error: {}", err) + } + }); + + Ok((addr, srv)) + } + + /// Create a server with graceful shutdown signal. + /// + /// When the signal completes, the server will start the graceful shutdown + /// process. + /// + /// Returns the bound address and a `Future` that can be executed on + /// any runtime. + /// + /// # Example + /// + /// ```no_run + /// use warp::Filter; + /// use futures::future::TryFutureExt; + /// use tokio::sync::oneshot; + /// + /// # fn main() { + /// let routes = warp::any() + /// .map(|| "Hello, World!"); + /// + /// let (tx, rx) = oneshot::channel(); + /// + /// let (addr, server) = warp::serve(routes) + /// .bind_with_graceful_shutdown(([127, 0, 0, 1], 3030), async { + /// rx.await.ok(); + /// }); + /// + /// // Spawn the server into a runtime + /// tokio::task::spawn(server); + /// + /// // Later, start the shutdown... + /// let _ = tx.send(()); + /// # } + /// ``` + pub fn bind_with_graceful_shutdown( + self, + addr: impl Into<SocketAddr> + 'static, + signal: impl Future<Output = ()> + Send + 'static, + ) -> (SocketAddr, impl Future<Output = ()> + 'static) { + let (addr, srv) = bind!(self, addr); + let fut = srv.with_graceful_shutdown(signal).map(|result| { + if let Err(err) = result { + log::error!("server error: {}", err) + } + }); + (addr, fut) + } + + /// Setup this `Server` with a specific stream of incoming connections. + /// + /// This can be used for Unix Domain Sockets, or TLS, etc. + /// + /// Returns a `Future` that can be executed on any runtime. + pub fn serve_incoming<I>(self, incoming: I) -> impl Future<Output = ()> + 'static + where + I: TryStream + Send + 'static, + I::Ok: AsyncRead + AsyncWrite + Send + 'static + Unpin, + I::Error: Into<Box<dyn StdError + Send + Sync>>, + { + let incoming = incoming.map_ok(crate::transport::LiftIo); + self.serve_incoming2(incoming) + } + + async fn serve_incoming2<I>(self, incoming: I) + where + I: TryStream + Send, + I::Ok: Transport + Send + 'static + Unpin, + I::Error: Into<Box<dyn StdError + Send + Sync>>, + { + let service = into_service!(self.filter); + + let srv = HyperServer::builder(hyper::server::accept::from_stream(incoming.into_stream())) + .http1_pipeline_flush(self.pipeline) + .serve(service) + .await; + + if let Err(err) = srv { + log::error!("server error: {}", err); + } + } + + // Generally shouldn't be used, as it can slow down non-pipelined responses. + // + // It's only real use is to make silly pipeline benchmarks look better. + #[doc(hidden)] + pub fn unstable_pipeline(mut self) -> Self { + self.pipeline = true; + self + } + + /// Configure a server to use TLS. + /// + /// *This function requires the `"tls"` feature.* + #[cfg(feature = "tls")] + pub fn tls(self) -> TlsServer<F> { + TlsServer { + server: self, + tls: TlsConfigBuilder::new(), + } + } +} + +// // ===== impl TlsServer ===== + +#[cfg(feature = "tls")] +impl<F> TlsServer<F> +where + F: Filter + Clone + Send + Sync + 'static, + <F::Future as TryFuture>::Ok: Reply, + <F::Future as TryFuture>::Error: IsReject, +{ + // TLS config methods + + /// Specify the file path to read the private key. + pub fn key_path(self, path: impl AsRef<Path>) -> Self { + self.with_tls(|tls| tls.key_path(path)) + } + + /// Specify the file path to read the certificate. + pub fn cert_path(self, path: impl AsRef<Path>) -> Self { + self.with_tls(|tls| tls.cert_path(path)) + } + + /// Specify the in-memory contents of the private key. + pub fn key(self, key: impl AsRef<[u8]>) -> Self { + self.with_tls(|tls| tls.key(key.as_ref())) + } + + /// Specify the in-memory contents of the certificate. + pub fn cert(self, cert: impl AsRef<[u8]>) -> Self { + self.with_tls(|tls| tls.cert(cert.as_ref())) + } + + fn with_tls<Func>(self, func: Func) -> Self + where + Func: FnOnce(TlsConfigBuilder) -> TlsConfigBuilder, + { + let TlsServer { server, tls } = self; + let tls = func(tls); + TlsServer { server, tls } + } + + // Server run methods + + /// Run this `TlsServer` forever on the current thread. + /// + /// *This function requires the `"tls"` feature.* + pub async fn run(self, addr: impl Into<SocketAddr> + 'static) { + let (addr, fut) = self.bind_ephemeral(addr); + + log::info!("listening on https://{}", addr); + + fut.await; + } + + /// Bind to a socket address, returning a `Future` that can be + /// executed on a runtime. + /// + /// *This function requires the `"tls"` feature.* + pub async fn bind(self, addr: impl Into<SocketAddr> + 'static) { + let (_, fut) = self.bind_ephemeral(addr); + fut.await; + } + + /// Bind to a possibly ephemeral socket address. + /// + /// Returns the bound address and a `Future` that can be executed on + /// any runtime. + /// + /// *This function requires the `"tls"` feature.* + pub fn bind_ephemeral( + self, + addr: impl Into<SocketAddr> + 'static, + ) -> (SocketAddr, impl Future<Output = ()> + 'static) { + let (addr, srv) = bind!(tls: self, addr); + let srv = srv.map(|result| { + if let Err(err) = result { + log::error!("server error: {}", err) + } + }); + + (addr, srv) + } + + /// Create a server with graceful shutdown signal. + /// + /// When the signal completes, the server will start the graceful shutdown + /// process. + /// + /// *This function requires the `"tls"` feature.* + pub fn bind_with_graceful_shutdown( + self, + addr: impl Into<SocketAddr> + 'static, + signal: impl Future<Output = ()> + Send + 'static, + ) -> (SocketAddr, impl Future<Output = ()> + 'static) { + let (addr, srv) = bind!(tls: self, addr); + + let fut = srv.with_graceful_shutdown(signal).map(|result| { + if let Err(err) = result { + log::error!("server error: {}", err) + } + }); + (addr, fut) + } +} + +#[cfg(feature = "tls")] +impl<F> ::std::fmt::Debug for TlsServer<F> +where + F: ::std::fmt::Debug, +{ + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_struct("TlsServer") + .field("server", &self.server) + .finish() + } +} diff --git a/third_party/rust/warp/src/service.rs b/third_party/rust/warp/src/service.rs new file mode 100644 index 0000000000..4f93809c4e --- /dev/null +++ b/third_party/rust/warp/src/service.rs @@ -0,0 +1,3 @@ +//! Convert `Filter`s into `Service`s + +pub use crate::filter::service::service; diff --git a/third_party/rust/warp/src/test.rs b/third_party/rust/warp/src/test.rs new file mode 100644 index 0000000000..bcb0023699 --- /dev/null +++ b/third_party/rust/warp/src/test.rs @@ -0,0 +1,684 @@ +//! Test utilities to test your filters. +//! +//! [`Filter`](../trait.Filter.html)s can be easily tested without starting up an HTTP +//! server, by making use of the [`RequestBuilder`](./struct.RequestBuilder.html) in this +//! module. +//! +//! # Testing Filters +//! +//! It's easy to test filters, especially if smaller filters are used to build +//! up your full set. Consider these example filters: +//! +//! ``` +//! use warp::Filter; +//! +//! fn sum() -> impl Filter<Extract = (u32,), Error = warp::Rejection> + Copy { +//! warp::path::param() +//! .and(warp::path::param()) +//! .map(|x: u32, y: u32| { +//! x + y +//! }) +//! } +//! +//! fn math() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Copy { +//! warp::post() +//! .and(sum()) +//! .map(|z: u32| { +//! format!("Sum = {}", z) +//! }) +//! } +//! ``` +//! +//! We can test some requests against the `sum` filter like this: +//! +//! ``` +//! # use warp::Filter; +//! #[test] +//! fn test_sum() { +//! # let sum = || warp::any().map(|| 3); +//! let filter = sum(); +//! +//! // Execute `sum` and get the `Extract` back. +//! let value = warp::test::request() +//! .path("/1/2") +//! .filter(&filter) +//! .unwrap(); +//! assert_eq!(value, 3); +//! +//! // Or simply test if a request matches (doesn't reject). +//! assert!( +//! !warp::test::request() +//! .path("/1/-5") +//! .matches(&filter) +//! ); +//! } +//! ``` +//! +//! If the filter returns something that implements `Reply`, and thus can be +//! turned into a response sent back to the client, we can test what exact +//! response is returned. The `math` filter uses the `sum` filter, but returns +//! a `String` that can be turned into a response. +//! +//! ``` +//! # use warp::Filter; +//! #[test] +//! fn test_math() { +//! # let math = || warp::any().map(warp::reply); +//! let filter = math(); +//! +//! let res = warp::test::request() +//! .path("/1/2") +//! .reply(&filter); +//! assert_eq!(res.status(), 405, "GET is not allowed"); +//! +//! let res = warp::test::request() +//! .method("POST") +//! .path("/1/2") +//! .reply(&filter); +//! assert_eq!(res.status(), 200); +//! assert_eq!(res.body(), "Sum is 3"); +//! } +//! ``` +use std::convert::TryFrom; +use std::error::Error as StdError; +use std::fmt; +use std::future::Future; +use std::net::SocketAddr; +#[cfg(feature = "websocket")] +use std::pin::Pin; +#[cfg(feature = "websocket")] +use std::task::{self, Poll}; + +use bytes::Bytes; +#[cfg(feature = "websocket")] +use futures::StreamExt; +use futures::{future, FutureExt, TryFutureExt}; +use http::{ + header::{HeaderName, HeaderValue}, + Response, +}; +use serde::Serialize; +use serde_json; +#[cfg(feature = "websocket")] +use tokio::sync::{mpsc, oneshot}; + +use crate::filter::Filter; +use crate::reject::IsReject; +use crate::reply::Reply; +use crate::route::{self, Route}; +use crate::Request; + +use self::inner::OneOrTuple; + +/// Starts a new test `RequestBuilder`. +pub fn request() -> RequestBuilder { + RequestBuilder { + remote_addr: None, + req: Request::default(), + } +} + +/// Starts a new test `WsBuilder`. +#[cfg(feature = "websocket")] +pub fn ws() -> WsBuilder { + WsBuilder { req: request() } +} + +/// A request builder for testing filters. +/// +/// See [module documentation](crate::test) for an overview. +#[must_use = "RequestBuilder does nothing on its own"] +#[derive(Debug)] +pub struct RequestBuilder { + remote_addr: Option<SocketAddr>, + req: Request, +} + +/// A Websocket builder for testing filters. +/// +/// See [module documentation](crate::test) for an overview. +#[cfg(feature = "websocket")] +#[must_use = "WsBuilder does nothing on its own"] +#[derive(Debug)] +pub struct WsBuilder { + req: RequestBuilder, +} + +/// A test client for Websocket filters. +#[cfg(feature = "websocket")] +pub struct WsClient { + tx: mpsc::UnboundedSender<crate::ws::Message>, + rx: mpsc::UnboundedReceiver<Result<crate::ws::Message, crate::error::Error>>, +} + +/// An error from Websocket filter tests. +#[derive(Debug)] +pub struct WsError { + cause: Box<dyn StdError + Send + Sync>, +} + +impl RequestBuilder { + /// Sets the method of this builder. + /// + /// The default if not set is `GET`. + /// + /// # Example + /// + /// ``` + /// let req = warp::test::request() + /// .method("POST"); + /// ``` + /// + /// # Panic + /// + /// This panics if the passed string is not able to be parsed as a valid + /// `Method`. + pub fn method(mut self, method: &str) -> Self { + *self.req.method_mut() = method.parse().expect("valid method"); + self + } + + /// Sets the request path of this builder. + /// + /// The default is not set is `/`. + /// + /// # Example + /// + /// ``` + /// let req = warp::test::request() + /// .path("/todos/33"); + /// ``` + /// + /// # Panic + /// + /// This panics if the passed string is not able to be parsed as a valid + /// `Uri`. + pub fn path(mut self, p: &str) -> Self { + let uri = p.parse().expect("test request path invalid"); + *self.req.uri_mut() = uri; + self + } + + /// Set a header for this request. + /// + /// # Example + /// + /// ``` + /// let req = warp::test::request() + /// .header("accept", "application/json"); + /// ``` + /// + /// # Panic + /// + /// This panics if the passed strings are not able to be parsed as a valid + /// `HeaderName` and `HeaderValue`. + pub fn header<K, V>(mut self, key: K, value: V) -> Self + where + HeaderName: TryFrom<K>, + HeaderValue: TryFrom<V>, + { + let name: HeaderName = TryFrom::try_from(key) + .map_err(|_| ()) + .expect("invalid header name"); + let value = TryFrom::try_from(value) + .map_err(|_| ()) + .expect("invalid header value"); + self.req.headers_mut().insert(name, value); + self + } + + /// Add a type to the request's `http::Extensions`. + pub fn extension<T>(mut self, ext: T) -> Self + where + T: Send + Sync + 'static, + { + self.req.extensions_mut().insert(ext); + self + } + + /// Set the bytes of this request body. + /// + /// Default is an empty body. + /// + /// # Example + /// + /// ``` + /// let req = warp::test::request() + /// .body("foo=bar&baz=quux"); + /// ``` + pub fn body(mut self, body: impl AsRef<[u8]>) -> Self { + let body = body.as_ref().to_vec(); + let len = body.len(); + *self.req.body_mut() = body.into(); + self.header("content-length", len.to_string()) + } + + /// Set the bytes of this request body by serializing a value into JSON. + /// + /// # Example + /// + /// ``` + /// let req = warp::test::request() + /// .json(&true); + /// ``` + pub fn json(mut self, val: &impl Serialize) -> Self { + let vec = serde_json::to_vec(val).expect("json() must serialize to JSON"); + let len = vec.len(); + *self.req.body_mut() = vec.into(); + self.header("content-length", len.to_string()) + .header("content-type", "application/json") + } + + /// Tries to apply the `Filter` on this request. + /// + /// # Example + /// + /// ```no_run + /// async { + /// let param = warp::path::param::<u32>(); + /// + /// let ex = warp::test::request() + /// .path("/41") + /// .filter(¶m) + /// .await + /// .unwrap(); + /// + /// assert_eq!(ex, 41); + /// + /// assert!( + /// warp::test::request() + /// .path("/foo") + /// .filter(¶m) + /// .await + /// .is_err() + /// ); + ///}; + /// ``` + pub async fn filter<F>(self, f: &F) -> Result<<F::Extract as OneOrTuple>::Output, F::Error> + where + F: Filter, + F::Future: Send + 'static, + F::Extract: OneOrTuple + Send + 'static, + F::Error: Send + 'static, + { + self.apply_filter(f).await.map(|ex| ex.one_or_tuple()) + } + + /// Returns whether the `Filter` matches this request, or rejects it. + /// + /// # Example + /// + /// ```no_run + /// async { + /// let get = warp::get(); + /// let post = warp::post(); + /// + /// assert!( + /// warp::test::request() + /// .method("GET") + /// .matches(&get) + /// .await + /// ); + /// + /// assert!( + /// !warp::test::request() + /// .method("GET") + /// .matches(&post) + /// .await + /// ); + ///}; + /// ``` + pub async fn matches<F>(self, f: &F) -> bool + where + F: Filter, + F::Future: Send + 'static, + F::Extract: Send + 'static, + F::Error: Send + 'static, + { + self.apply_filter(f).await.is_ok() + } + + /// Returns `Response` provided by applying the `Filter`. + /// + /// This requires that the supplied `Filter` return a [`Reply`](Reply). + pub async fn reply<F>(self, f: &F) -> Response<Bytes> + where + F: Filter + 'static, + F::Extract: Reply + Send, + F::Error: IsReject + Send, + { + // TODO: de-duplicate this and apply_filter() + assert!(!route::is_set(), "nested test filter calls"); + + let route = Route::new(self.req, self.remote_addr); + let mut fut = Box::pin( + route::set(&route, move || f.filter(crate::filter::Internal)).then(|result| { + let res = match result { + Ok(rep) => rep.into_response(), + Err(rej) => { + log::debug!("rejected: {:?}", rej); + rej.into_response() + } + }; + let (parts, body) = res.into_parts(); + hyper::body::to_bytes(body) + .map_ok(|chunk| Response::from_parts(parts, chunk.into())) + }), + ); + + let fut = future::poll_fn(move |cx| route::set(&route, || fut.as_mut().poll(cx))); + + fut.await.expect("reply shouldn't fail") + } + + fn apply_filter<F>(self, f: &F) -> impl Future<Output = Result<F::Extract, F::Error>> + where + F: Filter, + F::Future: Send + 'static, + F::Extract: Send + 'static, + F::Error: Send + 'static, + { + assert!(!route::is_set(), "nested test filter calls"); + + let route = Route::new(self.req, self.remote_addr); + let mut fut = Box::pin(route::set(&route, move || { + f.filter(crate::filter::Internal) + })); + future::poll_fn(move |cx| route::set(&route, || fut.as_mut().poll(cx))) + } +} + +#[cfg(feature = "websocket")] +impl WsBuilder { + /// Sets the request path of this builder. + /// + /// The default is not set is `/`. + /// + /// # Example + /// + /// ``` + /// let req = warp::test::ws() + /// .path("/chat"); + /// ``` + /// + /// # Panic + /// + /// This panics if the passed string is not able to be parsed as a valid + /// `Uri`. + pub fn path(self, p: &str) -> Self { + WsBuilder { + req: self.req.path(p), + } + } + + /// Set a header for this request. + /// + /// # Example + /// + /// ``` + /// let req = warp::test::ws() + /// .header("foo", "bar"); + /// ``` + /// + /// # Panic + /// + /// This panics if the passed strings are not able to be parsed as a valid + /// `HeaderName` and `HeaderValue`. + pub fn header<K, V>(self, key: K, value: V) -> Self + where + HeaderName: TryFrom<K>, + HeaderValue: TryFrom<V>, + { + WsBuilder { + req: self.req.header(key, value), + } + } + + /// Execute this Websocket request against te provided filter. + /// + /// If the handshake succeeds, returns a `WsClient`. + /// + /// # Example + /// + /// ```no_run + /// use futures::future; + /// use warp::Filter; + /// #[tokio::main] + /// # async fn main() { + /// + /// // Some route that accepts websockets (but drops them immediately). + /// let route = warp::ws() + /// .map(|ws: warp::ws::Ws| { + /// ws.on_upgrade(|_| future::ready(())) + /// }); + /// + /// let client = warp::test::ws() + /// .handshake(route) + /// .await + /// .expect("handshake"); + /// # } + /// ``` + pub async fn handshake<F>(self, f: F) -> Result<WsClient, WsError> + where + F: Filter + Clone + Send + Sync + 'static, + F::Extract: Reply + Send, + F::Error: IsReject + Send, + { + let (upgraded_tx, upgraded_rx) = oneshot::channel(); + let (wr_tx, wr_rx) = mpsc::unbounded_channel(); + let (rd_tx, rd_rx) = mpsc::unbounded_channel(); + + tokio::spawn(async move { + use tokio_tungstenite::tungstenite::protocol; + + let (addr, srv) = crate::serve(f).bind_ephemeral(([127, 0, 0, 1], 0)); + + let mut req = self + .req + .header("connection", "upgrade") + .header("upgrade", "websocket") + .header("sec-websocket-version", "13") + .header("sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ==") + .req; + + let uri = format!("http://{}{}", addr, req.uri().path()) + .parse() + .expect("addr + path is valid URI"); + + *req.uri_mut() = uri; + + // let mut rt = current_thread::Runtime::new().unwrap(); + tokio::spawn(srv); + + let upgrade = ::hyper::Client::builder() + .build(AddrConnect(addr)) + .request(req) + .and_then(|res| res.into_body().on_upgrade()); + + let upgraded = match upgrade.await { + Ok(up) => { + let _ = upgraded_tx.send(Ok(())); + up + } + Err(err) => { + let _ = upgraded_tx.send(Err(err)); + return; + } + }; + let ws = crate::ws::WebSocket::from_raw_socket( + upgraded, + protocol::Role::Client, + Default::default(), + ) + .await; + + let (tx, rx) = ws.split(); + let write = wr_rx.map(Ok).forward(tx).map(|_| ()); + + let read = rx + .take_while(|result| match result { + Err(_) => future::ready(false), + Ok(m) => future::ready(!m.is_close()), + }) + .for_each(move |item| { + rd_tx.send(item).expect("ws receive error"); + future::ready(()) + }); + + future::join(write, read).await; + }); + + match upgraded_rx.await { + Ok(Ok(())) => Ok(WsClient { + tx: wr_tx, + rx: rd_rx, + }), + Ok(Err(err)) => Err(WsError::new(err)), + Err(_canceled) => panic!("websocket handshake thread panicked"), + } + } +} + +#[cfg(feature = "websocket")] +impl WsClient { + /// Send a "text" websocket message to the server. + pub async fn send_text(&mut self, text: impl Into<String>) { + self.send(crate::ws::Message::text(text)).await; + } + + /// Send a websocket message to the server. + pub async fn send(&mut self, msg: crate::ws::Message) { + self.tx.send(msg).unwrap(); + } + + /// Receive a websocket message from the server. + pub async fn recv(&mut self) -> Result<crate::filters::ws::Message, WsError> { + self.rx + .next() + .await + .map(|unbounded_result| unbounded_result.map_err(WsError::new)) + .unwrap_or_else(|| { + // websocket is closed + Err(WsError::new("closed")) + }) + } + + /// Assert the server has closed the connection. + pub async fn recv_closed(&mut self) -> Result<(), WsError> { + self.rx + .next() + .await + .map(|result| match result { + Ok(msg) => Err(WsError::new(format!("received message: {:?}", msg))), + Err(err) => Err(WsError::new(err)), + }) + .unwrap_or_else(|| { + // closed successfully + Ok(()) + }) + } +} + +#[cfg(feature = "websocket")] +impl fmt::Debug for WsClient { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("WsClient").finish() + } +} + +// ===== impl WsError ===== + +#[cfg(feature = "websocket")] +impl WsError { + fn new<E: Into<Box<dyn StdError + Send + Sync>>>(cause: E) -> Self { + WsError { + cause: cause.into(), + } + } +} + +impl fmt::Display for WsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "websocket error: {}", self.cause) + } +} + +impl StdError for WsError { + fn description(&self) -> &str { + "websocket error" + } +} + +// ===== impl AddrConnect ===== + +#[cfg(feature = "websocket")] +#[derive(Clone)] +struct AddrConnect(SocketAddr); + +#[cfg(feature = "websocket")] +impl tower_service::Service<::http::Uri> for AddrConnect { + type Response = ::tokio::net::TcpStream; + type Error = ::std::io::Error; + type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; + + fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _: ::http::Uri) -> Self::Future { + Box::pin(tokio::net::TcpStream::connect(self.0)) + } +} + +mod inner { + pub trait OneOrTuple { + type Output; + + fn one_or_tuple(self) -> Self::Output; + } + + impl OneOrTuple for () { + type Output = (); + fn one_or_tuple(self) -> Self::Output {} + } + + macro_rules! one_or_tuple { + ($type1:ident) => { + impl<$type1> OneOrTuple for ($type1,) { + type Output = $type1; + fn one_or_tuple(self) -> Self::Output { + self.0 + } + } + }; + ($type1:ident, $( $type:ident ),*) => { + one_or_tuple!($( $type ),*); + + impl<$type1, $($type),*> OneOrTuple for ($type1, $($type),*) { + type Output = Self; + fn one_or_tuple(self) -> Self::Output { + self + } + } + } + } + + one_or_tuple! { + T1, + T2, + T3, + T4, + T5, + T6, + T7, + T8, + T9, + T10, + T11, + T12, + T13, + T14, + T15, + T16 + } +} diff --git a/third_party/rust/warp/src/tls.rs b/third_party/rust/warp/src/tls.rs new file mode 100644 index 0000000000..7a73be724c --- /dev/null +++ b/third_party/rust/warp/src/tls.rs @@ -0,0 +1,314 @@ +use std::fs::File; +use std::future::Future; +use std::io::{self, BufReader, Cursor, Read}; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite}; + +use futures::ready; +use hyper::server::accept::Accept; +use hyper::server::conn::{AddrIncoming, AddrStream}; + +use crate::transport::Transport; +use tokio_rustls::rustls::{NoClientAuth, ServerConfig, TLSError}; + +/// Represents errors that can occur building the TlsConfig +#[derive(Debug)] +pub(crate) enum TlsConfigError { + Io(io::Error), + /// An Error parsing the Certificate + CertParseError, + /// An Error parsing a Pkcs8 key + Pkcs8ParseError, + /// An Error parsing a Rsa key + RsaParseError, + /// An error from an empty key + EmptyKey, + /// An error from an invalid key + InvalidKey(TLSError), +} + +impl std::fmt::Display for TlsConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TlsConfigError::Io(err) => err.fmt(f), + TlsConfigError::CertParseError => write!(f, "certificate parse error"), + TlsConfigError::Pkcs8ParseError => write!(f, "pkcs8 parse error"), + TlsConfigError::RsaParseError => write!(f, "rsa parse error"), + TlsConfigError::EmptyKey => write!(f, "key contains no private key"), + TlsConfigError::InvalidKey(err) => write!(f, "key contains an invalid key, {}", err), + } + } +} + +impl std::error::Error for TlsConfigError {} + +/// Builder to set the configuration for the Tls server. +pub(crate) struct TlsConfigBuilder { + cert: Box<dyn Read + Send + Sync>, + key: Box<dyn Read + Send + Sync>, +} + +impl std::fmt::Debug for TlsConfigBuilder { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_struct("TlsConfigBuilder").finish() + } +} + +impl TlsConfigBuilder { + /// Create a new TlsConfigBuilder + pub(crate) fn new() -> TlsConfigBuilder { + TlsConfigBuilder { + key: Box::new(io::empty()), + cert: Box::new(io::empty()), + } + } + + /// sets the Tls key via File Path, returns `TlsConfigError::IoError` if the file cannot be open + pub(crate) fn key_path(mut self, path: impl AsRef<Path>) -> Self { + self.key = Box::new(LazyFile { + path: path.as_ref().into(), + file: None, + }); + self + } + + /// sets the Tls key via bytes slice + pub(crate) fn key(mut self, key: &[u8]) -> Self { + self.key = Box::new(Cursor::new(Vec::from(key))); + self + } + + /// Specify the file path for the TLS certificate to use. + pub(crate) fn cert_path(mut self, path: impl AsRef<Path>) -> Self { + self.cert = Box::new(LazyFile { + path: path.as_ref().into(), + file: None, + }); + self + } + + /// sets the Tls certificate via bytes slice + pub(crate) fn cert(mut self, cert: &[u8]) -> Self { + self.cert = Box::new(Cursor::new(Vec::from(cert))); + self + } + + pub(crate) fn build(mut self) -> Result<ServerConfig, TlsConfigError> { + let mut cert_rdr = BufReader::new(self.cert); + let cert = tokio_rustls::rustls::internal::pemfile::certs(&mut cert_rdr) + .map_err(|()| TlsConfigError::CertParseError)?; + + let key = { + // convert it to Vec<u8> to allow reading it again if key is RSA + let mut key_vec = Vec::new(); + self.key + .read_to_end(&mut key_vec) + .map_err(TlsConfigError::Io)?; + + if key_vec.is_empty() { + return Err(TlsConfigError::EmptyKey); + } + + let mut pkcs8 = tokio_rustls::rustls::internal::pemfile::pkcs8_private_keys( + &mut key_vec.as_slice(), + ) + .map_err(|()| TlsConfigError::Pkcs8ParseError)?; + + if !pkcs8.is_empty() { + pkcs8.remove(0) + } else { + let mut rsa = tokio_rustls::rustls::internal::pemfile::rsa_private_keys( + &mut key_vec.as_slice(), + ) + .map_err(|()| TlsConfigError::RsaParseError)?; + + if !rsa.is_empty() { + rsa.remove(0) + } else { + return Err(TlsConfigError::EmptyKey); + } + } + }; + + let mut config = ServerConfig::new(NoClientAuth::new()); + config + .set_single_cert(cert, key) + .map_err(|err| TlsConfigError::InvalidKey(err))?; + config.set_protocols(&["h2".into(), "http/1.1".into()]); + Ok(config) + } +} + +struct LazyFile { + path: PathBuf, + file: Option<File>, +} + +impl LazyFile { + fn lazy_read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + if self.file.is_none() { + self.file = Some(File::open(&self.path)?); + } + + self.file.as_mut().unwrap().read(buf) + } +} + +impl Read for LazyFile { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + self.lazy_read(buf).map_err(|err| { + let kind = err.kind(); + io::Error::new( + kind, + format!("error reading file ({:?}): {}", self.path.display(), err), + ) + }) + } +} + +impl Transport for TlsStream { + fn remote_addr(&self) -> Option<SocketAddr> { + match self.state { + State::Handshaking(_) => None, + State::Streaming(ref stream) => Some(stream.get_ref().0.remote_addr()), + } + } +} + +enum State { + Handshaking(tokio_rustls::Accept<AddrStream>), + Streaming(tokio_rustls::server::TlsStream<AddrStream>), +} + +// tokio_rustls::server::TlsStream doesn't expose constructor methods, +// so we have to TlsAcceptor::accept and handshake to have access to it +// TlsStream implements AsyncRead/AsyncWrite handshaking tokio_rustls::Accept first +pub(crate) struct TlsStream { + state: State, +} + +impl TlsStream { + fn new(stream: AddrStream, config: Arc<ServerConfig>) -> TlsStream { + let accept = tokio_rustls::TlsAcceptor::from(config).accept(stream); + TlsStream { + state: State::Handshaking(accept), + } + } +} + +impl AsyncRead for TlsStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll<io::Result<usize>> { + let pin = self.get_mut(); + match pin.state { + State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { + Ok(mut stream) => { + let result = Pin::new(&mut stream).poll_read(cx, buf); + pin.state = State::Streaming(stream); + result + } + Err(err) => Poll::Ready(Err(err)), + }, + State::Streaming(ref mut stream) => Pin::new(stream).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for TlsStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll<io::Result<usize>> { + let pin = self.get_mut(); + match pin.state { + State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { + Ok(mut stream) => { + let result = Pin::new(&mut stream).poll_write(cx, buf); + pin.state = State::Streaming(stream); + result + } + Err(err) => Poll::Ready(Err(err)), + }, + State::Streaming(ref mut stream) => Pin::new(stream).poll_write(cx, buf), + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { + match self.state { + State::Handshaking(_) => Poll::Ready(Ok(())), + State::Streaming(ref mut stream) => Pin::new(stream).poll_flush(cx), + } + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { + match self.state { + State::Handshaking(_) => Poll::Ready(Ok(())), + State::Streaming(ref mut stream) => Pin::new(stream).poll_shutdown(cx), + } + } +} + +pub(crate) struct TlsAcceptor { + config: Arc<ServerConfig>, + incoming: AddrIncoming, +} + +impl TlsAcceptor { + pub(crate) fn new(config: ServerConfig, incoming: AddrIncoming) -> TlsAcceptor { + TlsAcceptor { + config: Arc::new(config), + incoming, + } + } +} + +impl Accept for TlsAcceptor { + type Conn = TlsStream; + type Error = io::Error; + + fn poll_accept( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll<Option<Result<Self::Conn, Self::Error>>> { + let pin = self.get_mut(); + match ready!(Pin::new(&mut pin.incoming).poll_accept(cx)) { + Some(Ok(sock)) => Poll::Ready(Some(Ok(TlsStream::new(sock, pin.config.clone())))), + Some(Err(e)) => Poll::Ready(Some(Err(e))), + None => Poll::Ready(None), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_cert_key() { + TlsConfigBuilder::new() + .key_path("examples/tls/key.rsa") + .cert_path("examples/tls/cert.pem") + .build() + .unwrap(); + } + + #[test] + fn bytes_cert_key() { + let key = include_str!("../examples/tls/key.rsa"); + let cert = include_str!("../examples/tls/cert.pem"); + + TlsConfigBuilder::new() + .key(key.as_bytes()) + .cert(cert.as_bytes()) + .build() + .unwrap(); + } +} diff --git a/third_party/rust/warp/src/transport.rs b/third_party/rust/warp/src/transport.rs new file mode 100644 index 0000000000..42f0431dfd --- /dev/null +++ b/third_party/rust/warp/src/transport.rs @@ -0,0 +1,53 @@ +use std::io; +use std::net::SocketAddr; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use hyper::server::conn::AddrStream; +use tokio::io::{AsyncRead, AsyncWrite}; + +pub trait Transport: AsyncRead + AsyncWrite { + fn remote_addr(&self) -> Option<SocketAddr>; +} + +impl Transport for AddrStream { + fn remote_addr(&self) -> Option<SocketAddr> { + Some(self.remote_addr()) + } +} + +pub(crate) struct LiftIo<T>(pub(crate) T); + +impl<T: AsyncRead + Unpin> AsyncRead for LiftIo<T> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll<io::Result<usize>> { + Pin::new(&mut self.get_mut().0).poll_read(cx, buf) + } +} + +impl<T: AsyncWrite + Unpin> AsyncWrite for LiftIo<T> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll<io::Result<usize>> { + Pin::new(&mut self.get_mut().0).poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> { + Pin::new(&mut self.get_mut().0).poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> { + Pin::new(&mut self.get_mut().0).poll_shutdown(cx) + } +} + +impl<T: AsyncRead + AsyncWrite + Unpin> Transport for LiftIo<T> { + fn remote_addr(&self) -> Option<SocketAddr> { + None + } +} diff --git a/third_party/rust/warp/tests/body.rs b/third_party/rust/warp/tests/body.rs new file mode 100644 index 0000000000..8d4bce7d05 --- /dev/null +++ b/third_party/rust/warp/tests/body.rs @@ -0,0 +1,202 @@ +#![deny(warnings)] + +use bytes::Buf; +use futures::TryStreamExt; +use warp::Filter; + +#[tokio::test] +async fn matches() { + let _ = pretty_env_logger::try_init(); + + let concat = warp::body::bytes(); + + let req = warp::test::request().path("/nothing-matches-me"); + + assert!(req.matches(&concat).await); + + let p = warp::path("body"); + let req = warp::test::request().path("/body"); + + let and = p.and(concat); + + assert!(req.matches(&and).await); +} + +#[tokio::test] +async fn server_error_if_taking_body_multiple_times() { + let _ = pretty_env_logger::try_init(); + + let concat = warp::body::bytes(); + let double = concat.and(concat).map(|_, _| warp::reply()); + + let res = warp::test::request().reply(&double).await; + + assert_eq!(res.status(), 500); + assert_eq!(res.body(), "Request body consumed multiple times"); +} + +#[tokio::test] +async fn content_length_limit() { + let _ = pretty_env_logger::try_init(); + + let limit = warp::body::content_length_limit(30).map(warp::reply); + + let res = warp::test::request().reply(&limit).await; + assert_eq!(res.status(), 411, "missing content-length returns 411"); + + let res = warp::test::request() + .header("content-length", "999") + .reply(&limit) + .await; + assert_eq!(res.status(), 413, "over limit returns 413"); + + let res = warp::test::request() + .header("content-length", "2") + .reply(&limit) + .await; + assert_eq!(res.status(), 200, "under limit succeeds"); +} + +#[tokio::test] +async fn json() { + let _ = pretty_env_logger::try_init(); + + let json = warp::body::json::<Vec<i32>>(); + + let req = warp::test::request().body("[1, 2, 3]"); + + let vec = req.filter(&json).await.unwrap(); + assert_eq!(vec, &[1, 2, 3]); + + let req = warp::test::request() + .header("content-type", "application/json") + .body("[3, 2, 1]"); + + let vec = req.filter(&json).await.unwrap(); + assert_eq!(vec, &[3, 2, 1], "matches content-type"); +} + +#[tokio::test] +async fn json_rejects_bad_content_type() { + let _ = pretty_env_logger::try_init(); + + let json = warp::body::json::<Vec<i32>>().map(|_| warp::reply()); + + let req = warp::test::request() + .header("content-type", "text/xml") + .body("[3, 2, 1]"); + + let res = req.reply(&json).await; + assert_eq!( + res.status(), + 415, + "bad content-type should be 415 Unsupported Media Type" + ); +} + +#[tokio::test] +async fn json_invalid() { + let _ = pretty_env_logger::try_init(); + + let json = warp::body::json::<Vec<i32>>().map(|vec| warp::reply::json(&vec)); + + let res = warp::test::request().body("lol#wat").reply(&json).await; + assert_eq!(res.status(), 400); + let prefix = b"Request body deserialize error: "; + assert_eq!(&res.body()[..prefix.len()], prefix); +} + +#[test] +fn json_size_of() { + let json = warp::body::json::<Vec<i32>>(); + assert_eq!(std::mem::size_of_val(&json), 0); +} + +#[tokio::test] +async fn form() { + let _ = pretty_env_logger::try_init(); + + let form = warp::body::form::<Vec<(String, String)>>(); + + let req = warp::test::request().body("foo=bar&baz=quux"); + + let vec = req.filter(&form).await.unwrap(); + let expected = vec![ + ("foo".to_owned(), "bar".to_owned()), + ("baz".to_owned(), "quux".to_owned()), + ]; + assert_eq!(vec, expected); +} + +#[tokio::test] +async fn form_rejects_bad_content_type() { + let _ = pretty_env_logger::try_init(); + + let form = warp::body::form::<Vec<(String, String)>>().map(|_| warp::reply()); + + let req = warp::test::request() + .header("content-type", "application/x-www-form-urlencoded") + .body("foo=bar"); + + let res = req.reply(&form).await; + assert_eq!(res.status(), 200); + + let req = warp::test::request() + .header("content-type", "text/xml") + .body("foo=bar"); + let res = req.reply(&form).await; + assert_eq!( + res.status(), + 415, + "bad content-type should be 415 Unsupported Media Type" + ); +} + +#[tokio::test] +async fn form_allows_charset() { + let _ = pretty_env_logger::try_init(); + + let form = warp::body::form::<Vec<(String, String)>>(); + + let req = warp::test::request() + .header( + "content-type", + "application/x-www-form-urlencoded; charset=utf-8", + ) + .body("foo=bar"); + + let vec = req.filter(&form).await.unwrap(); + let expected = vec![("foo".to_owned(), "bar".to_owned())]; + assert_eq!(vec, expected); +} + +#[tokio::test] +async fn form_invalid() { + let _ = pretty_env_logger::try_init(); + + let form = warp::body::form::<Vec<i32>>().map(|vec| warp::reply::json(&vec)); + + let res = warp::test::request().body("nope").reply(&form).await; + assert_eq!(res.status(), 400); + let prefix = b"Request body deserialize error: "; + assert_eq!(&res.body()[..prefix.len()], prefix); +} + +#[tokio::test] +async fn stream() { + let _ = pretty_env_logger::try_init(); + + let stream = warp::body::stream(); + + let body = warp::test::request() + .body("foo=bar") + .filter(&stream) + .await + .expect("filter() stream"); + + let bufs: Result<Vec<_>, warp::Error> = body.try_collect().await; + let bufs = bufs.unwrap(); + + assert_eq!(bufs.len(), 1); + assert_eq!(bufs[0].bytes(), b"foo=bar"); +} diff --git a/third_party/rust/warp/tests/cookie.rs b/third_party/rust/warp/tests/cookie.rs new file mode 100644 index 0000000000..fadf3e3cea --- /dev/null +++ b/third_party/rust/warp/tests/cookie.rs @@ -0,0 +1,50 @@ +#![deny(warnings)] + +#[tokio::test] +async fn cookie() { + let foo = warp::cookie("foo"); + + let req = warp::test::request().header("cookie", "foo=bar"); + assert_eq!(req.filter(&foo).await.unwrap(), "bar"); + + let req = warp::test::request().header("cookie", "abc=def; foo=baz"); + assert_eq!(req.filter(&foo).await.unwrap(), "baz"); + + let req = warp::test::request().header("cookie", "abc=def"); + assert!(!req.matches(&foo).await); + + let req = warp::test::request().header("cookie", "foobar=quux"); + assert!(!req.matches(&foo).await); +} + +#[tokio::test] +async fn optional() { + let foo = warp::cookie::optional("foo"); + + let req = warp::test::request().header("cookie", "foo=bar"); + assert_eq!(req.filter(&foo).await.unwrap().unwrap(), "bar"); + + let req = warp::test::request().header("cookie", "abc=def; foo=baz"); + assert_eq!(req.filter(&foo).await.unwrap().unwrap(), "baz"); + + let req = warp::test::request().header("cookie", "abc=def"); + assert!(req.matches(&foo).await); + + let req = warp::test::request().header("cookie", "foobar=quux"); + assert!(req.matches(&foo).await); +} + +#[tokio::test] +async fn missing() { + let _ = pretty_env_logger::try_init(); + + let cookie = warp::cookie("foo"); + + let res = warp::test::request() + .header("cookie", "not=here") + .reply(&cookie) + .await; + + assert_eq!(res.status(), 400); + assert_eq!(res.body(), "Missing request cookie \"foo\""); +} diff --git a/third_party/rust/warp/tests/cors.rs b/third_party/rust/warp/tests/cors.rs new file mode 100644 index 0000000000..86ec8a386d --- /dev/null +++ b/third_party/rust/warp/tests/cors.rs @@ -0,0 +1,184 @@ +#![deny(warnings)] +use warp::{http::Method, Filter}; + +#[tokio::test] +async fn allow_methods() { + let cors = warp::cors().allow_methods(&[Method::GET, Method::POST, Method::DELETE]); + + let route = warp::any().map(warp::reply).with(cors); + + let res = warp::test::request() + .method("OPTIONS") + .header("origin", "warp") + .header("access-control-request-method", "DELETE") + .reply(&route) + .await; + + assert_eq!(res.status(), 200); + + let res = warp::test::request() + .method("OPTIONS") + .header("origin", "warp") + .header("access-control-request-method", "PUT") + .reply(&route) + .await; + + assert_eq!(res.status(), 403); +} + +#[tokio::test] +async fn origin_not_allowed() { + let cors = warp::cors() + .allow_methods(&[Method::DELETE]) + .allow_origin("https://hyper.rs"); + + let route = warp::any().map(warp::reply).with(cors); + + let res = warp::test::request() + .method("OPTIONS") + .header("origin", "https://warp.rs") + .header("access-control-request-method", "DELETE") + .reply(&route) + .await; + + assert_eq!(res.status(), 403); + + let res = warp::test::request() + .header("origin", "https://warp.rs") + .header("access-control-request-method", "DELETE") + .reply(&route) + .await; + + assert_eq!(res.status(), 403); +} + +#[tokio::test] +async fn headers_not_exposed() { + let cors = warp::cors() + .allow_any_origin() + .allow_methods(&[Method::GET]); + + let route = warp::any().map(warp::reply).with(cors); + + let res = warp::test::request() + .method("OPTIONS") + .header("origin", "https://warp.rs") + .header("access-control-request-method", "GET") + .reply(&route) + .await; + + assert_eq!( + res.headers().contains_key("access-control-expose-headers"), + false + ); + + let res = warp::test::request() + .method("GET") + .header("origin", "https://warp.rs") + .reply(&route) + .await; + + assert_eq!( + res.headers().contains_key("access-control-expose-headers"), + false + ); +} + +#[tokio::test] +async fn headers_not_allowed() { + let cors = warp::cors() + .allow_methods(&[Method::DELETE]) + .allow_headers(vec!["x-foo"]); + + let route = warp::any().map(warp::reply).with(cors); + + let res = warp::test::request() + .method("OPTIONS") + .header("origin", "https://warp.rs") + .header("access-control-request-headers", "x-bar") + .header("access-control-request-method", "DELETE") + .reply(&route) + .await; + + assert_eq!(res.status(), 403); +} + +#[tokio::test] +async fn success() { + let cors = warp::cors() + .allow_credentials(true) + .allow_headers(vec!["x-foo", "x-bar"]) + .allow_methods(&[Method::POST, Method::DELETE]) + .expose_header("x-header1") + .expose_headers(vec!["x-header2"]) + .max_age(30); + + let route = warp::any().map(warp::reply).with(cors); + + // preflight + let res = warp::test::request() + .method("OPTIONS") + .header("origin", "https://hyper.rs") + .header("access-control-request-headers", "x-bar,x-foo") + .header("access-control-request-method", "DELETE") + .reply(&route) + .await; + assert_eq!(res.status(), 200); + assert_eq!( + res.headers()["access-control-allow-origin"], + "https://hyper.rs" + ); + assert_eq!(res.headers()["access-control-allow-credentials"], "true"); + let allowed_headers = &res.headers()["access-control-allow-headers"]; + assert!(allowed_headers == "x-bar, x-foo" || allowed_headers == "x-foo, x-bar"); + let exposed_headers = &res.headers()["access-control-expose-headers"]; + assert!(exposed_headers == "x-header1, x-header2" || exposed_headers == "x-header2, x-header1"); + assert_eq!(res.headers()["access-control-max-age"], "30"); + let methods = &res.headers()["access-control-allow-methods"]; + assert!( + // HashSet randomly orders these... + methods == "DELETE, POST" || methods == "POST, DELETE", + "access-control-allow-methods: {:?}", + methods, + ); + + // cors request + let res = warp::test::request() + .method("DELETE") + .header("origin", "https://hyper.rs") + .header("x-foo", "hello") + .header("x-bar", "world") + .reply(&route) + .await; + assert_eq!(res.status(), 200); + assert_eq!( + res.headers()["access-control-allow-origin"], + "https://hyper.rs" + ); + assert_eq!(res.headers()["access-control-allow-credentials"], "true"); + assert_eq!(res.headers().get("access-control-max-age"), None); + assert_eq!(res.headers().get("access-control-allow-methods"), None); + let exposed_headers = &res.headers()["access-control-expose-headers"]; + assert!(exposed_headers == "x-header1, x-header2" || exposed_headers == "x-header2, x-header1"); +} + +#[tokio::test] +async fn with_log() { + let cors = warp::cors() + .allow_any_origin() + .allow_methods(&[Method::GET]); + + let route = warp::any() + .map(warp::reply) + .with(cors) + .with(warp::log("cors test")); + + let res = warp::test::request() + .method("OPTIONS") + .header("origin", "warp") + .header("access-control-request-method", "GET") + .reply(&route) + .await; + + assert_eq!(res.status(), 200); +} diff --git a/third_party/rust/warp/tests/ext.rs b/third_party/rust/warp/tests/ext.rs new file mode 100644 index 0000000000..0393d8893d --- /dev/null +++ b/third_party/rust/warp/tests/ext.rs @@ -0,0 +1,28 @@ +#![deny(warnings)] +use warp::Filter; + +#[derive(Clone, Debug, PartialEq)] +struct Ext1(i32); + +#[tokio::test] +async fn set_and_get() { + let ext = warp::ext::get::<Ext1>(); + + let extracted = warp::test::request() + .extension(Ext1(55)) + .filter(&ext) + .await + .unwrap(); + + assert_eq!(extracted, Ext1(55)); +} + +#[tokio::test] +async fn get_missing() { + let ext = warp::ext::get().map(|e: Ext1| e.0.to_string()); + + let res = warp::test::request().reply(&ext).await; + + assert_eq!(res.status(), 500); + assert_eq!(res.body(), "Missing request extension"); +} diff --git a/third_party/rust/warp/tests/filter.rs b/third_party/rust/warp/tests/filter.rs new file mode 100644 index 0000000000..5f69418847 --- /dev/null +++ b/third_party/rust/warp/tests/filter.rs @@ -0,0 +1,158 @@ +#![deny(warnings)] +use std::convert::Infallible; +use warp::Filter; + +#[tokio::test] +async fn flattens_tuples() { + let _ = pretty_env_logger::try_init(); + + let str1 = warp::any().map(|| "warp"); + let true1 = warp::any().map(|| true); + let unit1 = warp::any(); + + // just 1 value + let ext = warp::test::request().filter(&str1).await.unwrap(); + assert_eq!(ext, "warp"); + + // just 1 unit + let ext = warp::test::request().filter(&unit1).await.unwrap(); + assert_eq!(ext, ()); + + // combine 2 values + let and = str1.and(true1); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, ("warp", true)); + + // combine 2 reversed + let and = true1.and(str1); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, (true, "warp")); + + // combine 1 with unit + let and = str1.and(unit1); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, "warp"); + + let and = unit1.and(str1); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, "warp"); + + // combine 3 values + let and = str1.and(str1).and(true1); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, ("warp", "warp", true)); + + // combine 2 with unit + let and = str1.and(unit1).and(true1); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, ("warp", true)); + + let and = unit1.and(str1).and(true1); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, ("warp", true)); + + let and = str1.and(true1).and(unit1); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, ("warp", true)); + + // nested tuples + let str_true_unit = str1.and(true1).and(unit1); + let unit_str_true = unit1.and(str1).and(true1); + + let and = str_true_unit.and(unit_str_true); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, ("warp", true, "warp", true)); + + let and = unit_str_true.and(unit1).and(str1).and(str_true_unit); + let ext = warp::test::request().filter(&and).await.unwrap(); + assert_eq!(ext, ("warp", true, "warp", "warp", true)); +} + +#[tokio::test] +async fn map() { + let _ = pretty_env_logger::try_init(); + + let ok = warp::any().map(warp::reply); + + let req = warp::test::request(); + let resp = req.reply(&ok).await; + assert_eq!(resp.status(), 200); +} + +#[tokio::test] +async fn or() { + let _ = pretty_env_logger::try_init(); + + // Or can be combined with an infallible filter + let a = warp::path::param::<u32>(); + let b = warp::any().map(|| 41i32); + let f = a.or(b); + + let _: Result<_, Infallible> = warp::test::request().filter(&f).await; +} + +#[tokio::test] +async fn or_else() { + let _ = pretty_env_logger::try_init(); + + let a = warp::path::param::<u32>(); + let f = a.or_else(|_| async { Ok::<_, warp::Rejection>((44u32,)) }); + + assert_eq!( + warp::test::request().path("/33").filter(&f).await.unwrap(), + 33, + ); + assert_eq!(warp::test::request().filter(&f).await.unwrap(), 44,); + + // OrElse can be combined with an infallible filter + let a = warp::path::param::<u32>(); + let f = a.or_else(|_| async { Ok::<_, Infallible>((44u32,)) }); + + let _: Result<_, Infallible> = warp::test::request().filter(&f).await; +} + +#[tokio::test] +async fn recover() { + let _ = pretty_env_logger::try_init(); + + let a = warp::path::param::<String>(); + let f = a.recover(|err| async move { Err::<String, _>(err) }); + + // not rejected + let resp = warp::test::request().path("/hi").reply(&f).await; + assert_eq!(resp.status(), 200); + assert_eq!(resp.body(), "hi"); + + // rejected, recovered, re-rejected + let resp = warp::test::request().reply(&f).await; + assert_eq!(resp.status(), 404); + + // Recover can be infallible + let f = a.recover(|_| async move { Ok::<_, Infallible>("shh") }); + + let _: Result<_, Infallible> = warp::test::request().filter(&f).await; +} + +#[tokio::test] +async fn unify() { + let _ = pretty_env_logger::try_init(); + + let a = warp::path::param::<u32>(); + let b = warp::path::param::<u32>(); + let f = a.or(b).unify(); + + let ex = warp::test::request().path("/1").filter(&f).await.unwrap(); + + assert_eq!(ex, 1); +} + +#[should_panic] +#[tokio::test] +async fn nested() { + let f = warp::any().and_then(|| async { + let p = warp::path::param::<u32>(); + warp::test::request().filter(&p).await + }); + + let _ = warp::test::request().filter(&f).await; +} diff --git a/third_party/rust/warp/tests/fs.rs b/third_party/rust/warp/tests/fs.rs new file mode 100644 index 0000000000..738ec06d6d --- /dev/null +++ b/third_party/rust/warp/tests/fs.rs @@ -0,0 +1,228 @@ +#![deny(warnings)] +use std::fs; + +#[tokio::test] +async fn file() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::file("README.md"); + + let req = warp::test::request(); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); + + let contents = fs::read("README.md").expect("fs::read README.md"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.headers()["accept-ranges"], "bytes"); + + let ct = &res.headers()["content-type"]; + assert!( + ct == "text/x-markdown" || ct == "text/markdown", + "content-type is not markdown: {:?}", + ct, + ); + + assert_eq!(res.body(), &*contents); +} + +#[tokio::test] +async fn dir() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::dir("examples"); + + let req = warp::test::request().path("/todos.rs"); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); + + let contents = fs::read("examples/todos.rs").expect("fs::read"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.headers()["content-type"], "text/x-rust"); + assert_eq!(res.headers()["accept-ranges"], "bytes"); + + assert_eq!(res.body(), &*contents); +} + +#[tokio::test] +async fn dir_encoded() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::dir("examples"); + + let req = warp::test::request().path("/todos%2ers"); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); + + let contents = fs::read("examples/todos.rs").expect("fs::read"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + + assert_eq!(res.body(), &*contents); +} + +#[tokio::test] +async fn dir_not_found() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::dir("examples"); + + let req = warp::test::request().path("/definitely-not-found"); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 404); +} + +#[tokio::test] +async fn dir_bad_path() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::dir("examples"); + + let req = warp::test::request().path("/../README.md"); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 404); +} + +#[tokio::test] +async fn dir_bad_encoded_path() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::dir("examples"); + + let req = warp::test::request().path("/%2E%2e/README.md"); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 404); +} + +#[tokio::test] +async fn dir_fallback_index_on_dir() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::dir("examples"); + let req = warp::test::request().path("/dir"); + let res = req.reply(&file).await; + let contents = fs::read("examples/dir/index.html").expect("fs::read"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.status(), 200); + let req = warp::test::request().path("/dir/"); + let res = req.reply(&file).await; + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.status(), 200); +} + +#[tokio::test] +async fn not_modified() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::file("README.md"); + + let req = warp::test::request(); + let body = fs::read("README.md").unwrap(); + let res1 = req.reply(&file).await; + assert_eq!(res1.status(), 200); + assert_eq!(res1.headers()["content-length"], body.len().to_string()); + + // if-modified-since + let res = warp::test::request() + .header("if-modified-since", &res1.headers()["last-modified"]) + .reply(&file) + .await; + assert_eq!(res.headers().get("content-length"), None); + assert_eq!(res.status(), 304); + assert_eq!(res.body(), ""); + + // clearly too old + let res = warp::test::request() + .header("if-modified-since", "Sun, 07 Nov 1994 01:00:00 GMT") + .reply(&file) + .await; + assert_eq!(res.status(), 200); + assert_eq!(res.body(), &body); + assert_eq!(res1.headers()["content-length"], body.len().to_string()); +} + +#[tokio::test] +async fn precondition() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::file("README.md"); + + let req = warp::test::request(); + let res1 = req.reply(&file).await; + assert_eq!(res1.status(), 200); + + // if-unmodified-since + let res = warp::test::request() + .header("if-unmodified-since", &res1.headers()["last-modified"]) + .reply(&file) + .await; + assert_eq!(res.status(), 200); + + // clearly too old + let res = warp::test::request() + .header("if-unmodified-since", "Sun, 07 Nov 1994 01:00:00 GMT") + .reply(&file) + .await; + assert_eq!(res.status(), 412); + assert_eq!(res.body(), ""); +} + +#[tokio::test] +async fn byte_ranges() { + let _ = pretty_env_logger::try_init(); + + let contents = fs::read("README.md").expect("fs::read README.md"); + let file = warp::fs::file("README.md"); + + let res = warp::test::request() + .header("range", "bytes=100-200") + .reply(&file) + .await; + assert_eq!(res.status(), 206); + assert_eq!( + res.headers()["content-range"], + format!("bytes 100-200/{}", contents.len()) + ); + assert_eq!(res.headers()["content-length"], "101"); + assert_eq!(res.body(), &contents[100..=200]); + + // bad range + let res = warp::test::request() + .header("range", "bytes=100-10") + .reply(&file) + .await; + assert_eq!(res.status(), 416); + assert_eq!( + res.headers()["content-range"], + format!("bytes */{}", contents.len()) + ); + assert_eq!(res.headers().get("content-length"), None); + assert_eq!(res.body(), ""); + + // out of range + let res = warp::test::request() + .header("range", "bytes=100-100000") + .reply(&file) + .await; + assert_eq!(res.status(), 416); + assert_eq!( + res.headers()["content-range"], + format!("bytes */{}", contents.len()) + ); + assert_eq!(res.headers().get("content-length"), None); + assert_eq!(res.body(), ""); + + // if-range too old + let res = warp::test::request() + .header("range", "bytes=100-200") + .header("if-range", "Sun, 07 Nov 1994 01:00:00 GMT") + .reply(&file) + .await; + assert_eq!(res.status(), 200); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.headers().get("content-range"), None); +} diff --git a/third_party/rust/warp/tests/header.rs b/third_party/rust/warp/tests/header.rs new file mode 100644 index 0000000000..ea882b44a1 --- /dev/null +++ b/third_party/rust/warp/tests/header.rs @@ -0,0 +1,71 @@ +#![deny(warnings)] +use warp::Filter; + +#[tokio::test] +async fn exact() { + let _ = pretty_env_logger::try_init(); + + let host = warp::header::exact("host", "localhost"); + + let req = warp::test::request().header("host", "localhost"); + + assert!(req.matches(&host).await); + + let req = warp::test::request(); + assert!(!req.matches(&host).await, "header missing"); + + let req = warp::test::request().header("host", "hyper.rs"); + assert!(!req.matches(&host).await, "header value different"); +} + +#[tokio::test] +async fn exact_rejections() { + let _ = pretty_env_logger::try_init(); + + let host = warp::header::exact("host", "localhost").map(warp::reply); + + let res = warp::test::request() + .header("host", "nope") + .reply(&host) + .await; + + assert_eq!(res.status(), 400); + assert_eq!(res.body(), "Invalid request header \"host\""); + + let res = warp::test::request() + .header("not-even-a-host", "localhost") + .reply(&host) + .await; + + assert_eq!(res.status(), 400); + assert_eq!(res.body(), "Missing request header \"host\""); +} + +#[tokio::test] +async fn optional() { + let _ = pretty_env_logger::try_init(); + + let con_len = warp::header::optional::<u64>("content-length"); + + let val = warp::test::request() + .filter(&con_len) + .await + .expect("missing header matches"); + assert_eq!(val, None); + + let val = warp::test::request() + .header("content-length", "5") + .filter(&con_len) + .await + .expect("existing header matches"); + + assert_eq!(val, Some(5)); + + assert!( + !warp::test::request() + .header("content-length", "boom") + .matches(&con_len) + .await, + "invalid optional header still rejects", + ); +} diff --git a/third_party/rust/warp/tests/method.rs b/third_party/rust/warp/tests/method.rs new file mode 100644 index 0000000000..d03126cabd --- /dev/null +++ b/third_party/rust/warp/tests/method.rs @@ -0,0 +1,52 @@ +#![deny(warnings)] +use warp::Filter; + +#[tokio::test] +async fn method() { + let _ = pretty_env_logger::try_init(); + let get = warp::get().map(warp::reply); + + let req = warp::test::request(); + assert!(req.matches(&get).await); + + let req = warp::test::request().method("POST"); + assert!(!req.matches(&get).await); + + let req = warp::test::request().method("POST"); + let resp = req.reply(&get).await; + assert_eq!(resp.status(), 405); +} + +#[tokio::test] +async fn method_not_allowed_trumps_not_found() { + let _ = pretty_env_logger::try_init(); + let get = warp::get().and(warp::path("hello").map(warp::reply)); + let post = warp::post().and(warp::path("bye").map(warp::reply)); + + let routes = get.or(post); + + let req = warp::test::request().method("GET").path("/bye"); + + let resp = req.reply(&routes).await; + // GET was allowed, but only for /hello, so POST returning 405 is fine. + assert_eq!(resp.status(), 405); +} + +#[tokio::test] +async fn bad_request_trumps_method_not_allowed() { + let _ = pretty_env_logger::try_init(); + let get = warp::get() + .and(warp::path("hello")) + .and(warp::header::exact("foo", "bar")) + .map(warp::reply); + let post = warp::post().and(warp::path("bye")).map(warp::reply); + + let routes = get.or(post); + + let req = warp::test::request().method("GET").path("/hello"); + + let resp = req.reply(&routes).await; + // GET was allowed, but header rejects with 400, should not + // assume POST was the appropriate method. + assert_eq!(resp.status(), 400); +} diff --git a/third_party/rust/warp/tests/multipart.rs b/third_party/rust/warp/tests/multipart.rs new file mode 100644 index 0000000000..7702e7361d --- /dev/null +++ b/third_party/rust/warp/tests/multipart.rs @@ -0,0 +1,54 @@ +#![deny(warnings)] +use bytes::BufMut; +use futures::{TryFutureExt, TryStreamExt}; +use warp::{multipart, Filter}; + +#[tokio::test] +async fn form_fields() { + let _ = pretty_env_logger::try_init(); + + let route = multipart::form().and_then(|form: multipart::FormData| { + async { + // Collect the fields into (name, value): (String, Vec<u8>) + let part: Result<Vec<(String, Vec<u8>)>, warp::Rejection> = form + .and_then(|part| { + let name = part.name().to_string(); + let value = part.stream().try_fold(Vec::new(), |mut vec, data| { + vec.put(data); + async move { Ok(vec) } + }); + value.map_ok(move |vec| (name, vec)) + }) + .try_collect() + .await + .map_err(|e| { + panic!("multipart error: {:?}", e); + }); + part + } + }); + + let boundary = "--abcdef1234--"; + let body = format!( + "\ + --{0}\r\n\ + content-disposition: form-data; name=\"foo\"\r\n\r\n\ + bar\r\n\ + --{0}--\r\n\ + ", + boundary + ); + + let req = warp::test::request() + .method("POST") + .header("content-length", body.len()) + .header( + "content-type", + format!("multipart/form-data; boundary={}", boundary), + ) + .body(body); + + let vec = req.filter(&route).await.unwrap(); + assert_eq!(&vec[0].0, "foo"); + assert_eq!(&vec[0].1, b"bar"); +} diff --git a/third_party/rust/warp/tests/path.rs b/third_party/rust/warp/tests/path.rs new file mode 100644 index 0000000000..5390e51efe --- /dev/null +++ b/third_party/rust/warp/tests/path.rs @@ -0,0 +1,386 @@ +#![deny(warnings)] +#[macro_use] +extern crate warp; + +use futures::future; +use warp::Filter; + +#[tokio::test] +async fn path() { + let _ = pretty_env_logger::try_init(); + + let foo = warp::path("foo"); + let bar = warp::path(String::from("bar")); + let foo_bar = foo.and(bar.clone()); + + // /foo + let foo_req = || warp::test::request().path("/foo"); + + assert!(foo_req().matches(&foo).await); + assert!(!foo_req().matches(&bar).await); + assert!(!foo_req().matches(&foo_bar).await); + + // /foo/bar + let foo_bar_req = || warp::test::request().path("/foo/bar"); + + assert!(foo_bar_req().matches(&foo).await); + assert!(!foo_bar_req().matches(&bar).await); + assert!(foo_bar_req().matches(&foo_bar).await); +} + +#[tokio::test] +async fn param() { + let _ = pretty_env_logger::try_init(); + + let num = warp::path::param::<u32>(); + + let req = warp::test::request().path("/321"); + assert_eq!(req.filter(&num).await.unwrap(), 321); + + let s = warp::path::param::<String>(); + + let req = warp::test::request().path("/warp"); + assert_eq!(req.filter(&s).await.unwrap(), "warp"); + + // u32 doesn't extract a non-int + let req = warp::test::request().path("/warp"); + assert!(!req.matches(&num).await); + + let combo = num.map(|n| n + 5).and(s); + + let req = warp::test::request().path("/42/vroom"); + assert_eq!(req.filter(&combo).await.unwrap(), (47, "vroom".to_string())); + + // empty segments never match + let req = warp::test::request(); + assert!( + !req.matches(&s).await, + "param should never match an empty segment" + ); +} + +#[tokio::test] +async fn end() { + let _ = pretty_env_logger::try_init(); + + let foo = warp::path("foo"); + let end = warp::path::end(); + let foo_end = foo.and(end); + + assert!( + warp::test::request().path("/").matches(&end).await, + "end() matches /" + ); + + assert!( + warp::test::request() + .path("http://localhost:1234") + .matches(&end) + .await, + "end() matches /" + ); + + assert!( + warp::test::request() + .path("http://localhost:1234?q=2") + .matches(&end) + .await, + "end() matches empty path" + ); + + assert!( + warp::test::request() + .path("localhost:1234") + .matches(&end) + .await, + "end() matches authority-form" + ); + + assert!( + !warp::test::request().path("/foo").matches(&end).await, + "end() doesn't match /foo" + ); + + assert!( + warp::test::request().path("/foo").matches(&foo_end).await, + "path().and(end()) matches /foo" + ); + + assert!( + warp::test::request().path("/foo/").matches(&foo_end).await, + "path().and(end()) matches /foo/" + ); +} + +#[tokio::test] +async fn tail() { + let tail = warp::path::tail(); + + // matches full path + let ex = warp::test::request() + .path("/42/vroom") + .filter(&tail) + .await + .unwrap(); + assert_eq!(ex.as_str(), "42/vroom"); + + // matches index + let ex = warp::test::request().path("/").filter(&tail).await.unwrap(); + assert_eq!(ex.as_str(), ""); + + // doesn't include query + let ex = warp::test::request() + .path("/foo/bar?baz=quux") + .filter(&tail) + .await + .unwrap(); + assert_eq!(ex.as_str(), "foo/bar"); + + // doesn't include previously matched prefix + let and = warp::path("foo").and(tail); + let ex = warp::test::request() + .path("/foo/bar") + .filter(&and) + .await + .unwrap(); + assert_eq!(ex.as_str(), "bar"); + + // sets unmatched path index to end + let m = tail.and(warp::path("foo")); + assert!(!warp::test::request().path("/foo/bar").matches(&m).await); + + let m = tail.and(warp::path::end()); + assert!(warp::test::request().path("/foo/bar").matches(&m).await); +} + +#[tokio::test] +async fn or() { + let _ = pretty_env_logger::try_init(); + + // /foo/bar OR /foo/baz + let foo = warp::path("foo"); + let bar = warp::path("bar"); + let baz = warp::path("baz"); + let p = foo.and(bar.or(baz)); + + // /foo/bar + let req = warp::test::request().path("/foo/bar"); + + assert!(req.matches(&p).await); + + // /foo/baz + let req = warp::test::request().path("/foo/baz"); + + assert!(req.matches(&p).await); + + // deeper nested ORs + // /foo/bar/baz OR /foo/baz/bar OR /foo/bar/bar + let p = foo + .and(bar.and(baz).map(|| panic!("shouldn't match"))) + .or(foo.and(baz.and(bar)).map(|| panic!("shouldn't match"))) + .or(foo.and(bar.and(bar))); + + // /foo/baz + let req = warp::test::request().path("/foo/baz/baz"); + assert!(!req.matches(&p).await); + + // /foo/bar/bar + let req = warp::test::request().path("/foo/bar/bar"); + assert!(req.matches(&p).await); +} + +#[tokio::test] +async fn or_else() { + let _ = pretty_env_logger::try_init(); + + let foo = warp::path("foo"); + let bar = warp::path("bar"); + + let p = foo.and(bar.or_else(|_| future::ok::<_, std::convert::Infallible>(()))); + + // /foo/bar + let req = warp::test::request().path("/foo/nope"); + + assert!(req.matches(&p).await); +} + +#[tokio::test] +async fn path_macro() { + let _ = pretty_env_logger::try_init(); + + let req = warp::test::request().path("/foo/bar"); + let p = path!("foo" / "bar"); + assert!(req.matches(&p).await); + + let req = warp::test::request().path("/foo/bar"); + let p = path!(String / "bar"); + assert_eq!(req.filter(&p).await.unwrap(), "foo"); + + let req = warp::test::request().path("/foo/bar"); + let p = path!("foo" / String); + assert_eq!(req.filter(&p).await.unwrap(), "bar"); + + // Requires path end + + let req = warp::test::request().path("/foo/bar/baz"); + let p = path!("foo" / "bar"); + assert!(!req.matches(&p).await); + + let req = warp::test::request().path("/foo/bar/baz"); + let p = path!("foo" / "bar").and(warp::path("baz")); + assert!(!req.matches(&p).await); + + // Prefix syntax + + let req = warp::test::request().path("/foo/bar/baz"); + let p = path!("foo" / "bar" / ..); + assert!(req.matches(&p).await); + + let req = warp::test::request().path("/foo/bar/baz"); + let p = path!("foo" / "bar" / ..).and(warp::path!("baz")); + assert!(req.matches(&p).await); +} + +#[tokio::test] +async fn full_path() { + let full_path = warp::path::full(); + + let foo = warp::path("foo"); + let bar = warp::path("bar"); + let param = warp::path::param::<u32>(); + + // matches full request path + let ex = warp::test::request() + .path("/42/vroom") + .filter(&full_path) + .await + .unwrap(); + assert_eq!(ex.as_str(), "/42/vroom"); + + // matches index + let ex = warp::test::request() + .path("/") + .filter(&full_path) + .await + .unwrap(); + assert_eq!(ex.as_str(), "/"); + + // does not include query + let ex = warp::test::request() + .path("/foo/bar?baz=quux") + .filter(&full_path) + .await + .unwrap(); + assert_eq!(ex.as_str(), "/foo/bar"); + + // includes previously matched prefix + let and = foo.and(full_path); + let ex = warp::test::request() + .path("/foo/bar") + .filter(&and) + .await + .unwrap(); + assert_eq!(ex.as_str(), "/foo/bar"); + + // includes following matches + let and = full_path.and(foo); + let ex = warp::test::request() + .path("/foo/bar") + .filter(&and) + .await + .unwrap(); + assert_eq!(ex.as_str(), "/foo/bar"); + + // includes previously matched param + let and = foo.and(param).and(full_path); + let (_, ex) = warp::test::request() + .path("/foo/123") + .filter(&and) + .await + .unwrap(); + assert_eq!(ex.as_str(), "/foo/123"); + + // does not modify matching + let m = full_path.and(foo).and(bar); + assert!(warp::test::request().path("/foo/bar").matches(&m).await); +} + +#[tokio::test] +async fn peek() { + let peek = warp::path::peek(); + + let foo = warp::path("foo"); + let bar = warp::path("bar"); + let param = warp::path::param::<u32>(); + + // matches full request path + let ex = warp::test::request() + .path("/42/vroom") + .filter(&peek) + .await + .unwrap(); + assert_eq!(ex.as_str(), "42/vroom"); + + // matches index + let ex = warp::test::request().path("/").filter(&peek).await.unwrap(); + assert_eq!(ex.as_str(), ""); + + // does not include query + let ex = warp::test::request() + .path("/foo/bar?baz=quux") + .filter(&peek) + .await + .unwrap(); + assert_eq!(ex.as_str(), "foo/bar"); + + // does not include previously matched prefix + let and = foo.and(peek); + let ex = warp::test::request() + .path("/foo/bar") + .filter(&and) + .await + .unwrap(); + assert_eq!(ex.as_str(), "bar"); + + // includes following matches + let and = peek.and(foo); + let ex = warp::test::request() + .path("/foo/bar") + .filter(&and) + .await + .unwrap(); + assert_eq!(ex.as_str(), "foo/bar"); + + // does not include previously matched param + let and = foo.and(param).and(peek); + let (_, ex) = warp::test::request() + .path("/foo/123") + .filter(&and) + .await + .unwrap(); + assert_eq!(ex.as_str(), ""); + + // does not modify matching + let and = peek.and(foo).and(bar); + assert!(warp::test::request().path("/foo/bar").matches(&and).await); +} + +#[tokio::test] +async fn peek_segments() { + let peek = warp::path::peek(); + + // matches full request path + let ex = warp::test::request() + .path("/42/vroom") + .filter(&peek) + .await + .unwrap(); + + assert_eq!(ex.segments().collect::<Vec<_>>(), &["42", "vroom"]); + + // matches index + let ex = warp::test::request().path("/").filter(&peek).await.unwrap(); + + let segs = ex.segments().collect::<Vec<_>>(); + assert_eq!(segs, Vec::<&str>::new()); +} diff --git a/third_party/rust/warp/tests/query.rs b/third_party/rust/warp/tests/query.rs new file mode 100644 index 0000000000..c93885acb5 --- /dev/null +++ b/third_party/rust/warp/tests/query.rs @@ -0,0 +1,123 @@ +#![deny(warnings)] + +use serde_derive::Deserialize; +use std::collections::HashMap; +use warp::Filter; + +#[tokio::test] +async fn query() { + let as_map = warp::query::<HashMap<String, String>>(); + + let req = warp::test::request().path("/?foo=bar&baz=quux"); + + let extracted = req.filter(&as_map).await.unwrap(); + assert_eq!(extracted["foo"], "bar"); + assert_eq!(extracted["baz"], "quux"); +} + +#[tokio::test] +async fn query_struct() { + let as_struct = warp::query::<MyArgs>(); + + let req = warp::test::request().path("/?foo=bar&baz=quux"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyArgs { + foo: Some("bar".into()), + baz: Some("quux".into()) + } + ); +} + +#[tokio::test] +async fn empty_query_struct() { + let as_struct = warp::query::<MyArgs>(); + + let req = warp::test::request().path("/?"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyArgs { + foo: None, + baz: None + } + ); +} + +#[tokio::test] +async fn missing_query_struct() { + let as_struct = warp::query::<MyArgs>(); + + let req = warp::test::request().path("/"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyArgs { + foo: None, + baz: None + } + ); +} + +#[derive(Deserialize, Debug, Eq, PartialEq)] +struct MyArgs { + foo: Option<String>, + baz: Option<String>, +} + +#[tokio::test] +async fn required_query_struct() { + let as_struct = warp::query::<MyRequiredArgs>(); + + let req = warp::test::request().path("/?foo=bar&baz=quux"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyRequiredArgs { + foo: "bar".into(), + baz: "quux".into() + } + ); +} + +#[tokio::test] +async fn missing_required_query_struct_partial() { + let as_struct = warp::query::<MyRequiredArgs>(); + + let req = warp::test::request().path("/?foo=something"); + + let extracted = req.filter(&as_struct).await; + assert!(extracted.is_err()) +} + +#[tokio::test] +async fn missing_required_query_struct_no_query() { + let as_struct = warp::query::<MyRequiredArgs>().map(|_| warp::reply()); + + let req = warp::test::request().path("/"); + + let res = req.reply(&as_struct).await; + assert_eq!(res.status(), 400); + assert_eq!(res.body(), "Invalid query string"); +} + +#[derive(Deserialize, Debug, Eq, PartialEq)] +struct MyRequiredArgs { + foo: String, + baz: String, +} + +#[tokio::test] +async fn raw_query() { + let as_raw = warp::query::raw(); + + let req = warp::test::request().path("/?foo=bar&baz=quux"); + + let extracted = req.filter(&as_raw).await.unwrap(); + assert_eq!(extracted, "foo=bar&baz=quux".to_owned()); +} diff --git a/third_party/rust/warp/tests/redirect.rs b/third_party/rust/warp/tests/redirect.rs new file mode 100644 index 0000000000..5754a9cdf6 --- /dev/null +++ b/third_party/rust/warp/tests/redirect.rs @@ -0,0 +1,13 @@ +#![deny(warnings)] +use warp::{http::Uri, Filter}; + +#[tokio::test] +async fn redirect_uri() { + let over_there = warp::any().map(|| warp::redirect(Uri::from_static("/over-there"))); + + let req = warp::test::request(); + let resp = req.reply(&over_there).await; + + assert_eq!(resp.status(), 301); + assert_eq!(resp.headers()["location"], "/over-there"); +} diff --git a/third_party/rust/warp/tests/reply_with.rs b/third_party/rust/warp/tests/reply_with.rs new file mode 100644 index 0000000000..0766924b41 --- /dev/null +++ b/third_party/rust/warp/tests/reply_with.rs @@ -0,0 +1,64 @@ +#![deny(warnings)] +use warp::http::header::{HeaderMap, HeaderValue}; +use warp::Filter; + +#[tokio::test] +async fn header() { + let header = warp::reply::with::header("foo", "bar"); + + let no_header = warp::any().map(warp::reply).with(&header); + + let req = warp::test::request(); + let resp = req.reply(&no_header).await; + assert_eq!(resp.headers()["foo"], "bar"); + + let prev_header = warp::reply::with::header("foo", "sean"); + let yes_header = warp::any().map(warp::reply).with(prev_header).with(header); + + let req = warp::test::request(); + let resp = req.reply(&yes_header).await; + assert_eq!(resp.headers()["foo"], "bar", "replaces header"); +} + +#[tokio::test] +async fn headers() { + let mut headers = HeaderMap::new(); + headers.insert("server", HeaderValue::from_static("warp")); + headers.insert("foo", HeaderValue::from_static("bar")); + + let headers = warp::reply::with::headers(headers); + + let no_header = warp::any().map(warp::reply).with(&headers); + + let req = warp::test::request(); + let resp = req.reply(&no_header).await; + assert_eq!(resp.headers()["foo"], "bar"); + assert_eq!(resp.headers()["server"], "warp"); + + let prev_header = warp::reply::with::header("foo", "sean"); + let yes_header = warp::any().map(warp::reply).with(prev_header).with(headers); + + let req = warp::test::request(); + let resp = req.reply(&yes_header).await; + assert_eq!(resp.headers()["foo"], "bar", "replaces header"); +} + +#[tokio::test] +async fn default_header() { + let def_header = warp::reply::with::default_header("foo", "bar"); + + let no_header = warp::any().map(warp::reply).with(&def_header); + + let req = warp::test::request(); + let resp = req.reply(&no_header).await; + + assert_eq!(resp.headers()["foo"], "bar"); + + let header = warp::reply::with::header("foo", "sean"); + let yes_header = warp::any().map(warp::reply).with(header).with(def_header); + + let req = warp::test::request(); + let resp = req.reply(&yes_header).await; + + assert_eq!(resp.headers()["foo"], "sean", "doesn't replace header"); +} diff --git a/third_party/rust/warp/tests/ws.rs b/third_party/rust/warp/tests/ws.rs new file mode 100644 index 0000000000..b53fd2f16f --- /dev/null +++ b/third_party/rust/warp/tests/ws.rs @@ -0,0 +1,182 @@ +#![deny(warnings)] + +use futures::{FutureExt, SinkExt, StreamExt}; +use warp::ws::Message; +use warp::Filter; + +#[tokio::test] +async fn upgrade() { + let _ = pretty_env_logger::try_init(); + + let route = warp::ws().map(|ws: warp::ws::Ws| ws.on_upgrade(|_| async {})); + + // From https://tools.ietf.org/html/rfc6455#section-1.2 + let key = "dGhlIHNhbXBsZSBub25jZQ=="; + let accept = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="; + + let resp = warp::test::request() + .header("connection", "upgrade") + .header("upgrade", "websocket") + .header("sec-websocket-version", "13") + .header("sec-websocket-key", key) + .reply(&route) + .await; + + assert_eq!(resp.status(), 101); + assert_eq!(resp.headers()["connection"], "upgrade"); + assert_eq!(resp.headers()["upgrade"], "websocket"); + assert_eq!(resp.headers()["sec-websocket-accept"], accept); + + let resp = warp::test::request() + .header("connection", "keep-alive, Upgrade") + .header("upgrade", "Websocket") + .header("sec-websocket-version", "13") + .header("sec-websocket-key", key) + .reply(&route) + .await; + + assert_eq!(resp.status(), 101); +} + +#[tokio::test] +async fn fail() { + let _ = pretty_env_logger::try_init(); + + let route = warp::any().map(warp::reply); + + warp::test::ws() + .handshake(route) + .await + .expect_err("handshake non-websocket route should fail"); +} + +#[tokio::test] +async fn text() { + let _ = pretty_env_logger::try_init(); + + let mut client = warp::test::ws() + .handshake(ws_echo()) + .await + .expect("handshake"); + + client.send_text("hello warp").await; + + let msg = client.recv().await.expect("recv"); + assert_eq!(msg.to_str(), Ok("hello warp")); +} + +#[tokio::test] +async fn binary() { + let _ = pretty_env_logger::try_init(); + + let mut client = warp::test::ws() + .handshake(ws_echo()) + .await + .expect("handshake"); + + client.send(warp::ws::Message::binary(&b"bonk"[..])).await; + let msg = client.recv().await.expect("recv"); + assert!(msg.is_binary()); + assert_eq!(msg.as_bytes(), &b"bonk"[..]); +} + +#[tokio::test] +async fn send_ping() { + let _ = pretty_env_logger::try_init(); + + let filter = warp::ws().map(|ws: warp::ws::Ws| { + ws.on_upgrade(|mut websocket| { + async move { + websocket.send(Message::ping("srv")).await.unwrap(); + // assume the client will pong back + let msg = websocket.next().await.expect("item").expect("ok"); + assert!(msg.is_pong()); + assert_eq!(msg.as_bytes(), &b"srv"[..]); + } + }) + }); + + let mut client = warp::test::ws().handshake(filter).await.expect("handshake"); + + let msg = client.recv().await.expect("recv"); + assert!(msg.is_ping()); + assert_eq!(msg.as_bytes(), &b"srv"[..]); + + client.recv_closed().await.expect("closed"); +} + +#[tokio::test] +async fn echo_pings() { + let _ = pretty_env_logger::try_init(); + + let mut client = warp::test::ws() + .handshake(ws_echo()) + .await + .expect("handshake"); + + client.send(Message::ping("clt")).await; + + // tungstenite sends the PONG first + let msg = client.recv().await.expect("recv"); + assert!(msg.is_pong()); + assert_eq!(msg.as_bytes(), &b"clt"[..]); + + // and then `ws_echo` sends us back the same PING + let msg = client.recv().await.expect("recv"); + assert!(msg.is_ping()); + assert_eq!(msg.as_bytes(), &b"clt"[..]); + + // and then our client would have sent *its* PONG + // and `ws_echo` would send *that* back too + let msg = client.recv().await.expect("recv"); + assert!(msg.is_pong()); + assert_eq!(msg.as_bytes(), &b"clt"[..]); +} + +#[tokio::test] +async fn closed() { + let _ = pretty_env_logger::try_init(); + + let route = + warp::ws().map(|ws: warp::ws::Ws| ws.on_upgrade(|websocket| websocket.close().map(|_| ()))); + + let mut client = warp::test::ws().handshake(route).await.expect("handshake"); + + client.recv_closed().await.expect("closed"); +} + +#[tokio::test] +async fn limit_message_size() { + let _ = pretty_env_logger::try_init(); + + let echo = warp::ws().map(|ws: warp::ws::Ws| { + ws.max_message_size(1024).on_upgrade(|websocket| { + // Just echo all messages back... + let (tx, rx) = websocket.split(); + rx.forward(tx).map(|result| { + assert!(result.is_err()); + assert_eq!( + format!("{}", result.unwrap_err()).as_str(), + "Space limit exceeded: Message too big: 0 + 1025 > 1024" + ); + }) + }) + }); + let mut client = warp::test::ws().handshake(echo).await.expect("handshake"); + + client.send(warp::ws::Message::binary(vec![0; 1025])).await; + client.send_text("hello warp").await; + assert!(client.recv().await.is_err()); +} + +fn ws_echo() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Copy { + warp::ws().map(|ws: warp::ws::Ws| { + ws.on_upgrade(|websocket| { + // Just echo all messages back... + let (tx, rx) = websocket.split(); + rx.inspect(|i| log::debug!("ws recv: {:?}", i)) + .forward(tx) + .map(|_| ()) + }) + }) +} |