From dd20b5bbc5094db3dedc26f9e2f87847f721de9d Mon Sep 17 00:00:00 2001 From: Caio Date: Thu, 28 Sep 2023 17:44:55 -0300 Subject: [PATCH 1/2] Rewrite http structures --- .scripts/internal-tests.sh | 7 +- Cargo.lock | 28 +- LICENSE | 201 +++++++++++ wtx-bench/src/main.rs | 12 +- wtx-ui/Cargo.toml | 1 + wtx-ui/LICENSE | 1 + wtx-ui/src/misc.rs | 3 +- wtx/Cargo.toml | 13 +- wtx/examples/common/mod.rs | 3 +- wtx/src/error.rs | 48 +-- wtx/src/http.rs | 13 + wtx/src/http/header.rs | 21 ++ wtx/src/http/httparse.rs | 55 +++ wtx/src/http/request.rs | 21 ++ wtx/src/http/response.rs | 13 + wtx/src/http/version.rs | 13 + wtx/src/http_structs.rs | 12 - wtx/src/http_structs/header.rs | 58 --- wtx/src/http_structs/parse_status.rs | 18 - wtx/src/http_structs/request.rs | 86 ----- wtx/src/http_structs/response.rs | 90 ----- wtx/src/lib.rs | 3 +- wtx/src/web_socket.rs | 10 +- wtx/src/web_socket/compression.rs | 10 +- wtx/src/web_socket/compression/flate2.rs | 9 +- wtx/src/web_socket/handshake.rs | 23 +- wtx/src/web_socket/handshake/misc.rs | 10 +- wtx/src/web_socket/handshake/raw.rs | 428 ++++++++++++----------- wtx/src/web_socket/handshake/tests.rs | 19 +- 29 files changed, 618 insertions(+), 611 deletions(-) create mode 100644 LICENSE create mode 120000 wtx-ui/LICENSE create mode 100644 wtx/src/http.rs create mode 100644 wtx/src/http/header.rs create mode 100644 wtx/src/http/httparse.rs create mode 100644 wtx/src/http/request.rs create mode 100644 wtx/src/http/response.rs create mode 100644 wtx/src/http/version.rs delete mode 100644 wtx/src/http_structs.rs delete mode 100644 wtx/src/http_structs/header.rs delete mode 100644 wtx/src/http_structs/parse_status.rs delete mode 100644 wtx/src/http_structs/request.rs delete mode 100644 wtx/src/http_structs/response.rs diff --git a/.scripts/internal-tests.sh b/.scripts/internal-tests.sh index d2e2fbc1..2c2cb658 100755 --- a/.scripts/internal-tests.sh +++ b/.scripts/internal-tests.sh @@ -16,13 +16,15 @@ $rt rustfmt $rt clippy $rt test-generic wtx +$rt test-with-features wtx arbitrary $rt test-with-features wtx async-std $rt test-with-features wtx base64 +$rt test-with-features wtx embassy-net,_hack $rt test-with-features wtx flate2 $rt test-with-features wtx futures-lite $rt test-with-features wtx glommio -$rt test-with-features wtx http $rt test-with-features wtx httparse +$rt test-with-features wtx monoio $rt test-with-features wtx rand $rt test-with-features wtx rustls-pemfile $rt test-with-features wtx sha1 @@ -31,4 +33,7 @@ $rt test-with-features wtx smol $rt test-with-features wtx std $rt test-with-features wtx tokio $rt test-with-features wtx tokio-rustls +$rt test-with-features wtx tokio-uring +$rt test-with-features wtx tracing $rt test-with-features wtx web-socket-handshake +$rt test-with-features wtx webpki-roots diff --git a/Cargo.lock b/Cargo.lock index 45e4dc94..08847d9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -735,12 +735,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "futures" version = "0.3.28" @@ -966,17 +960,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "httparse" version = "1.8.0" @@ -1043,12 +1026,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - [[package]] name = "jobserver" version = "0.1.26" @@ -1623,9 +1600,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "c1b21f559e07218024e7e9f90f96f601825397de0e25420135f7f952453fed0b" dependencies = [ "lazy_static", ] @@ -2115,7 +2092,6 @@ dependencies = [ "flate2", "futures-lite", "glommio", - "http", "httparse", "monoio", "rand", diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ae9ecd45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Caio + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/wtx-bench/src/main.rs b/wtx-bench/src/main.rs index acf50030..4b834880 100644 --- a/wtx-bench/src/main.rs +++ b/wtx-bench/src/main.rs @@ -20,9 +20,10 @@ use tokio::{net::TcpStream, task::JoinSet}; use wtx::{ rng::StaticRng, web_socket::{ - handshake::WebSocketConnectRaw, FrameBufferVec, FrameMutVec, OpCode, WebSocketClientOwned, + handshake::{WebSocketConnect, WebSocketConnectRaw}, + FrameBufferVec, FrameMutVec, OpCode, }, - UriParts, + PartitionedBuffer, UriParts, }; // Verifies the handling of concurrent calls. @@ -71,15 +72,16 @@ async fn bench(addr: &str, agent: &mut Agent, uri: &str) { let local_uri = uri.to_owned(); async move { let fb = &mut FrameBufferVec::default(); - let (_, mut ws) = WebSocketClientOwned::connect(WebSocketConnectRaw { + let (_, mut ws) = WebSocketConnectRaw { compression: (), fb, headers_buffer: &mut <_>::default(), - pb: <_>::default(), + pb: PartitionedBuffer::default(), rng: StaticRng::default(), stream: TcpStream::connect(&local_addr).await.unwrap(), uri: &local_uri, - }) + } + .connect() .await .unwrap(); for _ in 0..NUM_MESSAGES { diff --git a/wtx-ui/Cargo.toml b/wtx-ui/Cargo.toml index 82e96815..68407307 100644 --- a/wtx-ui/Cargo.toml +++ b/wtx-ui/Cargo.toml @@ -10,6 +10,7 @@ default = [] authors = ["Caio Fernandes "] categories = ["asynchronous", "command-line-interface", "gui"] description = "Different user interfaces for WTX" +documentation = "https://docs.rs/wtx-ui" edition = "2021" keywords = ["io", "network", "websocket"] license = "Apache-2.0" diff --git a/wtx-ui/LICENSE b/wtx-ui/LICENSE new file mode 120000 index 00000000..ea5b6064 --- /dev/null +++ b/wtx-ui/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/wtx-ui/src/misc.rs b/wtx-ui/src/misc.rs index cd1d9784..ef65b9d1 100644 --- a/wtx-ui/src/misc.rs +++ b/wtx-ui/src/misc.rs @@ -59,9 +59,8 @@ pub(crate) async fn _serve( let _jh = tokio::spawn(async move { let sun = || async move { let pb = PartitionedBuffer::default(); - let (_, mut ws) = WebSocketServer::accept(WebSocketAcceptRaw { + let mut ws = WebSocketServer::accept(WebSocketAcceptRaw { compression: (), - headers_buffer: &mut <_>::default(), key_buffer: &mut <_>::default(), pb, rng: StdRng::default(), diff --git a/wtx/Cargo.toml b/wtx/Cargo.toml index 7f558bad..8732fc78 100644 --- a/wtx/Cargo.toml +++ b/wtx/Cargo.toml @@ -50,7 +50,6 @@ embassy-net = { default-features = false, features = ["tcp"], optional = true, v flate2 = { default-features = false, features = ["zlib-ng"], optional = true, version = "1.0" } futures-lite = { default-features = false, optional = true, version = "1.0" } glommio = { default-features = false, optional = true, version = "0.8" } -http = { default-features = false, optional = true, version = "0.2" } httparse = { default-features = false, optional = true, version = "1.0" } monoio = { default-features = false, optional = true, version = "0.1" } rand = { default-features = false, features = ["small_rng"], optional = true, version = "0.8" } @@ -64,19 +63,17 @@ tokio-uring = { default-features = false, optional = true, version = "0.4" } tracing = { default-features = false, features = ["attributes"], optional = true, version = "0.1" } webpki-roots = { default-features = false, optional = true, version = "0.25" } -tracing-subscriber = { default-features = false, features = ["env-filter", "fmt"], version = "0.3" } -tracing-tree = { default-features = false, version = "0.2" } - [dev-dependencies] tokio = { default-features = false, features = ["macros", "rt", "time"], version = "1.0" } -wtx = { default-features = false, features = ["flate2", "std", "tokio"], path = "." } +tracing-subscriber = { default-features = false, features = ["env-filter", "fmt"], version = "0.3" } +tracing-tree = { default-features = false, version = "0.2" } +wtx = { default-features = false, features = ["flate2", "std", "tokio", "web-socket-handshake"], path = "." } [features] arbitrary = ["dep:arbitrary", "std"] async-std = ["dep:async-std", "std"] default = [] glommio = ["futures-lite", "dep:glommio"] -nightly = [] std = [] tokio = ["std", "dep:tokio"] tokio-rustls = ["tokio", "dep:tokio-rustls"] @@ -90,8 +87,10 @@ _hack = ["embassy-net/medium-ethernet", "embassy-net/proto-ipv4"] [package] authors = ["Caio Fernandes "] categories = ["asynchronous", "data-structures", "network-programming", "no-std", "web-programming"] -description = "Asynchronous WebSocket implementations" +description = "A collection of different web transport implementations." +documentation = "https://docs.rs/wtx" edition = "2021" +homepage = "https://c410-f3r.github.io/wtx" keywords = ["client", "io", "network", "server", "websocket"] license = "Apache-2.0" name = "wtx" diff --git a/wtx/examples/common/mod.rs b/wtx/examples/common/mod.rs index f261e3f0..faa9f934 100644 --- a/wtx/examples/common/mod.rs +++ b/wtx/examples/common/mod.rs @@ -19,9 +19,8 @@ where PB: BorrowMut, S: Stream, { - let (_, mut ws) = WebSocketServer::accept(WebSocketAcceptRaw { + let mut ws = WebSocketServer::accept(WebSocketAcceptRaw { compression, - headers_buffer: &mut <_>::default(), key_buffer: &mut <_>::default(), pb, rng: <_>::default(), diff --git a/wtx/src/error.rs b/wtx/src/error.rs index 4a6dd8ab..b2d5ba59 100644 --- a/wtx/src/error.rs +++ b/wtx/src/error.rs @@ -54,19 +54,7 @@ pub enum Error { /// See [glommio::GlommioError]. #[cfg(feature = "glommio")] Glommio(Box>), - #[cfg(feature = "http")] - /// See [http::Error] - HttpError(http::Error), - /// See [http::header::InvalidHeaderName] - #[cfg(feature = "http")] - HttpInvalidHeaderName(http::header::InvalidHeaderName), - /// See [http::header::InvalidHeaderValue] - #[cfg(feature = "http")] - HttpInvalidHeaderValue(http::header::InvalidHeaderValue), - /// See [http::status::InvalidStatusCode] - #[cfg(feature = "http")] - HttpInvalidStatusCode(http::status::InvalidStatusCode), - #[cfg(feature = "web-socket-handshake")] + #[cfg(feature = "httparse")] /// See [httparse::Error]. HttpParse(httparse::Error), #[cfg(feature = "std")] @@ -125,39 +113,7 @@ impl From> for Error { } } -#[cfg(feature = "http")] -impl From for Error { - #[inline] - fn from(from: http::Error) -> Self { - Self::HttpError(from) - } -} - -#[cfg(feature = "http")] -impl From for Error { - #[inline] - fn from(from: http::header::InvalidHeaderName) -> Self { - Self::HttpInvalidHeaderName(from) - } -} - -#[cfg(feature = "http")] -impl From for Error { - #[inline] - fn from(from: http::header::InvalidHeaderValue) -> Self { - Self::HttpInvalidHeaderValue(from) - } -} - -#[cfg(feature = "http")] -impl From for Error { - #[inline] - fn from(from: http::status::InvalidStatusCode) -> Self { - Self::HttpInvalidStatusCode(from) - } -} - -#[cfg(feature = "web-socket-handshake")] +#[cfg(feature = "httparse")] impl From for Error { #[inline] fn from(from: httparse::Error) -> Self { diff --git a/wtx/src/http.rs b/wtx/src/http.rs new file mode 100644 index 00000000..3eb98277 --- /dev/null +++ b/wtx/src/http.rs @@ -0,0 +1,13 @@ +//! HTTP + +mod header; +#[cfg(feature = "httparse")] +mod httparse; +mod request; +mod response; +mod version; + +pub use header::{Header, Http1Header}; +pub use request::Request; +pub use response::Response; +pub use version::Version; diff --git a/wtx/src/http/header.rs b/wtx/src/http/header.rs new file mode 100644 index 00000000..993840ec --- /dev/null +++ b/wtx/src/http/header.rs @@ -0,0 +1,21 @@ +pub trait Header { + fn name(&self) -> &[u8]; + + fn value(&self) -> &[u8]; +} + +impl Header for () { + #[inline] + fn name(&self) -> &[u8] { + &[] + } + + #[inline] + fn value(&self) -> &[u8] { + &[] + } +} + +pub trait Http1Header: Header {} + +impl Http1Header for () {} diff --git a/wtx/src/http/httparse.rs b/wtx/src/http/httparse.rs new file mode 100644 index 00000000..54c364d2 --- /dev/null +++ b/wtx/src/http/httparse.rs @@ -0,0 +1,55 @@ +#![allow( + // All methods are called after parsing + clippy::unreachable +)] + +use crate::http::{Header, Http1Header, Request, Response, Version}; + +impl<'buffer> Header for httparse::Header<'buffer> { + #[inline] + fn name(&self) -> &[u8] { + self.name.as_bytes() + } + + #[inline] + fn value(&self) -> &[u8] { + self.value + } +} + +impl<'buffer> Http1Header for httparse::Header<'buffer> {} + +impl<'buffer, 'headers> Request for httparse::Request<'headers, 'buffer> { + #[inline] + fn method(&self) -> &[u8] { + if let Some(el) = self.method { + el.as_bytes() + } else { + unreachable!() + } + } + + #[inline] + fn version(&self) -> Version { + match self.version { + Some(0) => Version::Http1, + Some(1) => Version::Http2, + _ => { + unreachable!() + } + } + } +} + +impl<'buffer, 'headers> Response for httparse::Response<'headers, 'buffer> { + #[inline] + fn version(&self) -> Version { + match self.version { + Some(0) => Version::Http1, + Some(1) => Version::Http2, + _ => { + unreachable!() + } + } + } +} diff --git a/wtx/src/http/request.rs b/wtx/src/http/request.rs new file mode 100644 index 00000000..3808285e --- /dev/null +++ b/wtx/src/http/request.rs @@ -0,0 +1,21 @@ +use crate::http::Version; + +pub trait Request { + /// Method + fn method(&self) -> &[u8]; + + /// Version + fn version(&self) -> Version; +} + +impl Request for () { + #[inline] + fn method(&self) -> &[u8] { + &[] + } + + #[inline] + fn version(&self) -> Version { + <_>::default() + } +} diff --git a/wtx/src/http/response.rs b/wtx/src/http/response.rs new file mode 100644 index 00000000..c7c51306 --- /dev/null +++ b/wtx/src/http/response.rs @@ -0,0 +1,13 @@ +use crate::http::Version; + +pub trait Response { + /// Version + fn version(&self) -> Version; +} + +impl Response for () { + #[inline] + fn version(&self) -> Version { + <_>::default() + } +} diff --git a/wtx/src/http/version.rs b/wtx/src/http/version.rs new file mode 100644 index 00000000..f0e9ec05 --- /dev/null +++ b/wtx/src/http/version.rs @@ -0,0 +1,13 @@ +create_enum! { + #[derive(Clone, Copy, Debug, Default, PartialEq)] + /// HTTP version + pub enum Version { + /// HTTP/1 + Http1 = 0, + /// HTTP/1.1 + Http1_1 = 1, + /// HTTP/2 + #[default] + Http2 = 2, + } +} diff --git a/wtx/src/http_structs.rs b/wtx/src/http_structs.rs deleted file mode 100644 index bdfbc7f1..00000000 --- a/wtx/src/http_structs.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Essential HTTP structures - -mod header; -mod parse_status; -mod request; -mod response; - -pub use header::Header; -pub(crate) use header::HeaderSlice; -pub use parse_status::ParseStatus; -pub use request::Request; -pub use response::Response; diff --git a/wtx/src/http_structs/header.rs b/wtx/src/http_structs/header.rs deleted file mode 100644 index 8db2c0f0..00000000 --- a/wtx/src/http_structs/header.rs +++ /dev/null @@ -1,58 +0,0 @@ -use core::{mem, slice}; - -#[derive(Debug)] -#[repr(transparent)] -pub struct Header<'buffer>(httparse::Header<'buffer>); - -impl<'buffer> Header<'buffer> { - pub(crate) const EMPTY: Header<'static> = Header(httparse::EMPTY_HEADER); - - pub(crate) fn new(name: &'buffer str, value: &'buffer [u8]) -> Self { - Self(httparse::Header { name, value }) - } - - pub(crate) fn name(&self) -> &str { - self.0.name - } - - pub(crate) fn value(&self) -> &[u8] { - self.0.value - } -} - -pub(crate) struct HeaderSlice(pub(crate) T); - -impl<'buffer, 'headers> From<&'headers mut [Header<'buffer>]> - for HeaderSlice<&'headers mut [httparse::Header<'buffer>]> -{ - #[inline] - fn from(from: &'headers mut [Header<'buffer>]) -> Self { - assert!(mem::size_of::
() == mem::size_of::()); - assert!(mem::align_of::
() == mem::align_of::()); - let len = from.len(); - // SAFETY: `Header` and `httparse::Header` have the same size and alignment due - // to `#[transparent]` - Self(unsafe { slice::from_raw_parts_mut(from.as_mut_ptr().cast(), len) }) - } -} - -impl<'buffer, 'headers> From<&'headers [httparse::Header<'buffer>]> - for HeaderSlice<&'headers [Header<'buffer>]> -{ - #[inline] - fn from(from: &'headers [httparse::Header<'buffer>]) -> Self { - assert!(mem::size_of::
() == mem::size_of::()); - assert!(mem::align_of::
() == mem::align_of::()); - let len = from.len(); - // SAFETY: `Header` and `httparse::Header` have the same size and alignment due - // to `#[transparent]` - Self(unsafe { slice::from_raw_parts(from.as_ptr().cast(), len) }) - } -} - -#[test] -fn does_not_trigger_a_panic() { - let inner_header0 = httparse::Header { name: "foo", value: &[1, 2, 3] }; - let inner_header1 = httparse::Header { name: "bar", value: &[4, 5, 6] }; - let _headers = HeaderSlice::from(&[inner_header0, inner_header1][..]); -} diff --git a/wtx/src/http_structs/parse_status.rs b/wtx/src/http_structs/parse_status.rs deleted file mode 100644 index 2a8d060f..00000000 --- a/wtx/src/http_structs/parse_status.rs +++ /dev/null @@ -1,18 +0,0 @@ -/// The result of a HTTP request or response frame parse -#[derive(Debug)] -pub enum ParseStatus { - /// The completed result. - Complete(usize), - /// A partial result. - Partial, -} - -impl From> for ParseStatus { - #[inline] - fn from(from: httparse::Status) -> Self { - match from { - httparse::Status::Complete(elem) => Self::Complete(elem), - httparse::Status::Partial => Self::Partial, - } - } -} diff --git a/wtx/src/http_structs/request.rs b/wtx/src/http_structs/request.rs deleted file mode 100644 index a050703b..00000000 --- a/wtx/src/http_structs/request.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::http_structs::{Header, HeaderSlice, ParseStatus}; - -/// Raw request that can be converted to other high-level requests. -#[derive(Debug)] -pub struct Request<'buffer, 'headers> { - req: httparse::Request<'headers, 'buffer>, -} - -impl<'buffer, 'headers> Request<'buffer, 'headers> { - pub(crate) fn new(headers: &'headers mut [Header<'buffer>]) -> Self { - Self { req: httparse::Request::new(HeaderSlice::from(headers).0) } - } - - /// Method - #[inline] - pub fn method(&self) -> Option<&'buffer str> { - self.req.method - } - - /// Version - #[inline] - pub fn version(&self) -> Option { - self.req.version - } - - pub(crate) fn headers(&self) -> &[Header<'buffer>] { - HeaderSlice::from(&*self.req.headers).0 - } - - pub(crate) fn parse(&mut self, buffer: &'buffer [u8]) -> crate::Result { - Ok(self.req.parse(buffer)?.into()) - } -} - -#[cfg(feature = "http")] -mod http { - use crate::http_structs::Request; - use http::{HeaderMap, HeaderName, HeaderValue, Method}; - - impl<'buffer, 'headers> TryFrom> for http::Request<&'buffer [u8]> { - type Error = crate::Error; - - #[inline] - fn try_from(from: Request<'buffer, 'headers>) -> Result { - let method = - Method::try_from(from.req.method.ok_or(crate::Error::UnexpectedHttpVersion)?).unwrap(); - let version = if let Some(1) = from.req.version { - http::Version::HTTP_11 - } else { - return Err(crate::Error::UnexpectedHttpVersion); - }; - let mut headers = HeaderMap::with_capacity(from.req.headers.len()); - for h in from.req.headers { - let key = HeaderName::from_bytes(h.name.as_bytes())?; - let value = HeaderValue::from_bytes(h.value)?; - let _ = headers.append(key, value); - } - let mut req = http::Request::new(&[][..]); - *req.headers_mut() = headers; - *req.method_mut() = method; - *req.uri_mut() = from.req.path.unwrap().parse().unwrap(); - *req.version_mut() = version; - Ok(req) - } - } - - impl<'buffer, 'headers> TryFrom> for http::Request<()> { - type Error = crate::Error; - - #[inline] - fn try_from(from: Request<'buffer, 'headers>) -> Result { - let (parts, _) = http::Request::<&'buffer [u8]>::try_from(from)?.into_parts(); - Ok(http::Request::from_parts(parts, ())) - } - } - - impl<'buffer, 'headers> TryFrom> for http::Request> { - type Error = crate::Error; - - #[inline] - fn try_from(from: Request<'buffer, 'headers>) -> Result { - let (parts, body) = http::Request::<&'buffer [u8]>::try_from(from)?.into_parts(); - Ok(http::Request::from_parts(parts, body.to_vec())) - } - } -} diff --git a/wtx/src/http_structs/response.rs b/wtx/src/http_structs/response.rs deleted file mode 100644 index 4b38d7e3..00000000 --- a/wtx/src/http_structs/response.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::http_structs::{Header, HeaderSlice, ParseStatus}; - -/// Raw response that can be converted to other high-level responses. -#[derive(Debug)] -pub struct Response<'buffer, 'headers> { - res: httparse::Response<'headers, 'buffer>, -} - -impl<'buffer, 'headers> Response<'buffer, 'headers> -where - 'buffer: 'headers, -{ - pub(crate) fn new(headers: &'headers mut [Header<'buffer>]) -> Self { - Self { res: httparse::Response::new(HeaderSlice::from(headers).0) } - } - - /// Status code - #[inline] - pub fn code(&self) -> Option { - self.res.code - } - - #[inline] - pub(crate) fn code_mut(&mut self) -> &mut Option { - &mut self.res.code - } - - pub(crate) fn headers(&self) -> &[Header<'buffer>] { - HeaderSlice::from(&*self.res.headers).0 - } - - pub(crate) fn parse(&mut self, buffer: &'buffer [u8]) -> crate::Result { - Ok(self.res.parse(buffer)?.into()) - } - - pub(crate) fn version_mut(&mut self) -> &mut Option { - &mut self.res.version - } -} - -#[cfg(feature = "http")] -mod http { - use crate::http_structs::Response; - use http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; - - impl<'buffer, 'headers> TryFrom> for http::Response<&'buffer [u8]> { - type Error = crate::Error; - - #[inline] - fn try_from(from: Response<'buffer, 'headers>) -> Result { - let status = StatusCode::from_u16(from.res.code.ok_or(crate::Error::UnexpectedHttpVersion)?)?; - let version = if let Some(1) = from.res.version { - http::Version::HTTP_11 - } else { - return Err(crate::Error::UnexpectedHttpVersion); - }; - let mut headers = HeaderMap::with_capacity(from.res.headers.len()); - for h in from.res.headers { - let key = HeaderName::from_bytes(h.name.as_bytes())?; - let value = HeaderValue::from_bytes(h.value)?; - let _ = headers.append(key, value); - } - let mut res = http::Response::new(&[][..]); - *res.headers_mut() = headers; - *res.status_mut() = status; - *res.version_mut() = version; - Ok(res) - } - } - - impl<'buffer, 'headers> TryFrom> for http::Response<()> { - type Error = crate::Error; - - #[inline] - fn try_from(from: Response<'buffer, 'headers>) -> Result { - let (parts, _) = http::Response::<&'buffer [u8]>::try_from(from)?.into_parts(); - Ok(http::Response::from_parts(parts, ())) - } - } - - impl<'buffer, 'headers> TryFrom> for http::Response> { - type Error = crate::Error; - - #[inline] - fn try_from(from: Response<'buffer, 'headers>) -> Result { - let (parts, body) = http::Response::<&'buffer [u8]>::try_from(from)?.into_parts(); - Ok(http::Response::from_parts(parts, body.to_vec())) - } - } -} diff --git a/wtx/src/lib.rs b/wtx/src/lib.rs index 923e8fbf..1c529271 100644 --- a/wtx/src/lib.rs +++ b/wtx/src/lib.rs @@ -11,8 +11,7 @@ mod buffer; mod cache; mod error; mod expected_header; -#[cfg(feature = "web-socket-handshake")] -pub mod http_structs; +pub mod http; mod misc; mod partitioned_buffer; pub mod rng; diff --git a/wtx/src/web_socket.rs b/wtx/src/web_socket.rs index f8011a2d..9b55a8ec 100644 --- a/wtx/src/web_socket.rs +++ b/wtx/src/web_socket.rs @@ -12,7 +12,6 @@ mod close_code; pub mod compression; mod frame; mod frame_buffer; -#[cfg(feature = "web-socket-handshake")] pub mod handshake; mod mask; mod misc; @@ -27,6 +26,7 @@ use crate::{ rng::Rng, web_socket::{ compression::NegotiatedCompression, + handshake::{WebSocketAccept, WebSocketConnect}, misc::{define_fb_from_header_params, header_placeholder, op_code}, }, PartitionedBuffer, Stream, MAX_PAYLOAD_LEN, @@ -991,25 +991,23 @@ where } } -#[cfg(feature = "web-socket-handshake")] impl WebSocketClient { /// Shortcut that has the same effect of [WebSocketConnect::connect]. #[inline] pub async fn connect(wsc: WSC) -> crate::Result<(WSC::Response, Self)> where - WSC: handshake::WebSocketConnect, + WSC: WebSocketConnect, { wsc.connect().await } } -#[cfg(feature = "web-socket-handshake")] impl WebSocketServer { /// Shortcut that has the same effect of [WebSocketAccept::accept]. #[inline] - pub async fn accept(wsc: WSA) -> crate::Result<(WSA::Response, Self)> + pub async fn accept(wsc: WSA) -> crate::Result where - WSA: handshake::WebSocketAccept, + WSA: WebSocketAccept, { wsc.accept().await } diff --git a/wtx/src/web_socket/compression.rs b/wtx/src/web_socket/compression.rs index c5d52373..8d2b86c1 100644 --- a/wtx/src/web_socket/compression.rs +++ b/wtx/src/web_socket/compression.rs @@ -6,6 +6,7 @@ mod deflate_config; mod flate2; mod window_bits; +use crate::http::Http1Header; pub use compression_level::CompressionLevel; pub use deflate_config::DeflateConfig; #[cfg(feature = "flate2")] @@ -19,11 +20,7 @@ pub trait Compression { /// Manages the defined parameters with the received parameters to decide which /// parameters will be settled. - #[cfg(feature = "web-socket-handshake")] - fn negotiate( - self, - headers: &[crate::http_structs::Header<'_>], - ) -> crate::Result; + fn negotiate(self, headers: &[impl Http1Header]) -> crate::Result; /// Writes headers bytes that will be sent to the server. fn write_req_headers(&self, buffer: &mut B) @@ -34,9 +31,8 @@ pub trait Compression { impl Compression for () { type Negotiated = (); - #[cfg(feature = "web-socket-handshake")] #[inline] - fn negotiate(self, _: &[crate::http_structs::Header<'_>]) -> crate::Result { + fn negotiate(self, _: &[impl Http1Header]) -> crate::Result { Ok(()) } diff --git a/wtx/src/web_socket/compression/flate2.rs b/wtx/src/web_socket/compression/flate2.rs index cbe35a67..0ccafb26 100644 --- a/wtx/src/web_socket/compression/flate2.rs +++ b/wtx/src/web_socket/compression/flate2.rs @@ -1,4 +1,5 @@ use crate::{ + http::Http1Header, misc::from_utf8_opt, web_socket::{compression::NegotiatedCompression, Compression, DeflateConfig}, }; @@ -20,12 +21,8 @@ impl Flate2 { impl Compression for Flate2 { type Negotiated = Option; - #[cfg(feature = "web-socket-handshake")] #[inline] - fn negotiate( - self, - headers: &[crate::http_structs::Header<'_>], - ) -> crate::Result { + fn negotiate(self, headers: &[impl Http1Header]) -> crate::Result { use crate::{misc::_trim, web_socket::WebSocketError}; let mut dc = DeflateConfig { @@ -38,7 +35,7 @@ impl Compression for Flate2 { for sec_websocket_extensions in headers .iter() - .filter(|local_header| local_header.name().eq_ignore_ascii_case("sec-websocket-extensions")) + .filter(|local_header| local_header.name().eq_ignore_ascii_case(b"sec-websocket-extensions")) { for permessage_deflate_option in sec_websocket_extensions.value().split(|el| el == &b',') { dc = DeflateConfig { diff --git a/wtx/src/web_socket/handshake.rs b/wtx/src/web_socket/handshake.rs index cb71374f..939c0514 100644 --- a/wtx/src/web_socket/handshake.rs +++ b/wtx/src/web_socket/handshake.rs @@ -1,29 +1,22 @@ //! Initial data negotiation on both client and server sides to start exchanging frames. +#[cfg(feature = "web-socket-handshake")] mod misc; mod raw; #[cfg(test)] mod tests; -use crate::{ - http_structs::Header, - web_socket::{Stream, WebSocketClient, WebSocketServer}, -}; +use crate::web_socket::{Stream, WebSocketClient, WebSocketServer}; use core::future::Future; -#[cfg(feature = "web-socket-handshake")] pub use raw::{WebSocketAcceptRaw, WebSocketConnectRaw}; /// Reads external data to figure out if incoming requests can be accepted as WebSocket connections. pub trait WebSocketAccept { - /// Specific implementation response. - type Response; /// Specific implementation stream. type Stream: Stream; /// Reads external data to figure out if incoming requests can be accepted as WebSocket connections. - async fn accept( - self, - ) -> crate::Result<(Self::Response, WebSocketServer)>; + async fn accept(self) -> crate::Result>; } /// Initial negotiation sent by a client to start a WebSocket connection. @@ -54,13 +47,15 @@ pub trait WebSocketUpgrade { /// Necessary to decode incoming bytes of responses or requests. #[derive(Debug)] -pub struct HeadersBuffer<'buffer, const N: usize> { - pub(crate) headers: [Header<'buffer>; N], +pub struct HeadersBuffer { + #[allow(unused)] + pub(crate) headers: [H; N], } -impl Default for HeadersBuffer<'_, N> { +#[cfg(feature = "httparse")] +impl Default for HeadersBuffer, N> { #[inline] fn default() -> Self { - Self { headers: core::array::from_fn(|_| Header::EMPTY) } + Self { headers: core::array::from_fn(|_| httparse::EMPTY_HEADER) } } } diff --git a/wtx/src/web_socket/handshake/misc.rs b/wtx/src/web_socket/handshake/misc.rs index f6e45f79..d58a08a0 100644 --- a/wtx/src/web_socket/handshake/misc.rs +++ b/wtx/src/web_socket/handshake/misc.rs @@ -1,15 +1,15 @@ -use crate::{misc::from_utf8_opt, rng::Rng}; +use crate::rng::Rng; use base64::{engine::general_purpose::STANDARD, Engine}; use sha1::{Digest, Sha1}; -pub(crate) fn derived_key<'buffer>(buffer: &'buffer mut [u8; 30], key: &[u8]) -> &'buffer str { +pub(crate) fn derived_key<'buffer>(buffer: &'buffer mut [u8; 30], key: &[u8]) -> &'buffer [u8] { let mut sha1 = Sha1::new(); sha1.update(key); sha1.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); base64_from_array(&sha1.finalize().into(), buffer) } -pub(crate) fn gen_key<'buffer>(buffer: &'buffer mut [u8; 26], rng: &mut impl Rng) -> &'buffer str { +pub(crate) fn gen_key<'buffer>(buffer: &'buffer mut [u8; 26], rng: &mut impl Rng) -> &'buffer [u8] { base64_from_array(&rng.u8_16(), buffer) } @@ -22,7 +22,7 @@ pub(crate) fn gen_key<'buffer>(buffer: &'buffer mut [u8; 26], rng: &mut impl Rng fn base64_from_array<'output, const I: usize, const O: usize>( input: &[u8; I], output: &'output mut [u8; O], -) -> &'output str { +) -> &'output [u8] { fn div_ceil(x: usize, y: usize) -> usize { let fun = || { let num = x.checked_add(y)?.checked_sub(1)?; @@ -32,5 +32,5 @@ fn base64_from_array<'output, const I: usize, const O: usize>( } assert!(O >= div_ceil(I, 3).wrapping_mul(4)); let len = STANDARD.encode_slice(input, output).unwrap(); - from_utf8_opt(output.get(..len).unwrap_or_default()).unwrap() + output.get(..len).unwrap_or_default() } diff --git a/wtx/src/web_socket/handshake/raw.rs b/wtx/src/web_socket/handshake/raw.rs index 62733800..a703325f 100644 --- a/wtx/src/web_socket/handshake/raw.rs +++ b/wtx/src/web_socket/handshake/raw.rs @@ -1,31 +1,14 @@ -use crate::{ - http_structs::{Header, ParseStatus, Request, Response}, - misc::_trim, - rng::Rng, - web_socket::{ - compression::NegotiatedCompression, - handshake::{ - misc::{derived_key, gen_key}, - HeadersBuffer, WebSocketAccept, WebSocketConnect, - }, - Compression, FrameBuffer, WebSocketClient, WebSocketError, WebSocketServer, - }, - ExpectedHeader, PartitionedBuffer, Stream, UriParts, -}; -use core::{borrow::BorrowMut, str}; +use crate::web_socket::{handshake::HeadersBuffer, FrameBuffer}; const MAX_READ_HEADER_LEN: usize = 64; -const MAX_READ_LEN: usize = 2 * 1024; /// Marker used to implement [WebSocketAccept]. #[derive(Debug)] -pub struct WebSocketAcceptRaw<'any, C, PB, RNG, S> { +pub struct WebSocketAcceptRaw<'kb, C, PB, RNG, S> { /// Compression pub compression: C, - /// Headers buffer - pub headers_buffer: &'any mut HeadersBuffer<'any, 3>, /// Key buffer - pub key_buffer: &'any mut [u8; 30], + pub key_buffer: &'kb mut [u8; 30], /// Partitioned buffer pub pb: PB, /// Random Number Generator @@ -34,77 +17,15 @@ pub struct WebSocketAcceptRaw<'any, C, PB, RNG, S> { pub stream: S, } -impl<'any, C, PB, RNG, S> WebSocketAccept - for WebSocketAcceptRaw<'any, C, PB, RNG, S> -where - C: Compression, - PB: BorrowMut, - RNG: Rng, - S: Stream, -{ - type Response = Response<'any, 'any>; - type Stream = S; - - #[inline] - async fn accept( - mut self, - ) -> crate::Result<(Self::Response, WebSocketServer)> { - let pb = self.pb.borrow_mut(); - pb._set_indices_through_expansion(0, 0, MAX_READ_LEN); - let mut read = 0; - loop { - let read_buffer = pb._following_mut().get_mut(read..).unwrap_or_default(); - let local_read = self.stream.read(read_buffer).await?; - if local_read == 0 { - return Err(crate::Error::UnexpectedEOF); - } - read = read.wrapping_add(local_read); - let mut req_buffer = [Header::EMPTY; MAX_READ_HEADER_LEN]; - let mut req = Request::new(&mut req_buffer); - match req.parse(pb._following())? { - ParseStatus::Complete(_) => { - if !req.method().map_or(false, |el| _trim(el.as_bytes()).eq_ignore_ascii_case(b"get")) { - return Err(crate::Error::UnexpectedHttpMethod); - } - verify_common_header(req.headers())?; - if !has_header_key_and_value(req.headers(), "sec-websocket-version", b"13") { - return Err(crate::Error::MissingHeader { - expected: ExpectedHeader::SecWebSocketVersion_13, - }); - }; - let Some(key) = req.headers().iter().find_map(|el| { - (el.name().eq_ignore_ascii_case("sec-websocket-key")).then_some(el.value()) - }) else { - return Err(crate::Error::MissingHeader { expected: ExpectedHeader::SecWebSocketKey }); - }; - let compression = self.compression.negotiate(req.headers())?; - let swa = derived_key(self.key_buffer, key); - self.headers_buffer.headers[0] = Header::new("Connection", b"Upgrade"); - self.headers_buffer.headers[1] = Header::new("Sec-WebSocket-Accept", swa.as_bytes()); - self.headers_buffer.headers[2] = Header::new("Upgrade", b"websocket"); - let mut res = Response::new(&mut self.headers_buffer.headers); - *res.code_mut() = Some(101); - *res.version_mut() = req.version(); - let res_bytes = build_res(&compression, res.headers(), pb); - self.stream.write_all(res_bytes).await?; - pb.clear(); - return Ok((res, WebSocketServer::new(compression, self.pb, self.rng, self.stream))); - } - ParseStatus::Partial => {} - } - } - } -} - /// Marker used to implement [WebSocketConnect]. #[derive(Debug)] -pub struct WebSocketConnectRaw<'any, B, C, PB, RNG, S> { +pub struct WebSocketConnectRaw<'fb, 'hb, 'uri, B, C, H, PB, RNG, S> { /// Initial compression pub compression: C, /// Frame buffer - pub fb: &'any mut FrameBuffer, + pub fb: &'fb mut FrameBuffer, /// Headers buffer - pub headers_buffer: &'any mut HeadersBuffer<'any, MAX_READ_HEADER_LEN>, + pub headers_buffer: &'hb mut HeadersBuffer, /// Partitioned Buffer pub pb: PB, /// Random Number Generator @@ -112,141 +33,232 @@ pub struct WebSocketConnectRaw<'any, B, C, PB, RNG, S> { /// Stream pub stream: S, /// Uri - pub uri: &'any str, + pub uri: &'uri str, } -impl<'any, B, C, PB, RNG, S> WebSocketConnect - for WebSocketConnectRaw<'any, B, C, PB, RNG, S> -where - B: AsMut<[u8]> + AsMut> + AsRef<[u8]>, - C: Compression, - PB: BorrowMut, - RNG: Rng, - S: Stream, -{ - type Response = Response<'any, 'any>; - type Stream = S; - - #[inline] - async fn connect( - mut self, - ) -> crate::Result<(Self::Response, WebSocketClient)> { - let key_buffer = &mut <_>::default(); - let pb = self.pb.borrow_mut(); - pb.clear(); - let (key, req) = build_req(&self.compression, key_buffer, pb, &mut self.rng, self.uri); - self.stream.write_all(req).await?; - let mut read = 0; - self.fb._set_indices_through_expansion(0, 0, MAX_READ_LEN); - let len = loop { - let mut local_header = [Header::EMPTY; MAX_READ_HEADER_LEN]; - let read_buffer = self.fb.payload_mut().get_mut(read..).unwrap_or_default(); - let local_read = self.stream.read(read_buffer).await?; - if local_read == 0 { - return Err(crate::Error::UnexpectedEOF); - } - read = read.wrapping_add(local_read); - match Response::new(&mut local_header).parse(self.fb.payload())? { - ParseStatus::Complete(len) => break len, - ParseStatus::Partial => {} +#[cfg(feature = "web-socket-handshake")] +mod httparse_impls { + use crate::{ + http::{Header as _, Request as _}, + misc::_trim, + rng::Rng, + web_socket::{ + compression::NegotiatedCompression, + handshake::{ + misc::{derived_key, gen_key}, + raw::MAX_READ_HEADER_LEN, + HeadersBuffer, WebSocketAccept, WebSocketAcceptRaw, WebSocketConnect, WebSocketConnectRaw, + }, + Compression, WebSocketClient, WebSocketError, WebSocketServer, + }, + ExpectedHeader, PartitionedBuffer, Stream, UriParts, + }; + use core::{borrow::BorrowMut, str}; + use httparse::{Header, Request, Response, Status, EMPTY_HEADER}; + + const MAX_READ_LEN: usize = 2 * 1024; + + impl<'kb, C, PB, RNG, S> WebSocketAccept + for WebSocketAcceptRaw<'kb, C, PB, RNG, S> + where + C: Compression, + PB: BorrowMut, + RNG: Rng, + S: Stream, + { + type Stream = S; + + #[inline] + async fn accept( + mut self, + ) -> crate::Result> { + let pb = self.pb.borrow_mut(); + pb._set_indices_through_expansion(0, 0, MAX_READ_LEN); + let mut read = 0; + loop { + let read_buffer = pb._following_mut().get_mut(read..).unwrap_or_default(); + let local_read = self.stream.read(read_buffer).await?; + if local_read == 0 { + return Err(crate::Error::UnexpectedEOF); + } + read = read.wrapping_add(local_read); + let mut req_buffer = [EMPTY_HEADER; MAX_READ_HEADER_LEN]; + let mut req = Request::new(&mut req_buffer); + match req.parse(pb._following())? { + Status::Complete(_) => { + if !_trim(req.method()).eq_ignore_ascii_case(b"get") { + return Err(crate::Error::UnexpectedHttpMethod); + } + verify_common_header(req.headers)?; + if !has_header_key_and_value(req.headers, b"sec-websocket-version", b"13") { + return Err(crate::Error::MissingHeader { + expected: ExpectedHeader::SecWebSocketVersion_13, + }); + }; + let Some(key) = req.headers.iter().find_map(|el| { + (el.name().eq_ignore_ascii_case(b"sec-websocket-key")).then_some(el.value()) + }) else { + return Err(crate::Error::MissingHeader { + expected: ExpectedHeader::SecWebSocketKey, + }); + }; + let compression = self.compression.negotiate(req.headers)?; + let swa = derived_key(self.key_buffer, key); + let mut headers_buffer = HeadersBuffer::<_, 3>::default(); + headers_buffer.headers[0] = Header { name: "Connection", value: b"Upgrade" }; + headers_buffer.headers[1] = Header { name: "Sec-WebSocket-Accept", value: swa }; + headers_buffer.headers[2] = Header { name: "Upgrade", value: b"websocket" }; + let mut res = Response::new(&mut headers_buffer.headers); + res.code = Some(101); + res.version = Some(req.version().into()); + let res_bytes = build_res(&compression, res.headers, pb); + self.stream.write_all(res_bytes).await?; + pb.clear(); + return Ok(WebSocketServer::new(compression, self.pb, self.rng, self.stream)); + } + Status::Partial => {} + } } - }; - let mut res = Response::new(&mut self.headers_buffer.headers); - let _status = res.parse(self.fb.payload())?; - if res.code() != Some(101) { - return Err(WebSocketError::MissingSwitchingProtocols.into()); } - verify_common_header(res.headers())?; - if !has_header_key_and_value( - res.headers(), - "sec-websocket-accept", - derived_key(&mut <_>::default(), key.as_bytes()).as_bytes(), - ) { - return Err(crate::Error::MissingHeader { expected: crate::ExpectedHeader::SecWebSocketKey }); + } + + impl<'fb, 'hb, 'uri, B, C, PB, RNG, S> WebSocketConnect + for WebSocketConnectRaw<'fb, 'hb, 'uri, B, C, Header<'fb>, PB, RNG, S> + where + B: AsMut<[u8]> + AsMut> + AsRef<[u8]>, + C: Compression, + PB: BorrowMut, + RNG: Rng, + S: Stream, + 'fb: 'hb, + { + type Response = Response<'hb, 'fb>; + type Stream = S; + + #[inline] + async fn connect( + mut self, + ) -> crate::Result<(Self::Response, WebSocketClient)> + { + let key_buffer = &mut <_>::default(); + let pb = self.pb.borrow_mut(); + pb.clear(); + let (key, req) = build_req(&self.compression, key_buffer, pb, &mut self.rng, self.uri); + self.stream.write_all(req).await?; + let mut read = 0; + self.fb._set_indices_through_expansion(0, 0, MAX_READ_LEN); + let len = loop { + let mut local_header = [EMPTY_HEADER; MAX_READ_HEADER_LEN]; + let read_buffer = self.fb.payload_mut().get_mut(read..).unwrap_or_default(); + let local_read = self.stream.read(read_buffer).await?; + if local_read == 0 { + return Err(crate::Error::UnexpectedEOF); + } + read = read.wrapping_add(local_read); + match Response::new(&mut local_header).parse(self.fb.payload())? { + Status::Complete(len) => break len, + Status::Partial => {} + } + }; + let mut res = Response::new(&mut self.headers_buffer.headers); + let _status = res.parse(self.fb.payload())?; + if res.code != Some(101) { + return Err(WebSocketError::MissingSwitchingProtocols.into()); + } + verify_common_header(res.headers)?; + if !has_header_key_and_value( + res.headers, + b"sec-websocket-accept", + derived_key(&mut <_>::default(), key), + ) { + return Err(crate::Error::MissingHeader { + expected: crate::ExpectedHeader::SecWebSocketKey, + }); + } + let compression = self.compression.negotiate(res.headers)?; + pb.borrow_mut()._set_indices_through_expansion(0, 0, read.wrapping_sub(len)); + pb._following_mut().copy_from_slice(self.fb.payload().get(len..read).unwrap_or_default()); + Ok((res, WebSocketClient::new(compression, self.pb, self.rng, self.stream))) } - let compression = self.compression.negotiate(res.headers())?; - pb.borrow_mut()._set_indices_through_expansion(0, 0, read.wrapping_sub(len)); - pb._following_mut().copy_from_slice(self.fb.payload().get(len..read).unwrap_or_default()); - Ok((res, WebSocketClient::new(compression, self.pb, self.rng, self.stream))) } -} -/// Client request -fn build_req<'pb, 'kb, C>( - compression: &C, - key_buffer: &'kb mut [u8; 26], - pb: &'pb mut PartitionedBuffer, - rng: &mut impl Rng, - uri: &str, -) -> (&'kb str, &'pb [u8]) -where - C: Compression, -{ - let uri_parts = UriParts::from(uri); - let key = gen_key(key_buffer, rng); - - let idx = pb._buffer().len(); - pb.extend(b"GET "); - pb.extend(uri_parts.href.as_bytes()); - pb.extend(b" HTTP/1.1\r\n"); - - pb.extend(b"Connection: Upgrade\r\n"); - pb.extend(b"Host: "); - pb.extend(uri_parts.host.as_bytes()); - pb.extend(b"\r\n"); - pb.extend(b"Sec-WebSocket-Key: "); - pb.extend(key.as_bytes()); - pb.extend(b"\r\n"); - pb.extend(b"Sec-WebSocket-Version: 13\r\n"); - pb.extend(b"Upgrade: websocket\r\n"); - - compression.write_req_headers(pb); - - pb.extend(b"\r\n"); - - (key, pb._buffer().get(idx..).unwrap_or_default()) -} + /// Client request + fn build_req<'pb, 'kb, C>( + compression: &C, + key_buffer: &'kb mut [u8; 26], + pb: &'pb mut PartitionedBuffer, + rng: &mut impl Rng, + uri: &str, + ) -> (&'kb [u8], &'pb [u8]) + where + C: Compression, + { + let uri_parts = UriParts::from(uri); + let key = gen_key(key_buffer, rng); + + let idx = pb._buffer().len(); + pb.extend(b"GET "); + pb.extend(uri_parts.href.as_bytes()); + pb.extend(b" HTTP/1.1\r\n"); + + pb.extend(b"Connection: Upgrade\r\n"); + pb.extend(b"Host: "); + pb.extend(uri_parts.host.as_bytes()); + pb.extend(b"\r\n"); + pb.extend(b"Sec-WebSocket-Key: "); + pb.extend(key); + pb.extend(b"\r\n"); + pb.extend(b"Sec-WebSocket-Version: 13\r\n"); + pb.extend(b"Upgrade: websocket\r\n"); + + compression.write_req_headers(pb); -/// Server response -fn build_res<'pb, C>( - compression: &C, - headers: &[Header<'_>], - pb: &'pb mut PartitionedBuffer, -) -> &'pb [u8] -where - C: NegotiatedCompression, -{ - let idx = pb._buffer().len(); - pb.extend(b"HTTP/1.1 101 Switching Protocols\r\n"); - for header in headers { - pb.extend(header.name().as_bytes()); - pb.extend(b": "); - pb.extend(header.value()); pb.extend(b"\r\n"); + + (key, pb._buffer().get(idx..).unwrap_or_default()) } - compression.write_res_headers(pb); - pb.extend(b"\r\n"); - pb._buffer().get(idx..).unwrap_or_default() -} -fn has_header_key_and_value(headers: &[Header<'_>], key: &str, value: &[u8]) -> bool { - headers - .iter() - .find_map(|h| { - let has_key = _trim(h.name().as_bytes()).eq_ignore_ascii_case(key.as_bytes()); - let has_value = - h.value().split(|el| el == &b',').any(|el| _trim(el).eq_ignore_ascii_case(value)); - (has_key && has_value).then_some(true) - }) - .unwrap_or(false) -} + /// Server response + fn build_res<'pb, C>( + compression: &C, + headers: &[Header<'_>], + pb: &'pb mut PartitionedBuffer, + ) -> &'pb [u8] + where + C: NegotiatedCompression, + { + let idx = pb._buffer().len(); + pb.extend(b"HTTP/1.1 101 Switching Protocols\r\n"); + for header in headers { + pb.extend(header.name()); + pb.extend(b": "); + pb.extend(header.value()); + pb.extend(b"\r\n"); + } + compression.write_res_headers(pb); + pb.extend(b"\r\n"); + pb._buffer().get(idx..).unwrap_or_default() + } -fn verify_common_header(buffer: &[Header<'_>]) -> crate::Result<()> { - if !has_header_key_and_value(buffer, "connection", b"upgrade") { - return Err(crate::Error::MissingHeader { expected: ExpectedHeader::Connection_Upgrade }); + fn has_header_key_and_value(headers: &[Header<'_>], key: &[u8], value: &[u8]) -> bool { + headers + .iter() + .find_map(|h| { + let has_key = _trim(h.name()).eq_ignore_ascii_case(key); + let has_value = + h.value().split(|el| el == &b',').any(|el| _trim(el).eq_ignore_ascii_case(value)); + (has_key && has_value).then_some(true) + }) + .unwrap_or(false) } - if !has_header_key_and_value(buffer, "upgrade", b"websocket") { - return Err(crate::Error::MissingHeader { expected: ExpectedHeader::Upgrade_WebSocket }); + + fn verify_common_header(buffer: &[Header<'_>]) -> crate::Result<()> { + if !has_header_key_and_value(buffer, b"connection", b"upgrade") { + return Err(crate::Error::MissingHeader { expected: ExpectedHeader::Connection_Upgrade }); + } + if !has_header_key_and_value(buffer, b"upgrade", b"websocket") { + return Err(crate::Error::MissingHeader { expected: ExpectedHeader::Upgrade_WebSocket }); + } + Ok(()) } - Ok(()) } diff --git a/wtx/src/web_socket/handshake/tests.rs b/wtx/src/web_socket/handshake/tests.rs index ecda28ef..b0ed91c3 100644 --- a/wtx/src/web_socket/handshake/tests.rs +++ b/wtx/src/web_socket/handshake/tests.rs @@ -51,9 +51,8 @@ where let _server_jh = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let mut fb = FrameBufferVec::with_capacity(0); - let (_, mut ws) = WebSocketServerOwned::accept(WebSocketAcceptRaw { + let mut ws = WebSocketServerOwned::accept(WebSocketAcceptRaw { compression: server_compression, - headers_buffer: &mut <_>::default(), key_buffer: &mut <_>::default(), pb: PartitionedBuffer::with_capacity(0), rng: StdRng::default(), @@ -63,10 +62,10 @@ where .unwrap(); call_tests!( (server, &mut fb, &mut ws), - //FragmentedText, - //LargeFragmentedText, - //PingAndText, - //PingBetweenFragmentedText, + FragmentedText, + LargeFragmentedText, + PingAndText, + PingBetweenFragmentedText, SeveralBytes, TwoPings, // Last, @@ -89,10 +88,10 @@ where .unwrap(); call_tests!( (client, &mut fb, &mut ws), - //FragmentedText, - //LargeFragmentedText, - //PingAndText, - //PingBetweenFragmentedText, + FragmentedText, + LargeFragmentedText, + PingAndText, + PingBetweenFragmentedText, SeveralBytes, TwoPings, // Last, From ff6f0e5a6190ab6db31139a4bf387b3af04e4204 Mon Sep 17 00:00:00 2001 From: Caio Date: Thu, 28 Sep 2023 21:37:30 -0300 Subject: [PATCH 2/2] Change ports --- .github/workflows/ci.yaml | 8 -------- .scripts/autobahn-fuzzingclient.sh | 1 + .scripts/autobahn-fuzzingserver.sh | 2 +- .scripts/autobahn/fuzzingclient-min.json | 2 +- .scripts/autobahn/fuzzingclient.json | 2 +- 5 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e78f72aa..0e8a9ed4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,8 +18,6 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: .scripts/autobahn-fuzzingclient.sh ci - strategy: - fail-fast: true autobahn-fuzzingserver: runs-on: ubuntu-latest @@ -33,8 +31,6 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: .scripts/autobahn-fuzzingserver.sh ci - strategy: - fail-fast: true fuzz: runs-on: ubuntu-latest @@ -51,8 +47,6 @@ jobs: use-tool-cache: true - run: .scripts/fuzz.sh - strategy: - fail-fast: true tests: runs-on: ubuntu-latest @@ -67,5 +61,3 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: .scripts/internal-tests.sh - strategy: - fail-fast: true \ No newline at end of file diff --git a/.scripts/autobahn-fuzzingclient.sh b/.scripts/autobahn-fuzzingclient.sh index 158cfb4a..f9e04c16 100755 --- a/.scripts/autobahn-fuzzingclient.sh +++ b/.scripts/autobahn-fuzzingclient.sh @@ -11,6 +11,7 @@ RUSTFLAGS='-C target-cpu=native' cargo build --bin autobahn-server --features fl RUSTFLAGS='-C target-cpu=native' cargo run --bin autobahn-server --features flate2,tokio,web-socket-handshake --release & cargo_pid=$! mkdir -p .scripts/autobahn/reports/fuzzingclient podman run \ + -p 9070:9070 \ -v .scripts/autobahn/fuzzingclient-min.json:/fuzzingclient.json:ro \ -v .scripts/autobahn:/autobahn \ --name fuzzingclient \ diff --git a/.scripts/autobahn-fuzzingserver.sh b/.scripts/autobahn-fuzzingserver.sh index 9be9624d..56b5b066 100755 --- a/.scripts/autobahn-fuzzingserver.sh +++ b/.scripts/autobahn-fuzzingserver.sh @@ -16,7 +16,7 @@ podman run \ --net=host \ docker.io/crossbario/autobahn-testsuite:0.8.2 wstest -m fuzzingserver -s fuzzingserver.json sleep 5 -RUSTFLAGS='-C target-cpu=native' cargo run --bin autobahn-client --features flate2,tokio,web-socket-handshake --release -- 127.0.0.1:9080 +RUSTFLAGS='-C target-cpu=native' cargo run --bin autobahn-client --features flate2,tokio,web-socket-handshake --release podman rm --force --ignore fuzzingserver if [ $(grep -ci "failed" .scripts/autobahn/reports/fuzzingserver/index.json) -gt 0 ] diff --git a/.scripts/autobahn/fuzzingclient-min.json b/.scripts/autobahn/fuzzingclient-min.json index 7b65ec3a..c0f1f472 100644 --- a/.scripts/autobahn/fuzzingclient-min.json +++ b/.scripts/autobahn/fuzzingclient-min.json @@ -29,7 +29,7 @@ "servers": [ { "agent": "wtx", - "url": "ws://127.0.0.1:8080" + "url": "ws://127.0.0.1:9070" } ] } diff --git a/.scripts/autobahn/fuzzingclient.json b/.scripts/autobahn/fuzzingclient.json index 42201b3d..3650e382 100644 --- a/.scripts/autobahn/fuzzingclient.json +++ b/.scripts/autobahn/fuzzingclient.json @@ -6,7 +6,7 @@ "servers": [ { "agent": "wtx", - "url": "ws://127.0.0.1:8080" + "url": "ws://127.0.0.1:9070" } ] }