diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/warp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/warp')
102 files changed, 15413 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..2c8f120eee --- /dev/null +++ b/third_party/rust/warp/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{".github/FUNDING.yml":"e65635279e36972a4cecce26f3bb8115c09fd6d5d3e3e79d11a973877d4600a8",".github/ISSUE_TEMPLATE/bug_report.md":"2d4a83ec915a1de017f74266da90c71bb522c8f3c16f13d19c6f60b214461ca7",".github/ISSUE_TEMPLATE/config.yml":"ca96483f1f9c2a83e2beb3025f6b9965928f4ce0774b36f38cd9e55c5cd7c2f2",".github/ISSUE_TEMPLATE/feature_request.md":"aa20f06cc398fde7b9ce6be01f4d39f7c5b6e8efbaf40b229983f994d5f5e49e",".github/workflows/ci.yml":"f40e040de6bc5d78c3e60102d3aaf9551f5c8e8eea24e129aed544a20500595e","CHANGELOG.md":"61f8420ef01d4b6d88ed4768cb1ed368a74107499b112f7c12e30bf66089e8f1","Cargo.toml":"c297b5e2bbf7319954bf15f36e97c22a96649d40c746ee849cefd17b4deb3df0","LICENSE":"f9e46cb84d28dce0b6c08e84496cde893896a2816f4ffaac23e6552b7daf20f0","README.md":"d0cb27a8b2be3f852a5d1e0dca6b114a204936ad4ba76c7ec7424f4b0c88a1c3","examples/README.md":"aa8613c11e1528037a4b2517c8261eaee531f2d0a109bd9784a3f2633e0147b8","examples/autoreload.rs":"c4ca42f4e44917e69da0ee27553d8ba159c0bc50b9930d30caa07a3bb945e721","examples/body.rs":"57eb99b3a59c33474c5d5c7c22964bea66c663f525c755924ed75eca8fba4aa1","examples/compression.rs":"3bc90f12200fed9374cdd7f23f2adae770d8a283740e3441731571dbb5e2217f","examples/custom_methods.rs":"40cb0d2cf0ec43f55bd61863ec552d526ca7d94195624d4d19e789412a85b17c","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":"d5f8a58de075663f20686590837d6dea4355df754e9c1425caaf1c5547b77f9d","examples/handlebars_template.rs":"cf13236fbe9ad294902c0da0c21eda58f9e498b9423ab2a34d54052102a859ba","examples/headers.rs":"651cd6425219d1d09cfb4f1114fefc2dff4435070b90f7c1d1696c95c96913b5","examples/hello.rs":"9ca9f197e2365c21729bbe74fe07590c615aaf87f07aece51b5640c97a453196","examples/query_string.rs":"be67abd54d67e8a16406957360b33349f4880cd45953b879e63fa6fe2a440515","examples/rejections.rs":"a6b811a70378b245afa9179d5829553e1ae2a0ec3fe6ec7288058a1aa789e1c1","examples/returning.rs":"e19e8df9ec34d7eeb60f6053e5b1f51043159dccb7f5e7148b69597eff2061ea","examples/routing.rs":"7663edfff0bf04d4314453570aa7427dd0a5ffcfb3f4a116852afb6c2d13af76","examples/sse.rs":"d1bebebd1403d601d23231cf9520fdf565aabde8352698675966877d27911952","examples/sse_chat.rs":"9455fc09f7808cc9581d194347186c0447090df9a793f3780c75912ba84a42b6","examples/tls.rs":"4c704782caa31e6b147b0fe969935ab9ea256fad52e74093fb77ef9c4f32902f","examples/tls/cert.pem":"6b974e5654edca3664916613c9f932a64990cff3f604df5369275c35f64d36a2","examples/tls/key.rsa":"ffe615f53e98682bc470ae6fa964ce34a383b4509ae7bb9a7ee2ece1b1bdc7e3","examples/todos.rs":"5d208cfb13cafd482c238fdec54666714717f8ded7d3c248038f166beee6a264","examples/tracing.rs":"2d47c96e4ac041dfd6c9b6d2a17fc9af1bd72b69c405d184ec07085491c6ac0e","examples/unix_socket.rs":"9bebb31119df075001dcdd51a2be6ad5c1d94741b5814d0016e3ed7229154f14","examples/websockets.rs":"d7eeb5dd7b936248d7fb3fd56dc4d51aa8a07d7403a82bd11b4a69d7c31d3672","examples/websockets_chat.rs":"42c281d3c189416ab54d34253e2d93bcb3f19c0d93d30feb9b372c5f9433a1dd","examples/wrapping.rs":"c0ad14ddbdd531023df48a96ddcf680eecb741a941c816cca6c6796de9ff146f","src/error.rs":"a3b8f7ba1bb3353c0894c9bb63edd38525a120067135775b650e5bfc36046e03","src/filter/and.rs":"519cef248f5e7166bb62e0bdb3ef5194997b8a9b3fa8ded057748639e0da5de5","src/filter/and_then.rs":"f3903d6473483355d010b2eeafc965d2998bdaaa0a47c090dc7681710ec2e4fb","src/filter/boxed.rs":"9f41f214da36b673ba6234cfccba597acafbb40a02181304f9f379dc7d7643b1","src/filter/map.rs":"7b239aaf5dc129b380530a338d091bb68ad336cfcb74d89a2cbe1ea2560ab155","src/filter/map_err.rs":"37b4a0c61de36a531e6b1fef588ce87b17d2d312ace010fb4ecd2406d4936231","src/filter/mod.rs":"56bb18280f7ec89a824a5214458c2e6c5f90573c6327f096aa239bf49e9e7496","src/filter/or.rs":"0bc2d2c6dad60fc0f60609cd884db08e3a468de5d08f068b87221def3e353142","src/filter/or_else.rs":"d1c18b3d09c8d71f6dbbf1f70dba26ec623222a5797bcbb2bef49239f2447b01","src/filter/recover.rs":"9ba28ad03861b57eee7c6ecb53ad57dfc8c9f1bf321fcc5c7f0a9c3bd07c04bb","src/filter/service.rs":"8f2ba4e414655e4800f85daa090ba0f4758943ede23ab0ab09c068608d4fe14b","src/filter/then.rs":"d5c568c47cba20f8683908a194f9451ff4265f4c76225452decb155d522e8b14","src/filter/unify.rs":"41616e079082f959444c05325176f5d761850b023a73601ff259d6a897247b9a","src/filter/untuple_one.rs":"c88f292545646ad7bd11b493b24ad1e51a926bfbe45998281e735fa133dbe6aa","src/filter/wrap.rs":"a5783e8451db2818da5a7b010390d5d77865590fcbb3a2a6ad0a1f5f4c158fae","src/filters/addr.rs":"52d3336a046620e645feb2e15cb743418889a3a7d5d5337ac5cd806dd0ade4ed","src/filters/any.rs":"ad57b333abad579c3822338bd44035f44902e5a9bbf167fe44f057cffb723f07","src/filters/body.rs":"565758830fa5accc63825f32d80e87e70d2b7a0f7eb3ad57863b97c4565fde9e","src/filters/compression.rs":"2e6428e546791f332be8fcb42b6a4c7df2c389f4d002848a206a0f0ee2ed5cd1","src/filters/cookie.rs":"09082d991cf6a7fcf372dd83611234d1250711f1ff2e5d6392b7241820a6fc95","src/filters/cors.rs":"7fd739d8dea6a3e7f0b2c8640907ec4a01142f972933065eb41244f1638ac6b7","src/filters/ext.rs":"76dd310d51d29fa2531a21a947bfdb7fda744b9a71fd8c8104e428f1f6b8f735","src/filters/fs.rs":"3f047ad611e347e031ef00c7ee4da6d8a0b1366362256a5c922cecaaab341764","src/filters/header.rs":"b02e37f47ffa86a53097127759a16fdbc0e09c5d02f961a15f5972a59a96fae9","src/filters/host.rs":"5cb5e207e934173b3299a30f427c19ae112dbd7613a770ceeda51ba0f38d845a","src/filters/log.rs":"29e0e243e0084b7d612b05369eb48482e957a207d8c651629bf217d05f747beb","src/filters/method.rs":"0a64e1c1c14ce82511cd99a489ff13eebaaacce04394a5eb282e5c43ff5a6e92","src/filters/mod.rs":"bd6d0af8fec10cf1cbf120a1dd4b271a129bfb09d4071526e865cb9e2d619a8b","src/filters/multipart.rs":"386b3c1ce184e702d47a480bb374fbf5eb48fb8942d2e80ece17e6e4083cf453","src/filters/path.rs":"d087f6f2f7cb869721a66ab16a5d52d69a0312a28ba4b349775a3f810b6a7e48","src/filters/query.rs":"7985b040b6f38252355d2d369b384d66bb4b3978012c47f2105d0b62e5221d3b","src/filters/reply.rs":"6102b63e142dfe2c55bdd37f1b3e9d25852a1ed7f1ec2ec4fec91f478cf672a4","src/filters/sse.rs":"7ce9362323a2d7a481fc72f429c5a45e433505645d29aa7672a21a21727173cf","src/filters/trace.rs":"c037cf03740d00fe6c67ad47f517452d242b623e8bb1a3ad5625e2f4009ecd04","src/filters/ws.rs":"8e0d8d5d3eead2676e64135f6f80af64c8dc2c07e2824f9f6b4c72da1094cff3","src/generic.rs":"a7afd6804059c16c4397028c85120bf1b29ce86bbfa134e3670291764a3ef257","src/lib.rs":"87cb3bec87439a1e90e4bd6bd5b86008c5334fe6e3bd616584ae9fe03cd51d5b","src/redirect.rs":"ab104a9c48e08ffeb93bff6c5e81174a378f6ee98b2dadd61e038641da5cf976","src/reject.rs":"63f56d59a0c530cea2dfbdf30838b15350bb5e460ae97a3559d74ee34cd9659d","src/reply.rs":"b45d7740903a47254d23ba23e1c72d1ed578515737a292a61ed7b03fba6976c0","src/route.rs":"1a246b76d481ac0184511d342ca0dae0754384221ef37445bcee28ee7cd40b06","src/server.rs":"f06cab83c7fe242a095df41f932c7f2916e44434085d7d0dd3b1a3d8b136a3fc","src/service.rs":"4564ec95e98a2314f73df24582ca8f6ec96cc8eda90fb5a5d1d83a9d5c841b86","src/test.rs":"b68c006fe0b5fc1f2f3ebef3e4c3423588fcad4c12c16cee6245c82e09fb0b30","src/tls.rs":"20029156fea45d6e090ccf5c18f24434d1b3541880470bf6147ed0c9197df2d8","src/transport.rs":"6940d28bd3b1720e6e86676d46ace2855c78cd302ef167bad955f5367b022a67","tests/addr.rs":"2946596c8c5eb71dbb7339492d1805d12b5f9941b9d855c3dc6bd17597687296","tests/body.rs":"595e25b2aabbdd6619da44a4b8cbb5c4f580c56450e7c614b8ac197bcbe32a29","tests/cookie.rs":"1bb5cdacddca5dc7028796d06bd1b7bf599bc81759a904215b61beda735bf2d1","tests/cors.rs":"9ee68d8212fdbd171bfa2636466db425cf7d7de32b6afdf387b2ba21569aca4f","tests/ext.rs":"93d6527288f71ee20b63f6a47f616f055735373b0f203f0863f27c2b65fd8991","tests/filter.rs":"76c05031f1e0d6271ff2ac7eb596b4c97ffd0fe93d4d49af4783cc4d862dae25","tests/fs.rs":"2656bdcfe76f90e30e044c6c464028e96d475464bee8acb2d5c46a3b76ac03fe","tests/header.rs":"78d7b4fd80025694cde65010cf04e4cc23ad4ac91fff3cb527542c278e6cfe4b","tests/host.rs":"712f883c133041ead3cec66379e85279547189ed9ac366796732cabf486114a4","tests/method.rs":"6ae1f188b06b07822bbd97f671886259ee8e7008313449ec04604c8f396cf59b","tests/multipart.rs":"03e366749f4dae82ea864d3da795d940163ba7c2b2214a1db36f66b3f4b9d655","tests/path.rs":"db1884d961d83a624f7c63c8de09d9196f10a21a11bb43005e2015aee68d7ccf","tests/query.rs":"993133adc0e47eea4defea60dbca5365ad54a7f49069a1b423b9642d9fda6e3b","tests/redirect.rs":"a292860964a3ad2edca4f43d1b462226a4ca4537a71aae31520857dcb662b33d","tests/reply_with.rs":"35fdfe9653ffab0776fe9fb65e231f9ea647c9f391b17794010adbcbd5009e65","tests/tracing.rs":"f5223781843d97f78e598ae580234567691afca38952b07cac161597749f6332","tests/ws.rs":"b130f4a58198641c0853269f1e21e1b1ba7acba10b114161d499881740621f8a"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/warp/.github/FUNDING.yml b/third_party/rust/warp/.github/FUNDING.yml new file mode 100644 index 0000000000..a6b3376dae --- /dev/null +++ b/third_party/rust/warp/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [seanmonstar] diff --git a/third_party/rust/warp/.github/ISSUE_TEMPLATE/bug_report.md b/third_party/rust/warp/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..34ee804cb6 --- /dev/null +++ b/third_party/rust/warp/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Version** +List the versions of all `warp` crates you are using. The easiest way to get +this information is using `cargo-tree`. + +`cargo install cargo-tree` +(see install here: https://github.com/sfackler/cargo-tree) + +Then: + +`cargo tree | grep warp` + +**Platform** +The output of `uname -a` (UNIX), or version and 32 or 64-bit (Windows) + +**Description** +Enter your issue details here. +One way to structure the description: + +[short summary of the bug] + +I tried this code: + +[code sample that causes the bug] + +I expected to see this happen: [explanation] + +Instead, this happened: [explanation] diff --git a/third_party/rust/warp/.github/ISSUE_TEMPLATE/config.yml b/third_party/rust/warp/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..be346fecb4 --- /dev/null +++ b/third_party/rust/warp/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: Question + url: https://discord.gg/RFsPjyt + about: 'Please post your question on the #warp discord channel. You may + also be able to find help at https://users.rust-lang.org/.' diff --git a/third_party/rust/warp/.github/ISSUE_TEMPLATE/feature_request.md b/third_party/rust/warp/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..4fe86d5ec8 --- /dev/null +++ b/third_party/rust/warp/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/third_party/rust/warp/.github/workflows/ci.yml b/third_party/rust/warp/.github/workflows/ci.yml new file mode 100644 index 0000000000..7a1cd98a24 --- /dev/null +++ b/third_party/rust/warp/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + pull_request: + push: + branches: + - master + +jobs: + style: + name: Check Style + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + profile: minimal + override: true + + - name: cargo fmt -- --check + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + test: + name: Test + needs: [style] + runs-on: ubuntu-latest + + strategy: + matrix: + build: [stable, beta, nightly, tls, no-default-features, compression] + + include: + - build: beta + rust: beta + - build: nightly + rust: nightly + benches: true + - build: tls + features: "--features tls" + - build: no-default-features + features: "--no-default-features" + - build: compression + features: "--features compression" + + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust || 'stable' }} + profile: minimal + override: true + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + args: ${{ matrix.features }} + + - name: Test all benches + if: matrix.benches + uses: actions-rs/cargo@v1 + with: + command: test + args: --benches ${{ matrix.features }} + + doc: + name: Build docs + needs: [style, test] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + + - name: cargo doc + uses: actions-rs/cargo@v1 + with: + command: rustdoc + args: -- -D broken_intra_doc_links diff --git a/third_party/rust/warp/CHANGELOG.md b/third_party/rust/warp/CHANGELOG.md new file mode 100644 index 0000000000..dbf80b8795 --- /dev/null +++ b/third_party/rust/warp/CHANGELOG.md @@ -0,0 +1,324 @@ +### v0.3.3 (September 27, 2022) + +- **Fixes**: + - Fix `fs` filters path sanitization to reject colons on Windows. + +### v0.3.2 (November 9, 2021) + +- **Features**: + - Add `Filter::then()`, which is like `Filter::map()` in that it's infallible, but is async like `Filter::and_then()`. + - Add `redirect::found()` reply helper that returns `302 Found`. + - Add `compression-brotli` and `compression-gzip` cargo features to enable only the compression you need. + - Allow `HEAD` requests to be served to `fs::dir()` filters. + - Allow `path!()` with no arguments. +- **Fixes**: + - Update private dependencies Tungstenite and Multipart. + - Replaces uses of `futures` with `futures-util`, which is a smaller dependency. + + +### v0.3.1 (March 24, 2021) + +- **Features**: + - Add `pong` constructor to websocket messages. + - Add `redirect::see_other` and `redirect::permanent` helpers. +- **Fixes**: + - Fix `fs` filters sometimes having an off-by-one error with range requests. + - Fix CORS to allow spaces when checking `Access-Control-Request-Headers`. + +## v0.3.0 (January 19, 2021) + +- **Features**: + - Add TLS client authentication support. + - Add TLS OCSP stapling support. + - Add `From<Reject>` for `Rejection`. + - Add `close_frame` accessor to `ws::Message`. +- **Changes**: + - Update to Tokio v1. + - Update to Bytes v1. + - Update to hyper v0.14. + - Rework `sse` filter to be more like `ws`, with a single `Event` type and builder. + - Change `cookie` filter to extract a generic `FromStr` value. + + +### v0.2.5 (August 31, 2020) + +- **Features**: + - Add `wrap_fn`, which can be used to create a `Wrap` from a closure. These in turn are used with `Filter::with()`. + - Add `warp::host` filters to deal with `Host`/`:authority` headers. + - Relax some lifetime bounds on `Server`. +- **Fixes**: + - Fix panic when URI doesn't have a slash (for example, `CONNECT foo.bar`). + +### v0.2.4 (July 20, 2020) + +- **Features**: + - Add `tracing` internals in place of `log` (log is still emitted for backwards compatibility). + - Add `warp::trace` module set of filters to customize `tracing` dianostics. + - Add `path` method to `warp::fs::File` reply. + - Add `source` implementation for `BodyDeserializeError`. + - Make `warp::ws::MissingConnectionUpgrade` rejection public. + +### v0.2.3 (May 19, 2020) + +- **Features**: + - Add `warp::compression` filters, which will compress response bodies. + - Add `warp::header::value()` filter to get a request `HeaderValue`. + - Add `request_headers` method to `warp::log::Info`. + - Add `max_frame_size` to `warp::ws::Ws` builder. + - Add `remote_addr` to `warp::test::RequestBuilder`. + - Add `try_bind_with_graceful_shutdown` to `warp::Server` builder. + - Add `serve_incoming_with_graceful_shutdown` to `warp::Server` builder. +- **Fixes**: + - Fix `warp::addr::remote` when used with `Server::tls`. + - Fix panic in `warp::path::{peek, tail, full}` filters when the request URI is in authority-form or asterisk-form. + +### 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) + +- Initial release. diff --git a/third_party/rust/warp/Cargo.toml b/third_party/rust/warp/Cargo.toml new file mode 100644 index 0000000000..71a2f85348 --- /dev/null +++ b/third_party/rust/warp/Cargo.toml @@ -0,0 +1,185 @@ +# 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 are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "warp" +version = "0.3.3" +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] +all-features = true + +[profile.bench] +codegen-units = 1 +incremental = false + +[profile.release] +codegen-units = 1 +incremental = false + +[[example]] +name = "compression" +required-features = ["compression"] + +[[example]] +name = "unix_socket" + +[[example]] +name = "websockets" +required-features = ["websocket"] + +[[example]] +name = "websockets_chat" +required-features = ["websocket"] + +[[example]] +name = "query_string" + +[[test]] +name = "multipart" +required-features = ["multipart"] + +[[test]] +name = "ws" +required-features = ["websocket"] + +[dependencies] +bytes = "1.0" +headers = "0.3" +http = "0.2" +log = "0.4" +mime = "0.3" +mime_guess = "2.0.0" +percent-encoding = "2.1" +pin-project = "1.0" +scoped-tls = "1.0" +serde = "1.0" +serde_json = "1.0" +serde_urlencoded = "0.7" +tokio-stream = "0.1.1" +tower-service = "0.3" + +[dependencies.async-compression] +version = "0.3.7" +features = ["tokio"] +optional = true + +[dependencies.futures-channel] +version = "0.3.17" +features = ["sink"] + +[dependencies.futures-util] +version = "0.3" +features = ["sink"] +default-features = false + +[dependencies.hyper] +version = "0.14" +features = [ + "stream", + "server", + "http1", + "http2", + "tcp", + "client", +] + +[dependencies.multipart] +version = "0.18" +features = ["server"] +optional = true +default-features = false + +[dependencies.rustls-pemfile] +version = "0.2" +optional = true + +[dependencies.tokio] +version = "1.0" +features = [ + "fs", + "sync", + "time", +] + +[dependencies.tokio-rustls] +version = "0.23" +optional = true + +[dependencies.tokio-tungstenite] +version = "0.17" +optional = true + +[dependencies.tokio-util] +version = "0.7" +features = ["io"] + +[dependencies.tracing] +version = "0.1.21" +features = [ + "log", + "std", +] +default-features = false + +[dev-dependencies] +handlebars = "4.0" +listenfd = "0.3" +pretty_env_logger = "0.4" +serde_derive = "1.0" +tracing-log = "0.1" +tracing-subscriber = "0.2.7" + +[dev-dependencies.tokio] +version = "1.0" +features = [ + "macros", + "rt-multi-thread", +] + +[dev-dependencies.tokio-stream] +version = "0.1.1" +features = ["net"] + +[features] +compression = [ + "compression-brotli", + "compression-gzip", +] +compression-brotli = ["async-compression/brotli"] +compression-gzip = [ + "async-compression/deflate", + "async-compression/gzip", +] +default = [ + "multipart", + "websocket", +] +tls = [ + "tokio-rustls", + "rustls-pemfile", +] +websocket = ["tokio-tungstenite"] diff --git a/third_party/rust/warp/LICENSE b/third_party/rust/warp/LICENSE new file mode 100644 index 0000000000..4ba959d325 --- /dev/null +++ b/third_party/rust/warp/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2018-2020 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..0f65594077 --- /dev/null +++ b/third_party/rust/warp/README.md @@ -0,0 +1,63 @@ +# warp + +[![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) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +[![GHA Build Status](https://github.com/seanmonstar/warp/workflows/CI/badge.svg)](https://github.com/seanmonstar/warp/actions?query=workflow%3ACI) +[![Discord chat][discord-badge]][discord-url] + +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 +* Gzip, Deflate, and Brotli compression + +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 = "1", features = ["full"] } +warp = "0.3" +``` + +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). + +[discord-badge]: https://img.shields.io/discord/500028886025895936.svg?logo=discord +[discord-url]: https://discord.gg/RFsPjyt diff --git a/third_party/rust/warp/examples/README.md b/third_party/rust/warp/examples/README.md new file mode 100644 index 0000000000..1b3bb8a2ae --- /dev/null +++ b/third_party/rust/warp/examples/README.md @@ -0,0 +1,68 @@ +# Examples + +Welcome to the examples! These show off `warp`'s functionality and explain how to use it. + +## Getting Started + +To get started, run `examples/hello.rs` with: + +```bash +> cargo run --example hello +``` + +This will start a simple "hello world" service running on your localhost port 3030. + +Open another terminal and run: + +```bash +> curl http://localhost:3030/hi +Hello, World!% +``` + +Congratulations, you have just run your first warp service! + +You can run other examples with `cargo run --example [example name]`: + +- [`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! + +### Debugging + +- [`tracing.rs`](./tracing.rs) - Warp has built-in support for rich diagnostics with [`tracing`](https://docs.rs/tracing)! + +## Custom HTTP Methods + +- [`custom_methods.rs`](./custom_methods.rs) - It is also possible to use Warp with custom HTTP methods. diff --git a/third_party/rust/warp/examples/autoreload.rs b/third_party/rust/warp/examples/autoreload.rs new file mode 100644 index 0000000000..a21d9b1369 --- /dev/null +++ b/third_party/rust/warp/examples/autoreload.rs @@ -0,0 +1,42 @@ +#![deny(warnings)] +use hyper::server::Server; +use listenfd::ListenFd; +use std::convert::Infallible; +use warp::Filter; + +/// 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(|_: _| { + // the clone is there because not all warp filters impl Copy + let svc = svc.clone(); + 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/compression.rs b/third_party/rust/warp/examples/compression.rs new file mode 100644 index 0000000000..1a52c7a7d0 --- /dev/null +++ b/third_party/rust/warp/examples/compression.rs @@ -0,0 +1,34 @@ +#![deny(warnings)] + +use warp::Filter; + +#[tokio::main] +async fn main() { + let file = warp::path("todos").and(warp::fs::file("./examples/todos.rs")); + // NOTE: You could double compress something by adding a compression + // filter here, a la + // ``` + // let file = warp::path("todos") + // .and(warp::fs::file("./examples/todos.rs")) + // .with(warp::compression::brotli()); + // ``` + // This would result in a browser error, or downloading a file whose contents + // are compressed + + let dir = warp::path("ws_chat").and(warp::fs::file("./examples/websockets_chat.rs")); + + let file_and_dir = warp::get() + .and(file.or(dir)) + .with(warp::compression::gzip()); + + let examples = warp::path("ex") + .and(warp::fs::dir("./examples/")) + .with(warp::compression::deflate()); + + // GET /todos => gzip -> toods.rs + // GET /ws_chat => gzip -> ws_chat.rs + // GET /ex/... => deflate -> ./examples/... + let routes = file_and_dir.or(examples); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/examples/custom_methods.rs b/third_party/rust/warp/examples/custom_methods.rs new file mode 100644 index 0000000000..2e26604132 --- /dev/null +++ b/third_party/rust/warp/examples/custom_methods.rs @@ -0,0 +1,61 @@ +#![deny(warnings)] +use std::net::SocketAddr; + +use warp::hyper::StatusCode; +use warp::{hyper::Method, reject, Filter, Rejection, Reply}; + +#[derive(Debug)] +struct MethodError; +impl reject::Reject for MethodError {} + +const FOO_METHOD: &'static str = "FOO"; +const BAR_METHOD: &'static str = "BAR"; + +fn method(name: &'static str) -> impl Filter<Extract = (), Error = Rejection> + Clone { + warp::method() + .and_then(move |m: Method| async move { + if m == name { + Ok(()) + } else { + Err(reject::custom(MethodError)) + } + }) + .untuple_one() +} + +pub async fn handle_not_found(reject: Rejection) -> Result<impl Reply, Rejection> { + if reject.is_not_found() { + Ok(StatusCode::NOT_FOUND) + } else { + Err(reject) + } +} + +pub async fn handle_custom(reject: Rejection) -> Result<impl Reply, Rejection> { + if reject.find::<MethodError>().is_some() { + Ok(StatusCode::METHOD_NOT_ALLOWED) + } else { + Err(reject) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box<dyn std::error::Error>> { + let address: SocketAddr = "[::]:3030".parse()?; + + let foo_route = method(FOO_METHOD) + .and(warp::path!("foo")) + .map(|| "Success") + .recover(handle_not_found); + + let bar_route = method(BAR_METHOD) + .and(warp::path!("bar")) + .map(|| "Success") + .recover(handle_not_found); + + warp::serve(foo_route.or(bar_route).recover(handle_custom)) + .run(address) + .await; + + Ok(()) +} 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..43bf2f6efa --- /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::sleep(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..78e040539e --- /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/query_string.rs b/third_party/rust/warp/examples/query_string.rs new file mode 100644 index 0000000000..869468eb0d --- /dev/null +++ b/third_party/rust/warp/examples/query_string.rs @@ -0,0 +1,59 @@ +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use warp::{ + http::{Response, StatusCode}, + Filter, +}; + +#[derive(Deserialize, Serialize)] +struct MyObject { + key1: String, + key2: u32, +} + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + // get /example1?key=value + // demonstrates an optional parameter. + let example1 = warp::get() + .and(warp::path("example1")) + .and(warp::query::<HashMap<String, String>>()) + .map(|p: HashMap<String, String>| match p.get("key") { + Some(key) => Response::builder().body(format!("key = {}", key)), + None => Response::builder().body(String::from("No \"key\" param in query.")), + }); + + // get /example2?key1=value&key2=42 + // uses the query string to populate a custom object + let example2 = warp::get() + .and(warp::path("example2")) + .and(warp::query::<MyObject>()) + .map(|p: MyObject| { + Response::builder().body(format!("key1 = {}, key2 = {}", p.key1, p.key2)) + }); + + let opt_query = warp::query::<MyObject>() + .map(Some) + .or_else(|_| async { Ok::<(Option<MyObject>,), std::convert::Infallible>((None,)) }); + + // get /example3?key1=value&key2=42 + // builds on example2 but adds custom error handling + let example3 = + warp::get() + .and(warp::path("example3")) + .and(opt_query) + .map(|p: Option<MyObject>| match p { + Some(obj) => { + Response::builder().body(format!("key1 = {}, key2 = {}", obj.key1, obj.key2)) + } + None => Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(String::from("Failed to decode query param.")), + }); + + warp::serve(example1.or(example2).or(example3)) + .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..721e69ecaa --- /dev/null +++ b/third_party/rust/warp/examples/rejections.rs @@ -0,0 +1,122 @@ +#![deny(warnings)] + +use std::convert::Infallible; +use std::error::Error; +use std::num::NonZeroU16; + +use serde_derive::{Deserialize, 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); + let div_with_header = math + .and(warp::get()) + .and(div_by()) + .map(|num: u16, denom: NonZeroU16| { + warp::reply::json(&Math { + op: format!("{} / {}", num, denom), + output: num / denom.get(), + }) + }); + + let div_with_body = + math.and(warp::post()) + .and(warp::body::json()) + .map(|num: u16, body: DenomRequest| { + warp::reply::json(&Math { + op: format!("{} / {}", num, body.denom), + output: num / body.denom.get(), + }) + }); + + let routes = div_with_header.or(div_with_body).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(Deserialize)] +struct DenomRequest { + pub denom: NonZeroU16, +} + +#[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(e) = err.find::<warp::filters::body::BodyDeserializeError>() { + // This error happens if the body could not be deserialized correctly + // We can use the cause to analyze the error and customize the error message + message = match e.source() { + Some(cause) => { + if cause.to_string().contains("denom") { + "FIELD_ERROR: denom" + } else { + "BAD_REQUEST" + } + } + None => "BAD_REQUEST", + }; + code = StatusCode::BAD_REQUEST; + } 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..b2ad8c278d --- /dev/null +++ b/third_party/rust/warp/examples/routing.rs @@ -0,0 +1,104 @@ +#![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 / + let hello_world = warp::path::end().map(|| "Hello, World at root!"); + + // 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 that the + // method is GET, so we'll do that too!) + // + // GET / + // GET /hi + // GET /hello/from/warp + // GET /bye/:string + // GET /math/sum/:u32/:u32 + // GET /math/:u16/times/:u16 + + let routes = warp::get().and( + hello_world + .or(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..bce1fb6b1a --- /dev/null +++ b/third_party/rust/warp/examples/sse.rs @@ -0,0 +1,31 @@ +use futures_util::StreamExt; +use std::convert::Infallible; +use std::time::Duration; +use tokio::time::interval; +use tokio_stream::wrappers::IntervalStream; +use warp::{sse::Event, Filter}; + +// create server-sent event +fn sse_counter(counter: u64) -> Result<Event, Infallible> { + Ok(warp::sse::Event::default().data(counter.to_string())) +} + +#[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 interval = interval(Duration::from_secs(1)); + let stream = IntervalStream::new(interval); + let event_stream = stream.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..6e064b1824 --- /dev/null +++ b/third_party/rust/warp/examples/sse_chat.rs @@ -0,0 +1,163 @@ +use futures_util::{Stream, StreamExt}; +use std::collections::HashMap; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, +}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; +use warp::{sse::Event, 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<Event, 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(); + let rx = UnboundedReceiverStream::new(rx); + + 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(Event::default().event("user").data(my_id.to_string())), + Message::Reply(reply) => Ok(Event::default().data(reply)), + }) +} + +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..7d28e03a3a --- /dev/null +++ b/third_party/rust/warp/examples/tls.rs @@ -0,0 +1,25 @@ +#![deny(warnings)] + +// Don't copy this `cfg`, it's only needed because this file is within +// the warp repository. +// Instead, specify the "tls" feature in your warp dependency declaration. +#[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..904d604e8f --- /dev/null +++ b/third_party/rust/warp/examples/todos.rs @@ -0,0 +1,291 @@ +#![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(&todo1()) + .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/tracing.rs b/third_party/rust/warp/examples/tracing.rs new file mode 100644 index 0000000000..103f747a93 --- /dev/null +++ b/third_party/rust/warp/examples/tracing.rs @@ -0,0 +1,59 @@ +//! [`tracing`] is a framework for instrumenting Rust programs to +//! collect scoped, structured, and async-aware diagnostics. This example +//! demonstrates how the `warp::trace` module can be used to instrument `warp` +//! applications with `tracing`. +//! +//! [`tracing`]: https://crates.io/crates/tracing +#![deny(warnings)] +use tracing_subscriber::fmt::format::FmtSpan; +use warp::Filter; + +#[tokio::main] +async fn main() { + // Filter traces based on the RUST_LOG env var, or, if it's not set, + // default to show the output of the example. + let filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "tracing=info,warp=debug".to_owned()); + + // Configure the default `tracing` subscriber. + // The `fmt` subscriber from the `tracing-subscriber` crate logs `tracing` + // events to stdout. Other subscribers are available for integrating with + // distributed tracing systems such as OpenTelemetry. + tracing_subscriber::fmt() + // Use the filter we built above to determine which traces to record. + .with_env_filter(filter) + // Record an event when each span closes. This can be used to time our + // routes' durations! + .with_span_events(FmtSpan::CLOSE) + .init(); + + let hello = warp::path("hello") + .and(warp::get()) + // When the `hello` route is called, emit a `tracing` event. + .map(|| { + tracing::info!("saying hello..."); + "Hello, World!" + }) + // Wrap the route in a `tracing` span to add the route's name as context + // to any events that occur inside it. + .with(warp::trace::named("hello")); + + let goodbye = warp::path("goodbye") + .and(warp::get()) + .map(|| { + tracing::info!("saying goodbye..."); + "So long and thanks for all the fish!" + }) + // We can also provide our own custom `tracing` spans to wrap a route. + .with(warp::trace(|info| { + // Construct our own custom span for this route. + tracing::info_span!("goodbye", req.path = ?info.path()) + })); + + let routes = hello + .or(goodbye) + // Wrap all the routes with a filter that creates a `tracing` span for + // each request we receive, including data about the request. + .with(warp::trace::request()); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} 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..521aeead21 --- /dev/null +++ b/third_party/rust/warp/examples/unix_socket.rs @@ -0,0 +1,22 @@ +#![deny(warnings)] + +#[cfg(unix)] +#[tokio::main] +async fn main() { + use tokio::net::UnixListener; + use tokio_stream::wrappers::UnixListenerStream; + + pretty_env_logger::init(); + + let listener = UnixListener::bind("/tmp/warp.sock").unwrap(); + let incoming = UnixListenerStream::new(listener); + warp::serve(warp::fs::dir("examples/dir")) + .run_incoming(incoming) + .await; +} + +#[cfg(not(unix))] +#[tokio::main] +async fn main() { + panic!("Must run under Unix-like platform!"); +} diff --git a/third_party/rust/warp/examples/websockets.rs b/third_party/rust/warp/examples/websockets.rs new file mode 100644 index 0000000000..b0de205743 --- /dev/null +++ b/third_party/rust/warp/examples/websockets.rs @@ -0,0 +1,27 @@ +#![deny(warnings)] + +use futures_util::{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..21e2286f6f --- /dev/null +++ b/third_party/rust/warp/examples/websockets_chat.rs @@ -0,0 +1,175 @@ +// #![deny(warnings)] +use std::collections::HashMap; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +use futures_util::{SinkExt, StreamExt, TryFutureExt}; +use tokio::sync::{mpsc, RwLock}; +use tokio_stream::wrappers::UnboundedReceiverStream; +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<RwLock<HashMap<usize, mpsc::UnboundedSender<Message>>>>; + +#[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 = Users::default(); + // 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 (mut 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(); + let mut rx = UnboundedReceiverStream::new(rx); + + tokio::task::spawn(async move { + while let Some(message) = rx.next().await { + user_ws_tx + .send(message) + .unwrap_or_else(|e| { + eprintln!("websocket send error: {}", e); + }) + .await; + } + }); + + // Save the sender in our list of connected users. + users.write().await.insert(my_id, tx); + + // Return a `Future` that is basically a state machine managing + // this specific user's connection. + + // 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, &users).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)... + for (&uid, tx) in users.read().await.iter() { + if my_id != uid { + if let Err(_disconnected) = tx.send(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.write().await.remove(&my_id); +} + +static INDEX_HTML: &str = r#"<!DOCTYPE html> +<html lang="en"> + <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"> + const chat = document.getElementById('chat'); + const text = document.getElementById('text'); + const uri = 'ws://' + location.host + '/chat'; + const ws = new WebSocket(uri); + + function message(data) { + const 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); + }; + + ws.onclose = function() { + chat.getElementsByTagName('em')[0].innerText = 'Disconnected!'; + }; + + send.onclick = function() { + const msg = text.value; + ws.send(msg); + text.value = ''; + + message('<You>: ' + msg); + }; + </script> + </body> +</html> +"#; diff --git a/third_party/rust/warp/examples/wrapping.rs b/third_party/rust/warp/examples/wrapping.rs new file mode 100644 index 0000000000..bfc84f980f --- /dev/null +++ b/third_party/rust/warp/examples/wrapping.rs @@ -0,0 +1,31 @@ +#![deny(warnings)] +use warp::Filter; + +fn hello_wrapper<F, T>( + filter: F, +) -> impl Filter<Extract = (&'static str,)> + Clone + Send + Sync + 'static +where + F: Filter<Extract = (T,), Error = std::convert::Infallible> + Clone + Send + Sync + 'static, + F::Extract: warp::Reply, +{ + warp::any() + .map(|| { + println!("before filter"); + }) + .untuple_one() + .and(filter) + .map(|_arg| "wrapped hello world") +} + +#[tokio::main] +async fn main() { + // Match any request and return hello world! + let routes = warp::any() + .map(|| "hello world") + .boxed() + .recover(|_err| async { Ok("recovered") }) + // wrap the filter with hello_wrapper + .with(warp::wrap_fn(hello_wrapper)); + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +} diff --git a/third_party/rust/warp/src/error.rs b/third_party/rust/warp/src/error.rs new file mode 100644 index 0000000000..64220b633e --- /dev/null +++ b/third_party/rust/warp/src/error.rs @@ -0,0 +1,79 @@ +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 { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(self.inner.as_ref()) + } +} + +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 + ); +} + +#[test] +fn error_source() { + let e = Error::new(std::fmt::Error {}); + assert!(e.source().unwrap().is::<std::fmt::Error>()); +} + +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..5edd90fed8 --- /dev/null +++ b/third_party/rust/warp/src/filter/and.rs @@ -0,0 +1,97 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::ready; +use pin_project::pin_project; + +use super::{Combine, Filter, FilterBase, Internal, Tuple}; +use crate::generic::CombinedTuples; +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, + CombinedTuples<T::Extract, U::Extract>: Send, + U::Error: CombineRejection<T::Error>, +{ + type Extract = CombinedTuples<T::Extract, U::Extract>; + 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::Future, T::Extract, U>, +} + +#[pin_project(project = StateProj)] +enum State<T, TE, U: Filter> { + First(#[pin] T, U), + Second(Option<TE>, #[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< + CombinedTuples<T::Extract, U::Extract>, + <U::Error as CombineRejection<T::Error>>::One, + >; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + self.project().state.poll(cx) + } +} + +impl<T, TE, U, E> Future for State<T, TE, U> +where + T: Future<Output = Result<TE, E>>, + U: Filter, + TE: Tuple, + TE::HList: Combine<<U::Extract as Tuple>::HList> + Send, + U::Error: CombineRejection<E>, +{ + type Output = Result<CombinedTuples<TE, U::Extract>, <U::Error as CombineRejection<E>>::One>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + loop { + match self.as_mut().project() { + StateProj::First(first, second) => { + let ex1 = ready!(first.poll(cx))?; + let fut2 = second.filter(Internal); + self.set(State::Second(Some(ex1), fut2)); + } + StateProj::Second(ex1, second) => { + let ex2 = ready!(second.poll(cx))?; + let ex3 = ex1.take().unwrap().combine(ex2); + self.set(State::Done); + return Poll::Ready(Ok(ex3)); + } + StateProj::Done => panic!("polled after complete"), + } + } + } +} 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..efed5fe8cf --- /dev/null +++ b/third_party/rust/warp/src/filter/and_then.rs @@ -0,0 +1,110 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::{ready, TryFuture}; +use pin_project::pin_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, F> +where + T: Filter, + F: Func<T::Extract>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: CombineRejection<T::Error>, +{ + #[pin] + state: State<T::Future, F>, +} + +#[pin_project(project = StateProj)] +enum State<T, F> +where + T: TryFuture, + F: Func<T::Ok>, + F::Output: TryFuture + Send, + <F::Output as TryFuture>::Error: CombineRejection<T::Error>, +{ + First(#[pin] T, 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, + >; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + self.project().state.poll(cx) + } +} + +impl<T, F> Future for State<T, F> +where + T: TryFuture, + F: Func<T::Ok>, + 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, + >; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + loop { + match self.as_mut().project() { + StateProj::First(first, second) => { + let ex1 = ready!(first.try_poll(cx))?; + let fut2 = second.call(ex1); + self.set(State::Second(fut2)); + } + StateProj::Second(second) => { + let ex2 = match ready!(second.try_poll(cx)) { + Ok(item) => Ok((item,)), + Err(err) => Err(From::from(err)), + }; + self.set(State::Done); + return Poll::Ready(ex2); + } + StateProj::Done => panic!("polled after complete"), + } + } + } +} 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..1ed4a7d800 --- /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_util::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..ec3173994e --- /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_util::{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..f4659e6ff6 --- /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_util::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..263650768d --- /dev/null +++ b/third_party/rust/warp/src/filter/mod.rs @@ -0,0 +1,492 @@ +mod and; +mod and_then; +mod boxed; +mod map; +mod map_err; +mod or; +mod or_else; +mod recover; +pub(crate) mod service; +mod then; +mod unify; +mod untuple_one; +mod wrap; + +use std::future::Future; + +use futures_util::{future, TryFuture, TryFutureExt}; + +pub(crate) use crate::generic::{one, Combine, Either, Func, 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::then::Then; +use self::unify::Unify; +use self::untuple_one::UntupleOne; +pub use self::wrap::wrap_fn; +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 an async function receiving + /// the extracted value. + /// + /// The function should return some `Future` type. + /// + /// # Example + /// + /// ``` + /// use warp::Filter; + /// + /// // Map `/:id` + /// warp::path::param().then(|id: u64| async move { + /// format!("Hello #{}", id) + /// }); + /// ``` + fn then<F>(self, fun: F) -> Then<Self, F> + where + Self: Sized, + F: Func<Self::Extract> + Clone, + F::Output: Future + Send, + { + Then { + filter: self, + callback: fun, + } + } + + /// Composes this `Filter` with a fallible async 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. + /// + /// Rejections are meant to say "this filter didn't accept the request, + /// maybe another can". So for application-level errors, consider using + /// [`Filter::then`] instead. + /// + /// # 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, +) -> impl Filter<Extract = (U::Ok,), Error = U::Error> + Copy +where + F: Fn(&mut Route) -> U + Copy, + U: TryFuture + Send + 'static, + U::Ok: Send, + U::Error: IsReject, +{ + filter_fn(move |route| func(route).map_ok(|item| (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 = future::IntoFuture<U>; + + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + 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..774067fb59 --- /dev/null +++ b/third_party/rust/warp/src/filter/or.rs @@ -0,0 +1,110 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::{ready, TryFuture}; +use pin_project::pin_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(project = StateProj)] +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>>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + loop { + let pin = self.as_mut().project(); + let (err1, fut2) = match pin.state.project() { + StateProj::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)) + } + }, + StateProj::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); + } + StateProj::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..aaf23243ec --- /dev/null +++ b/third_party/rust/warp/src/filter/or_else.rs @@ -0,0 +1,107 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::{ready, TryFuture}; +use pin_project::pin_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(project = StateProj)] +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>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + loop { + let pin = self.as_mut().project(); + let (err, second) = match pin.state.project() { + StateProj::First(first, second) => match ready!(first.try_poll(cx)) { + Ok(ex) => return Poll::Ready(Ok(ex)), + Err(err) => (err, second), + }, + StateProj::Second(second) => { + let ex2 = ready!(second.try_poll(cx)); + self.set(OrElseFuture { + state: State::Done, + ..*self + }); + return Poll::Ready(ex2); + } + StateProj::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..100e9398c2 --- /dev/null +++ b/third_party/rust/warp/src/filter/recover.rs @@ -0,0 +1,117 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::{ready, TryFuture}; +use pin_project::pin_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(project = StateProj)] +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, + >; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + loop { + let pin = self.as_mut().project(); + let (err, second) = match pin.state.project() { + StateProj::First(first, second) => match ready!(first.try_poll(cx)) { + Ok(ex) => return Poll::Ready(Ok((Either::A(ex),))), + Err(err) => (err, second), + }, + StateProj::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); + } + StateProj::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..3de12a02ed --- /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_util::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)) => { + tracing::debug!("rejected: {:?}", err); + Poll::Ready(Ok(err.into_response())) + } + } + } +} diff --git a/third_party/rust/warp/src/filter/then.rs b/third_party/rust/warp/src/filter/then.rs new file mode 100644 index 0000000000..543a22669a --- /dev/null +++ b/third_party/rust/warp/src/filter/then.rs @@ -0,0 +1,95 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::{ready, TryFuture}; +use pin_project::pin_project; + +use super::{Filter, FilterBase, Func, Internal}; + +#[derive(Clone, Copy, Debug)] +pub struct Then<T, F> { + pub(super) filter: T, + pub(super) callback: F, +} + +impl<T, F> FilterBase for Then<T, F> +where + T: Filter, + F: Func<T::Extract> + Clone + Send, + F::Output: Future + Send, +{ + type Extract = (<F::Output as Future>::Output,); + type Error = T::Error; + type Future = ThenFuture<T, F>; + #[inline] + fn filter(&self, _: Internal) -> Self::Future { + ThenFuture { + state: State::First(self.filter.filter(Internal), self.callback.clone()), + } + } +} + +#[allow(missing_debug_implementations)] +#[pin_project] +pub struct ThenFuture<T, F> +where + T: Filter, + F: Func<T::Extract>, + F::Output: Future + Send, +{ + #[pin] + state: State<T::Future, F>, +} + +#[pin_project(project = StateProj)] +enum State<T, F> +where + T: TryFuture, + F: Func<T::Ok>, + F::Output: Future + Send, +{ + First(#[pin] T, F), + Second(#[pin] F::Output), + Done, +} + +impl<T, F> Future for ThenFuture<T, F> +where + T: Filter, + F: Func<T::Extract>, + F::Output: Future + Send, +{ + type Output = Result<(<F::Output as Future>::Output,), T::Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + self.project().state.poll(cx) + } +} + +impl<T, F> Future for State<T, F> +where + T: TryFuture, + F: Func<T::Ok>, + F::Output: Future + Send, +{ + type Output = Result<(<F::Output as Future>::Output,), T::Error>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + loop { + match self.as_mut().project() { + StateProj::First(first, second) => { + let ex1 = ready!(first.try_poll(cx))?; + let fut2 = second.call(ex1); + self.set(State::Second(fut2)); + } + StateProj::Second(second) => { + let ex2 = (ready!(second.poll(cx)),); + self.set(State::Done); + return Poll::Ready(Ok(ex2)); + } + StateProj::Done => panic!("polled after complete"), + } + } + } +} 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..0cf670daa2 --- /dev/null +++ b/third_party/rust/warp/src/filter/unify.rs @@ -0,0 +1,50 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::{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> { + Poll::Ready(match ready!(self.project().inner.try_poll(cx))? { + (Either::A(x),) | (Either::B(x),) => Ok(x), + }) + } +} 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..0fb0de6748 --- /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_util::{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..faceb705f3 --- /dev/null +++ b/third_party/rust/warp/src/filter/wrap.rs @@ -0,0 +1,67 @@ +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, +{ +} + +/// Combines received filter with pre and after filters +/// +/// # Example +/// +/// ``` +/// use crate::warp::Filter; +/// +/// let route = warp::any() +/// .map(|| "hello world") +/// .with(warp::wrap_fn(|filter| filter)); +/// ``` +/// +/// You can find the full example in the [usage example](https://github.com/seanmonstar/warp/blob/master/examples/wrapping.rs). +pub fn wrap_fn<F, T, U>(func: F) -> WrapFn<F> +where + F: Fn(T) -> U, + T: Filter, + U: Filter, +{ + WrapFn { func } +} + +#[derive(Debug)] +pub struct WrapFn<F> { + func: F, +} + +impl<F, T, U> WrapSealed<T> for WrapFn<F> +where + F: Fn(T) -> U, + T: Filter, + U: Filter, +{ + type Wrapped = U; + + fn wrap(&self, filter: T) -> Self::Wrapped { + (self.func)(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..3d630705a1 --- /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_util::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..707fd82dd4 --- /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..3bb08d2b4c --- /dev/null +++ b/third_party/rust/warp/src/filters/body.rs @@ -0,0 +1,345 @@ +//! 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, Bytes}; +use futures_util::{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(|| { + tracing::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, |_| { + tracing::debug!("content-length missing"); + reject::length_required() + }) + .and_then(move |ContentLength(length)| { + if length <= limit { + future::ok(()) + } else { + tracing::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| { + tracing::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.chunk()); +/// let cnt = body.chunk().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| { + tracing::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(bytes()) + .and_then(|buf| async move { + Json::decode(buf).map_err(|err| { + tracing::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| { + tracing::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>(mut buf: B) -> Result<T, BoxError> { + serde_json::from_slice(&buf.copy_to_bytes(buf.remaining())).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) { + tracing::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 { + tracing::debug!( + "content-type {:?} doesn't match {}/{}", + value, + type_, + subtype + ); + future::err(reject::unsupported_media_type()) + } + } else { + tracing::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! + tracing::trace!("no content-type header, assuming {}/{}", type_, subtype); + future::ok(()) + } else { + tracing::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 { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(self.cause.as_ref()) + } +} + +#[derive(Debug)] +pub(crate) struct BodyReadError(::hyper::Error); + +impl fmt::Display for BodyReadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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/compression.rs b/third_party/rust/warp/src/filters/compression.rs new file mode 100644 index 0000000000..244e768356 --- /dev/null +++ b/third_party/rust/warp/src/filters/compression.rs @@ -0,0 +1,292 @@ +//! Compression Filters +//! +//! Filters that compress the body of a response. + +#[cfg(feature = "compression-brotli")] +use async_compression::tokio::bufread::BrotliEncoder; + +#[cfg(feature = "compression-gzip")] +use async_compression::tokio::bufread::{DeflateEncoder, GzipEncoder}; + +use http::header::HeaderValue; +use hyper::{ + header::{CONTENT_ENCODING, CONTENT_LENGTH}, + Body, +}; +use tokio_util::io::{ReaderStream, StreamReader}; + +use crate::filter::{Filter, WrapSealed}; +use crate::reject::IsReject; +use crate::reply::{Reply, Response}; + +use self::internal::{CompressionProps, WithCompression}; + +enum CompressionAlgo { + #[cfg(feature = "compression-brotli")] + BR, + #[cfg(feature = "compression-gzip")] + DEFLATE, + #[cfg(feature = "compression-gzip")] + GZIP, +} + +impl From<CompressionAlgo> for HeaderValue { + #[inline] + fn from(algo: CompressionAlgo) -> Self { + HeaderValue::from_static(match algo { + #[cfg(feature = "compression-brotli")] + CompressionAlgo::BR => "br", + #[cfg(feature = "compression-gzip")] + CompressionAlgo::DEFLATE => "deflate", + #[cfg(feature = "compression-gzip")] + CompressionAlgo::GZIP => "gzip", + }) + } +} + +/// Compression +#[derive(Clone, Copy, Debug)] +pub struct Compression<F> { + func: F, +} + +// TODO: The implementation of `gzip()`, `deflate()`, and `brotli()` could be replaced with +// generics or a macro + +/// Create a wrapping filter that compresses the Body of a [`Response`](crate::reply::Response) +/// using gzip, adding `content-encoding: gzip` to the Response's [`HeaderMap`](hyper::HeaderMap) +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::get() +/// .and(warp::path::end()) +/// .and(warp::fs::file("./README.md")) +/// .with(warp::compression::gzip()); +/// ``` +#[cfg(feature = "compression-gzip")] +pub fn gzip() -> Compression<impl Fn(CompressionProps) -> Response + Copy> { + let func = move |mut props: CompressionProps| { + let body = Body::wrap_stream(ReaderStream::new(GzipEncoder::new(StreamReader::new( + props.body, + )))); + props + .head + .headers + .append(CONTENT_ENCODING, CompressionAlgo::GZIP.into()); + props.head.headers.remove(CONTENT_LENGTH); + Response::from_parts(props.head, body) + }; + Compression { func } +} + +/// Create a wrapping filter that compresses the Body of a [`Response`](crate::reply::Response) +/// using deflate, adding `content-encoding: deflate` to the Response's [`HeaderMap`](hyper::HeaderMap) +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::get() +/// .and(warp::path::end()) +/// .and(warp::fs::file("./README.md")) +/// .with(warp::compression::deflate()); +/// ``` +#[cfg(feature = "compression-gzip")] +pub fn deflate() -> Compression<impl Fn(CompressionProps) -> Response + Copy> { + let func = move |mut props: CompressionProps| { + let body = Body::wrap_stream(ReaderStream::new(DeflateEncoder::new(StreamReader::new( + props.body, + )))); + props + .head + .headers + .append(CONTENT_ENCODING, CompressionAlgo::DEFLATE.into()); + props.head.headers.remove(CONTENT_LENGTH); + Response::from_parts(props.head, body) + }; + Compression { func } +} + +/// Create a wrapping filter that compresses the Body of a [`Response`](crate::reply::Response) +/// using brotli, adding `content-encoding: br` to the Response's [`HeaderMap`](hyper::HeaderMap) +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::get() +/// .and(warp::path::end()) +/// .and(warp::fs::file("./README.md")) +/// .with(warp::compression::brotli()); +/// ``` +#[cfg(feature = "compression-brotli")] +pub fn brotli() -> Compression<impl Fn(CompressionProps) -> Response + Copy> { + let func = move |mut props: CompressionProps| { + let body = Body::wrap_stream(ReaderStream::new(BrotliEncoder::new(StreamReader::new( + props.body, + )))); + props + .head + .headers + .append(CONTENT_ENCODING, CompressionAlgo::BR.into()); + props.head.headers.remove(CONTENT_LENGTH); + Response::from_parts(props.head, body) + }; + Compression { func } +} + +impl<FN, F> WrapSealed<F> for Compression<FN> +where + FN: Fn(CompressionProps) -> Response + Clone + Send, + F: Filter + Clone + Send, + F::Extract: Reply, + F::Error: IsReject, +{ + type Wrapped = WithCompression<FN, F>; + + fn wrap(&self, filter: F) -> Self::Wrapped { + WithCompression { + filter, + compress: self.clone(), + } + } +} + +mod internal { + use std::future::Future; + use std::pin::Pin; + use std::task::{Context, Poll}; + + use bytes::Bytes; + use futures_util::{ready, Stream, TryFuture}; + use hyper::Body; + use pin_project::pin_project; + + use crate::filter::{Filter, FilterBase, Internal}; + use crate::reject::IsReject; + use crate::reply::{Reply, Response}; + + use super::Compression; + + /// A wrapper around any type that implements [`Stream`](futures::Stream) to be + /// compatible with async_compression's Stream based encoders + #[pin_project] + #[derive(Debug)] + pub struct CompressableBody<S, E> + where + E: std::error::Error, + S: Stream<Item = Result<Bytes, E>>, + { + #[pin] + body: S, + } + + impl<S, E> Stream for CompressableBody<S, E> + where + E: std::error::Error, + S: Stream<Item = Result<Bytes, E>>, + { + type Item = std::io::Result<Bytes>; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { + use std::io::{Error, ErrorKind}; + + let pin = self.project(); + S::poll_next(pin.body, cx).map_err(|_| Error::from(ErrorKind::InvalidData)) + } + } + + impl From<Body> for CompressableBody<Body, hyper::Error> { + fn from(body: Body) -> Self { + CompressableBody { body } + } + } + + /// Compression Props + #[derive(Debug)] + pub struct CompressionProps { + pub(super) body: CompressableBody<Body, hyper::Error>, + pub(super) head: http::response::Parts, + } + + impl From<http::Response<Body>> for CompressionProps { + fn from(resp: http::Response<Body>) -> Self { + let (head, body) = resp.into_parts(); + CompressionProps { + body: body.into(), + head, + } + } + } + + #[allow(missing_debug_implementations)] + pub struct Compressed(pub(super) Response); + + impl Reply for Compressed { + #[inline] + fn into_response(self) -> Response { + self.0 + } + } + + #[allow(missing_debug_implementations)] + #[derive(Clone, Copy)] + pub struct WithCompression<FN, F> { + pub(super) compress: Compression<FN>, + pub(super) filter: F, + } + + impl<FN, F> FilterBase for WithCompression<FN, F> + where + FN: Fn(CompressionProps) -> Response + Clone + Send, + F: Filter + Clone + Send, + F::Extract: Reply, + F::Error: IsReject, + { + type Extract = (Compressed,); + type Error = F::Error; + type Future = WithCompressionFuture<FN, F::Future>; + + fn filter(&self, _: Internal) -> Self::Future { + WithCompressionFuture { + compress: self.compress.clone(), + future: self.filter.filter(Internal), + } + } + } + + #[allow(missing_debug_implementations)] + #[pin_project] + pub struct WithCompressionFuture<FN, F> { + compress: Compression<FN>, + #[pin] + future: F, + } + + impl<FN, F> Future for WithCompressionFuture<FN, F> + where + FN: Fn(CompressionProps) -> Response, + F: TryFuture, + F::Ok: Reply, + F::Error: IsReject, + { + type Output = Result<(Compressed,), F::Error>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let pin = self.as_mut().project(); + let result = ready!(pin.future.try_poll(cx)); + match result { + Ok(reply) => { + let resp = (self.compress.func)(reply.into_response().into()); + Poll::Ready(Ok((Compressed(resp),))) + } + Err(reject) => Poll::Ready(Err(reject)), + } + } + } +} 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..53695e85c8 --- /dev/null +++ b/third_party/rust/warp/src/filters/cookie.rs @@ -0,0 +1,46 @@ +//! Cookie Filters + +use futures_util::future; +use headers::Cookie; + +use super::header; +use crate::filter::{Filter, One}; +use crate::reject::Rejection; +use std::convert::Infallible; +use std::str::FromStr; + +/// Creates a `Filter` that requires a cookie by name. +/// +/// If found, extracts the value of the cookie, otherwise rejects. +pub fn cookie<T>(name: &'static str) -> impl Filter<Extract = One<T>, Error = Rejection> + Copy +where + T: FromStr + Send + 'static, +{ + header::header2().and_then(move |cookie: Cookie| { + let cookie = cookie + .get(name) + .ok_or_else(|| crate::reject::missing_cookie(name)) + .and_then(|s| T::from_str(s).map_err(|_| 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<T>( + name: &'static str, +) -> impl Filter<Extract = One<Option<T>>, Error = Infallible> + Copy +where + T: FromStr + Send + 'static, +{ + header::optional2().map(move |opt: Option<Cookie>| { + let cookie = opt.and_then(|cookie| cookie.get(name).map(|x| T::from_str(x))); + match cookie { + Some(Ok(t)) => Some(t), + Some(Err(_)) => None, + None => None, + } + }) +} 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..fa28893c20 --- /dev/null +++ b/third_party/rust/warp/src/filters/cors.rs @@ -0,0 +1,626 @@ +//! CORS Filters + +use std::collections::HashSet; +use std::convert::TryFrom; +use std::error::Error as StdError; +use std::fmt; +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. + /// + /// **Note**: These should match the values the browser sends via `Access-Control-Request-Headers`, e.g. `content-type`. + /// + /// # 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. + /// + /// **Note**: These should match the values the browser sends via `Access-Control-Request-Headers`, e.g.`content-type`. + /// + /// # 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 intend 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 fmt::Debug for CorsForbidden { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("CorsForbidden").field(&self.kind).finish() + } +} + +impl fmt::Display for CorsForbidden { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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 { + tracing::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.trim()) { + return Err(Forbidden::HeaderNotAllowed); + } + } + } + + Ok(Validated::Preflight(origin.clone())) + } + (Some(origin), _) => { + // Any other method, simply check for a valid origin... + + tracing::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_util::{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..985bbfb61c --- /dev/null +++ b/third_party/rust/warp/src/filters/ext.rs @@ -0,0 +1,36 @@ +//! Request Extensions + +use std::convert::Infallible; + +use futures_util::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..0949b66ecb --- /dev/null +++ b/third_party/rust/warp/src/filters/fs.rs @@ -0,0 +1,539 @@ +//! 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_util::future::Either; +use futures_util::{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 percent_encoding::percent_decode_str; +use tokio::fs::File as TkFile; +use tokio::io::AsyncSeekExt; +use tokio_util::io::poll_read_buf; + +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 || { + tracing::trace!("file: {:?}", path); + ArcPath(path.clone()) + }) + .and(conditionals()) + .and_then(file_reply) +} + +/// 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() + .or(crate::head()) + .unify() + .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 { + tracing::debug!("dir: appending index.html to directory path"); + buf.push("index.html"); + } + tracing::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 percent_decode_str(tail).decode_utf8() { + Ok(p) => p, + Err(err) => { + tracing::debug!("dir: failed to decode route={:?}: {:?}", tail, err); + return Err(reject::not_found()); + } + }; + tracing::trace!("dir? base={:?}, route={:?}", base.as_ref(), p); + for seg in p.split('/') { + if seg.starts_with("..") { + tracing::warn!("dir: rejecting segment starting with '..'"); + return Err(reject::not_found()); + } else if seg.contains('\\') { + tracing::warn!("dir: rejecting segment containing backslash (\\)"); + return Err(reject::not_found()); + } else if cfg!(windows) && seg.contains(':') { + tracing::warn!("dir: rejecting segment containing colon (:)"); + 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); + + tracing::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 { + tracing::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 { + tracing::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, + path: ArcPath, +} + +impl File { + /// Extract the `&Path` of the file this `Response` delivers. + /// + /// # Example + /// + /// The example below changes the Content-Type response header for every file called `video.mp4`. + /// + /// ``` + /// use warp::{Filter, reply::Reply}; + /// + /// let route = warp::path("static") + /// .and(warp::fs::dir("/www/static")) + /// .map(|reply: warp::filters::fs::File| { + /// if reply.path().ends_with("video.mp4") { + /// warp::reply::with_header(reply, "Content-Type", "video/mp4").into_response() + /// } else { + /// reply.into_response() + /// } + /// }); + /// ``` + pub fn path(&self) -> &Path { + self.path.as_ref() + } +} + +// 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 => { + tracing::debug!("file not found: {:?}", path.as_ref().display()); + reject::not_found() + } + io::ErrorKind::PermissionDenied => { + tracing::warn!("file permission denied: {:?}", path.as_ref().display()); + reject::known(FilePermissionError { _p: () }) + } + _ => { + tracing::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) => { + tracing::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, path } + }) +} + +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) => { + // For the special case where s == the file size + if s == max_len { + s + } else { + s + 1 + } + } + Bound::Excluded(s) => s, + }; + + if start < end && end <= max_len { + Ok((start, end)) + } else { + tracing::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!(poll_read_buf(Pin::new(&mut f), cx, &mut buf)) { + Ok(n) => n as u64, + Err(err) => { + tracing::debug!("file read error: {}", err); + return Poll::Ready(Some(Err(err))); + } + }; + + if n == 0 { + tracing::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..0c535a38b5 --- /dev/null +++ b/third_party/rust/warp/src/filters/header.rs @@ -0,0 +1,230 @@ +//! 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_util::future; +use headers::{Header, HeaderMapExt}; +use http::header::HeaderValue; +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| { + tracing::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| { + tracing::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| { + tracing::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| { + tracing::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| { + tracing::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_ignore_case("connection", "keep-alive"); +/// ``` +pub fn exact_ignore_case( + name: &'static str, + value: &'static str, +) -> impl Filter<Extract = (), Error = Rejection> + Copy { + filter_fn(move |route| { + tracing::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 gets a `HeaderValue` for the name. +/// +/// # Example +/// +/// ``` +/// use warp::{Filter, http::header::HeaderValue}; +/// +/// let filter = warp::header::value("x-token") +/// .map(|value: HeaderValue| { +/// format!("header value bytes: {:?}", value) +/// }); +/// ``` +pub fn value( + name: &'static str, +) -> impl Filter<Extract = One<HeaderValue>, Error = Rejection> + Copy { + filter_fn_one(move |route| { + tracing::trace!("value({:?})", name); + let route = route + .headers() + .get(name) + .cloned() + .ok_or_else(|| reject::missing_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/host.rs b/third_party/rust/warp/src/filters/host.rs new file mode 100644 index 0000000000..9aae039c19 --- /dev/null +++ b/third_party/rust/warp/src/filters/host.rs @@ -0,0 +1,96 @@ +//! Host ("authority") filter +//! +use crate::filter::{filter_fn_one, Filter, One}; +use crate::reject::{self, Rejection}; +use futures_util::future; +pub use http::uri::Authority; +use std::str::FromStr; + +/// Creates a `Filter` that requires a specific authority (target server's +/// host and port) in the request. +/// +/// Authority is specified either in the `Host` header or in the target URI. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let multihost = +/// warp::host::exact("foo.com").map(|| "you've reached foo.com") +/// .or(warp::host::exact("bar.com").map(|| "you've reached bar.com")); +/// ``` +pub fn exact(expected: &str) -> impl Filter<Extract = (), Error = Rejection> + Clone { + let expected = Authority::from_str(expected).expect("invalid host/authority"); + optional() + .and_then(move |option: Option<Authority>| match option { + Some(authority) if authority == expected => future::ok(()), + _ => future::err(reject::not_found()), + }) + .untuple_one() +} + +/// Creates a `Filter` that looks for an authority (target server's host +/// and port) in the request. +/// +/// Authority is specified either in the `Host` header or in the target URI. +/// +/// If found, extracts the `Authority`, otherwise continues the request, +/// extracting `None`. +/// +/// Rejects with `400 Bad Request` if the `Host` header is malformed or if there +/// is a mismatch between the `Host` header and the target URI. +/// +/// # Example +/// +/// ``` +/// use warp::{Filter, host::Authority}; +/// +/// let host = warp::host::optional() +/// .map(|authority: Option<Authority>| { +/// if let Some(a) = authority { +/// format!("{} is currently not at home", a.host()) +/// } else { +/// "please state who you're trying to reach".to_owned() +/// } +/// }); +/// ``` +pub fn optional() -> impl Filter<Extract = One<Option<Authority>>, Error = Rejection> + Copy { + filter_fn_one(move |route| { + // The authority can be sent by clients in various ways: + // + // 1) in the "target URI" + // a) serialized in the start line (HTTP/1.1 proxy requests) + // b) serialized in `:authority` pseudo-header (HTTP/2 generated - "SHOULD") + // 2) in the `Host` header (HTTP/1.1 origin requests, HTTP/2 converted) + // + // Hyper transparently handles 1a/1b, but not 2, so we must look at both. + + let from_uri = route.uri().authority(); + + let name = "host"; + let from_header = route.headers() + .get(name) + .map(|value| + // Header present, parse it + value.to_str().map_err(|_| reject::invalid_header(name)) + .and_then(|value| Authority::from_str(value).map_err(|_| reject::invalid_header(name))) + ); + + future::ready(match (from_uri, from_header) { + // no authority in the request (HTTP/1.0 or non-conforming) + (None, None) => Ok(None), + + // authority specified in either or both matching + (Some(a), None) => Ok(Some(a.clone())), + (None, Some(Ok(a))) => Ok(Some(a)), + (Some(a), Some(Ok(b))) if *a == b => Ok(Some(b)), + + // mismatch + (Some(_), Some(Ok(_))) => Err(reject::invalid_header(name)), + + // parse error + (_, Some(Err(r))) => Err(r), + }) + }) +} 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..eae5974024 --- /dev/null +++ b/third_party/rust/warp/src/filters/log.rs @@ -0,0 +1,280 @@ +//! 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()) + } + + /// Access the full headers of the request + pub fn request_headers(&self) -> &http::HeaderMap { + self.route.headers() + } +} + +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_util::{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..c4d7462720 --- /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_util::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(); + tracing::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..bd1c48718c --- /dev/null +++ b/third_party/rust/warp/src/filters/mod.rs @@ -0,0 +1,29 @@ +//! 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; +#[cfg(any(feature = "compression-brotli", feature = "compression-gzip"))] +pub mod compression; +pub mod cookie; +pub mod cors; +pub mod ext; +pub mod fs; +pub mod header; +pub mod host; +pub mod log; +pub mod method; +#[cfg(feature = "multipart")] +pub mod multipart; +pub mod path; +pub mod query; +pub mod reply; +pub mod sse; +pub mod trace; +#[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..ef2ec92682 --- /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_util::{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 extract 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_deref() + } + + /// Get the content-type of this part, if present. + pub fn content_type(&self) -> Option<&str> { + self.content_type.as_deref() + } + + /// 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..179a8d1c91 --- /dev/null +++ b/third_party/rust/warp/src/filters/path.rs @@ -0,0 +1,652 @@ +//! 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_util::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| { + tracing::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| { + tracing::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| { + tracing::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 the 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 the tail part of a request path, returned by the [`peek()`] 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() + .cloned() + .unwrap_or_else(|| PathAndQuery::from_static("")) +} + +/// 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`](crate::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) => ( + $crate::path::end() + ); + (@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..7aee569484 --- /dev/null +++ b/third_party/rust/warp/src/filters/query.rs @@ -0,0 +1,91 @@ +//! Query Filters + +use futures_util::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`. +/// +/// # Example +/// +/// ``` +/// use std::collections::HashMap; +/// use warp::{ +/// http::Response, +/// Filter, +/// }; +/// +/// let route = warp::any() +/// .and(warp::query::<HashMap<String, String>>()) +/// .map(|map: HashMap<String, String>| { +/// let mut response: Vec<String> = Vec::new(); +/// for (key, value) in map.into_iter() { +/// response.push(format!("{}={}", key, value)) +/// } +/// Response::builder().body(response.join(";")) +/// }); +/// ``` +/// +/// You can define your custom query object and deserialize with [Serde][Serde]. Ensure to include +/// the crate in your dependencies before usage. +/// +/// ``` +/// use serde_derive::{Deserialize, Serialize}; +/// use std::collections::HashMap; +/// use warp::{ +/// http::Response, +/// Filter, +/// }; +/// +/// #[derive(Serialize, Deserialize)] +/// struct FooQuery { +/// foo: Option<String>, +/// bar: u8, +/// } +/// +/// let route = warp::any() +/// .and(warp::query::<FooQuery>()) +/// .map(|q: FooQuery| { +/// if let Some(foo) = q.foo { +/// Response::builder().body(format!("foo={}", foo)) +/// } else { +/// Response::builder().body(format!("bar={}", q.bar)) +/// } +/// }); +/// ``` +/// +/// For more examples, please take a look at [examples/query_string.rs](https://github.com/seanmonstar/warp/blob/master/examples/query_string.rs). +/// +/// [Serde]: https://docs.rs/serde +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(|| { + tracing::debug!("route was called without a query string, defaulting to empty"); + "" + }); + + let query_encoded = serde_urlencoded::from_str(query_string).map_err(|e| { + tracing::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..2fb9b7ef11 --- /dev/null +++ b/third_party/rust/warp/src/filters/sse.rs @@ -0,0 +1,511 @@ +//! Server-Sent Events (SSE) +//! +//! # Example +//! +//! ``` +//! +//! use std::time::Duration; +//! use std::convert::Infallible; +//! use warp::{Filter, sse::Event}; +//! use futures_util::{stream::iter, Stream}; +//! +//! fn sse_events() -> impl Stream<Item = Result<Event, Infallible>> { +//! iter(vec![ +//! Ok(Event::default().data("unnamed event")), +//! Ok( +//! Event::default().event("chat") +//! .data("chat message") +//! ), +//! Ok( +//! Event::default().id(13.to_string()) +//! .event("chat") +//! .data("other chat message\nwith next line") +//! .retry(Duration::from_millis(5000)) +//! ) +//! ]) +//! } +//! +//! 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 serde::Serialize; +use std::borrow::Cow; +use std::error::Error as StdError; +use std::fmt::{self, Write}; +use std::future::Future; +use std::pin::Pin; +use std::str::FromStr; +use std::task::{Context, Poll}; +use std::time::Duration; + +use futures_util::{future, Stream, TryStream, TryStreamExt}; +use http::header::{HeaderValue, CACHE_CONTROL, CONTENT_TYPE}; +use hyper::Body; +use pin_project::pin_project; +use serde_json::{self, Error}; +use tokio::time::{self, Sleep}; + +use self::sealed::SseError; +use super::header; +use crate::filter::One; +use crate::reply::Response; +use crate::{Filter, Rejection, Reply}; + +// Server-sent event data type +#[derive(Debug)] +enum DataType { + Text(String), + Json(String), +} + +/// Server-sent event +#[derive(Default, Debug)] +pub struct Event { + id: Option<String>, + data: Option<DataType>, + event: Option<String>, + comment: Option<String>, + retry: Option<Duration>, +} + +impl Event { + /// Set Server-sent event data + /// data field(s) ("data:<content>") + pub fn data<T: Into<String>>(mut self, data: T) -> Event { + self.data = Some(DataType::Text(data.into())); + self + } + + /// Set Server-sent event data + /// data field(s) ("data:<content>") + pub fn json_data<T: Serialize>(mut self, data: T) -> Result<Event, Error> { + self.data = Some(DataType::Json(serde_json::to_string(&data)?)); + Ok(self) + } + + /// Set Server-sent event comment + /// Comment field (":<comment-text>") + pub fn comment<T: Into<String>>(mut self, comment: T) -> Event { + self.comment = Some(comment.into()); + self + } + + /// Set Server-sent event event + /// Event name field ("event:<event-name>") + pub fn event<T: Into<String>>(mut self, event: T) -> Event { + self.event = Some(event.into()); + self + } + + /// Set Server-sent event retry + /// Retry timeout field ("retry:<timeout>") + pub fn retry(mut self, duration: Duration) -> Event { + self.retry = Some(duration); + self + } + + /// Set Server-sent event id + /// Identifier field ("id:<identifier>") + pub fn id<T: Into<String>>(mut self, id: T) -> Event { + self.id = Some(id.into()); + self + } +} + +impl fmt::Display for Event { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref comment) = &self.comment { + ":".fmt(f)?; + comment.fmt(f)?; + f.write_char('\n')?; + } + + if let Some(ref event) = &self.event { + "event:".fmt(f)?; + event.fmt(f)?; + f.write_char('\n')?; + } + + match self.data { + Some(DataType::Text(ref data)) => { + for line in data.split('\n') { + "data:".fmt(f)?; + line.fmt(f)?; + f.write_char('\n')?; + } + } + Some(DataType::Json(ref data)) => { + "data:".fmt(f)?; + data.fmt(f)?; + f.write_char('\n')?; + } + None => {} + } + + if let Some(ref id) = &self.id { + "id:".fmt(f)?; + id.fmt(f)?; + f.write_char('\n')?; + } + + if let Some(ref duration) = &self.retry { + "retry:".fmt(f)?; + + let secs = duration.as_secs(); + let millis = duration.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')?; + } + + f.write_char('\n')?; + Ok(()) + } +} + +/// 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_util::Stream; +/// use futures_util::stream::iter; +/// use std::convert::Infallible; +/// use warp::{Filter, sse::Event}; +/// use serde_derive::Serialize; +/// +/// #[derive(Serialize)] +/// struct Msg { +/// from: u32, +/// text: String, +/// } +/// +/// fn event_stream() -> impl Stream<Item = Result<Event, Infallible>> { +/// iter(vec![ +/// // Unnamed event with data only +/// Ok(Event::default().data("payload")), +/// // Named event with ID and retry timeout +/// Ok( +/// Event::default().data("other message\nwith next line") +/// .event("chat") +/// .id(1.to_string()) +/// .retry(Duration::from_millis(15000)) +/// ), +/// // Event with JSON data +/// Ok( +/// Event::default().id(2.to_string()) +/// .json_data(Msg { +/// from: 2, +/// text: "hello".into(), +/// }).unwrap(), +/// ) +/// ]) +/// } +/// +/// 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<Ok = Event> + Send + 'static, + 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<Ok = Event> + Send + 'static, + 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(Ok(event.to_string()))); + + 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 = Event, Error = impl StdError + Send + Sync + 'static> + Send + 'static + where + S: TryStream<Ok = Event> + Send + 'static, + S::Error: StdError + Send + Sync + 'static, + { + let alive_timer = time::sleep(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, + #[pin] + alive_timer: Sleep, +} + +/// 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_util::StreamExt; +/// use tokio::time::interval; +/// use tokio_stream::wrappers::IntervalStream; +/// use warp::{Filter, Stream, sse::Event}; +/// +/// // create server-sent event +/// fn sse_counter(counter: u64) -> Result<Event, Infallible> { +/// Ok(Event::default().data(counter.to_string())) +/// } +/// +/// fn main() { +/// let routes = warp::path("ticks") +/// .and(warp::get()) +/// .map(|| { +/// let mut counter: u64 = 0; +/// let interval = interval(Duration::from_secs(15)); +/// let stream = IntervalStream::new(interval); +/// let event_stream = stream.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<Ok = Event> + Send + 'static, + S::Error: StdError + Send + Sync + 'static, +{ + type Item = Result<Event, 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(); + let event = Event::default().comment(comment_str); + Poll::Ready(Some(Ok(event))) + } + }, + Poll::Ready(Some(Ok(event))) => { + // restart timer + pin.alive_timer + .reset(tokio::time::Instant::now() + *pin.max_interval); + Poll::Ready(Some(Ok(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 fmt::Display for SseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "sse error") + } + } + + impl StdError for SseError {} +} diff --git a/third_party/rust/warp/src/filters/trace.rs b/third_party/rust/warp/src/filters/trace.rs new file mode 100644 index 0000000000..5ca4e9df96 --- /dev/null +++ b/third_party/rust/warp/src/filters/trace.rs @@ -0,0 +1,309 @@ +//! [`tracing`] filters. +//! +//! [`tracing`] is a framework for instrumenting Rust programs to +//! collect scoped, structured, and async-aware diagnostics. This module +//! provides a set of filters for instrumenting Warp applications with `tracing` +//! spans. [`Spans`] can be used to associate individual events with a request, +//! and track contexts through the application. +//! +//! [`tracing`]: https://crates.io/crates/tracing +//! [`Spans`]: https://docs.rs/tracing/latest/tracing/#spans +use tracing::Span; + +use std::net::SocketAddr; + +use http::{self, header}; + +use crate::filter::{Filter, WrapSealed}; +use crate::reject::IsReject; +use crate::reply::Reply; +use crate::route::Route; + +use self::internal::WithTrace; + +/// Create a wrapping filter that instruments every request with a `tracing` +/// [`Span`] at the [`INFO`] level, containing a summary of the request. +/// Additionally, if the [`DEBUG`] level is enabled, the span will contain an +/// event recording the request's headers. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::any() +/// .map(warp::reply) +/// .with(warp::trace::request()); +/// ``` +/// +/// [`Span`]: https://docs.rs/tracing/latest/tracing/#spans +/// [`INFO`]: https://docs.rs/tracing/0.1.16/tracing/struct.Level.html#associatedconstant.INFO +/// [`DEBUG`]: https://docs.rs/tracing/0.1.16/tracing/struct.Level.html#associatedconstant.DEBUG +pub fn request() -> Trace<impl Fn(Info<'_>) -> Span + Clone> { + use tracing::field::{display, Empty}; + trace(|info: Info<'_>| { + let span = tracing::info_span!( + "request", + remote.addr = Empty, + method = %info.method(), + path = %info.path(), + version = ?info.route.version(), + referer = Empty, + ); + + // Record optional fields. + if let Some(remote_addr) = info.remote_addr() { + span.record("remote.addr", &display(remote_addr)); + } + + if let Some(referer) = info.referer() { + span.record("referer", &display(referer)); + } + + tracing::debug!(parent: &span, "received request"); + + span + }) +} + +/// Create a wrapping filter that instruments every request with a custom +/// `tracing` [`Span`] provided by a function. +/// +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::any() +/// .map(warp::reply) +/// .with(warp::trace(|info| { +/// // Create a span using tracing macros +/// tracing::info_span!( +/// "request", +/// method = %info.method(), +/// path = %info.path(), +/// ) +/// })); +/// ``` +/// +/// [`Span`]: https://docs.rs/tracing/latest/tracing/#spans +pub fn trace<F>(func: F) -> Trace<F> +where + F: Fn(Info<'_>) -> Span + Clone, +{ + Trace { func } +} + +/// Create a wrapping filter that instruments every request with a `tracing` +/// [`Span`] at the [`DEBUG`] level representing a named context. +/// +/// This can be used to instrument multiple routes with their own sub-spans in a +/// per-request trace. +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let hello = warp::path("hello") +/// .map(warp::reply) +/// .with(warp::trace::named("hello")); +/// +/// let goodbye = warp::path("goodbye") +/// .map(warp::reply) +/// .with(warp::trace::named("goodbye")); +/// +/// let routes = hello.or(goodbye); +/// ``` +/// +/// [`Span`]: https://docs.rs/tracing/latest/tracing/#spans +/// [`DEBUG`]: https://docs.rs/tracing/0.1.16/tracing/struct.Level.html#associatedconstant.DEBUG +pub fn named(name: &'static str) -> Trace<impl Fn(Info<'_>) -> Span + Copy> { + trace(move |_| tracing::debug_span!("context", "{}", name,)) +} + +/// Decorates a [`Filter`](crate::Filter) to create a [`tracing`] [span] for +/// requests and responses. +/// +/// [`tracing`]: https://crates.io/crates/tracing +/// [span]: https://docs.rs/tracing/latest/tracing/#spans +#[derive(Clone, Copy, Debug)] +pub struct Trace<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, +} + +impl<FN, F> WrapSealed<F> for Trace<FN> +where + FN: Fn(Info<'_>) -> Span + Clone + Send, + F: Filter + Clone + Send, + F::Extract: Reply, + F::Error: IsReject, +{ + type Wrapped = WithTrace<FN, F>; + + fn wrap(&self, filter: F) -> Self::Wrapped { + WithTrace { + filter, + trace: 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 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 host of the request + pub fn host(&self) -> Option<&str> { + self.route + .headers() + .get(header::HOST) + .and_then(|v| v.to_str().ok()) + } + + /// View the request headers. + pub fn request_headers(&self) -> &http::HeaderMap { + self.route.headers() + } +} + +mod internal { + use futures_util::{future::Inspect, future::MapOk, FutureExt, TryFutureExt}; + + use super::{Info, Trace}; + use crate::filter::{Filter, FilterBase, Internal}; + use crate::reject::IsReject; + use crate::reply::Reply; + use crate::reply::Response; + use crate::route; + + #[allow(missing_debug_implementations)] + pub struct Traced(pub(super) Response); + + impl Reply for Traced { + #[inline] + fn into_response(self) -> Response { + self.0 + } + } + + #[allow(missing_debug_implementations)] + #[derive(Clone, Copy)] + pub struct WithTrace<FN, F> { + pub(super) filter: F, + pub(super) trace: Trace<FN>, + } + + use tracing::instrument::{Instrument, Instrumented}; + use tracing::Span; + + fn finished_logger<E: IsReject>(reply: &Result<(Traced,), E>) { + let (status, error) = match reply { + Ok((Traced(resp),)) => (resp.status(), None), + Err(error) => (error.status(), Some(error)), + }; + + if status.is_success() { + tracing::info!( + target: "warp::filters::trace", + status = status.as_u16(), + "finished processing with success" + ); + } else if status.is_server_error() { + tracing::error!( + target: "warp::filters::trace", + status = status.as_u16(), + error = ?error, + "unable to process request (internal error)" + ); + } else if status.is_client_error() { + tracing::warn!( + target: "warp::filters::trace", + status = status.as_u16(), + error = ?error, + "unable to serve request (client error)" + ); + } else { + // Either informational or redirect + tracing::info!( + target: "warp::filters::trace", + status = status.as_u16(), + error = ?error, + "finished processing with status" + ); + } + } + + fn convert_reply<R: Reply>(reply: R) -> (Traced,) { + (Traced(reply.into_response()),) + } + + impl<FN, F> FilterBase for WithTrace<FN, F> + where + FN: Fn(Info<'_>) -> Span + Clone + Send, + F: Filter + Clone + Send, + F::Extract: Reply, + F::Error: IsReject, + { + type Extract = (Traced,); + type Error = F::Error; + type Future = Instrumented< + Inspect< + MapOk<F::Future, fn(F::Extract) -> Self::Extract>, + fn(&Result<Self::Extract, F::Error>), + >, + >; + + fn filter(&self, _: Internal) -> Self::Future { + let span = route::with(|route| (self.trace.func)(Info { route })); + let _entered = span.enter(); + + tracing::info!(target: "warp::filters::trace", "processing request"); + self.filter + .filter(Internal) + .map_ok(convert_reply as fn(F::Extract) -> Self::Extract) + .inspect(finished_logger as fn(&Result<Self::Extract, F::Error>)) + .instrument(span.clone()) + } + } +} 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..1e953c7a35 --- /dev/null +++ b/third_party/rust/warp/src/filters/ws.rs @@ -0,0 +1,410 @@ +//! Websockets Filters + +use std::borrow::Cow; +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use super::header; +use crate::filter::{filter_fn_one, Filter, One}; +use crate::reject::Rejection; +use crate::reply::{Reply, Response}; +use futures_util::{future, ready, FutureExt, Sink, Stream, TryFutureExt}; +use headers::{Connection, HeaderMapExt, SecWebsocketAccept, SecWebsocketKey, Upgrade}; +use http; +use hyper::upgrade::OnUpgrade; +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(on_upgrade()) + .map( + move |key: SecWebsocketKey, on_upgrade: Option<OnUpgrade>| Ws { + config: None, + key, + on_upgrade, + }, + ) +} + +/// Extracted by the [`ws`](ws) filter, and used to finish an upgrade. +pub struct Ws { + config: Option<WebSocketConfig>, + key: SecWebsocketKey, + on_upgrade: Option<OnUpgrade>, +} + +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 + } + + /// Set the maximum frame size (defaults to 16 megabytes) + pub fn max_frame_size(mut self, max: usize) -> Self { + self.config + .get_or_insert_with(WebSocketConfig::default) + .max_frame_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 { + if let Some(on_upgrade) = self.ws.on_upgrade { + let on_upgrade_cb = self.on_upgrade; + let config = self.ws.config; + let fut = on_upgrade + .and_then(move |upgraded| { + tracing::trace!("websocket upgrade complete"); + WebSocket::from_raw_socket(upgraded, protocol::Role::Server, config).map(Ok) + }) + .and_then(move |socket| on_upgrade_cb(socket).map(Ok)) + .map(|result| { + if let Err(err) = result { + tracing::debug!("ws upgrade error: {}", err); + } + }); + ::tokio::task::spawn(fut); + } else { + tracing::debug!("ws couldn't be upgraded since no upgrade state was present"); + } + + 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 + } +} + +// Extracts OnUpgrade state from the route. +fn on_upgrade() -> impl Filter<Extract = (Option<OnUpgrade>,), Error = Rejection> + Copy { + filter_fn_one(|route| future::ready(Ok(route.extensions_mut().remove::<OnUpgrade>()))) +} + +/// A websocket `Stream` and `Sink`, provided to `ws` filters. +/// +/// Ping messages sent from the client will be handled internally by replying with a Pong message. +/// Close messages need to be handled explicitly: usually by closing the `Sink` end of the +/// `WebSocket`. +/// +/// **Note!** +/// Due to rust futures nature, pings won't be handled until read part of `WebSocket` is polled + +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)) => { + tracing::debug!("websocket poll error: {}", e); + Poll::Ready(Some(Err(crate::Error::new(e)))) + } + None => { + tracing::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) => { + tracing::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) => { + tracing::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. +/// +/// 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 a new Pong `Message`. + /// + /// Note that one rarely needs to manually construct a Pong message because the underlying tungstenite socket + /// automatically responds to the Ping messages it receives. Manual construction might still be useful in some cases + /// like in tests or to send unidirectional heartbeats. + pub fn pong<V: Into<Vec<u8>>>(v: V) -> Message { + Message { + inner: protocol::Message::Pong(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 the close frame (close code and reason) + pub fn close_frame(&self) -> Option<(u16, &str)> { + if let protocol::Message::Close(Some(ref close_frame)) = self.inner { + Some((close_frame.code.into(), close_frame.reason.as_ref())) + } else { + None + } + } + + /// 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(_) => &[], + protocol::Message::Frame(ref frame) => frame.payload(), + } + } + + /// 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 From<Message> for Vec<u8> { + fn from(m: Message) -> Self { + m.into_bytes() + } +} + +// ===== Rejections ===== + +/// Connection header did not include 'upgrade' +#[derive(Debug)] +pub struct MissingConnectionUpgrade; + +impl fmt::Display for MissingConnectionUpgrade { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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..5350d22156 --- /dev/null +++ b/third_party/rust/warp/src/generic.rs @@ -0,0 +1,250 @@ +#[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; + + #[inline] + fn combine<T>(self, other: T) -> CombinedTuples<Self, T> + where + Self: Sized, + T: Tuple, + Self::HList: Combine<T::HList>, + { + self.hlist().combine(other.hlist()).flatten() + } +} + +pub type CombinedTuples<T, U> = + <<<T as Tuple>::HList as Combine<<U as Tuple>::HList>>::Output as HList>::Tuple; + +// 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..a965b1d1b8 --- /dev/null +++ b/third_party/rust/warp/src/lib.rs @@ -0,0 +1,179 @@ +#![doc(html_root_url = "https://docs.rs/warp/0.3.3")] +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(rust_2018_idioms)] +#![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`](self::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 = "compression")] +#[doc(hidden)] +pub use self::filters::compression; +#[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, + host, + 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, + trace, + // trace() function + trace::trace, +}; +// ws() function +pub use self::filter::wrap_fn; +#[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_util::{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..ee2c3ad798 --- /dev/null +++ b/third_party/rust/warp/src/redirect.rs @@ -0,0 +1,132 @@ +//! 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}; + +pub use self::sealed::AsLocation; +use crate::reply::{self, Reply}; + +/// A simple `301` permanent 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 `302` found redirect to a different location +/// +/// # Example +/// +/// ``` +/// use warp::{http::Uri, Filter}; +/// +/// let route = warp::path("v1") +/// .map(|| { +/// warp::redirect::found(Uri::from_static("/v2")) +/// }); +/// ``` +pub fn found(uri: impl AsLocation) -> impl Reply { + reply::with_header(StatusCode::FOUND, header::LOCATION, uri.header_value()) +} + +/// A simple `303` redirect to a different location. +/// +/// The HTTP method of the request to the new location will always be `GET`. +/// +/// # Example +/// +/// ``` +/// use warp::{http::Uri, Filter}; +/// +/// let route = warp::path("v1") +/// .map(|| { +/// warp::redirect::see_other(Uri::from_static("/v2")) +/// }); +/// ``` +pub fn see_other(uri: impl AsLocation) -> impl Reply { + reply::with_header(StatusCode::SEE_OTHER, header::LOCATION, uri.header_value()) +} + +/// A simple `307` temporary redirect to a different location. +/// +/// This is similar to [`see_other`](fn@see_other) but the HTTP method and the body of the request +/// to the new location will be the same as the method and body of the current request. +/// +/// # 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(), + ) +} + +/// A simple `308` permanent redirect to a different location. +/// +/// This is similar to [`redirect`](fn@redirect) but the HTTP method of the request to the new +/// location will be the same as the method of the current request. +/// +/// # Example +/// +/// ``` +/// use warp::{http::Uri, Filter}; +/// +/// let route = warp::path("v1") +/// .map(|| { +/// warp::redirect::permanent(Uri::from_static("/v2")) +/// }); +/// ``` +pub fn permanent(uri: impl AsLocation) -> impl Reply { + reply::with_header( + StatusCode::PERMANENT_REDIRECT, + header::LOCATION, + uri.header_value(), + ) +} + +mod sealed { + use bytes::Bytes; + use http::{header::HeaderValue, Uri}; + + /// Trait for redirect locations. Currently only a `Uri` can be used in + /// redirect. + /// This sealed trait exists 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..fd09188412 --- /dev/null +++ b/third_party/rust/warp/src/reject.rs @@ -0,0 +1,844 @@ +//! 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. +//! +//! As a request is processed by a Filter chain, the rejections are accumulated into +//! a list contained by the [`Rejection`](struct.Rejection.html) type. Rejections from +//! filters can be handled using [`Filter::recover`](../trait.Filter.html#method.recover). +//! This is a convenient way to map rejections into a [`Reply`](../reply/trait.Reply.html). +//! +//! For a more complete example see the +//! [Rejection Example](https://github.com/seanmonstar/warp/blob/master/examples/rejections.rs) +//! from the repository. +//! +//! # Example +//! +//! ``` +//! use warp::{reply, Reply, Filter, reject, Rejection, http::StatusCode}; +//! +//! #[derive(Debug)] +//! struct InvalidParameter; +//! +//! impl reject::Reject for InvalidParameter {} +//! +//! // Custom rejection handler that maps rejections into responses. +//! async fn handle_rejection(err: Rejection) -> Result<impl Reply, std::convert::Infallible> { +//! if err.is_not_found() { +//! Ok(reply::with_status("NOT_FOUND", StatusCode::NOT_FOUND)) +//! } else if let Some(e) = err.find::<InvalidParameter>() { +//! Ok(reply::with_status("BAD_REQUEST", StatusCode::BAD_REQUEST)) +//! } else { +//! eprintln!("unhandled rejection: {:?}", err); +//! Ok(reply::with_status("INTERNAL_SERVER_ERROR", StatusCode::INTERNAL_SERVER_ERROR)) +//! } +//! } +//! +//! +//! // Filter on `/:id`, but reject with InvalidParameter if the `id` is `0`. +//! // Recover from this rejection using a custom rejection handler. +//! let route = warp::path::param() +//! .and_then(|id: u32| async move { +//! if id == 0 { +//! Err(warp::reject::custom(InvalidParameter)) +//! } else { +//! Ok("id is valid") +//! } +//! }) +//! .recover(handle_rejection); +//! ``` + +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. +/// +/// Can be converted into Rejection. +/// +/// # 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 { + matches!(self.reason, Reason::NotFound) + } +} + +impl<T: Reject> From<T> for Rejection { + #[inline] + fn from(err: T) -> Rejection { + custom(err) + } +} + +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) => { + tracing::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 fmt::Display for MissingHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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 fmt::Display for InvalidHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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 fmt::Display for MissingCookie { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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..74dee278de --- /dev/null +++ b/third_party/rust/warp/src/reply.rs @@ -0,0 +1,584 @@ +//! 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| { + tracing::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 = ♥</h1> +/// </body> +/// </html> +/// "#; +/// +/// let route = warp::any() +/// .map(move || { +/// warp::reply::html(body) +/// }); +/// ``` +pub fn html<T>(body: T) -> Html<T> +where + Body: From<T>, + T: Send, +{ + Html { body } +} + +/// An HTML reply. +#[allow(missing_debug_implementations)] +pub 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) => { + tracing::error!("with_header value error: {}", err.into()); + Reply_(::reject::server_error() + .into_response()) + } + }, + Err(err) => { + tracing::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) => { + let err = err.into(); + tracing::error!("with_header value error: {}", err); + None + } + }, + Err(err) => { + let err = err.into(); + tracing::error!("with_header name error: {}", err); + 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) => { + tracing::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..afbac4d8ba --- /dev/null +++ b/third_party/rust/warp/src/route.rs @@ -0,0 +1,140 @@ +use scoped_tls::scoped_thread_local; +use std::cell::RefCell; +use std::mem; +use std::net::SocketAddr; + +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() + } + + #[cfg(feature = "websocket")] + 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.is_empty() { + // malformed path + return; + } else 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..f1eb33b952 --- /dev/null +++ b/third_party/rust/warp/src/server.rs @@ -0,0 +1,576 @@ +#[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_util::{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 tracing::Instrument; + +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>) { + let (addr, fut) = self.bind_ephemeral(addr); + let span = tracing::info_span!("Server::run", ?addr); + tracing::info!(parent: &span, "listening on http://{}", addr); + + fut.instrument(span).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()) + .instrument(tracing::info_span!("Server::run_incoming")) + .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); + + tracing::info!("listening with custom incoming"); + + fut.await; + } + + /// Bind to a socket address, returning a `Future` that can be + /// executed on the current 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>) { + let addr = addr.into(); + let srv = match try_bind!(self, &addr) { + Ok((_, srv)) => srv, + Err(err) => { + tracing::error!("error binding to {}: {}", addr, err); + return; + } + }; + + srv.map(|result| { + if let Err(err) = result { + tracing::error!("server error: {}", err) + } + }) + .await; + } + + /// Bind to a possibly ephemeral socket address. + /// + /// Returns the bound address and a `Future` that can be executed on + /// the current runtime. + /// + /// # Panics + /// + /// Panics if we are unable to bind to the provided address. + pub fn bind_ephemeral( + self, + addr: impl Into<SocketAddr>, + ) -> (SocketAddr, impl Future<Output = ()> + 'static) { + let (addr, srv) = bind!(self, addr); + let srv = srv.map(|result| { + if let Err(err) = result { + tracing::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 + /// the current runtime. + pub fn try_bind_ephemeral( + self, + addr: impl Into<SocketAddr>, + ) -> 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 { + tracing::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 + /// the current runtime. + /// + /// # Example + /// + /// ```no_run + /// use warp::Filter; + /// use futures_util::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 { + tracing::error!("server error: {}", err) + } + }); + (addr, fut) + } + + /// Create a server with graceful shutdown signal. + /// + /// When the signal completes, the server will start the graceful shutdown + /// process. + pub fn try_bind_with_graceful_shutdown( + self, + addr: impl Into<SocketAddr> + 'static, + signal: impl Future<Output = ()> + Send + '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.with_graceful_shutdown(signal).map(|result| { + if let Err(err) = result { + tracing::error!("server error: {}", err) + } + }); + + Ok((addr, srv)) + } + + /// 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 the current runtime. + pub fn serve_incoming<I>(self, incoming: I) -> impl Future<Output = ()> + where + I: TryStream + Send, + 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) + .instrument(tracing::info_span!("Server::serve_incoming")) + } + + /// Setup this `Server` with a specific stream of incoming connections and a + /// signal to initiate graceful shutdown. + /// + /// This can be used for Unix Domain Sockets, or TLS, etc. + /// + /// When the signal completes, the server will start the graceful shutdown + /// process. + /// + /// Returns a `Future` that can be executed on the current runtime. + pub fn serve_incoming_with_graceful_shutdown<I>( + self, + incoming: I, + signal: impl Future<Output = ()> + Send + 'static, + ) -> impl Future<Output = ()> + where + I: TryStream + Send, + I::Ok: AsyncRead + AsyncWrite + Send + 'static + Unpin, + I::Error: Into<Box<dyn StdError + Send + Sync>>, + { + let incoming = incoming.map_ok(crate::transport::LiftIo); + let service = into_service!(self.filter); + let pipeline = self.pipeline; + + async move { + let srv = + HyperServer::builder(hyper::server::accept::from_stream(incoming.into_stream())) + .http1_pipeline_flush(pipeline) + .serve(service) + .with_graceful_shutdown(signal) + .await; + + if let Err(err) = srv { + tracing::error!("server error: {}", err); + } + } + .instrument(tracing::info_span!( + "Server::serve_incoming_with_graceful_shutdown" + )) + } + + 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 { + tracing::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. + /// + /// *This function requires the `"tls"` feature.* + 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. + /// + /// *This function requires the `"tls"` feature.* + pub fn cert_path(self, path: impl AsRef<Path>) -> Self { + self.with_tls(|tls| tls.cert_path(path)) + } + + /// Specify the file path to read the trust anchor for optional client authentication. + /// + /// Anonymous and authenticated clients will be accepted. If no trust anchor is provided by any + /// of the `client_auth_` methods, then client authentication is disabled by default. + /// + /// *This function requires the `"tls"` feature.* + pub fn client_auth_optional_path(self, path: impl AsRef<Path>) -> Self { + self.with_tls(|tls| tls.client_auth_optional_path(path)) + } + + /// Specify the file path to read the trust anchor for required client authentication. + /// + /// Only authenticated clients will be accepted. If no trust anchor is provided by any of the + /// `client_auth_` methods, then client authentication is disabled by default. + /// + /// *This function requires the `"tls"` feature.* + pub fn client_auth_required_path(self, path: impl AsRef<Path>) -> Self { + self.with_tls(|tls| tls.client_auth_required_path(path)) + } + + /// Specify the in-memory contents of the private key. + /// + /// *This function requires the `"tls"` feature.* + 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. + /// + /// *This function requires the `"tls"` feature.* + pub fn cert(self, cert: impl AsRef<[u8]>) -> Self { + self.with_tls(|tls| tls.cert(cert.as_ref())) + } + + /// Specify the in-memory contents of the trust anchor for optional client authentication. + /// + /// Anonymous and authenticated clients will be accepted. If no trust anchor is provided by any + /// of the `client_auth_` methods, then client authentication is disabled by default. + /// + /// *This function requires the `"tls"` feature.* + pub fn client_auth_optional(self, trust_anchor: impl AsRef<[u8]>) -> Self { + self.with_tls(|tls| tls.client_auth_optional(trust_anchor.as_ref())) + } + + /// Specify the in-memory contents of the trust anchor for required client authentication. + /// + /// Only authenticated clients will be accepted. If no trust anchor is provided by any of the + /// `client_auth_` methods, then client authentication is disabled by default. + /// + /// *This function requires the `"tls"` feature.* + pub fn client_auth_required(self, trust_anchor: impl AsRef<[u8]>) -> Self { + self.with_tls(|tls| tls.client_auth_required(trust_anchor.as_ref())) + } + + /// Specify the DER-encoded OCSP response. + /// + /// *This function requires the `"tls"` feature.* + pub fn ocsp_resp(self, resp: impl AsRef<[u8]>) -> Self { + self.with_tls(|tls| tls.ocsp_resp(resp.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>) { + let (addr, fut) = self.bind_ephemeral(addr); + let span = tracing::info_span!("TlsServer::run", %addr); + tracing::info!(parent: &span, "listening on https://{}", addr); + + fut.instrument(span).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>) { + 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 + /// the current runtime. + /// + /// *This function requires the `"tls"` feature.* + pub fn bind_ephemeral( + self, + addr: impl Into<SocketAddr>, + ) -> (SocketAddr, impl Future<Output = ()> + 'static) { + let (addr, srv) = bind!(tls: self, addr); + let srv = srv.map(|result| { + if let Err(err) = result { + tracing::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 { + tracing::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..3c67c34d84 --- /dev/null +++ b/third_party/rust/warp/src/test.rs @@ -0,0 +1,764 @@ +//! 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; +//! #[tokio::test] +//! async 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) +//! .await +//! .unwrap(); +//! assert_eq!(value, 3); +//! +//! // Or simply test if a request matches (doesn't reject). +//! assert!( +//! warp::test::request() +//! .path("/1/-5") +//! .matches(&filter) +//! .await +//! ); +//! } +//! ``` +//! +//! 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::Context; +#[cfg(feature = "websocket")] +use std::task::{self, Poll}; + +use bytes::Bytes; +#[cfg(feature = "websocket")] +use futures_channel::mpsc; +#[cfg(feature = "websocket")] +use futures_util::StreamExt; +use futures_util::{future, FutureExt, TryFutureExt}; +use http::{ + header::{HeaderName, HeaderValue}, + Response, +}; +use serde::Serialize; +use serde_json; +#[cfg(feature = "websocket")] +use tokio::sync::oneshot; + +use crate::filter::Filter; +#[cfg(feature = "websocket")] +use crate::filters::ws::Message; +use crate::reject::IsReject; +use crate::reply::Reply; +use crate::route::{self, Route}; +use crate::Request; +#[cfg(feature = "websocket")] +use crate::{Sink, Stream}; + +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 + } + + /// Set the remote address of this request + /// + /// Default is no remote address. + /// + /// # Example + /// ``` + /// use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + /// + /// let req = warp::test::request() + /// .remote_addr(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080)); + /// ``` + pub fn remote_addr(mut self, addr: SocketAddr) -> Self { + self.remote_addr = Some(addr); + 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) => { + tracing::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)) + }), + ); + + 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 the provided filter. + /// + /// If the handshake succeeds, returns a `WsClient`. + /// + /// # Example + /// + /// ```no_run + /// use futures_util::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(); + let (rd_tx, rd_rx) = mpsc::unbounded(); + + 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 query_string = match req.uri().query() { + Some(q) => format!("?{}", q), + None => String::from(""), + }; + + let uri = format!("http://{}{}{}", addr, req.uri().path(), query_string) + .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(hyper::upgrade::on); + + 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.unbounded_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.unbounded_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(|result| 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(()) + }) + } + + fn pinned_tx(self: Pin<&mut Self>) -> Pin<&mut mpsc::UnboundedSender<crate::ws::Message>> { + let this = Pin::into_inner(self); + Pin::new(&mut this.tx) + } +} + +#[cfg(feature = "websocket")] +impl fmt::Debug for WsClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WsClient").finish() + } +} + +#[cfg(feature = "websocket")] +impl Sink<crate::ws::Message> for WsClient { + type Error = WsError; + + fn poll_ready( + self: Pin<&mut Self>, + context: &mut Context<'_>, + ) -> Poll<Result<(), Self::Error>> { + self.pinned_tx().poll_ready(context).map_err(WsError::new) + } + + fn start_send(self: Pin<&mut Self>, message: Message) -> Result<(), Self::Error> { + self.pinned_tx().start_send(message).map_err(WsError::new) + } + + fn poll_flush( + self: Pin<&mut Self>, + context: &mut Context<'_>, + ) -> Poll<Result<(), Self::Error>> { + self.pinned_tx().poll_flush(context).map_err(WsError::new) + } + + fn poll_close( + self: Pin<&mut Self>, + context: &mut Context<'_>, + ) -> Poll<Result<(), Self::Error>> { + self.pinned_tx().poll_close(context).map_err(WsError::new) + } +} + +#[cfg(feature = "websocket")] +impl Stream for WsClient { + type Item = Result<crate::ws::Message, WsError>; + + fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> { + let this = Pin::into_inner(self); + let rx = Pin::new(&mut this.rx); + match rx.poll_next(context) { + Poll::Ready(Some(result)) => Poll::Ready(Some(result.map_err(WsError::new))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +// ===== 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..1f81a6bd21 --- /dev/null +++ b/third_party/rust/warp/src/tls.rs @@ -0,0 +1,411 @@ +use std::fmt; +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, ReadBuf}; + +use futures_util::ready; +use hyper::server::accept::Accept; +use hyper::server::conn::{AddrIncoming, AddrStream}; + +use crate::transport::Transport; +use tokio_rustls::rustls::{ + server::{AllowAnyAnonymousOrAuthenticatedClient, AllowAnyAuthenticatedClient, NoClientAuth}, + Certificate, Error as TlsError, PrivateKey, RootCertStore, ServerConfig, +}; + +/// 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 fmt::Display for TlsConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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 {} + +/// Tls client authentication configuration. +pub(crate) enum TlsClientAuth { + /// No client auth. + Off, + /// Allow any anonymous or authenticated client. + Optional(Box<dyn Read + Send + Sync>), + /// Allow any authenticated client. + Required(Box<dyn Read + Send + Sync>), +} + +/// 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>, + client_auth: TlsClientAuth, + ocsp_resp: Vec<u8>, +} + +impl fmt::Debug for TlsConfigBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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()), + client_auth: TlsClientAuth::Off, + ocsp_resp: Vec::new(), + } + } + + /// 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 + } + + /// Sets the trust anchor for optional Tls client authentication via file path. + /// + /// Anonymous and authenticated clients will be accepted. If no trust anchor is provided by any + /// of the `client_auth_` methods, then client authentication is disabled by default. + pub(crate) fn client_auth_optional_path(mut self, path: impl AsRef<Path>) -> Self { + let file = Box::new(LazyFile { + path: path.as_ref().into(), + file: None, + }); + self.client_auth = TlsClientAuth::Optional(file); + self + } + + /// Sets the trust anchor for optional Tls client authentication via bytes slice. + /// + /// Anonymous and authenticated clients will be accepted. If no trust anchor is provided by any + /// of the `client_auth_` methods, then client authentication is disabled by default. + pub(crate) fn client_auth_optional(mut self, trust_anchor: &[u8]) -> Self { + let cursor = Box::new(Cursor::new(Vec::from(trust_anchor))); + self.client_auth = TlsClientAuth::Optional(cursor); + self + } + + /// Sets the trust anchor for required Tls client authentication via file path. + /// + /// Only authenticated clients will be accepted. If no trust anchor is provided by any of the + /// `client_auth_` methods, then client authentication is disabled by default. + pub(crate) fn client_auth_required_path(mut self, path: impl AsRef<Path>) -> Self { + let file = Box::new(LazyFile { + path: path.as_ref().into(), + file: None, + }); + self.client_auth = TlsClientAuth::Required(file); + self + } + + /// Sets the trust anchor for required Tls client authentication via bytes slice. + /// + /// Only authenticated clients will be accepted. If no trust anchor is provided by any of the + /// `client_auth_` methods, then client authentication is disabled by default. + pub(crate) fn client_auth_required(mut self, trust_anchor: &[u8]) -> Self { + let cursor = Box::new(Cursor::new(Vec::from(trust_anchor))); + self.client_auth = TlsClientAuth::Required(cursor); + self + } + + /// sets the DER-encoded OCSP response + pub(crate) fn ocsp_resp(mut self, ocsp_resp: &[u8]) -> Self { + self.ocsp_resp = Vec::from(ocsp_resp); + self + } + + pub(crate) fn build(mut self) -> Result<ServerConfig, TlsConfigError> { + let mut cert_rdr = BufReader::new(self.cert); + let cert = rustls_pemfile::certs(&mut cert_rdr) + .map_err(|_e| TlsConfigError::CertParseError)? + .into_iter() + .map(Certificate) + .collect(); + + 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 = rustls_pemfile::pkcs8_private_keys(&mut key_vec.as_slice()) + .map_err(|_e| TlsConfigError::Pkcs8ParseError)?; + + if !pkcs8.is_empty() { + PrivateKey(pkcs8.remove(0)) + } else { + let mut rsa = rustls_pemfile::rsa_private_keys(&mut key_vec.as_slice()) + .map_err(|_e| TlsConfigError::RsaParseError)?; + + if !rsa.is_empty() { + PrivateKey(rsa.remove(0)) + } else { + return Err(TlsConfigError::EmptyKey); + } + } + }; + + fn read_trust_anchor( + trust_anchor: Box<dyn Read + Send + Sync>, + ) -> Result<RootCertStore, TlsConfigError> { + let trust_anchors = { + let mut reader = BufReader::new(trust_anchor); + rustls_pemfile::certs(&mut reader).map_err(TlsConfigError::Io)? + }; + + let mut store = RootCertStore::empty(); + let (added, _skipped) = store.add_parsable_certificates(&trust_anchors); + if added == 0 { + return Err(TlsConfigError::CertParseError); + } + + Ok(store) + } + + let client_auth = match self.client_auth { + TlsClientAuth::Off => NoClientAuth::new(), + TlsClientAuth::Optional(trust_anchor) => { + AllowAnyAnonymousOrAuthenticatedClient::new(read_trust_anchor(trust_anchor)?) + } + TlsClientAuth::Required(trust_anchor) => { + AllowAnyAuthenticatedClient::new(read_trust_anchor(trust_anchor)?) + } + }; + + let mut config = ServerConfig::builder() + .with_safe_defaults() + .with_client_cert_verifier(client_auth.into()) + .with_single_cert_with_ocsp_and_sct(cert, key, self.ocsp_resp, Vec::new()) + .map_err(TlsConfigError::InvalidKey)?; + config.alpn_protocols = vec!["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> { + Some(self.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, + remote_addr: SocketAddr, +} + +impl TlsStream { + fn new(stream: AddrStream, config: Arc<ServerConfig>) -> TlsStream { + let remote_addr = stream.remote_addr(); + let accept = tokio_rustls::TlsAcceptor::from(config).accept(stream); + TlsStream { + state: State::Handshaking(accept), + remote_addr, + } + } +} + +impl AsyncRead for TlsStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll<io::Result<()>> { + 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..be553e706e --- /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, ReadBuf}; + +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 ReadBuf<'_>, + ) -> Poll<io::Result<()>> { + 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/addr.rs b/third_party/rust/warp/tests/addr.rs new file mode 100644 index 0000000000..12fc46936f --- /dev/null +++ b/third_party/rust/warp/tests/addr.rs @@ -0,0 +1,24 @@ +#![deny(warnings)] + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +#[tokio::test] +async fn remote_addr_missing() { + let extract_remote_addr = warp::addr::remote(); + + let req = warp::test::request(); + let resp = req.filter(&extract_remote_addr).await.unwrap(); + assert_eq!(resp, None) +} + +#[tokio::test] +async fn remote_addr_present() { + let extract_remote_addr = warp::addr::remote(); + + let req = warp::test::request().remote_addr("1.2.3.4:5678".parse().unwrap()); + let resp = req.filter(&extract_remote_addr).await.unwrap(); + assert_eq!( + resp, + Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 5678)) + ) +} diff --git a/third_party/rust/warp/tests/body.rs b/third_party/rust/warp/tests/body.rs new file mode 100644 index 0000000000..112eaff141 --- /dev/null +++ b/third_party/rust/warp/tests/body.rs @@ -0,0 +1,202 @@ +#![deny(warnings)] + +use bytes::Buf; +use futures_util::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].chunk(), 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..4cf286a3e1 --- /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::<String>("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::<String>("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::<String>("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..977eab06fc --- /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..4faa933d5e --- /dev/null +++ b/third_party/rust/warp/tests/fs.rs @@ -0,0 +1,271 @@ +#![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); + + let malformed_req = warp::test::request().path("todos.rs"); + assert_eq!(malformed_req.reply(&file).await.status(), 404); +} + +#[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", "Mon, 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", "Mon, 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", "Mon, 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); +} + +#[tokio::test] +async fn byte_ranges_with_excluded_file_size() { + let _ = pretty_env_logger::try_init(); + + let contents = fs::read("README.md").expect("fs::read README.md"); + let file = warp::fs::file("README.md"); + + // range including end of file (non-inclusive result) + let res = warp::test::request() + .header("range", format!("bytes=100-{}", contents.len())) + .reply(&file) + .await; + assert_eq!(res.status(), 206); + assert_eq!( + res.headers()["content-range"], + format!("bytes 100-{}/{}", contents.len() - 1, contents.len()) + ); + assert_eq!( + res.headers()["content-length"], + format!("{}", contents.len() - 100) + ); + assert_eq!(res.body(), &contents[100..=contents.len() - 1]); + + // range with 1 byte to end yields same result as above. (inclusive result) + let res = warp::test::request() + .header("range", format!("bytes=100-{}", contents.len() - 1)) + .reply(&file) + .await; + assert_eq!(res.status(), 206); + assert_eq!( + res.headers()["content-range"], + format!("bytes 100-{}/{}", contents.len() - 1, contents.len()) + ); + assert_eq!( + res.headers()["content-length"], + format!("{}", contents.len() - 100) + ); + assert_eq!(res.body(), &contents[100..=contents.len() - 1]); +} 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/host.rs b/third_party/rust/warp/tests/host.rs new file mode 100644 index 0000000000..2fb5f42475 --- /dev/null +++ b/third_party/rust/warp/tests/host.rs @@ -0,0 +1,87 @@ +#![deny(warnings)] +use warp::host::Authority; + +#[tokio::test] +async fn exact() { + let filter = warp::host::exact("known.com"); + + // no authority + let req = warp::test::request(); + assert!(req.filter(&filter).await.unwrap_err().is_not_found()); + + // specified in URI + let req = warp::test::request().path("http://known.com/about-us"); + assert!(req.filter(&filter).await.is_ok()); + + let req = warp::test::request().path("http://unknown.com/about-us"); + assert!(req.filter(&filter).await.unwrap_err().is_not_found()); + + // specified in Host header + let req = warp::test::request() + .header("host", "known.com") + .path("/about-us"); + assert!(req.filter(&filter).await.is_ok()); + + let req = warp::test::request() + .header("host", "unknown.com") + .path("/about-us"); + assert!(req.filter(&filter).await.unwrap_err().is_not_found()); + + // specified in both - matching + let req = warp::test::request() + .header("host", "known.com") + .path("http://known.com/about-us"); + assert!(req.filter(&filter).await.is_ok()); + + let req = warp::test::request() + .header("host", "unknown.com") + .path("http://unknown.com/about-us"); + assert!(req.filter(&filter).await.unwrap_err().is_not_found()); + + // specified in both - mismatch + let req = warp::test::request() + .header("host", "known.com") + .path("http://known2.com/about-us"); + assert!(req + .filter(&filter) + .await + .unwrap_err() + .find::<warp::reject::InvalidHeader>() + .is_some()); + + // bad host header - invalid chars + let req = warp::test::request() + .header("host", "ðŸ˜") + .path("http://known.com/about-us"); + assert!(req + .filter(&filter) + .await + .unwrap_err() + .find::<warp::reject::InvalidHeader>() + .is_some()); + + // bad host header - invalid format + let req = warp::test::request() + .header("host", "hello space.com") + .path("http://known.com//about-us"); + assert!(req + .filter(&filter) + .await + .unwrap_err() + .find::<warp::reject::InvalidHeader>() + .is_some()); +} + +#[tokio::test] +async fn optional() { + let filter = warp::host::optional(); + + let req = warp::test::request().header("host", "example.com"); + assert_eq!( + req.filter(&filter).await.unwrap(), + Some(Authority::from_static("example.com")) + ); + + let req = warp::test::request(); + assert_eq!(req.filter(&filter).await.unwrap(), None); +} 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..3172367bcb --- /dev/null +++ b/third_party/rust/warp/tests/multipart.rs @@ -0,0 +1,54 @@ +#![deny(warnings)] +use bytes::BufMut; +use futures_util::{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..4f9302036e --- /dev/null +++ b/third_party/rust/warp/tests/path.rs @@ -0,0 +1,411 @@ +#![deny(warnings)] +#[macro_use] +extern crate warp; + +use futures_util::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); + + let ex = warp::test::request() + .path("localhost") + .filter(&tail) + .await + .unwrap(); + assert_eq!(ex.as_str(), "/"); +} + +#[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); + + // Empty + + let req = warp::test::request().path("/"); + let p = path!(); + assert!(req.matches(&p).await); + + let req = warp::test::request().path("/foo"); + let p = path!(); + 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); + + // doesn't panic on authority-form + let ex = warp::test::request() + .path("localhost:1234") + .filter(&full_path) + .await + .unwrap(); + assert_eq!(ex.as_str(), "/"); +} + +#[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..a20f104e67 --- /dev/null +++ b/third_party/rust/warp/tests/query.rs @@ -0,0 +1,139 @@ +#![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 query_struct_no_values() { + let as_struct = warp::query::<MyArgs>(); + + let req = warp::test::request().path("/?foo&baz"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyArgs { + foo: Some("".into()), + baz: Some("".into()) + } + ); +} + +#[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..be7fd33e4a --- /dev/null +++ b/third_party/rust/warp/tests/redirect.rs @@ -0,0 +1,57 @@ +#![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"); +} + +#[tokio::test] +async fn redirect_found_uri() { + let over_there = warp::any().map(|| warp::redirect::found(Uri::from_static("/over-there"))); + + let req = warp::test::request(); + let resp = req.reply(&over_there).await; + + assert_eq!(resp.status(), 302); + assert_eq!(resp.headers()["location"], "/over-there"); +} + +#[tokio::test] +async fn redirect_see_other_uri() { + let over_there = warp::any().map(|| warp::redirect::see_other(Uri::from_static("/over-there"))); + + let req = warp::test::request(); + let resp = req.reply(&over_there).await; + + assert_eq!(resp.status(), 303); + assert_eq!(resp.headers()["location"], "/over-there"); +} + +#[tokio::test] +async fn redirect_temporary_uri() { + let over_there = warp::any().map(|| warp::redirect::temporary(Uri::from_static("/over-there"))); + + let req = warp::test::request(); + let resp = req.reply(&over_there).await; + + assert_eq!(resp.status(), 307); + assert_eq!(resp.headers()["location"], "/over-there"); +} + +#[tokio::test] +async fn redirect_permanent_uri() { + let over_there = warp::any().map(|| warp::redirect::permanent(Uri::from_static("/over-there"))); + + let req = warp::test::request(); + let resp = req.reply(&over_there).await; + + assert_eq!(resp.status(), 308); + 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/tracing.rs b/third_party/rust/warp/tests/tracing.rs new file mode 100644 index 0000000000..0ae88fbc38 --- /dev/null +++ b/third_party/rust/warp/tests/tracing.rs @@ -0,0 +1,48 @@ +use warp::Filter; + +#[tokio::test] +async fn uses_tracing() { + // Setup a log subscriber (responsible to print to output) + let subscriber = tracing_subscriber::fmt::Subscriber::builder() + .with_env_filter("trace") + .without_time() + .finish(); + + // Set the previously created subscriber as the global subscriber + tracing::subscriber::set_global_default(subscriber).unwrap(); + // Redirect normal log messages to the tracing subscriber + tracing_log::LogTracer::init().unwrap(); + + // Start a span with some metadata (fields) + let span = tracing::info_span!("app", domain = "www.example.org"); + let _guard = span.enter(); + + log::info!("logged using log macro"); + + let ok = warp::any() + .map(|| { + tracing::info!("printed for every request"); + }) + .untuple_one() + .and(warp::path("aa")) + .map(|| { + tracing::info!("only printed when path '/aa' matches"); + }) + .untuple_one() + .map(warp::reply) + // Here we add the tracing logger which will ensure that all requests has a span with + // useful information about the request (method, url, version, remote_addr, etc.) + .with(warp::trace::request()); + + tracing::info!("logged using tracing macro"); + + // Send a request for / + let req = warp::test::request(); + let resp = req.reply(&ok); + assert_eq!(resp.await.status(), 404); + + // Send a request for /aa + let req = warp::test::request().path("/aa"); + let resp = req.reply(&ok); + assert_eq!(resp.await.status(), 200); +} diff --git a/third_party/rust/warp/tests/ws.rs b/third_party/rust/warp/tests/ws.rs new file mode 100644 index 0000000000..d5b60356e4 --- /dev/null +++ b/third_party/rust/warp/tests/ws.rs @@ -0,0 +1,288 @@ +#![deny(warnings)] + +use futures_util::{FutureExt, SinkExt, StreamExt}; +use serde_derive::Deserialize; +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 wsclient_sink_and_stream() { + let _ = pretty_env_logger::try_init(); + + let mut client = warp::test::ws() + .handshake(ws_echo()) + .await + .expect("handshake"); + + let message = warp::ws::Message::text("hello"); + SinkExt::send(&mut client, message.clone()).await.unwrap(); + let received_message = client.next().await.unwrap().unwrap(); + assert_eq!(message, received_message); +} + +#[tokio::test] +async fn close_frame() { + let _ = pretty_env_logger::try_init(); + + let route = warp::ws().map(|ws: warp::ws::Ws| { + ws.on_upgrade(|mut websocket| async move { + let msg = websocket.next().await.expect("item").expect("ok"); + let _ = msg.close_frame().expect("close frame"); + }) + }); + + let client = warp::test::ws().handshake(route).await.expect("handshake"); + drop(client); +} + +#[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 pongs_only() { + let _ = pretty_env_logger::try_init(); + + let mut client = warp::test::ws() + .handshake(ws_echo()) + .await + .expect("handshake"); + + // construct a pong message and make sure it is correct + let msg = Message::pong("clt"); + assert!(msg.is_pong()); + assert_eq!(msg.as_bytes(), &b"clt"[..]); + + // send it to echo and wait for `ws_echo` to send it back + client.send(msg).await; + + 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()); +} + +#[tokio::test] +async fn limit_frame_size() { + let _ = pretty_env_logger::try_init(); + + let echo = warp::ws().map(|ws: warp::ws::Ws| { + ws.max_frame_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 length too big: 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()); +} + +#[derive(Deserialize)] +struct MyQuery { + hello: String, +} + +#[tokio::test] +async fn ws_with_query() { + let ws_filter = warp::path("my-ws") + .and(warp::query::<MyQuery>()) + .and(warp::ws()) + .map(|query: MyQuery, ws: warp::ws::Ws| { + assert_eq!(query.hello, "world"); + + ws.on_upgrade(|websocket| { + let (tx, rx) = websocket.split(); + rx.inspect(|i| log::debug!("ws recv: {:?}", i)) + .forward(tx) + .map(|_| ()) + }) + }); + + warp::test::ws() + .path("/my-ws?hello=world") + .handshake(ws_filter) + .await + .expect("handshake"); +} + +// Websocket filter that echoes all messages back. +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(|_| ()) + }) + }) +} |