diff --git a/.cargo/config.toml b/.cargo/config.toml index 8ce2d190..de606e3e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,9 @@ [build] rustflags = ["-C", "target-cpu=native"] +[target.aarch64-apple-darwin] +rustflags = ["-C", "target-cpu=apple-m1"] + [target.x86_64-pc-windows-gnu] rustflags = ["-C", "target-cpu=x86-64-v2"] diff --git a/Cargo.lock b/Cargo.lock index bb2afcbd..997899e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fn-stream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba0c4baf81a0d8ab31618ffa3ae29ceeb970a6d0d82f76130753462e39d0ea" +dependencies = [ + "futures-util", + "pin-project-lite", + "smallvec", +] + [[package]] name = "async-fs" version = "2.2.0" @@ -279,9 +290,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "zeroize", @@ -289,9 +300,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", @@ -415,9 +426,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.4.0" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -540,9 +551,9 @@ dependencies = [ [[package]] name = "compio" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5cf9e29d3c2d2a37078198795631b61d2b5c9cab68191e31526fe342d742135" +checksum = "c19d81c636b0f47aa50041d25b02d9d8289a3319727a8051209f71bea10e89d6" dependencies = [ "compio-buf", "compio-driver", @@ -569,9 +580,9 @@ dependencies = [ [[package]] name = "compio-driver" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1f07e864382cbb51d601416dc41cb1dc99ad3a01c6745cece55e431ca547fe" +checksum = "293e8086a35f52b5002402937cf4e69b5e414917e511c29c5e7ba2cebe6ef7b3" dependencies = [ "bitflags 2.13.1", "cfg_aliases", @@ -598,9 +609,9 @@ dependencies = [ [[package]] name = "compio-executor" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2402a55af5e31454cd77d4b6044e2e6b8c30aa037655585ebd6505b97680468c" +checksum = "94961c5908b02bbb082e046968b8ee3d18995845a5c261376f0b0b5c95c87c2b" dependencies = [ "compio-log", "compio-send-wrapper", @@ -611,9 +622,9 @@ dependencies = [ [[package]] name = "compio-fs" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934c384c7c1dca1d68540bd0ef16186a8e7f33fab330e54652b45ee80f051ee4" +checksum = "8a374c5a03bdf7ecb42894a49c3a40c22f743f8d7dc39cc21a5dd3258d8219c7" dependencies = [ "cfg_aliases", "compio-buf", @@ -647,31 +658,31 @@ dependencies = [ [[package]] name = "compio-log" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef39fff6341af7ab6c27fae9a3887e1ec618320d43f3b9c924c7a644f693a859" +checksum = "9dd867f29de59e4eff577dfeee3744cb0a264249f3faaf11945e8be79a3c3c62" dependencies = [ "tracing", ] [[package]] name = "compio-macros" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ac573b3bb60fffabf47c2ce01c61536fb5eb2168d66e0375e85603e7d51614" +checksum = "dc53390a911fe6b317abe66c0583723539477f81a940e124b40d462fb976674a" dependencies = [ "darling", "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "compio-net" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "717d72ea6640bf0900ef9431fa770e0d120864bbf248cf15cb5da371cfdcff9a" +checksum = "31737ec57865d2b46284def673cd21fe478c592ca05c1e040e10d457aac6c2a8" dependencies = [ "compio-buf", "compio-driver", @@ -690,9 +701,9 @@ dependencies = [ [[package]] name = "compio-quic" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6502920691530a27afb6e83d30177fe8b35e1548866af47931b7295413fadc79" +checksum = "65b1501ba168bfd2cf63ffc120d81a015a1d78cc853bbd9b4f2a7cb85cd2ab9a" dependencies = [ "cfg_aliases", "compio-buf", @@ -715,9 +726,9 @@ dependencies = [ [[package]] name = "compio-runtime" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af69014028390d92b7e37ee90036481fc1c5c4bff4dd0f6db5896a172734d3d6" +checksum = "20406f0a4e69cdd0c0a58c4a7cf51bcc965a543fb5da2fe931e970354e291ded" dependencies = [ "compio-buf", "compio-driver", @@ -928,9 +939,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ "darling_core", "darling_macro", @@ -938,31 +949,31 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "deboa" -version = "0.1.2" +version = "0.1.3" dependencies = [ "async-lock", "base64", @@ -995,7 +1006,7 @@ dependencies = [ [[package]] name = "deboa-compio" -version = "0.1.3" +version = "0.1.4" dependencies = [ "async-executor", "async-lock", @@ -1026,7 +1037,6 @@ dependencies = [ "io-uring", "log", "minimime", - "mockall", "multer", "pin-project-lite", "rand", @@ -1036,7 +1046,6 @@ dependencies = [ "rustls-native-certs", "rustls-pki-types", "rustls-platform-verifier", - "send_wrapper", "serde", "tackle", "thiserror 2.0.20", @@ -1128,7 +1137,7 @@ dependencies = [ [[package]] name = "deboa-h3" -version = "0.1.1" +version = "0.1.2" dependencies = [ "bytes", "compio-quic", @@ -1136,6 +1145,7 @@ dependencies = [ "h3", "h3-quinn", "http", + "http-body-util", "hyper-body-utils", ] @@ -1154,7 +1164,7 @@ dependencies = [ [[package]] name = "deboa-smol" -version = "0.1.2" +version = "0.1.3" dependencies = [ "async-executor", "async-lock", @@ -1167,9 +1177,12 @@ dependencies = [ "deboa", "deboa-h3", "deboa-test-utils", + "deboa-tls", + "deboa-ws", "easyhttpmock-vetis-smol", "futures", "futures-rustls", + "futures-timeout", "futures-util", "h3", "h3-quinn", @@ -1184,8 +1197,8 @@ dependencies = [ "log", "macro_rules_attribute", "minimime", - "mockall", "multer", + "pin-project-lite", "quinn", "rand", "regex", @@ -1237,9 +1250,22 @@ dependencies = [ "vetis", ] +[[package]] +name = "deboa-tls" +version = "0.1.0" +dependencies = [ + "deboa", + "futures-rustls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "rustls-platform-verifier", + "webpki-roots", +] + [[package]] name = "deboa-tokio" -version = "0.1.2" +version = "0.1.3" dependencies = [ "async-executor", "async-lock", @@ -1252,9 +1278,10 @@ dependencies = [ "deboa", "deboa-h3", "deboa-test-utils", + "deboa-tls", + "deboa-ws", "easyhttpmock-vetis-tokio", "futures", - "futures-rustls", "futures-util", "h3", "h3-quinn", @@ -1268,7 +1295,6 @@ dependencies = [ "indexmap", "log", "minimime", - "mockall", "multer", "quinn", "rand", @@ -1281,13 +1307,30 @@ dependencies = [ "serde", "tackle", "thiserror 2.0.20", - "time", "tokio", "tokio-rustls", "tokio-util", "url", "urlencoding", "webpki-roots", +] + +[[package]] +name = "deboa-ws" +version = "0.1.0" +dependencies = [ + "base64", + "deboa", + "futures-util", + "http", + "hyper", + "hyper-util", + "pin-project-lite", + "rand", + "smol", + "smol-hyper", + "thiserror 2.0.20", + "tokio", "ws-framer", ] @@ -1353,16 +1396,13 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "easyhttpmock" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011017111116454614fb322b9e3b509017299a500ce15d8adb251dc9173cb91b" +checksum = "8978279c19ec0106d627e62568ebf17525bcdeb600ca4e8acdc16f037a9d95ec" dependencies = [ "bytes", "caramelo", "http", - "http-body-util", - "hyper", - "hyper-util", "jsonpath-rust", "once_cell", "rand", @@ -1377,9 +1417,9 @@ dependencies = [ [[package]] name = "easyhttpmock-vetis-compio" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db61a36f46e5941891a23eebcaab158fa54c5ec74a6dcf89d1d3a8962e2f9c59" +checksum = "d1c072751bd8ca548561b7b747fd8a7404b6ba3ec5b9e5e5bdac89f9a797b706" dependencies = [ "caramelo", "compio", @@ -1393,9 +1433,9 @@ dependencies = [ [[package]] name = "easyhttpmock-vetis-smol" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00fd98229ee3aba909f5136035421e30cab1fccabe70a9ec6e25a33b2b4d8e81" +checksum = "22271cc3b2b8406392d7829c0bc02a45b362c429ab18520ec9ae3a9e172effef" dependencies = [ "caramelo", "easyhttpmock", @@ -1410,9 +1450,9 @@ dependencies = [ [[package]] name = "easyhttpmock-vetis-tokio" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6d736454f17efcc242a1e4fdf5512d860a26053d2416d151e4c7927bd065a9" +checksum = "ef9b7c4d96df390ef7577f4efe7427a713e9e0a4b24e82250f2238a9c8f55d09" dependencies = [ "caramelo", "easyhttpmock", @@ -1425,9 +1465,9 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "enclose" @@ -1552,9 +1592,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flexbuffers" @@ -1773,11 +1813,26 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +[[package]] +name = "futures-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58ec6ce8fe4cc0b1f82f58eec8e6d1cd08584cc40aadab74cb925ff1b2750f2" +dependencies = [ + "futures-core", + "futures-timer", + "pin-project", +] + [[package]] name = "futures-timer" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" +dependencies = [ + "gloo-timers", + "send_wrapper", +] [[package]] name = "futures-util" @@ -1864,9 +1919,9 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "glommio-ng" -version = "0.10.0" +version = "0.10.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6c9b99577dfaf726e575122b026fef3b57e2c39f03ae042460fdb5be7a1cc8" +checksum = "f9461b07028859e56e12171f3fa2aa7a80b5eadab6864a3f1d25bac41499528e" dependencies = [ "ahash", "backtrace", @@ -1874,12 +1929,12 @@ dependencies = [ "bitflags 2.13.1", "bitmaps", "buddy-alloc", - "cc", "concurrent-queue", "crossbeam", "enclose", "flume", "futures-lite", + "glommio-ng-macros", "intrusive-collections", "io-uring", "lazy_static", @@ -1899,11 +1954,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "glommio-ng-macros" +version = "0.10.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0ed934b16e3b98b4eecd41c79cf1047207d3c06752544d81a43f8cd9037f36" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gloo-timers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482ce8a491a501da4cd806bd190275363d674f2845005c6ddbd5d3e1dd54495d" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "granit-parser" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c92814c286f45b5ed1498ce9547bc9637b473eba7037f8aee6a4d5a3c1e051c" +checksum = "4ccd1be9ebf2bd5520dbfcfb72b70ef492f654061299a097056f3e73410e38ec" dependencies = [ "arraydeque", "smallvec", @@ -1911,9 +1989,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228" dependencies = [ "atomic-waker", "bytes", @@ -2056,10 +2134,10 @@ dependencies = [ ] [[package]] -name = "http-serde" -version = "2.1.1" +name = "http-serde-ext" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" +checksum = "665c24b8e7e21688dc74edb228f07c1815bbc7ff3b48a3ee72fa20937fbde095" dependencies = [ "http", "serde", @@ -2101,11 +2179,13 @@ dependencies = [ [[package]] name = "hyper-body-utils" -version = "0.1.9" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbcedb0160531e4026eaa915976bb1f86f724ac71e8a7df94e78679004a97a0" +checksum = "56483f9b44efa33654cf0b443557e231c85632197ada81442e9fe951b468a8e9" dependencies = [ + "async-fn-stream", "bytes", + "compio", "compio-quic", "futures", "h3", @@ -2113,6 +2193,7 @@ dependencies = [ "http", "http-body-util", "hyper", + "send_wrapper", ] [[package]] @@ -2397,9 +2478,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -2448,15 +2529,15 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "local-event" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76dda8459b10a8960dfae91c1c77316fc15e7caf94f9f18794126512a8dc77e5" +checksum = "23ab4b951e96ffb2da6e25cec3d038d0e2930f4c406d1ec194b5120ae41ec6d3" [[package]] name = "lock_api" @@ -2469,9 +2550,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "loom" @@ -2629,26 +2710,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21909324aa58f5c284d91cac514c6d081210901dc372d7c8ea6a9d7e0406097a" -[[package]] -name = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "async-lock", - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "event-listener", - "futures-util", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - [[package]] name = "multer" version = "3.1.0" @@ -2780,9 +2841,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2903,29 +2964,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "paste" version = "1.0.15" @@ -2947,6 +2985,7 @@ dependencies = [ "bytes", "futures-util", "pin-project-lite", + "smallvec", "tokio", ] @@ -2958,9 +2997,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" dependencies = [ "memchr", "ucd-trie", @@ -2968,9 +3007,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" +checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" dependencies = [ "pest", "pest_generator", @@ -2978,9 +3017,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" +checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" dependencies = [ "pest", "pest_meta", @@ -2991,13 +3030,33 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" +checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" dependencies = [ "pest", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3023,9 +3082,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plotters" @@ -3071,9 +3130,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" @@ -3086,9 +3145,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -3198,9 +3257,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "aws-lc-rs", "bytes", @@ -3319,29 +3378,20 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - [[package]] name = "ref-cast" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", @@ -3590,9 +3640,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -3692,9 +3742,9 @@ dependencies = [ [[package]] name = "serde-saphyr" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2c6921c8ce48fb1b052b457619d008834039d80325992ed3bc55bbe6c155ad" +checksum = "a1ec1f5cac0eb96063c64b28705255a7ed6e7d77f95c1d25e9f8b8c928006ce1" dependencies = [ "annotate-snippets", "base64", @@ -4060,12 +4110,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ed1c6888f08659f2071c6c8dcb312c8086cb3061be4c17bab42ccd49a22a5e0" -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "tempfile" version = "3.27.0" @@ -4175,9 +4219,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -4486,11 +4530,10 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.0" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" dependencies = [ - "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -4559,16 +4602,17 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vetis" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a819a9e8b04d67ae7c84f473e857fdb65e8f0a9a7c39e510b877ec73452d1a" +checksum = "b408d1909a307d5849cb865f69bd73d44ec02d0035460a5365adfe37ab540fff" dependencies = [ "async-lock", "bytes", "futures-util", "http", "http-body-util", - "http-serde", + "http-serde-ext", + "hyper", "hyper-body-utils", "log", "radix_trie", @@ -4584,9 +4628,9 @@ dependencies = [ [[package]] name = "vetis-compio" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd264471b7f38719137dc259ccafdf8747eb0786ae4fa9a1f03b5bd410b44a0" +checksum = "5cbc90a480c41dc77f7e79d2024ae822d5ad80ccda6c94b630b85891e791ebb8" dependencies = [ "async-lock", "bytes", @@ -4618,9 +4662,9 @@ dependencies = [ [[package]] name = "vetis-smol" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "398c463fd61a9f8ae387e72e5e5d7ccf540c2b0d12c722dd50f7cea1b509eb76" +checksum = "7266cd40ed5803f7855fa1b179aab0a2c996d28919d1a142384b5975d321e412" dependencies = [ "async-lock", "async-net", @@ -4660,14 +4704,15 @@ dependencies = [ [[package]] name = "vetis-tokio" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998b6cc5a6afd7cf73642e492710b94b8f489a3cc00d0ebc9216de584ad55b9b" +checksum = "3e6a3eb16897a150d1b9c09645166414e923ee9ee03f7d209f9de6f693a67d0d" dependencies = [ "async-lock", "bytes", "clap", "env_logger", + "futures-lite", "futures-rustls", "futures-util", "http", @@ -4677,12 +4722,10 @@ dependencies = [ "hyper-util", "log", "mimalloc", - "moka", "peekable", "quinn", "radix_trie", "rand", - "regex", "rustls", "serde", "serde_yaml_ng", @@ -4733,9 +4776,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -4746,9 +4789,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4756,9 +4799,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -4769,18 +4812,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -4974,9 +5017,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "ws-framer" @@ -4984,6 +5027,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bacd6cff21323641597fe251e294f823e04292202a6e3c9721a1e8b82bea57bf" dependencies = [ + "getrandom 0.4.3", "httparse", "itoa", "ws-framer-macros", @@ -5091,9 +5135,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -5102,9 +5146,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -5113,13 +5157,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f207e5d0..cdcb3242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ members = [ "deboa-macros", "deboa-smol", "deboa-test-utils", + "deboa-tls", "deboa-tokio", + "deboa-ws", ] [workspace.package] @@ -23,7 +25,14 @@ license = "MIT OR Apache-2.0" rust-version = "1.85.0" [workspace.dependencies] -deboa = { version = "0.1.2" } +async-executor = { version = "1.13.3", default-features = false } +async-lock = "3.4.2" +async-native-tls = { version = "0.6.0", default-features = false } +base64 = { version = "0.23.0" } +bytes = { version = "1.11" } +caramelo = "0.1.2" +cookie = { version = "0.18.1" } +deboa = { path = "deboa" } deboa-compio = { path = "deboa-compio" } deboa-glommio = { path = "deboa-glommio" } deboa-h3 = { path = "deboa-h3", version = "^0.1.1" } @@ -32,10 +41,46 @@ deboa-fory = { version = ">= 0.1.6" } deboa-macros = { path = "deboa-macros" } deboa-smol = { path = "deboa-smol" } deboa-test-utils = { path = "deboa-test-utils" } +deboa-tls = { path = "deboa-tls" } deboa-tokio = { path = "deboa-tokio" } -hyper-body-utils = { version = ">= 0.1.9" } +deboa-ws = { path = "deboa-ws" } +futures = { version = "0.3.31", default-features = false } +futures-rustls = { version = "0.26.0", default-features = false } +futures-util = { version = "0.3.31", default-features = false } +h3 = { version = "0.0.8", default-features = false } +h3-quinn = { version = "0.0.10", default-features = false } +hashbrown = "0.17.1" +http = "1" +http-body = "1" +http-body-util = "0.1" +hyper = { version = "1.8.1", features = ["client"], default-features = false } +hyper-body-utils = { version = ">= 0.1.14", default-features = false } +hyper-util = { version = "0.1.20", features = [ + "client", + "client-legacy", +], default-features = false } +log = "0.4.34" +minimime = "1.0.0" +multer = "3.1.0" +native-tls = { version = "0.2.18" } +pin-project-lite = "0.2.17" +rand = "0.10.2" +regex = "1.9.6" +rstest = "0.26.1" +rustls = { version = "0.23.36", default-features = false } +rustls-native-certs = { version = "0.8.0", default-features = false } +rustls-pki-types = { version = "1.13.2", default-features = false } +rustls-platform-verifier = { version = "0.7.0", default-features = false } +serde = { version = "1.0.217", features = ["derive"] } +tackle = { version = "0.1.1"} +thiserror = "2.0.20" +time = { version = "0.3.53" } +url = "2.5.8" +urlencoding = "2.1.3" vamo = { version = ">= 0.0.9" } vamo-macros = { version = ">= 0.0.9" } +webpki-roots = { version = "1.0.6" } +ws-framer = { version = "0.3.2" } [profile.test] lto = false diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md deleted file mode 100644 index ba6d0d87..00000000 --- a/MIGRATION_GUIDE.md +++ /dev/null @@ -1,99 +0,0 @@ -# Migration guide - -## From 0.0.9 to 0.1.0 - -### Breaking changes - -* Client no longer need to be mutable, you might need update all client usages -* Removed set methods from ClientBuilder -* Identity code has been refactored -* Added Certificate struct - -### Non-breaking changes - -* Alias to raw_body method on DeboaResponse -* MethodExt trait -* Added skip_cert_verification to ClientBuilder -* Deboa now supports HTTP/3 -* Support to use either native-tls or rustls via feature flags - -## From 0.0.8 to 0.0.9 - -### Breaking changes - -* Made request, response, connection and runtime traits sealed -* Removed deboa-bora crate -* Removed bora macro from deboa-macros crate -* Moved bora macro to vamo-macros crate - -### Non-breaking changes - -* Improved documentation -* Added more examples -* Deprecated Fetch trait, added FetchWith trait -* Deprecated go method, added send_with method to DeboaRequestBuilder - -## From 0.0.7 to 0.0.8 - -### Breaking changes - -* Improved error handling, added more error variants - -### Non-breaking changes - -* Added multipart support - -## From 0.0.6 to 0.0.7 - -### Non-breaking changes - -* Improved documentation -* Added more examples - -## From 0.0.5 to 0.0.6 - -### Breaking changes - -* Macro migration: `bora` macro moved to `deboa-bora`, update all macro imports. - -## From 0.0.4 to 0.0.5 - -### Breaking changes - -* Config struct was removed -* DeboaResponse allow traits to add body deserialization -* Removed builtin json support -* Added DeboaBuilder -* Added DeboaError -* Added DeboaRequest and DeboaRequestBuilder - -### Non-breaking changes - -* Catchers (interceptors) support -* HTTP2 support -* Responses decompression -* Introduced deboa_extras crate -* Introduced deboa_macro crate -* Introduced vamo crate - -## From 0.0.3 to 0.0.4 - -### Breaking changes - -* Added built-in json support -* Removed data and config params from requests -* Introduced DeboaResponse -* Removed anyhow support - -### Non-breaking changes - -* Added benchmarks -* Added unit tests -* Added integration tests - -## From 0.0.2 to 0.0.3 - -### Non-breaking changes - -* Added support to smol runtime - diff --git a/deboa-compio/Cargo.toml b/deboa-compio/Cargo.toml index 427015ef..18d66db9 100644 --- a/deboa-compio/Cargo.toml +++ b/deboa-compio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deboa-compio" -version = "0.1.3" +version = "0.1.4" edition.workspace = true authors.workspace = true repository.workspace = true @@ -13,11 +13,19 @@ publish = true rust-version.workspace = true [package.metadata.docs.rs] -all-features = true +features = [ + "http1", + "http2", + "rust-tls", + "default-rustls-provider", + "default-rustls-verifier", +] [features] default = [ + "http1", "http2", + #"http3", "rust-tls", "default-rustls-provider", "default-rustls-verifier", @@ -25,6 +33,7 @@ default = [ rust-tls = [ "compio-tls/rustls", + "rustls/tls12", "dep:rustls-native-certs", "dep:webpki-roots", ] @@ -54,82 +63,77 @@ http2 = ["hyper/http2", "hyper-util/http2"] http3 = [ "deboa-h3/compio", "dep:h3", - "hyper-body-utils/http3", - "hyper-body-utils/compio", + "hyper-body-utils/compio-h3", "compio-quic/h3", "compio-quic/ring", ] [dependencies] -async-executor = { version = "1.13.3", optional = true, default-features = false } -async-lock = "3.4.2" -base64 = "0.23.0" -bytes = { version = "1.11" } -compio-quic = { version = "0.8.0", default-features = false, optional = true } +async-executor = { workspace = true } +async-lock = { workspace = true } +base64 = { workspace = true } +bytes = { workspace = true } +compio-quic = { version = "0.8.2", default-features = false, optional = true } compio-tls = { version = "0.10.0", default-features = false, optional = true } -cookie = "0.18.1" +cookie = { workspace = true } cyper-core = "0.9.0" deboa = { workspace = true } deboa-h3 = { workspace = true, optional = true } -futures = "0.3.31" -futures-util = { version = "0.3.31", default-features = false } -h3 = { version = "0.0.8", optional = true, default-features = false } -hashbrown = "0.17.1" -http = "1" -http-body = "1" -http-body-util = "0.1" -hyper = { version = "1.8.1", features = ["client"], default-features = false } +futures = { workspace = true, optional = true } +futures-util = { workspace = true, optional = true } +h3 = { version = "0.0.8", optional = true } +hashbrown = { workspace = true } +http = { workspace = true } +http-body = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true } hyper-body-utils = { workspace = true, default-features = false } -hyper-util = { version = "0.1.20", features = [ - "client", - "client-legacy", -], default-features = false } +hyper-util = { workspace = true } indexmap = "2.11.4" io-uring = "0.7.13" -log = "0.4.28" -minimime = "1.0.0" -mockall = "0.15.0" -pin-project-lite = "0.2.17" -rand = "0.10.2" -regex = "1.9.6" -rustls = { version = "0.23.36", optional = true, default-features = false } -rustls-native-certs = { version = "0.8.0", optional = true, default-features = false } -rustls-pki-types = { version = "1.13.2", optional = true, default-features = false } -rustls-platform-verifier = { version = "0.7.0", optional = true, default-features = false } -send_wrapper = { version = "0.6.0", default-features = false } -serde = { version = "1.0.217", features = ["derive"] } -tackle = { version = "0.1.1"} -thiserror = "2.0.17" -time = "0.3.53" -url = "2.5.8" -urlencoding = "2.1.3" -webpki-roots = { version = "1.0.6", optional = true, default-features = false } +log = { workspace = true } +minimime = { workspace = true } +pin-project-lite = { workspace = true } +rand = { workspace = true } +regex = { workspace = true } +rustls = { workspace = true, optional = true } +rustls-native-certs = { workspace = true, optional = true } +rustls-pki-types = { workspace = true, optional = true } +rustls-platform-verifier = { workspace = true, optional = true } +serde = { workspace = true , features = ["derive"] } +tackle = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } +urlencoding = { workspace = true } +webpki-roots = { workspace = true, optional = true, default-features = false } [target.'cfg(target_os = "linux")'.dependencies] -compio = { version = "0.19.1", features = [ +compio = { version = "0.19.2", features = [ "fs", "io", "net", "runtime", "io-uring", "macros", + "time", "tls" ], default-features = false } [target.'cfg(all(unix, not(target_os = "linux")))'.dependencies] -compio = { version = "0.19.1", features = [ +compio = { version = "0.19.2", features = [ "fs", "io", "net", "runtime", "polling", "macros", + "time", "tls" ], default-features = false } [dev-dependencies] -caramelo = "0.1.2" -compio = { version = "0.19.1", default-features = false } +caramelo = { workspace = true } +compio = { version = "0.19.2", default-features = false } criterion = { version = "0.8.2", features = [ "html_reports", "async", @@ -137,10 +141,13 @@ criterion = { version = "0.8.2", features = [ "async_tokio", ] } deboa-test-utils = { workspace = true } -easyhttpmock-vetis-compio = { version = "0.1.0", features = [ +easyhttpmock-vetis-compio = { version = "0.1.1", features = [ + "http1", "http2", + #"http3", "rust-tls", ], default-features = false } -futures-util = "0.3.31" -multer = "3.1.0" -rstest = "0.26.1" +futures-util = { workspace = true } +multer = { workspace = true } +rstest = { workspace = true } +time = { workspace = true } diff --git a/deboa-compio/src/cert.rs b/deboa-compio/src/cert.rs index 7c39bb77..8e5b8cb7 100644 --- a/deboa-compio/src/cert.rs +++ b/deboa-compio/src/cert.rs @@ -94,7 +94,7 @@ impl deboa::cert::Identity for DeboaIdentity { &self.cert } - fn ket(&self) -> &Option> { + fn key(&self) -> &Option> { &self.key } diff --git a/deboa-compio/src/client/http/conn/mod.rs b/deboa-compio/src/client/http/conn/mod.rs index 0095a432..aeb6eddd 100644 --- a/deboa-compio/src/client/http/conn/mod.rs +++ b/deboa-compio/src/client/http/conn/mod.rs @@ -16,13 +16,18 @@ //! - Thread-safe connection handling //! ``` use crate::cert::{DeboaCertificate, DeboaIdentity}; +#[cfg(feature = "rust-tls")] +use compio::net::TcpStream; +#[cfg(any(feature = "http1", feature = "http2"))] +use cyper_core::HyperStream; #[cfg(feature = "http1")] use deboa::request::Http1Request; #[cfg(feature = "http2")] use deboa::request::Http2Request; use deboa::{ conn::{ConnectionConfig, HttpConnectionDispatcher, ProtoConnection}, - errors::{DeboaError, RequestError}, + dns::DnsResolver, + errors::{ConnectionError, DeboaError, RequestError}, response::DeboaResponse, Result, }; @@ -30,7 +35,10 @@ use deboa::{ use deboa_h3::compio::Http3Request; use http::{Request, Version}; use hyper_body_utils::HttpBody; -use std::marker::PhantomData; +use log::info; +#[cfg(feature = "rust-tls")] +use std::borrow::Cow; +use std::{marker::PhantomData, time::Duration}; /// Connection pooling for efficient HTTP connections. /// @@ -45,11 +53,6 @@ use std::marker::PhantomData; /// - Configurable pool size (coming soon) pub mod pool; -/// Stream module for runtime-specific stream implementations. -/// -/// This module provides stream implementations for different runtimes (Tokio, Smol, etc.). -pub(crate) mod stream; - #[cfg(feature = "http1")] pub(crate) type Http1Connection = BaseHttpConnection; #[cfg(feature = "http2")] @@ -88,20 +91,8 @@ impl DeboaConnection { pub fn http3(conn: Http3Connection) -> Self { DeboaConnection::Http3(Box::new(conn)) } -} -impl HttpConnectionDispatcher for DeboaConnection { - /// Send a request over the connection. - /// - /// # Arguments - /// - /// * `url` - The URL to send the request to. - /// * `request` - The request to send. - /// - /// # Returns - /// - /// * `Result` - The response or error. - async fn send_request(&mut self, request: Request) -> Result { + async fn send(&mut self, request: Request) -> Result { match self { #[cfg(feature = "http1")] DeboaConnection::Http1(ref mut conn) => { @@ -155,6 +146,30 @@ impl HttpConnectionDispatcher for DeboaConnection { } } +impl HttpConnectionDispatcher for DeboaConnection { + /// Send a request over the connection. + /// + /// # Arguments + /// + /// * `url` - The URL to send the request to. + /// * `request` - The request to send. + /// + /// # Returns + /// + /// * `Result` - The response or error. + async fn send_request( + &mut self, + request: Request, + timeout: Duration, + ) -> Result { + compio::time::timeout(timeout, self.send(request)) + .await + .map_err(|_| { + DeboaError::Request(RequestError::Send { message: "Request timed out".to_string() }) + })? + } +} + /// Struct that represents the connection. /// /// # Fields @@ -172,27 +187,182 @@ impl BaseHttpConnection { } } +#[cfg(feature = "rust-tls")] +async fn connect_with_rustls<'a>( + tcp_stream: TcpStream, + config: &ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, +) -> Result<(Version, HyperStream)> { + use crate::client::tls::rustls::{tcp::connect, TlsConnectionBuilder}; + let tls_config = TlsConnectionBuilder::default() + .certificate(config.certificate()) + .identity(config.identity()) + .build_config()?; + + let stream = connect(tls_config, tcp_stream, config.host()).await?; + + if let Some(alpn) = stream.negotiated_alpn() { + let Cow::Borrowed(alpn_code) = String::from_utf8_lossy(&alpn) else { + return Err(DeboaError::Connection(ConnectionError::Tcp { + message: "Invalid ALPN code".to_string(), + })); + }; + + let version = match alpn_code { + "http1.1" => Version::HTTP_11, + "h2" => Version::HTTP_2, + "h3" => Version::HTTP_3, + _ => *config.protocol_version(), + }; + + info!("ALPN info found, switching connection to {:?}", version); + Ok((version, HyperStream::new_tls(stream))) + } else { + info!("No ALPN info available, falling back to HTTP/1.1"); + Ok((*config.protocol_version(), HyperStream::new_tls(stream))) + } +} + +#[cfg(feature = "native-tls")] +async fn connect_with_nativels<'a>( + tcp_stream: TcpStream, + config: &ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, +) -> Result<(Version, HyperStream)> { + use crate::client::tls::native::TlsConnectionBuilder; + let stream = TlsConnectionBuilder::new(tcp_stream, config.host()) + .certificate(config.certificate()) + .identity(config.identity()) + .connect() + .await?; + + if let Some(alpn) = stream.negotiated_alpn() { + let Cow::Borrowed(alpn_code) = String::from_utf8_lossy(alpn) else { + return Err(DeboaError::Connection(ConnectionError::Tcp { + message: "Invalid ALPN code".to_string(), + })); + }; + + let version = match alpn_code { + "http1.1" => Version::HTTP_11, + "h2" => Version::HTTP_2, + "h3" => Version::HTTP_3, + _ => *config.protocol_version(), + }; + + info!("ALPN info found, switching connection to {:?}", version); + Ok((version, HyperStream::Tls(stream))) + } else { + info!("No ALPN info available, falling back to HTTP/1.1"); + Ok((Version::HTTP_11, HyperStream::Tls(stream))) + } +} + pub struct ConnectionFactory {} impl ConnectionFactory { - pub async fn create_connection<'a>( - protocol: &Version, + /// Create a new connection. + pub async fn create_connection<'a, D>( config: &'a ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, - ) -> Result { - let conn = match protocol { + dns_resolver: &D, + ) -> Result + where + D: DnsResolver, + { + let ips = dns_resolver + .resolve( + config + .host() + .to_string(), + config.port(), + ) + .await?; + let ips = if config + .client_bind_addr() + .is_ipv4() + { + ips.into_iter() + .filter(|ip| ip.is_ipv4()) + .collect::>() + } else { + ips.into_iter() + .filter(|ip| ip.is_ipv6()) + .collect::>() + }; + + let Some(ip) = ips.first() else { + return Err(DeboaError::Request(RequestError::Send { + message: format!("No IP addresses found for hostname: {}", config.host()), + })); + }; + + #[cfg(any(feature = "http1", feature = "http2"))] + let conn_pair = { + use compio::net::TcpStream; + use cyper_core::HyperStream; + + let tcp_stream = TcpStream::connect(format!("{}:{}", ip, config.port())) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tcp { message: e.to_string() }) + })?; + let use_tls = config.scheme() == "https" || config.scheme() == "wss"; + if !use_tls { + (Version::HTTP_11, HyperStream::new_plain(tcp_stream)) + } else { + #[cfg(feature = "rust-tls")] + { + connect_with_rustls(tcp_stream, config).await? + } + + #[cfg(feature = "native-tls")] + { + connect_with_nativels(tcp_stream, config).await? + } + } + }; + + let conn = match conn_pair.0 { #[cfg(feature = "http1")] - &Version::HTTP_11 => { - let conn = Http1Connection::connect(config).await?; + Version::HTTP_11 => { + let conn = Http1Connection::connect(conn_pair.1).await?; DeboaConnection::http1(conn) } #[cfg(feature = "http2")] - &Version::HTTP_2 => { - let conn = Http2Connection::connect(config).await?; + Version::HTTP_2 => { + let conn = Http2Connection::connect(conn_pair.1).await?; DeboaConnection::http2(conn) } #[cfg(feature = "http3")] - &Version::HTTP_3 => { - let conn = Http3Connection::connect(config).await?; + Version::HTTP_3 => { + let stream = { + use crate::client::tls::rustls::{udp::connect, TlsConnectionBuilder}; + use compio_quic::Endpoint; + use deboa::errors::ConnectionError; + use std::net::SocketAddr; + + let mut client_endpoint = + Endpoint::client(SocketAddr::new(*config.client_bind_addr(), 0)) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: e.to_string(), + }) + })?; + + let tls_config = TlsConnectionBuilder::default() + .certificate(config.certificate()) + .identity(config.identity()) + .build_config()?; + + connect( + tls_config, + &mut client_endpoint, + SocketAddr::new(*ip, config.port()), + config.host(), + ) + .await? + }; + + let conn = Http3Connection::connect(stream).await?; DeboaConnection::http3(conn) } _ => { diff --git a/deboa-compio/src/client/http/conn/pool.rs b/deboa-compio/src/client/http/conn/pool.rs index da7ded96..813de8e0 100644 --- a/deboa-compio/src/client/http/conn/pool.rs +++ b/deboa-compio/src/client/http/conn/pool.rs @@ -2,9 +2,14 @@ use crate::{ cert::{DeboaCertificate, DeboaIdentity}, client::http::conn::{ConnectionFactory, DeboaConnection}, }; -use deboa::{conn::ConnectionConfig, Result}; +use deboa::{ + conn::ConnectionConfig, + dns::DnsResolver, + errors::{ConnectionError, DeboaError}, + Result, +}; use hashbrown::HashMap; -use time::Duration; +use std::time::Duration; /// Struct that represents the HTTP connection pool. /// @@ -27,7 +32,7 @@ impl Default for HttpConnectionPool { fn default() -> Self { Self { max_idle_connections: 5, - keep_alive_duration: Duration::minutes(5), + keep_alive_duration: Duration::from_mins(5), connections: HashMap::new(), } } @@ -76,15 +81,14 @@ impl deboa::conn::HttpConnectionPool for HttpConnectionPool { .len() as u32 } - async fn create_connection<'a>( - &'a mut self, + async fn create_connection<'a, D>( + &mut self, config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result<&'a mut DeboaConnection> { - if self.max_idle_connections == 0 { - self.connections - .clear(); - } - + dns_resolver: &D, + ) -> Result<&mut DeboaConnection> + where + D: DnsResolver, + { let host = config.host(); if self .connections @@ -98,8 +102,20 @@ impl deboa::conn::HttpConnectionPool for HttpConnectionPool { } log::debug!("Creating new connection for {}", host); - let connection = - ConnectionFactory::create_connection(config.protocol_version(), config).await?; + let connection = compio::time::timeout( + config.connection_timeout(), + ConnectionFactory::create_connection(config, dns_resolver), + ) + .await + .map_err(|_| { + DeboaError::Connection(ConnectionError::Timeout { + message: format!( + "Connection to {} timed out after {:?}", + host, + config.connection_timeout() + ), + }) + })??; self.connections .insert(host.to_string(), connection); diff --git a/deboa-compio/src/client/http/conn/stream/mod.rs b/deboa-compio/src/client/http/conn/stream/mod.rs deleted file mode 100644 index e323fdda..00000000 --- a/deboa-compio/src/client/http/conn/stream/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub(crate) mod plain; -pub(crate) use plain::*; - -#[cfg(all( - any(feature = "rust-tls", feature = "native-tls"), - any(feature = "http1", feature = "http2", feature = "http3") -))] -pub(crate) mod tls; -#[cfg(all( - any(feature = "rust-tls", feature = "native-tls"), - any(feature = "http1", feature = "http2", feature = "http3") -))] -pub(crate) use tls::*; diff --git a/deboa-compio/src/client/http/conn/stream/plain.rs b/deboa-compio/src/client/http/conn/stream/plain.rs deleted file mode 100644 index 0fa152e6..00000000 --- a/deboa-compio/src/client/http/conn/stream/plain.rs +++ /dev/null @@ -1,31 +0,0 @@ -use compio::net::TcpStream; -use cyper_core::HyperStream; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; -use std::net::IpAddr; - -pub async fn create_stream(ip: IpAddr, host: &str, port: u16) -> Result { - let tcp_stream = TcpStream::connect((ip, port)).await; - let tcp_stream = match tcp_stream { - Ok(tcp_stream) => tcp_stream, - Err(e) => { - return Err(DeboaError::Connection(ConnectionError::Tcp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - }; - - Ok(tcp_stream) -} - -pub(crate) async fn plain_connection( - ip: IpAddr, - host: &str, - port: u16, -) -> Result> { - let stream = create_stream(ip, host, port).await?; - Ok(HyperStream::new_plain(stream)) -} diff --git a/deboa-compio/src/client/http/conn/stream/tls/mod.rs b/deboa-compio/src/client/http/conn/stream/tls/mod.rs deleted file mode 100644 index 4632c49b..00000000 --- a/deboa-compio/src/client/http/conn/stream/tls/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[cfg(feature = "native-tls")] -mod native; - -#[cfg(feature = "rust-tls")] -mod rustls; - -#[cfg(feature = "rust-tls")] -pub(crate) use rustls::*; - -#[cfg(feature = "native-tls")] -pub(crate) use native::*; diff --git a/deboa-compio/src/client/http/conn/stream/tls/native.rs b/deboa-compio/src/client/http/conn/stream/tls/native.rs deleted file mode 100644 index 734f0e96..00000000 --- a/deboa-compio/src/client/http/conn/stream/tls/native.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::{ - cert::{Certificate as DeboaCertificate, Identity as DeboaIdentity}, - client::http::conn::stream::create_stream, -}; -use compio::net::TcpStream; -use compio_tls::native_tls::TlsConnector; -use cyper_core::HyperStream; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; -use std::net::IpAddr; - -pub(crate) async fn tls_connection( - ip: IpAddr, - host: &str, - port: u16, - identity: &Option, - certificate: &Option, - skip_server_verification: bool, - alpn: &[&str], -) -> Result> { - let socket = create_stream(ip, host, port).await?; - let mut builder = TlsConnector::builder(); - - let builder = if skip_server_verification { - builder - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - } else { - &mut builder - }; - - let builder = builder.request_alpns(&alpn); - - let builder = if let Some(ca) = certificate { - let cert = ca.try_into(); - if let Err(e) = cert { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid CA certificate: {}", e), - })); - } - - builder.add_root_certificate(cert.unwrap()) - } else { - builder - }; - - let builder = if let Some(identity) = identity { - let ident = identity.try_into(); - if let Err(e) = ident { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid client identity: {}", e), - })); - } - builder.identity(ident.unwrap()) - } else { - builder - }; - - let connector = builder - .build() - .map_err(|e| { - DeboaError::Connection(ConnectionError::Tls { - host: host.to_owned(), - message: e.to_string(), - }) - })?; - - let connector = compio_tls::TlsConnector::from(connector); - - let stream = connector - .connect(host, socket) - .await; - - if let Err(e) = stream { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - - let stream = stream.unwrap(); - Ok(HyperStream::new_tls(stream)) -} diff --git a/deboa-compio/src/client/http/conn/stream/tls/rustls.rs b/deboa-compio/src/client/http/conn/stream/tls/rustls.rs deleted file mode 100644 index 6a2528ef..00000000 --- a/deboa-compio/src/client/http/conn/stream/tls/rustls.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::stream::create_stream, -}; -use compio::net::TcpStream; -use compio_tls::TlsConnector; -use cyper_core::HyperStream; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; -use rustls::{ - pki_types::{CertificateDer, PrivateKeyDer, ServerName}, - ClientConfig, -}; -use std::net::IpAddr; -use std::sync::Arc; - -pub(crate) async fn tls_connection( - ip: IpAddr, - host: &str, - port: u16, - identity: &Option, - certificate: &Option, - skip_server_verification: bool, - alpn: Vec>, -) -> Result> { - let socket = create_stream(ip, host, port).await?; - let config = setup_rust_tls(host, identity, certificate, skip_server_verification, alpn)?; - let connector = TlsConnector::from(Arc::new(config)); - let hostname = ServerName::try_from(host.to_string()); - - if let Err(e) = hostname { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: e.to_string(), - })); - } - - let stream = connector - .connect(host, socket) - .await; - - if let Err(e) = stream { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - - let stream = stream.unwrap(); - Ok(HyperStream::new_tls(stream)) -} - -pub(crate) fn default_provider() -> Arc { - #[cfg(feature = "__rustls_aws_lc_rs")] - let provider = rustls::crypto::aws_lc_rs::default_provider(); - #[cfg(feature = "__rustls_ring")] - let provider = rustls::crypto::ring::default_provider(); - Arc::new(provider) -} - -pub(crate) fn setup_rust_tls( - host: &str, - identity: &Option, - certificate: &Option, - skip_server_verification: bool, - alpn: Vec>, -) -> Result { - let provider = default_provider(); - - if skip_server_verification { - use crate::client::http::conn::stream::tls::rustls::verify::SkipServerVerification; - let config = rustls::ClientConfig::builder_with_provider(provider) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version") - .dangerous() - .with_custom_certificate_verifier(SkipServerVerification::new()) - .with_no_client_auth(); - return Ok(config); - } - - #[cfg(feature = "__webpki_rustls_verifier")] - let config = { - let config = rustls::ClientConfig::builder_with_provider(provider) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version"); - - let mut root_store = - rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; - if let Some(ca) = certificate { - let cert = ca.try_into(); - if let Err(e) = cert { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid CA certificate: {}", e), - })); - } - - let result = root_store.add(cert.unwrap()); - if let Err(e) = result { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not add CA certificate to the store: {}", e), - })); - } - - config.with_root_certificates(root_store) - } else { - config.with_root_certificates(root_store) - } - }; - - #[cfg(feature = "__platform_rustls_verifier")] - let config = { - use rustls_platform_verifier::Verifier; - let verifier = Verifier::new(provider).expect("Failed to create platform verifier"); - rustls::ClientConfig::builder_with_provider(default_provider()) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version") - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - }; - - let mut config = if let Some(id) = identity { - let pair: std::result::Result< - (CertificateDer<'static>, PrivateKeyDer<'static>), - std::io::Error, - > = id.try_into(); - if let Err(e) = pair { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid client identity: {}", e), - })); - } - - let pair = pair.unwrap(); - - config - .with_client_auth_cert(vec![pair.0], pair.1) - .expect("Failed to set client identity") - } else { - config.with_no_client_auth() - }; - - config.enable_early_data = true; - - config.alpn_protocols = alpn; - - Ok(config) -} - -pub(crate) mod verify { - use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; - use std::sync::Arc; - - #[derive(Debug)] - pub(crate) struct SkipServerVerification(Arc); - - impl SkipServerVerification { - pub(crate) fn new() -> Arc { - let provider = super::default_provider(); - Arc::new(Self(provider)) - } - } - - impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { - fn verify_server_cert( - &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp: &[u8], - _now: UnixTime, - ) -> std::result::Result - { - Ok(rustls::client::danger::ServerCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &rustls::DigitallySignedStruct, - ) -> std::result::Result - { - rustls::crypto::verify_tls12_signature( - message, - cert, - dss, - &self - .0 - .signature_verification_algorithms, - ) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &rustls::DigitallySignedStruct, - ) -> std::result::Result - { - rustls::crypto::verify_tls13_signature( - message, - cert, - dss, - &self - .0 - .signature_verification_algorithms, - ) - } - - fn supported_verify_schemes(&self) -> Vec { - self.0 - .signature_verification_algorithms - .supported_schemes() - } - } -} diff --git a/deboa-compio/src/client/http/http1.rs b/deboa-compio/src/client/http/http1.rs index 8efd5902..991694a8 100644 --- a/deboa-compio/src/client/http/http1.rs +++ b/deboa-compio/src/client/http/http1.rs @@ -1,19 +1,14 @@ -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::alpn; -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::client::http::conn::stream::tls_connection; -use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::plain_connection, BaseHttpConnection, Http1Connection}, -}; +use crate::client::http::conn::{BaseHttpConnection, Http1Connection}; +use compio::net::TcpStream; +use cyper_core::HyperStream; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, + errors::{ConnectionError, DeboaError}, request::Http1Request, Result, }; use http::version::Version; use hyper::client::conn::http1::handshake; -use hyper_body_utils::HttpBody; impl HttpConnection for Http1Connection { type Sender = Http1Request; @@ -23,42 +18,20 @@ impl HttpConnection for Http1Connection { } impl ProtoConnection for Http1Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http1Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = HyperStream; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_11 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - let stream = if config.is_secure() { - tls_connection( - *config.ip(), - config.host(), - config.port(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - ) + async fn connect(stream: Self::RuntimeStream) -> Result { + let (sender, conn) = handshake(stream) .await - } else { - plain_connection(*config.ip(), config.host(), config.port()).await - }; - - if let Err(e) = stream { - return Err(e); - } - - let result = handshake(stream.unwrap()).await; - - let (sender, conn) = result.unwrap(); + .map_err(|e| { + DeboaError::Connection(ConnectionError::Handshake { message: e.to_string() }) + })?; compio::runtime::spawn(async move { match conn @@ -66,7 +39,9 @@ impl ProtoConnection for Http1Connection { .await { Ok(_) => (), - Err(_err) => {} + Err(err) => { + log::error!("Error: {:#}", err) + } }; }) .detach(); diff --git a/deboa-compio/src/client/http/http2.rs b/deboa-compio/src/client/http/http2.rs index a5c27a95..2bcfa74e 100644 --- a/deboa-compio/src/client/http/http2.rs +++ b/deboa-compio/src/client/http/http2.rs @@ -1,20 +1,14 @@ -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::alpn; -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::client::http::conn::stream::tls_connection; -use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::plain_connection, BaseHttpConnection, Http2Connection}, -}; -use cyper_core::CompioExecutor; +use crate::client::http::conn::{BaseHttpConnection, Http2Connection}; +use compio::net::TcpStream; +use cyper_core::{CompioExecutor, HyperStream}; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, + errors::{ConnectionError, DeboaError}, request::Http2Request, Result, }; use http::version::Version; use hyper::client::conn::http2::handshake; -use hyper_body_utils::HttpBody; impl HttpConnection for Http2Connection { type Sender = Http2Request; @@ -24,48 +18,26 @@ impl HttpConnection for Http2Connection { } impl ProtoConnection for Http2Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http2Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = HyperStream; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_2 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - let stream = if config.is_secure() { - tls_connection( - *config.ip(), - config.host(), - config.port(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - ) + async fn connect(stream: HyperStream) -> Result { + let (sender, conn) = handshake(CompioExecutor, stream) .await - } else { - plain_connection(*config.ip(), config.host(), config.port()).await - }; - - if let Err(e) = stream { - return Err(e); - } - - let result = handshake(CompioExecutor, stream.unwrap()).await; - - let (sender, conn) = result.unwrap(); + .map_err(|e| { + DeboaError::Connection(ConnectionError::Handshake { message: e.to_string() }) + })?; compio::runtime::spawn(async move { match conn.await { Ok(_) => (), Err(err) => { - println!("Error: {:#}", err); + log::error!("Error: {:#}", err) } }; }) diff --git a/deboa-compio/src/client/http/http3.rs b/deboa-compio/src/client/http/http3.rs index b0c99b7d..dd4b6e14 100644 --- a/deboa-compio/src/client/http/http3.rs +++ b/deboa-compio/src/client/http/http3.rs @@ -1,56 +1,13 @@ -use crate::{ - alpn, - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::setup_rust_tls, BaseHttpConnection, Http3Connection}, -}; -use compio_quic::{crypto::rustls::QuicClientConfig, ClientConfig, Endpoint}; +use crate::client::http::conn::{BaseHttpConnection, Http3Connection}; +use compio_quic::Connection; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, errors::{ConnectionError, DeboaError}, Result, }; use deboa_h3::compio::{Http3Request, SendRequest}; use futures::future; use http::version::Version; -use hyper_body_utils::HttpBody; -use std::{ - net::{IpAddr, SocketAddr}, - sync::Arc, -}; - -async fn lookup_and_connect( - ip: IpAddr, - host: &str, - port: u16, - client_endpoint: &Endpoint, - client_config: ClientConfig, -) -> std::result::Result { - let conn = client_endpoint.connect(SocketAddr::new(ip, port), host, Some(client_config)); - - let conn = match conn { - Ok(conn) => conn, - Err(e) => { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })) - } - }; - - let conn = conn.await; - - let conn = match conn { - Ok(conn) => conn, - Err(e) => { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })) - } - }; - - Ok(conn) -} impl HttpConnection for Http3Connection { type Sender = Http3Request; @@ -60,81 +17,18 @@ impl HttpConnection for Http3Connection { } impl ProtoConnection for Http3Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http3Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = Connection; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_3 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - let client_endpoint = - Endpoint::client(SocketAddr::new(*config.client_bind_addr(), 0)).await; - - if let Err(e) = client_endpoint { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let client_endpoint = client_endpoint.unwrap(); - - let tls_config = setup_rust_tls( - config.host(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - )?; - - let quic_config = QuicClientConfig::try_from(tls_config); - if let Err(e) = quic_config { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let quic_config = quic_config.unwrap(); - let client_config = ClientConfig::new(Arc::new(quic_config)); - let result = lookup_and_connect( - *config.ip(), - config.host(), - config.port(), - &client_endpoint, - client_config, - ) - .await; - - if let Err(e) = result { - return Err(e); - } - - let conn = result.unwrap(); - - let client = compio_quic::h3::client::new(conn).await; - - if let Err(e) = client { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let (mut conn, sender) = client.unwrap(); + async fn connect(stream: Self::RuntimeStream) -> Result { + let (mut conn, sender) = compio_quic::h3::client::new(stream) + .await + .map_err(|e| DeboaError::Connection(ConnectionError::Udp { message: e.to_string() }))?; compio::runtime::spawn(async move { future::poll_fn(|cx| conn.poll_close(cx)).await; diff --git a/deboa-compio/src/client/mod.rs b/deboa-compio/src/client/mod.rs index 428f5c44..5f73cf1f 100644 --- a/deboa-compio/src/client/mod.rs +++ b/deboa-compio/src/client/mod.rs @@ -1,2 +1,3 @@ pub mod dns; pub mod http; +pub mod tls; diff --git a/deboa-compio/src/client/tls/mod.rs b/deboa-compio/src/client/tls/mod.rs new file mode 100644 index 00000000..afc96df1 --- /dev/null +++ b/deboa-compio/src/client/tls/mod.rs @@ -0,0 +1,9 @@ +//! TLS transport implementations for the Deboa HTTP client. +//! +//! This module provides TLS functionality for secure HTTP connections. +//! It supports both native-tls and rustls backends. + +#[cfg(feature = "native-tls")] +pub mod native; +#[cfg(feature = "rust-tls")] +pub mod rustls; diff --git a/deboa-compio/src/client/tls/native.rs b/deboa-compio/src/client/tls/native.rs new file mode 100644 index 00000000..12b02538 --- /dev/null +++ b/deboa-compio/src/client/tls/native.rs @@ -0,0 +1,126 @@ +use crate::cert::{DeboaCertificate, DeboaIdentity}; +use compio::net::TcpStream; +use compio_tls::{ + native_tls::TlsConnector, + native_tls::{Certificate, Identity}, + TlsStream, +}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, +}; + +#[inline] +pub(crate) fn alpn() -> &'static [&'static str] { + &[ + #[cfg(feature = "http3")] + "h3", + #[cfg(feature = "http2")] + "h2", + #[cfg(feature = "http1")] + "http/1.1", + ] +} + +pub struct TlsConnectionBuilder<'a> { + tcp_stream: TcpStream, + host: &'a str, + identity: Option<&'a DeboaIdentity>, + certificate: Option<&'a DeboaCertificate>, + skip_server_verification: bool, + alpn: &'a [&'a str], +} + +impl<'a> TlsConnectionBuilder<'a> { + pub fn new(tcp_stream: TcpStream, host: &'a str) -> Self { + Self { + tcp_stream, + host, + identity: None, + certificate: None, + skip_server_verification: false, + alpn: alpn(), + } + } + + pub fn identity(mut self, identity: Option<&'a DeboaIdentity>) -> Self { + self.identity = identity; + self + } + + pub fn certificate(mut self, certificate: Option<&'a DeboaCertificate>) -> Self { + self.certificate = certificate; + self + } + + pub fn skip_server_verification(mut self, skip_server_verification: bool) -> Self { + self.skip_server_verification = skip_server_verification; + self + } + + pub fn alpn(mut self, alpn: &'a [&str]) -> Self { + self.alpn = alpn; + self + } + + pub async fn connect(self) -> Result> { + let mut builder = TlsConnector::builder(); + + let builder = if self.skip_server_verification { + builder + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + } else { + &mut builder + }; + + let builder = builder.request_alpns(self.alpn); + + let builder = if let Some(ca) = self.certificate { + let cert: Certificate = ca + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid CA certificate: {}", e), + }) + })?; + builder.add_root_certificate(cert) + } else { + builder + }; + + let builder = if let Some(identity) = self.identity { + let ident: Identity = identity + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid client identity: {}", e), + }) + })?; + builder.identity(ident) + } else { + builder + }; + + let connector = builder + .build() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not build TLS connector: {}", e), + }) + })?; + + let connector = compio_tls::TlsConnector::from(connector); + + let stream = connector + .connect(self.host, self.tcp_stream) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not connect to server: {}", e), + }) + })?; + + Ok(stream) + } +} diff --git a/deboa-compio/src/client/tls/rustls.rs b/deboa-compio/src/client/tls/rustls.rs new file mode 100644 index 00000000..0bb431c1 --- /dev/null +++ b/deboa-compio/src/client/tls/rustls.rs @@ -0,0 +1,320 @@ +//! TLS implementation using rustls + +use crate::cert::{DeboaCertificate, DeboaIdentity}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, +}; +use rustls::{ + crypto::CryptoProvider, + pki_types::{CertificateDer, PrivateKeyDer}, + ClientConfig, +}; + +pub(crate) fn default_provider() -> CryptoProvider { + #[cfg(feature = "__rustls_aws_lc_rs")] + return rustls::crypto::aws_lc_rs::default_provider(); + #[cfg(feature = "__rustls_ring")] + return rustls::crypto::ring::default_provider(); +} + +#[inline] +pub(crate) fn alpn() -> Vec> { + vec![ + #[cfg(feature = "http3")] + b"h3".to_vec(), + #[cfg(feature = "http2")] + b"h2".to_vec(), + #[cfg(feature = "http1")] + b"http/1.1".to_vec(), + ] +} + +/// Builder for TLS connections using rustls +pub struct TlsConnectionBuilder<'a> { + identity: Option<&'a DeboaIdentity>, + certificate: Option<&'a DeboaCertificate>, + skip_server_verification: bool, + alpn: Vec>, + provider: CryptoProvider, +} + +impl Default for TlsConnectionBuilder<'_> { + fn default() -> Self { + Self { + identity: None, + certificate: None, + skip_server_verification: false, + alpn: alpn(), + provider: default_provider(), + } + } +} + +impl<'a> TlsConnectionBuilder<'a> { + /// Set the identity to use for the connection + pub fn identity(mut self, identity: Option<&'a DeboaIdentity>) -> Self { + self.identity = identity; + self + } + + /// Set the certificate to use for the connection + pub fn certificate(mut self, certificate: Option<&'a DeboaCertificate>) -> Self { + self.certificate = certificate; + self + } + + /// Skip server verification + pub fn skip_server_verification(mut self, skip_server_verification: bool) -> Self { + self.skip_server_verification = skip_server_verification; + self + } + + /// Set the ALPN protocols to use for the connection + pub fn alpn(mut self, alpn: Vec>) -> Self { + self.alpn = alpn; + self + } + + /// Build the TLS client configuration + pub fn build_config(self) -> Result { + let client_config = { + if self.skip_server_verification { + ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(verify::SkipServerVerification::new( + self.provider, + )) + .with_no_client_auth() + } else { + #[cfg(feature = "__webpki_rustls_verifier")] + let config = { + let config = ClientConfig::builder_with_provider(self.provider.into()) + .with_protocol_versions(rustls::ALL_VERSIONS) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set TLS version: {}", e), + }) + })?; + + let mut root_store = + rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; + let config = if let Some(ca) = self.certificate { + let cert = ca + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid CA certificate: {}", e), + }) + })?; + + root_store + .add(cert) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!( + "Could not add CA certificate to the store: {}", + e + ), + }) + })?; + + config.with_root_certificates(root_store) + } else { + config.with_root_certificates(root_store) + }; + + config + }; + + #[cfg(feature = "__platform_rustls_verifier")] + let config = { + use rustls_platform_verifier::BuilderVerifierExt; + rustls::ClientConfig::builder_with_provider(default_provider()) + .with_protocol_versions(rustls::ALL_VERSIONS) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set TLS version: {}", e), + }) + })? + .with_platform_verifier() + }; + + let mut config = if let Some(id) = self.identity { + let pair: (CertificateDer<'_>, PrivateKeyDer<'_>) = id + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid client identity: {}", e), + }) + })?; + + config + .with_client_auth_cert(vec![pair.0], pair.1) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set client identity: {}", e), + }) + })? + } else { + config.with_no_client_auth() + }; + + config.enable_early_data = true; + + config.alpn_protocols = self.alpn; + + config + } + }; + + Ok(client_config) + } +} + +#[cfg(any(feature = "http1", feature = "http2"))] +/// TCP connection module for TLS +pub mod tcp { + use compio::net::TcpStream; + use compio_tls::{TlsConnector, TlsStream}; + use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, + }; + use rustls::ClientConfig; + use std::sync::Arc; + + /// Establish a TLS connection over TCP + pub async fn connect( + config: ClientConfig, + inner_stream: TcpStream, + host: &str, + ) -> Result> { + let connector = TlsConnector::from(Arc::new(config)); + + connector + .connect(host, inner_stream) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not connect to server: {}", e), + }) + }) + } +} + +#[cfg(feature = "http3")] +/// UDP connection module for TLS +pub mod udp { + use compio_quic::{Connection, Endpoint}; + use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, + }; + use rustls::ClientConfig; + use std::{net::SocketAddr, sync::Arc}; + + /// Establish a TLS connection over UDP + pub async fn connect( + config: ClientConfig, + endpoint: &mut Endpoint, + socket_addr: SocketAddr, + host: &str, + ) -> Result { + let quic_config = + compio_quic::crypto::rustls::QuicClientConfig::try_from(config).map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not create QUIC client config: {}", e), + }) + })?; + + let client_config = compio_quic::ClientConfig::new(Arc::new(quic_config)); + + let conn = endpoint + .connect(socket_addr, host, Some(client_config)) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: format!("Could not connect to server: {}", e), + }) + })?; + + let conn = conn + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: format!("Could not connect to server: {}", e), + }) + })?; + + Ok(conn) + } +} + +pub(crate) mod verify { + use rustls::{ + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::CryptoProvider, + pki_types::{CertificateDer, ServerName, UnixTime}, + }; + use std::sync::Arc; + + #[derive(Debug)] + pub(crate) struct SkipServerVerification(CryptoProvider); + + impl SkipServerVerification { + pub(crate) fn new(provider: CryptoProvider) -> Arc { + Arc::new(Self(provider)) + } + } + + impl ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> std::result::Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self + .0 + .signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self + .0 + .signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0 + .signature_verification_algorithms + .supported_schemes() + } + } +} diff --git a/deboa-compio/src/lib.rs b/deboa-compio/src/lib.rs index a16f6148..49a4c455 100644 --- a/deboa-compio/src/lib.rs +++ b/deboa-compio/src/lib.rs @@ -22,44 +22,20 @@ compile_error!( #[cfg(all(feature = "native-tls", feature = "rust-tls"))] compile_error!("You cannot enable native-tls and rust-tls features at the same time."); +#[cfg(all(not(any(feature = "native-tls", feature = "rust-tls")), feature = "http2"))] +compile_error!("HTTP2 requires native-tls or rust-tls support."); + #[cfg(all(feature = "native-tls", feature = "http3"))] compile_error!("HTTP3 is not supported within tokio-native-tls runtime."); #[cfg(not(any(feature = "http1", feature = "http2", feature = "http3")))] compile_error!("At least one HTTP version feature must be enabled."); -#[cfg(feature = "rust-tls")] -#[inline] -pub(crate) fn alpn() -> Vec> { - vec![ - #[cfg(feature = "http2")] - b"h2".to_vec(), - #[cfg(feature = "http1")] - b"http/1.1".to_vec(), - #[cfg(feature = "http3")] - b"h3".to_vec(), - ] -} - -#[cfg(feature = "native-tls")] -#[inline] -pub(crate) fn alpn() -> &'static [&'static str] { - &[ - #[cfg(feature = "http2")] - "h2", - #[cfg(feature = "http1")] - "http/1.1", - #[cfg(feature = "http3")] - "h3", - ] -} - -use deboa::InnerClient; - use crate::{ cert::{DeboaCertificate, DeboaIdentity}, client::{dns::DefaultDnsResolver, http::conn::pool::HttpConnectionPool}, }; +use deboa::InnerClient; pub mod cert; pub mod client; diff --git a/deboa-compio/tests/base/get.rs b/deboa-compio/tests/base/get.rs index 303944c6..ff91fb24 100644 --- a/deboa-compio/tests/base/get.rs +++ b/deboa-compio/tests/base/get.rs @@ -30,28 +30,22 @@ async fn test_get_http( #[rstest] #[compio::test] async fn test_get_http_skip_verification( - create_client: Client, #[future] create_server: EasyHttpMock, protocol_version: http::Version, ) -> TestResult<()> { - let identity = DeboaIdentity::from_pkcs8( - deboa_test_utils::common::helpers::CLIENT_CERT, - deboa_test_utils::common::helpers::CLIENT_KEY, - ContentEncoding::DER, - ); - let client = Client::builder() .certificate(DeboaCertificate::from_slice( deboa_test_utils::common::helpers::CA_CERT, ContentEncoding::DER, )) - .identity(identity) + .skip_cert_verification(true) .build(); - deboa_test_utils::base::get::test_get_http_mutual_authentication( + deboa_test_utils::base::get::test_skip_cert_verification( &client, &mut create_server.await, protocol_version, + true, ) .await } @@ -59,12 +53,15 @@ async fn test_get_http_skip_verification( #[rstest] #[compio::test] async fn test_get_http_verify( - create_client: Client, #[future] create_server: EasyHttpMock, protocol_version: http::Version, ) -> TestResult<()> { + let client = Client::builder() + .skip_cert_verification(false) + .build(); + deboa_test_utils::base::get::test_skip_cert_verification( - &create_client, + &client, &mut create_server.await, protocol_version, false, diff --git a/deboa-compio/tests/common/helpers.rs b/deboa-compio/tests/common/helpers.rs index ecb1e0eb..9115dda2 100644 --- a/deboa-compio/tests/common/helpers.rs +++ b/deboa-compio/tests/common/helpers.rs @@ -72,9 +72,13 @@ pub async fn tls_mock_server() -> EasyHttpMock { let vetis_adapter_config = VetisAdapterConfig::builder() .hostname(&hostname) - .interface(&interface) - .protocol_version(protocol_version()) - .with_random_port() + .interface( + interface + .parse() + .unwrap(), + ) + .protos(vec![protocol_version()]) + .port(free_port(&interface)) .cert(server_cert.to_vec()) .key(server_key.to_vec()) .ca(CA_CERT.to_vec()) @@ -102,8 +106,12 @@ pub async fn plain_mock_server() -> EasyHttpMock { let vetis_adapter_config = VetisAdapterConfig::builder() .hostname(&hostname) - .interface(&interface) - .protocol_version(protocol_version()) + .interface( + interface + .parse() + .unwrap(), + ) + .protos(vec![protocol_version()]) .with_random_port() .build(); @@ -129,3 +137,26 @@ pub async fn create_server() -> EasyHttpMock { #[cfg(not(any(feature = "rust-tls", feature = "native-tls")))] return plain_mock_server().await; } + +/// An ephemeral port the OS says is free, rather than a guessed one. +/// +/// `with_random_port()` picks `rand::random_range(9000..65535)` and +/// de-duplicates against a process-local set. Under `cargo test` that mostly +/// holds, because every test shares one process and therefore one set. Under +/// `cargo nextest run` — which is what CI uses — each test is its own process, +/// so the set protects nothing and two tests eventually roll the same number. +/// The loser dies with `Address already in use (os error 98)` before its body +/// runs, which is why the failing test is a different one each time. +/// +/// Binding port 0 asks the kernel for a port it knows is free. There is still a +/// window between dropping this listener and the server binding, but the kernel +/// will not hand the same ephemeral port to someone else inside it, which is +/// the part guessing cannot promise. +fn free_port(interface: &str) -> u16 { + let listener = std::net::TcpListener::bind((interface, 0)) + .expect("bind an ephemeral port to ask the OS for a free one"); + listener + .local_addr() + .expect("read back the bound port") + .port() +} diff --git a/deboa-glommio/Cargo.toml b/deboa-glommio/Cargo.toml index 99ad4ba4..63cc54b5 100644 --- a/deboa-glommio/Cargo.toml +++ b/deboa-glommio/Cargo.toml @@ -12,9 +12,20 @@ publish = false rust-version.workspace = true [features] -default = ["http1", "http2", "rust-tls", "default-rustls-provider", "default-rustls-verifier"] +default = [ + "http1", + "http2", + "rust-tls", + "default-rustls-provider", + "default-rustls-verifier", +] -rust-tls = ["dep:rustls", "dep:rustls-native-certs", "dep:futures-rustls", "dep:rustls-pki-types"] +rust-tls = [ + "dep:rustls", + "dep:rustls-native-certs", + "dep:futures-rustls", + "dep:rustls-pki-types", +] # Declared so the cfg is known and the error below is a clear message rather # than a wall of type errors. `async-native-tls` has no glommio binding — it @@ -67,7 +78,10 @@ http-body = "1" http-body-util = "0.1" hyper = { version = "1.10.1", features = ["client"], default-features = false } hyper-body-utils = { workspace = true, default-features = false } -hyper-util = { version = "0.1.20", features = ["client", "client-legacy"], default-features = false } +hyper-util = { version = "0.1.20", features = [ + "client", + "client-legacy", +], default-features = false } indexmap = "2.11.4" log = "0.4.32" macro_rules_attribute = { version = "0.2.2", default-features = false } diff --git a/deboa-glommio/examples/simple.rs b/deboa-glommio/examples/simple.rs index 16711b00..38b9276e 100644 --- a/deboa-glommio/examples/simple.rs +++ b/deboa-glommio/examples/simple.rs @@ -4,7 +4,7 @@ //! cargo run -p deboa-glommio --example simple -- https://example.com //! ``` -use deboa::request::{get, FetchWith}; +use deboa::request::get; use deboa_glommio::Client; fn main() { diff --git a/deboa-glommio/src/cert.rs b/deboa-glommio/src/cert.rs index 4761e91b..13ce03c3 100644 --- a/deboa-glommio/src/cert.rs +++ b/deboa-glommio/src/cert.rs @@ -93,7 +93,7 @@ impl deboa::cert::Identity for DeboaIdentity { &self.cert } - fn ket(&self) -> &Option> { + fn key(&self) -> &Option> { &self.key } diff --git a/deboa-glommio/src/client/http/conn/mod.rs b/deboa-glommio/src/client/http/conn/mod.rs index b64798ae..7840e2ea 100644 --- a/deboa-glommio/src/client/http/conn/mod.rs +++ b/deboa-glommio/src/client/http/conn/mod.rs @@ -16,21 +16,30 @@ //! - Thread-safe connection handling //! ``` use crate::cert::{DeboaCertificate, DeboaIdentity}; +#[cfg(feature = "rust-tls")] +use crate::rt::stream::GlommioStream; #[cfg(feature = "http1")] use deboa::request::Http1Request; #[cfg(feature = "http2")] use deboa::request::Http2Request; use deboa::{ conn::{ConnectionConfig, HttpConnectionDispatcher, ProtoConnection}, - errors::{DeboaError, RequestError}, + dns::DnsResolver, + errors::{ConnectionError, DeboaError, RequestError}, response::DeboaResponse, Result, }; #[cfg(feature = "http3")] use deboa_h3::generic::Http3Request; +#[cfg(feature = "rust-tls")] +use glommio::net::TcpStream; use http::{Request, Version}; use hyper_body_utils::HttpBody; -use std::marker::PhantomData; +#[cfg(feature = "rust-tls")] +use log::info; +#[cfg(feature = "rust-tls")] +use std::borrow::Cow; +use std::{marker::PhantomData, time::Duration}; /// Connection pooling for efficient HTTP connections. /// @@ -111,20 +120,8 @@ impl DeboaConnection { pub fn http3(conn: Http3Connection) -> Self { DeboaConnection::Http3(Box::new(conn)) } -} -impl HttpConnectionDispatcher for DeboaConnection { - /// Send a request through the connection. - /// - /// # Arguments - /// - /// * `url` - The URL to send the request to. - /// * `request` - The request to send. - /// - /// # Returns - /// - /// * `Result` - The response from the server. - async fn send_request(&mut self, request: Request) -> Result { + async fn send(&mut self, request: Request) -> Result { match self { #[cfg(feature = "http1")] DeboaConnection::Http1(ref mut conn) => { @@ -178,29 +175,210 @@ impl HttpConnectionDispatcher for DeboaConnection { } } +impl HttpConnectionDispatcher for DeboaConnection { + /// Send a request through the connection. + /// + /// # Arguments + /// + /// * `request` - The request to send. + /// + /// # Returns + /// + /// * `Result` - The response from the server. + async fn send_request( + &mut self, + request: Request, + timeout: Duration, + ) -> Result { + glommio::future::timeout(timeout, self.send(request)) + .await + .map_err(|_| { + DeboaError::Request(RequestError::Send { message: "Request timed out".to_string() }) + })? + } +} + +#[cfg(feature = "rust-tls")] +async fn connect_with_rustls<'a>( + tcp_stream: TcpStream, + config: &ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, +) -> Result<(Version, GlommioStream)> { + use crate::client::tls::rustls::{tcp::connect, TlsConnectionBuilder}; + let tls_config = TlsConnectionBuilder::default() + .certificate(config.certificate()) + .identity(config.identity()) + .build_config()?; + + let stream = connect(tls_config, tcp_stream, config.host()).await?; + + if let Some(alpn) = stream + .get_ref() + .1 + .alpn_protocol() + { + let Cow::Borrowed(alpn_code) = String::from_utf8_lossy(&alpn) else { + return Err(DeboaError::Connection(ConnectionError::Tcp { + message: "Invalid ALPN code".to_string(), + })); + }; + + let version = match alpn_code { + "http1.1" => Version::HTTP_11, + "h2" => Version::HTTP_2, + "h3" => Version::HTTP_3, + _ => *config.protocol_version(), + }; + + info!("ALPN info found, switching connection to {:?}", version); + Ok((version, GlommioStream::Tls(Box::new(stream)))) + } else { + info!("No ALPN info available, falling back to HTTP/1.1"); + Ok((*config.protocol_version(), GlommioStream::Tls(Box::new(stream)))) + } +} + +#[cfg(feature = "native-tls")] +async fn connect_with_nativels<'a>( + tcp_stream: TcpStream, + config: &ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, +) -> Result<(Version, HyperStream)> { + use crate::client::tls::native::TlsConnectionBuilder; + let stream = TlsConnectionBuilder::new(tcp_stream, config.host()) + .certificate(config.certificate()) + .identity(config.identity()) + .connect() + .await?; + + if let Some(alpn) = stream.negotiated_alpn() { + let Cow::Borrowed(alpn_code) = String::from_utf8_lossy(alpn) else { + return Err(DeboaError::Connection(ConnectionError::Tcp { + message: "Invalid ALPN code".to_string(), + })); + }; + + let version = match alpn_code { + "http1.1" => Version::HTTP_11, + "h2" => Version::HTTP_2, + "h3" => Version::HTTP_3, + _ => *config.protocol_version(), + }; + + info!("ALPN info found, switching connection to {:?}", version); + Ok((version, HyperStream::Tls(stream))) + } else { + info!("No ALPN info available, falling back to HTTP/1.1"); + Ok((Version::HTTP_11, HyperStream::Tls(stream))) + } +} + /// Connection factory. pub struct ConnectionFactory {} impl ConnectionFactory { /// Create a new connection. - pub async fn create_connection<'a>( - protocol: &Version, + pub async fn create_connection<'a, D>( config: &'a ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, - ) -> Result { - let conn = match protocol { + dns_resolver: &D, + ) -> Result + where + D: DnsResolver, + { + let ips = dns_resolver + .resolve( + config + .host() + .to_string(), + config.port(), + ) + .await?; + let ips = if config + .client_bind_addr() + .is_ipv4() + { + ips.into_iter() + .filter(|ip| ip.is_ipv4()) + .collect::>() + } else { + ips.into_iter() + .filter(|ip| ip.is_ipv6()) + .collect::>() + }; + + let Some(ip) = ips.first() else { + return Err(DeboaError::Request(RequestError::Send { + message: format!("No IP addresses found for hostname: {}", config.host()), + })); + }; + + #[cfg(any(feature = "http1", feature = "http2"))] + let conn_pair = { + use crate::rt::stream::GlommioStream; + use glommio::net::TcpStream; + + let tcp_stream = TcpStream::connect(format!("{}:{}", ip, config.port())) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tcp { message: e.to_string() }) + })?; + let use_tls = config.scheme() == "https" || config.scheme() == "wss"; + if !use_tls { + (Version::HTTP_11, GlommioStream::Plain(tcp_stream)) + } else { + #[cfg(feature = "rust-tls")] + { + connect_with_rustls(tcp_stream, config).await? + } + + #[cfg(feature = "native-tls")] + { + connect_with_nativels(tcp_stream, config).await? + } + } + }; + + let conn = match conn_pair.0 { #[cfg(feature = "http1")] - &Version::HTTP_11 => { - let conn = Http1Connection::connect(config).await?; + Version::HTTP_11 => { + let conn = Http1Connection::connect(conn_pair.1).await?; DeboaConnection::http1(conn) } #[cfg(feature = "http2")] - &Version::HTTP_2 => { - let conn = Http2Connection::connect(config).await?; + Version::HTTP_2 => { + let conn = Http2Connection::connect(conn_pair.1).await?; DeboaConnection::http2(conn) } - #[cfg(all(feature = "http3", feature = "rust-tls"))] - &Version::HTTP_3 => { - let conn = Http3Connection::connect(&config).await?; + #[cfg(feature = "http3")] + Version::HTTP_3 => { + let stream = { + use crate::client::tls::rustls::{udp::connect, TlsConnectionBuilder}; + use compio_quic::Endpoint; + use deboa::errors::ConnectionError; + use std::net::SocketAddr; + + let mut client_endpoint = + Endpoint::client(SocketAddr::new(*config.client_bind_addr(), 0)) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: e.to_string(), + }) + })?; + + let tls_config = TlsConnectionBuilder::default() + .certificate(config.certificate()) + .identity(config.identity()) + .build_config()?; + + connect( + tls_config, + &mut client_endpoint, + SocketAddr::new(*ip, config.port()), + config.host(), + ) + .await? + }; + + let conn = Http3Connection::connect(stream).await?; DeboaConnection::http3(conn) } _ => { diff --git a/deboa-glommio/src/client/http/conn/pool.rs b/deboa-glommio/src/client/http/conn/pool.rs index 2765899c..4085d101 100644 --- a/deboa-glommio/src/client/http/conn/pool.rs +++ b/deboa-glommio/src/client/http/conn/pool.rs @@ -2,9 +2,13 @@ use crate::{ cert::{DeboaCertificate, DeboaIdentity}, client::http::conn::{ConnectionConfig, ConnectionFactory, DeboaConnection}, }; -use deboa::Result; +use deboa::{ + dns::DnsResolver, + errors::{ConnectionError, DeboaError}, + Result, +}; use hashbrown::HashMap; -use time::Duration; +use std::time::Duration; /// Struct that represents the HTTP connection pool. /// @@ -27,7 +31,7 @@ impl Default for HttpConnectionPool { fn default() -> Self { Self { max_idle_connections: 5, - keep_alive_duration: Duration::minutes(5), + keep_alive_duration: Duration::from_mins(5), connections: HashMap::new(), } } @@ -76,15 +80,14 @@ impl deboa::conn::HttpConnectionPool for HttpConnectionPool { .len() as u32 } - async fn create_connection<'a>( - &'a mut self, + async fn create_connection<'a, D>( + &mut self, config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result<&'a mut Self::ConnectionDispather> { - if self.max_idle_connections == 0 { - self.connections - .clear(); - } - + dns_resolver: &D, + ) -> Result<&mut DeboaConnection> + where + D: DnsResolver, + { let host = config.host(); if self .connections @@ -98,8 +101,20 @@ impl deboa::conn::HttpConnectionPool for HttpConnectionPool { } log::debug!("Creating new connection for {}", host); - let connection = - ConnectionFactory::create_connection(config.protocol_version(), config).await?; + let connection = glommio::future::timeout( + config.connection_timeout(), + ConnectionFactory::create_connection(config, dns_resolver), + ) + .await + .map_err(|_| { + DeboaError::Connection(ConnectionError::Timeout { + message: format!( + "Connection to {} timed out after {:?}", + host, + config.connection_timeout() + ), + }) + })??; self.connections .insert(host.to_string(), connection); diff --git a/deboa-glommio/src/client/http/conn/stream/plain.rs b/deboa-glommio/src/client/http/conn/stream/plain.rs index 6fe1f0c0..3aa23a8d 100644 --- a/deboa-glommio/src/client/http/conn/stream/plain.rs +++ b/deboa-glommio/src/client/http/conn/stream/plain.rs @@ -12,7 +12,6 @@ pub(crate) async fn create_stream(addr: IpAddr, host: &str, port: u16) -> Result Ok(tcp_stream) => tcp_stream, Err(e) => { return Err(DeboaError::Connection(ConnectionError::Tcp { - host: host.to_string(), message: format!("Could not connect to server: {}", e), })); } diff --git a/deboa-glommio/src/client/http/conn/stream/tls/rustls.rs b/deboa-glommio/src/client/http/conn/stream/tls/rustls.rs index df235853..62f5d4d3 100644 --- a/deboa-glommio/src/client/http/conn/stream/tls/rustls.rs +++ b/deboa-glommio/src/client/http/conn/stream/tls/rustls.rs @@ -27,10 +27,7 @@ pub(crate) async fn tls_connection<'a>( let hostname = ServerName::try_from(host.to_string()); if let Err(e) = hostname { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: e.to_string(), - })); + return Err(DeboaError::Connection(ConnectionError::Tls { message: e.to_string() })); } let stream = connector @@ -39,7 +36,6 @@ pub(crate) async fn tls_connection<'a>( if let Err(e) = stream { return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), message: format!("Could not connect to server: {}", e), })); } @@ -88,7 +84,6 @@ pub(crate) fn setup_rust_tls<'a>( let cert = ca.try_into(); if let Err(e) = cert { return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), message: format!("Invalid CA certificate: {}", e), })); } @@ -96,7 +91,6 @@ pub(crate) fn setup_rust_tls<'a>( let result = root_store.add(cert.unwrap()); if let Err(e) = result { return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), message: format!("Could not add CA certificate to the store: {}", e), })); } @@ -122,7 +116,6 @@ pub(crate) fn setup_rust_tls<'a>( let pair = id.try_into(); if let Err(e) = pair { return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), message: format!("Invalid client identity: {}", e), })); } diff --git a/deboa-glommio/src/client/http/http1.rs b/deboa-glommio/src/client/http/http1.rs index 5e113eca..ee2924a5 100644 --- a/deboa-glommio/src/client/http/http1.rs +++ b/deboa-glommio/src/client/http/http1.rs @@ -1,19 +1,15 @@ -#[cfg(feature = "rust-tls")] -use crate::alpn; -#[cfg(feature = "rust-tls")] -use crate::client::http::conn::stream::tls_connection; use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::plain_connection, BaseHttpConnection, Http1Connection}, + client::http::conn::{BaseHttpConnection, Http1Connection}, + rt::stream::GlommioStream, }; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, + errors::{ConnectionError, DeboaError}, request::Http1Request, Result, }; use http::version::Version; use hyper::client::conn::http1::handshake; -use hyper_body_utils::HttpBody; use smol_hyper::rt::FuturesIo; impl HttpConnection for Http1Connection { @@ -24,46 +20,20 @@ impl HttpConnection for Http1Connection { } impl ProtoConnection for Http1Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http1Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = GlommioStream; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_11 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - #[cfg(feature = "rust-tls")] - let stream = if config.is_secure() { - tls_connection( - *config.ip(), - config.host(), - config.port(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - ) + async fn connect(stream: Self::RuntimeStream) -> Result { + let (sender, conn) = handshake(FuturesIo::new(stream)) .await - } else { - plain_connection(*config.ip(), config.host(), config.port()).await - }; - - #[cfg(not(feature = "rust-tls"))] - let stream = plain_connection(*config.ip(), config.host(), config.port()).await; - - if let Err(e) = stream { - return Err(e); - } - - let result = handshake(FuturesIo::new(stream.unwrap())).await; - - let (sender, conn) = result.unwrap(); + .map_err(|e| { + DeboaError::Connection(ConnectionError::Handshake { message: e.to_string() }) + })?; glommio::spawn_local(async move { match conn.await { diff --git a/deboa-glommio/src/client/http/http2.rs b/deboa-glommio/src/client/http/http2.rs index 7e066dd0..4828a075 100644 --- a/deboa-glommio/src/client/http/http2.rs +++ b/deboa-glommio/src/client/http/http2.rs @@ -1,20 +1,15 @@ -#[cfg(feature = "rust-tls")] -use crate::alpn; -#[cfg(feature = "rust-tls")] -use crate::client::http::conn::stream::tls_connection; use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::plain_connection, BaseHttpConnection, Http2Connection}, - rt::executor::GlommioExecutor, + client::http::conn::{BaseHttpConnection, Http2Connection}, + rt::{executor::GlommioExecutor, stream::GlommioStream}, }; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, + errors::{ConnectionError, DeboaError}, request::Http2Request, Result, }; use http::version::Version; use hyper::client::conn::http2::handshake; -use hyper_body_utils::HttpBody; use smol_hyper::rt::FuturesIo; impl HttpConnection for Http2Connection { @@ -25,52 +20,26 @@ impl HttpConnection for Http2Connection { } impl ProtoConnection for Http2Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http2Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = GlommioStream; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_2 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - #[cfg(feature = "rust-tls")] - let stream = if config.is_secure() { - tls_connection( - *config.ip(), - config.host(), - config.port(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - ) + async fn connect(stream: GlommioStream) -> Result { + let (sender, conn) = handshake(GlommioExecutor::new(), FuturesIo::new(stream)) .await - } else { - plain_connection(*config.ip(), config.host(), config.port()).await - }; - - #[cfg(not(feature = "rust-tls"))] - let stream = plain_connection(*config.ip(), config.host(), config.port()).await; - - if let Err(e) = stream { - return Err(e); - } - - let result = handshake(GlommioExecutor::new(), FuturesIo::new(stream.unwrap())).await; - - let (sender, conn) = result.unwrap(); + .map_err(|e| { + DeboaError::Connection(ConnectionError::Handshake { message: e.to_string() }) + })?; glommio::spawn_local(async move { match conn.await { Ok(_) => (), Err(err) => { - log::debug!("http2 connection ended: {err:#}"); + log::error!("Error: {:#}", err) } }; }) diff --git a/deboa-glommio/src/client/mod.rs b/deboa-glommio/src/client/mod.rs index 0b7c1357..988b235d 100644 --- a/deboa-glommio/src/client/mod.rs +++ b/deboa-glommio/src/client/mod.rs @@ -3,3 +3,4 @@ /// This module provides DNS resolution functionality for the Deboa HTTP client.pub(crate) mod dns; pub mod dns; pub mod http; +pub mod tls; diff --git a/deboa-glommio/src/client/tls/mod.rs b/deboa-glommio/src/client/tls/mod.rs new file mode 100644 index 00000000..afc96df1 --- /dev/null +++ b/deboa-glommio/src/client/tls/mod.rs @@ -0,0 +1,9 @@ +//! TLS transport implementations for the Deboa HTTP client. +//! +//! This module provides TLS functionality for secure HTTP connections. +//! It supports both native-tls and rustls backends. + +#[cfg(feature = "native-tls")] +pub mod native; +#[cfg(feature = "rust-tls")] +pub mod rustls; diff --git a/deboa-glommio/src/client/tls/native.rs b/deboa-glommio/src/client/tls/native.rs new file mode 100644 index 00000000..b3d061b3 --- /dev/null +++ b/deboa-glommio/src/client/tls/native.rs @@ -0,0 +1,126 @@ +use crate::cert::{DeboaCertificate, DeboaIdentity}; +use compio_tls::{ + native_tls::TlsConnector, + native_tls::{Certificate, Identity}, + TlsStream, +}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, +}; +use glommio::net::TcpStream; + +#[inline] +pub(crate) fn alpn() -> &'static [&'static str] { + &[ + #[cfg(feature = "http3")] + "h3", + #[cfg(feature = "http2")] + "h2", + #[cfg(feature = "http1")] + "http/1.1", + ] +} + +pub struct TlsConnectionBuilder<'a> { + tcp_stream: TcpStream, + host: &'a str, + identity: Option<&'a DeboaIdentity>, + certificate: Option<&'a DeboaCertificate>, + skip_server_verification: bool, + alpn: &'a [&'a str], +} + +impl<'a> TlsConnectionBuilder<'a> { + pub fn new(tcp_stream: TcpStream, host: &'a str) -> Self { + Self { + tcp_stream, + host, + identity: None, + certificate: None, + skip_server_verification: false, + alpn: alpn(), + } + } + + pub fn identity(mut self, identity: Option<&'a DeboaIdentity>) -> Self { + self.identity = identity; + self + } + + pub fn certificate(mut self, certificate: Option<&'a DeboaCertificate>) -> Self { + self.certificate = certificate; + self + } + + pub fn skip_server_verification(mut self, skip_server_verification: bool) -> Self { + self.skip_server_verification = skip_server_verification; + self + } + + pub fn alpn(mut self, alpn: &'a [&str]) -> Self { + self.alpn = alpn; + self + } + + pub async fn connect(self) -> Result> { + let mut builder = TlsConnector::builder(); + + let builder = if self.skip_server_verification { + builder + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + } else { + &mut builder + }; + + let builder = builder.request_alpns(self.alpn); + + let builder = if let Some(ca) = self.certificate { + let cert: Certificate = ca + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid CA certificate: {}", e), + }) + })?; + builder.add_root_certificate(cert) + } else { + builder + }; + + let builder = if let Some(identity) = self.identity { + let ident: Identity = identity + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid client identity: {}", e), + }) + })?; + builder.identity(ident) + } else { + builder + }; + + let connector = builder + .build() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not build TLS connector: {}", e), + }) + })?; + + let connector = compio_tls::TlsConnector::from(connector); + + let stream = connector + .connect(self.host, self.tcp_stream) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not connect to server: {}", e), + }) + })?; + + Ok(stream) + } +} diff --git a/deboa-glommio/src/client/tls/rustls.rs b/deboa-glommio/src/client/tls/rustls.rs new file mode 100644 index 00000000..cb9f273a --- /dev/null +++ b/deboa-glommio/src/client/tls/rustls.rs @@ -0,0 +1,324 @@ +//! TLS implementation using rustls + +use crate::cert::{DeboaCertificate, DeboaIdentity}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, +}; +use rustls::{ + crypto::CryptoProvider, + pki_types::{CertificateDer, PrivateKeyDer}, + ClientConfig, +}; + +pub(crate) fn default_provider() -> CryptoProvider { + #[cfg(feature = "__rustls_aws_lc_rs")] + return rustls::crypto::aws_lc_rs::default_provider(); + #[cfg(feature = "__rustls_ring")] + return rustls::crypto::ring::default_provider(); +} + +#[inline] +pub(crate) fn alpn() -> Vec> { + vec![ + #[cfg(feature = "http3")] + b"h3".to_vec(), + #[cfg(feature = "http2")] + b"h2".to_vec(), + #[cfg(feature = "http1")] + b"http/1.1".to_vec(), + ] +} + +/// Builder for TLS connections using rustls +pub struct TlsConnectionBuilder<'a> { + identity: Option<&'a DeboaIdentity>, + certificate: Option<&'a DeboaCertificate>, + skip_server_verification: bool, + alpn: Vec>, + provider: CryptoProvider, +} + +impl Default for TlsConnectionBuilder<'_> { + fn default() -> Self { + Self { + identity: None, + certificate: None, + skip_server_verification: false, + alpn: alpn(), + provider: default_provider(), + } + } +} + +impl<'a> TlsConnectionBuilder<'a> { + /// Set the identity to use for the connection + pub fn identity(mut self, identity: Option<&'a DeboaIdentity>) -> Self { + self.identity = identity; + self + } + + /// Set the certificate to use for the connection + pub fn certificate(mut self, certificate: Option<&'a DeboaCertificate>) -> Self { + self.certificate = certificate; + self + } + + /// Skip server verification + pub fn skip_server_verification(mut self, skip_server_verification: bool) -> Self { + self.skip_server_verification = skip_server_verification; + self + } + + /// Set the ALPN protocols to use for the connection + pub fn alpn(mut self, alpn: Vec>) -> Self { + self.alpn = alpn; + self + } + + /// Build the TLS client configuration + pub fn build_config(self) -> Result { + let client_config = { + if self.skip_server_verification { + ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(verify::SkipServerVerification::new( + self.provider, + )) + .with_no_client_auth() + } else { + #[cfg(feature = "__webpki_rustls_verifier")] + let config = { + let config = ClientConfig::builder_with_provider(self.provider.into()) + .with_protocol_versions(rustls::ALL_VERSIONS) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set TLS version: {}", e), + }) + })?; + + let mut root_store = + rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; + let config = if let Some(ca) = self.certificate { + let cert = ca + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid CA certificate: {}", e), + }) + })?; + + root_store + .add(cert) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!( + "Could not add CA certificate to the store: {}", + e + ), + }) + })?; + + config.with_root_certificates(root_store) + } else { + config.with_root_certificates(root_store) + }; + + config + }; + + #[cfg(feature = "__platform_rustls_verifier")] + let config = { + use rustls_platform_verifier::BuilderVerifierExt; + rustls::ClientConfig::builder_with_provider(default_provider()) + .with_protocol_versions(rustls::ALL_VERSIONS) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set TLS version: {}", e), + }) + })? + .with_platform_verifier() + }; + + let mut config = if let Some(id) = self.identity { + let pair: (CertificateDer<'_>, PrivateKeyDer<'_>) = id + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid client identity: {}", e), + }) + })?; + + config + .with_client_auth_cert(vec![pair.0], pair.1) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set client identity: {}", e), + }) + })? + } else { + config.with_no_client_auth() + }; + + config.enable_early_data = true; + + config.alpn_protocols = self.alpn; + + config + } + }; + + Ok(client_config) + } +} + +#[cfg(any(feature = "http1", feature = "http2"))] +/// TCP connection module for TLS +pub mod tcp { + use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, + }; + use futures_rustls::{client::TlsStream, TlsConnector}; + use glommio::net::TcpStream; + use rustls::ClientConfig; + use rustls_pki_types::ServerName; + use std::sync::Arc; + + /// Establish a TLS connection over TCP + pub async fn connect( + config: ClientConfig, + inner_stream: TcpStream, + host: &str, + ) -> Result> { + let connector = TlsConnector::from(Arc::new(config)); + + let host = ServerName::try_from(host.to_string()) + .map_err(|e| DeboaError::Connection(ConnectionError::Tls { message: e.to_string() }))?; + + connector + .connect(host, inner_stream) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not connect to server: {}", e), + }) + }) + } +} + +#[cfg(feature = "http3")] +/// UDP connection module for TLS +pub mod udp { + use compio_quic::{Connection, Endpoint}; + use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, + }; + use rustls::ClientConfig; + use std::{net::SocketAddr, sync::Arc}; + + /// Establish a TLS connection over UDP + pub async fn connect( + config: ClientConfig, + endpoint: &mut Endpoint, + socket_addr: SocketAddr, + host: &str, + ) -> Result { + let quic_config = + compio_quic::crypto::rustls::QuicClientConfig::try_from(config).map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not create QUIC client config: {}", e), + }) + })?; + + let client_config = compio_quic::ClientConfig::new(Arc::new(quic_config)); + + let conn = endpoint + .connect(socket_addr, host, Some(client_config)) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: format!("Could not connect to server: {}", e), + }) + })?; + + let conn = conn + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: format!("Could not connect to server: {}", e), + }) + })?; + + Ok(conn) + } +} + +pub(crate) mod verify { + use rustls::{ + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::CryptoProvider, + pki_types::{CertificateDer, ServerName, UnixTime}, + }; + use std::sync::Arc; + + #[derive(Debug)] + pub(crate) struct SkipServerVerification(CryptoProvider); + + impl SkipServerVerification { + pub(crate) fn new(provider: CryptoProvider) -> Arc { + Arc::new(Self(provider)) + } + } + + impl ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> std::result::Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self + .0 + .signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self + .0 + .signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0 + .signature_verification_algorithms + .supported_schemes() + } + } +} diff --git a/deboa-glommio/src/rt/stream.rs b/deboa-glommio/src/rt/stream.rs index 045c0d11..05466eef 100644 --- a/deboa-glommio/src/rt/stream.rs +++ b/deboa-glommio/src/rt/stream.rs @@ -10,7 +10,7 @@ use std::{ }; /// A stream that can be either plain TCP or TLS-secured. -pub(crate) enum GlommioStream { +pub enum GlommioStream { /// A plain TCP connection. Plain(TcpStream), diff --git a/deboa-glommio/tests/round_trip.rs b/deboa-glommio/tests/round_trip.rs index 0de238ab..6ae39c6f 100644 --- a/deboa-glommio/tests/round_trip.rs +++ b/deboa-glommio/tests/round_trip.rs @@ -8,7 +8,7 @@ use std::convert::Infallible; use std::net::SocketAddr; -use deboa::request::{get, FetchWith}; +use deboa::request::get; use deboa_glommio::Client; use http_body_util::Full; use hyper::body::Bytes; diff --git a/deboa-h3/Cargo.toml b/deboa-h3/Cargo.toml index 44b500a8..932dcdbb 100644 --- a/deboa-h3/Cargo.toml +++ b/deboa-h3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deboa-h3" -version = "0.1.1" +version = "0.1.2" edition.workspace = true authors.workspace = true repository.workspace = true @@ -12,14 +12,15 @@ rust-version.workspace = true [features] default = [] -generic = ["dep:h3", "dep:h3-quinn", "hyper-body-utils/http3", "hyper-body-utils/generic"] -compio = ["dep:h3", "dep:compio-quic", "hyper-body-utils/http3", "hyper-body-utils/compio"] +generic = ["dep:h3", "dep:h3-quinn", "hyper-body-utils/generic-h3", "hyper-body-utils/generic"] +compio = ["dep:h3", "dep:compio-quic", "hyper-body-utils/compio-h3", "hyper-body-utils/compio"] [dependencies] -bytes = { version = "1.11" } +bytes = { workspace = true } compio-quic = { version = "0.8.0", optional = true, default-features = false } deboa = { workspace = true } -h3 = { version = "0.0.8", optional = true, default-features = false } -h3-quinn = { version = "0.0.10", optional = true, default-features = false } -http = { version = "1.4.2", default-features = false } +h3 = { workspace = true, optional = true } +h3-quinn = { workspace = true, optional = true } +http = { workspace = true } +http-body-util = { workspace = true } hyper-body-utils = { workspace = true, optional = true, default-features = false } diff --git a/deboa-h3/src/lib.rs b/deboa-h3/src/lib.rs index 4dfae043..08b41f59 100644 --- a/deboa-h3/src/lib.rs +++ b/deboa-h3/src/lib.rs @@ -1,10 +1,12 @@ #[cfg(feature = "generic")] pub mod generic { + use bytes::Bytes; use h3::{client::RequestStream, error::StreamError}; use h3_quinn::{OpenStreams, RecvStream}; use http::{Request, Response}; - use hyper_body_utils::{BodyExt, HttpBody}; + use http_body_util::BodyExt as _; + use hyper_body_utils::HttpBody; use std::marker::PhantomData; pub type QuicRequest = h3::client::SendRequest; @@ -69,7 +71,8 @@ pub mod compio { use compio_quic::{h3::OpenStreams, RecvStream}; use h3::{client::RequestStream, error::StreamError}; use http::{Request, Response}; - use hyper_body_utils::{BodyExt, HttpBody}; + use http_body_util::BodyExt as _; + use hyper_body_utils::HttpBody; use std::marker::PhantomData; pub type QuicRequest = h3::client::SendRequest; diff --git a/deboa-macros/Cargo.toml b/deboa-macros/Cargo.toml index 6068b4ce..0e189849 100644 --- a/deboa-macros/Cargo.toml +++ b/deboa-macros/Cargo.toml @@ -26,8 +26,8 @@ msgpack = ["deboa-extras/msgpack"] [dependencies] deboa = { workspace = true, default-features = false } deboa-extras = { workspace = true, default-features = false, optional = true } -http = "1.4.2" -proc-macro2 = "1.0.106" -quote = "1.0.45" -serde = "1.0.228" -syn = "3.0.0" +http = { workspace = true } +proc-macro2 = "1.0.107" +quote = "1.0.47" +serde = { workspace = true } +syn = "3.0.3" diff --git a/deboa-smol/Cargo.toml b/deboa-smol/Cargo.toml index b5112a87..5dfb2fb2 100644 --- a/deboa-smol/Cargo.toml +++ b/deboa-smol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deboa-smol" -version = "0.1.2" +version = "0.1.3" edition.workspace = true authors.workspace = true repository.workspace = true @@ -16,25 +16,25 @@ rust-version.workspace = true features = [ "http1", "http2", - "http3", "rust-tls", - "native-tls", - "websockets", "default-rustls-provider", "default-rustls-verifier", ] [features] default = [ + "http1", "http2", + #"http3", "rust-tls", "default-rustls-provider", "default-rustls-verifier", + "websockets" ] # tls rust-tls = [ - "dep:rustls", + "rustls/tls12", "dep:rustls-native-certs", "dep:futures-rustls", "dep:rustls-pki-types", @@ -59,70 +59,71 @@ __rustls_ring = ["futures-rustls/ring", "quinn/rustls-ring"] native-tls = ["async-native-tls/runtime-smol"] # protocols -http1 = ["hyper/http1", "hyper-util/http1"] -http2 = ["hyper/http2", "hyper-util/http2"] +http1 = ["rustls/tls12", "hyper/http1", "hyper-util/http1"] +http2 = ["rustls/tls12", "hyper/http2", "hyper-util/http2"] http3 = [ "deboa-h3/generic", + "dep:futures", "dep:h3", "dep:h3-quinn", + "dep:rustls", "quinn/runtime-smol", - "hyper-body-utils/http3", "hyper-body-utils/generic", + "hyper-body-utils/generic-h3", ] -websockets = ["ws-framer/http", "ws-framer/alloc"] +websockets = ["deboa-ws"] [dependencies] -async-executor = { version = "1.13.3", optional = true, default-features = false } -async-lock = "3.4.2" -async-native-tls = { version = "0.6.0", optional = true, default-features = false } -base64 = "0.23.0" -bytes = { version = "1.11", default-features = false } -cookie = { version = "0.18.1", default-features = false } +async-executor = { workspace = true, optional = true } +async-lock = { workspace = true } +async-native-tls = { workspace = true, optional = true } +base64 = { workspace = true } +bytes = { workspace = true } +cookie = { workspace = true } deboa = { workspace = true } deboa-h3 = { workspace = true, optional = true } -futures = { version = "0.3.31", default-features = false } +deboa-tls = { workspace = true } +deboa-ws = { workspace = true, optional = true } +futures = { workspace = true, optional = true } futures-rustls = { version = "0.26.0", optional = true, default-features = false } -futures-util = { version = "0.3.31", optional = true, default-features = false } -h3 = { version = "0.0.8", optional = true, default-features = false } -h3-quinn = { version = "0.0.10", optional = true, default-features = false } -hashbrown = "0.17.1" -http = "1" -http-body = "1" -http-body-util = "0.1" -hyper = { version = "1.10.1", features = ["client"], default-features = false } +futures-timeout = "0.2.1" +futures-util = { workspace = true, optional = true } +h3 = { workspace = true, optional = true } +h3-quinn = { workspace = true, optional = true } +hashbrown = { workspace = true } +http = { workspace = true } +http-body = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true } hyper-body-utils = { workspace = true, default-features = false } -hyper-util = { version = "0.1.20", features = [ - "client", - "client-legacy", -], default-features = false } +hyper-util = { workspace = true } indexmap = "2.11.4" -log = "0.4.32" +log = { workspace = true } macro_rules_attribute = { version = "0.2.2", default-features = false } -minimime = "1.0.0" -mockall = "0.15.0" -quinn = { version = "0.11.7", optional = true, default-features = false } -rand = { version = "0.10.1", default-features = false } -regex = { version = "1.12.4", default-features = false } +minimime = { workspace = true } +pin-project-lite = {workspace = true, optional = true } +quinn = { version = "0.11.11", optional = true, default-features = false } +rand = { workspace = true } +regex = { workspace = true } rstest = "0.26.1" -rustls = { version = "0.23.36", optional = true, default-features = false } -rustls-native-certs = { version = "0.8.4", optional = true, default-features = false } -rustls-pki-types = { version = "1.14.1", optional = true, default-features = false } -rustls-platform-verifier = { version = "0.7.0", optional = true, default-features = false } -serde = { version = "1.0.217", features = ["derive"] } +rustls = { workspace = true, optional = true } +rustls-native-certs = { workspace = true, optional = true } +rustls-pki-types = { workspace = true, optional = true } +rustls-platform-verifier = { workspace = true, optional = true } +serde = { workspace = true , features = ["derive"] } smol = { version = "2.0.2", default-features = false } smol-hyper = { version = "0.1.0", default-features = false } smol-macros = { version = "0.1.1", default-features = false } -tackle = { version = "0.1.1"} -thiserror = "2.0.17" -time = { version = "0.3.51", default-features = false } -url = "2.5.8" -urlencoding = "2.1.3" -webpki-roots = { version = "1.0.6", optional = true, default-features = false } -ws-framer = { version = "0.3.1", optional = true, default-features = false } +tackle = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } +urlencoding = { workspace = true } +webpki-roots = { workspace = true, optional = true, default-features = false } +ws-framer = { workspace = true, optional = true } [dev-dependencies] -caramelo = "0.1.2" +caramelo = { workspace = true } criterion = { version = "0.8.2", features = [ "html_reports", "async", @@ -131,9 +132,12 @@ criterion = { version = "0.8.2", features = [ deboa-test-utils = { workspace = true } easyhttpmock-vetis-smol = { version = "0.1.0", features = [ "http1", + "http2", + #"http3", "rust-tls", ], default-features = false } -futures-util = "0.3.31" -multer = "3.1.0" -rstest = "0.26.1" +futures-util = { workspace = true } +multer = { workspace = true } +rstest = { workspace = true } smol = "2.0.2" +time = { workspace = true } diff --git a/deboa-smol/README.md b/deboa-smol/README.md index f3b4369f..14769082 100644 --- a/deboa-smol/README.md +++ b/deboa-smol/README.md @@ -61,7 +61,7 @@ http = "1.3.1" ## Usage -```rust +```rust,ignore use deboa::{ Client, request::{DeboaRequest, FetchWith, get}, diff --git a/deboa-smol/src/cert.rs b/deboa-smol/src/cert.rs index 74219170..591438d7 100644 --- a/deboa-smol/src/cert.rs +++ b/deboa-smol/src/cert.rs @@ -11,7 +11,7 @@ use async_native_tls::{Certificate as NativeCertificate, Identity as NativeIdent use deboa::cert::IdentityNativeExt; use deboa::cert::{Certificate as _, CertificateExt, ContentEncoding, IdentityExt}; #[cfg(feature = "rust-tls")] -use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use futures_rustls::pki_types::{CertificateDer, PrivateKeyDer}; /// Represents a client certificate and its associated data for mutual TLS authentication. /// @@ -93,7 +93,7 @@ impl deboa::cert::Identity for DeboaIdentity { &self.cert } - fn ket(&self) -> &Option> { + fn key(&self) -> &Option> { &self.key } diff --git a/deboa-smol/src/client/http/conn/mod.rs b/deboa-smol/src/client/http/conn/mod.rs index b64798ae..ce11687a 100644 --- a/deboa-smol/src/client/http/conn/mod.rs +++ b/deboa-smol/src/client/http/conn/mod.rs @@ -16,21 +16,28 @@ //! - Thread-safe connection handling //! ``` use crate::cert::{DeboaCertificate, DeboaIdentity}; +#[cfg(any(feature = "http1", feature = "http2"))] +use crate::rt::stream::SmolStream; #[cfg(feature = "http1")] use deboa::request::Http1Request; #[cfg(feature = "http2")] use deboa::request::Http2Request; use deboa::{ conn::{ConnectionConfig, HttpConnectionDispatcher, ProtoConnection}, - errors::{DeboaError, RequestError}, + dns::DnsResolver, + errors::{ConnectionError, DeboaError, RequestError}, response::DeboaResponse, Result, }; #[cfg(feature = "http3")] use deboa_h3::generic::Http3Request; +use futures_timeout::TimeoutFutureExt; use http::{Request, Version}; use hyper_body_utils::HttpBody; -use std::marker::PhantomData; +use log::info; +#[cfg(any(feature = "http1", feature = "http2"))] +use smol::net::TcpStream; +use std::{borrow::Cow, marker::PhantomData, time::Duration}; /// Connection pooling for efficient HTTP connections. /// @@ -45,11 +52,6 @@ use std::marker::PhantomData; /// - Configurable pool size (coming soon) pub mod pool; -/// Stream module for runtime-specific stream implementations. -/// -/// This module provides stream implementations for different runtimes (Tokio, Smol, etc.). -pub(crate) mod stream; - #[cfg(feature = "http1")] pub(crate) type Http1Connection = BaseHttpConnection; #[cfg(feature = "http2")] @@ -111,20 +113,8 @@ impl DeboaConnection { pub fn http3(conn: Http3Connection) -> Self { DeboaConnection::Http3(Box::new(conn)) } -} -impl HttpConnectionDispatcher for DeboaConnection { - /// Send a request through the connection. - /// - /// # Arguments - /// - /// * `url` - The URL to send the request to. - /// * `request` - The request to send. - /// - /// # Returns - /// - /// * `Result` - The response from the server. - async fn send_request(&mut self, request: Request) -> Result { + async fn send(&mut self, request: Request) -> Result { match self { #[cfg(feature = "http1")] DeboaConnection::Http1(ref mut conn) => { @@ -178,29 +168,208 @@ impl HttpConnectionDispatcher for DeboaConnection { } } +impl HttpConnectionDispatcher for DeboaConnection { + /// Send a request through the connection. + /// + /// # Arguments + /// + /// * `url` - The URL to send the request to. + /// * `request` - The request to send. + /// + /// # Returns + /// + /// * `Result` - The response from the server. + async fn send_request( + &mut self, + request: Request, + timeout: Duration, + ) -> Result { + self.send(request) + .timeout(timeout) + .await + .map_err(|_| { + DeboaError::Request(RequestError::Send { message: "Request timed out".to_string() }) + })? + } +} + +#[cfg(feature = "rust-tls")] +async fn connect_with_rustls<'a>( + tcp_stream: TcpStream, + config: &ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, +) -> Result<(Version, SmolStream)> { + use crate::client::tls::rustls::{tcp::connect, TlsConnectionBuilder}; + let tls_config = TlsConnectionBuilder::default() + .certificate(config.certificate()) + .identity(config.identity()) + .build_config()?; + + let stream = Box::new(connect(tls_config, tcp_stream, config.host()).await?); + + if let Some(alpn) = stream + .get_ref() + .1 + .alpn_protocol() + { + let Cow::Borrowed(alpn_code) = String::from_utf8_lossy(alpn) else { + return Err(DeboaError::Connection(ConnectionError::Tcp { + message: "Invalid ALPN code".to_string(), + })); + }; + + let version = match alpn_code { + "http1.1" => Version::HTTP_11, + "h2" => Version::HTTP_2, + "h3" => Version::HTTP_3, + _ => *config.protocol_version(), + }; + + info!("ALPN info found, switching connection to {:?}", version); + Ok((version, SmolStream::Tls(stream))) + } else { + info!("No ALPN info available, falling back to HTTP/1.1"); + Ok((*config.protocol_version(), SmolStream::Tls(stream))) + } +} + +#[cfg(feature = "native-tls")] +async fn connect_with_nativels<'a>( + tcp_stream: TcpStream, + config: &ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, +) -> Result<(Version, TokioStream)> { + use crate::client::tls::native::TlsConnectionBuilder; + let stream = TlsConnectionBuilder::new(tcp_stream, config.host()) + .certificate(config.certificate()) + .identity(config.identity()) + .connect() + .await?; + + if let Some(alpn) = stream.negotiated_alpn() { + let Cow::Borrowed(alpn_code) = String::from_utf8_lossy(alpn) else { + return Err(DeboaError::Connection(ConnectionError::Tcp { + message: "Invalid ALPN code".to_string(), + })); + }; + + let version = match alpn_code { + "http1.1" => Version::HTTP_11, + "h2" => Version::HTTP_2, + "h3" => Version::HTTP_3, + _ => *config.protocol_version(), + }; + + info!("ALPN info found, switching connection to {:?}", version); + Ok((version, TokioStream::Tls(stream))) + } else { + info!("No ALPN info available, falling back to HTTP/1.1"); + Ok((*config.protocol_version(), TokioStream::Tls(stream))) + } +} + /// Connection factory. pub struct ConnectionFactory {} impl ConnectionFactory { /// Create a new connection. - pub async fn create_connection<'a>( - protocol: &Version, + pub async fn create_connection<'a, D>( config: &'a ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, - ) -> Result { - let conn = match protocol { + dns_resolver: &D, + ) -> Result + where + D: DnsResolver, + { + let ips = dns_resolver + .resolve( + config + .host() + .to_string(), + config.port(), + ) + .await?; + let ips = if config + .client_bind_addr() + .is_ipv4() + { + ips.into_iter() + .filter(|ip| ip.is_ipv4()) + .collect::>() + } else { + ips.into_iter() + .filter(|ip| ip.is_ipv6()) + .collect::>() + }; + + let Some(ip) = ips.first() else { + return Err(DeboaError::Request(RequestError::Send { + message: format!("No IP addresses found for hostname: {}", config.host()), + })); + }; + + #[cfg(any(feature = "http1", feature = "http2"))] + let conn_pair = { + let tcp_stream = smol::net::TcpStream::connect(format!("{}:{}", ip, config.port())) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tcp { message: e.to_string() }) + })?; + let use_tls = config.scheme() == "https" || config.scheme() == "wss"; + if !use_tls { + (Version::HTTP_11, SmolStream::Plain(tcp_stream)) + } else { + #[cfg(feature = "rust-tls")] + { + connect_with_rustls(tcp_stream, config).await? + } + + #[cfg(feature = "native-tls")] + { + connect_with_nativels(tcp_stream, config).await? + } + } + }; + + let conn = match conn_pair.0 { #[cfg(feature = "http1")] - &Version::HTTP_11 => { - let conn = Http1Connection::connect(config).await?; + Version::HTTP_11 => { + let conn = Http1Connection::connect(conn_pair.1).await?; DeboaConnection::http1(conn) } #[cfg(feature = "http2")] - &Version::HTTP_2 => { - let conn = Http2Connection::connect(config).await?; + Version::HTTP_2 => { + let conn = Http2Connection::connect(conn_pair.1).await?; DeboaConnection::http2(conn) } - #[cfg(all(feature = "http3", feature = "rust-tls"))] - &Version::HTTP_3 => { - let conn = Http3Connection::connect(&config).await?; + #[cfg(feature = "http3")] + Version::HTTP_3 => { + let stream = { + use crate::client::tls::rustls::{udp::connect, TlsConnectionBuilder}; + use deboa::errors::ConnectionError; + use quinn::Endpoint; + use std::net::SocketAddr; + + let mut client_endpoint = Endpoint::client(SocketAddr::new( + *config.client_bind_addr(), + 0, + )) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { message: e.to_string() }) + })?; + + let tls_config = TlsConnectionBuilder::default() + .certificate(config.certificate()) + .identity(config.identity()) + .build_config()?; + + connect( + tls_config, + &mut client_endpoint, + SocketAddr::new(*ip, config.port()), + config.host(), + ) + .await? + }; + + let conn = Http3Connection::connect(stream).await?; DeboaConnection::http3(conn) } _ => { diff --git a/deboa-smol/src/client/http/conn/pool.rs b/deboa-smol/src/client/http/conn/pool.rs index 2765899c..0549578c 100644 --- a/deboa-smol/src/client/http/conn/pool.rs +++ b/deboa-smol/src/client/http/conn/pool.rs @@ -2,9 +2,14 @@ use crate::{ cert::{DeboaCertificate, DeboaIdentity}, client::http::conn::{ConnectionConfig, ConnectionFactory, DeboaConnection}, }; -use deboa::Result; +use deboa::{ + dns::DnsResolver, + errors::{ConnectionError, DeboaError}, + Result, +}; +use futures_timeout::TimeoutFutureExt; use hashbrown::HashMap; -use time::Duration; +use std::time::Duration; /// Struct that represents the HTTP connection pool. /// @@ -27,7 +32,7 @@ impl Default for HttpConnectionPool { fn default() -> Self { Self { max_idle_connections: 5, - keep_alive_duration: Duration::minutes(5), + keep_alive_duration: Duration::from_mins(5), connections: HashMap::new(), } } @@ -76,15 +81,14 @@ impl deboa::conn::HttpConnectionPool for HttpConnectionPool { .len() as u32 } - async fn create_connection<'a>( - &'a mut self, + async fn create_connection<'a, D>( + &mut self, config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result<&'a mut Self::ConnectionDispather> { - if self.max_idle_connections == 0 { - self.connections - .clear(); - } - + dns_resolver: &D, + ) -> Result<&mut DeboaConnection> + where + D: DnsResolver, + { let host = config.host(); if self .connections @@ -98,8 +102,18 @@ impl deboa::conn::HttpConnectionPool for HttpConnectionPool { } log::debug!("Creating new connection for {}", host); - let connection = - ConnectionFactory::create_connection(config.protocol_version(), config).await?; + let connection = ConnectionFactory::create_connection(config, dns_resolver) + .timeout(config.connection_timeout()) + .await + .map_err(|_| { + DeboaError::Connection(ConnectionError::Timeout { + message: format!( + "Connection to {} timed out after {:?}", + host, + config.connection_timeout() + ), + }) + })??; self.connections .insert(host.to_string(), connection); diff --git a/deboa-smol/src/client/http/conn/stream/mod.rs b/deboa-smol/src/client/http/conn/stream/mod.rs deleted file mode 100644 index e323fdda..00000000 --- a/deboa-smol/src/client/http/conn/stream/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub(crate) mod plain; -pub(crate) use plain::*; - -#[cfg(all( - any(feature = "rust-tls", feature = "native-tls"), - any(feature = "http1", feature = "http2", feature = "http3") -))] -pub(crate) mod tls; -#[cfg(all( - any(feature = "rust-tls", feature = "native-tls"), - any(feature = "http1", feature = "http2", feature = "http3") -))] -pub(crate) use tls::*; diff --git a/deboa-smol/src/client/http/conn/stream/plain.rs b/deboa-smol/src/client/http/conn/stream/plain.rs deleted file mode 100644 index a31a24bb..00000000 --- a/deboa-smol/src/client/http/conn/stream/plain.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::rt::stream::SmolStream; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; -use smol::net::TcpStream; -use std::net::IpAddr; - -pub(crate) async fn create_stream(addr: IpAddr, host: &str, port: u16) -> Result { - let tcp_stream = TcpStream::connect((addr, port)).await; - let tcp_stream = match tcp_stream { - Ok(tcp_stream) => tcp_stream, - Err(e) => { - return Err(DeboaError::Connection(ConnectionError::Tcp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - }; - - Ok(tcp_stream) -} - -pub(crate) async fn plain_connection(addr: IpAddr, host: &str, port: u16) -> Result { - let stream = create_stream(addr, host, port).await?; - Ok(SmolStream::Plain(stream)) -} diff --git a/deboa-smol/src/client/http/conn/stream/tls/mod.rs b/deboa-smol/src/client/http/conn/stream/tls/mod.rs deleted file mode 100644 index 4632c49b..00000000 --- a/deboa-smol/src/client/http/conn/stream/tls/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[cfg(feature = "native-tls")] -mod native; - -#[cfg(feature = "rust-tls")] -mod rustls; - -#[cfg(feature = "rust-tls")] -pub(crate) use rustls::*; - -#[cfg(feature = "native-tls")] -pub(crate) use native::*; diff --git a/deboa-smol/src/client/http/conn/stream/tls/native.rs b/deboa-smol/src/client/http/conn/stream/tls/native.rs deleted file mode 100644 index 77ed3bde..00000000 --- a/deboa-smol/src/client/http/conn/stream/tls/native.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::net::IpAddr; - -use crate::{ - cert::{Certificate as DeboaCertificate, Identity as DeboaIdentity}, - client::http::conn::stream::create_stream, - rt::stream::SmolStream, -}; -use async_native_tls::{Certificate, Identity, TlsConnector}; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; - -pub(crate) async fn tls_connection( - ip: IpAddr, - host: &str, - port: u16, - identity: &Option, - certificate: &Option, - skip_server_verification: bool, - alpn: &[&str], -) -> Result { - let socket = create_stream(ip, host, port).await?; - let builder = TlsConnector::new(); - - let builder = if skip_server_verification { - builder - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - } else { - builder - }; - - let builder = builder.request_alpns(&alpn); - - let builder = if let Some(ca) = certificate { - let cert: std::result::Result = ca.try_into(); - if let Err(e) = cert { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid CA certificate: {}", e), - })); - } - - builder.add_root_certificate(cert.unwrap()) - } else { - builder - }; - - let builder = if let Some(identity) = identity { - let ident: std::result::Result = identity.try_into(); - if let Err(e) = ident { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid client identity: {}", e), - })); - } - builder.identity(ident.unwrap()) - } else { - builder - }; - - let stream = builder - .connect(host.to_string(), socket) - .await; - - if let Err(e) = stream { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - - let stream = stream.unwrap(); - Ok(SmolStream::Tls(stream)) -} diff --git a/deboa-smol/src/client/http/conn/stream/tls/rustls.rs b/deboa-smol/src/client/http/conn/stream/tls/rustls.rs deleted file mode 100644 index a1718172..00000000 --- a/deboa-smol/src/client/http/conn/stream/tls/rustls.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::stream::plain::create_stream, - rt::stream::SmolStream, -}; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; -use futures_rustls::TlsConnector; -use rustls::{pki_types::ServerName, ClientConfig}; -use rustls_pki_types::{CertificateDer, PrivateKeyDer}; -use std::{net::IpAddr, sync::Arc}; - -pub(crate) async fn tls_connection<'a>( - ip: IpAddr, - host: &str, - port: u16, - identity: &'a Option, - certificate: &'a Option, - skip_server_verification: bool, - alpn: Vec>, -) -> Result { - let socket = create_stream(ip, host, port).await?; - let config = setup_rust_tls(host, identity, certificate, skip_server_verification, alpn)?; - let connector = TlsConnector::from(Arc::new(config)); - let hostname = ServerName::try_from(host.to_string()); - - if let Err(e) = hostname { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: e.to_string(), - })); - } - - let stream = connector - .connect(hostname.unwrap(), socket) - .await; - - if let Err(e) = stream { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - - let stream = stream.unwrap(); - Ok(SmolStream::Tls(Box::new(stream))) -} - -pub(crate) fn default_provider() -> Arc { - #[cfg(feature = "__rustls_aws_lc_rs")] - let provider = rustls::crypto::aws_lc_rs::default_provider(); - #[cfg(feature = "__rustls_ring")] - let provider = rustls::crypto::ring::default_provider(); - Arc::new(provider) -} - -pub(crate) fn setup_rust_tls<'a>( - host: &str, - identity: &'a Option, - certificate: &'a Option, - skip_server_verification: bool, - alpn: Vec>, -) -> Result { - let provider = default_provider(); - - if skip_server_verification { - use verify::SkipServerVerification; - let config = rustls::ClientConfig::builder_with_provider(provider) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version") - .dangerous() - .with_custom_certificate_verifier(SkipServerVerification::new()) - .with_no_client_auth(); - return Ok(config); - } - - #[cfg(feature = "__webpki_rustls_verifier")] - let config = { - let config = rustls::ClientConfig::builder_with_provider(provider) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version"); - - let mut root_store = - rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; - if let Some(ca) = certificate { - let cert = ca.try_into(); - if let Err(e) = cert { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid CA certificate: {}", e), - })); - } - - let result = root_store.add(cert.unwrap()); - if let Err(e) = result { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not add CA certificate to the store: {}", e), - })); - } - - config.with_root_certificates(root_store) - } else { - config.with_root_certificates(root_store) - } - }; - - #[cfg(feature = "__platform_rustls_verifier")] - let config = { - use rustls_platform_verifier::Verifier; - let verifier = Verifier::new(provider).expect("Failed to create platform verifier"); - rustls::ClientConfig::builder_with_provider(default_provider()) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version") - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - }; - - let mut config = if let Some(id) = identity { - let pair = id.try_into(); - if let Err(e) = pair { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid client identity: {}", e), - })); - } - - let pair: (CertificateDer<'static>, PrivateKeyDer<'static>) = pair.unwrap(); - - config - .with_client_auth_cert(vec![pair.0], pair.1) - .expect("Failed to set client identity") - } else { - config.with_no_client_auth() - }; - - config.enable_early_data = true; - - config.alpn_protocols = alpn; - - Ok(config) -} - -pub(crate) mod verify { - use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; - use std::sync::Arc; - - #[derive(Debug)] - pub(crate) struct SkipServerVerification(Arc); - - impl SkipServerVerification { - pub(crate) fn new() -> Arc { - let provider = super::default_provider(); - Arc::new(Self(provider)) - } - } - - impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { - fn verify_server_cert( - &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp: &[u8], - _now: UnixTime, - ) -> std::result::Result - { - Ok(rustls::client::danger::ServerCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &rustls::DigitallySignedStruct, - ) -> std::result::Result - { - rustls::crypto::verify_tls12_signature( - message, - cert, - dss, - &self - .0 - .signature_verification_algorithms, - ) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &rustls::DigitallySignedStruct, - ) -> std::result::Result - { - rustls::crypto::verify_tls13_signature( - message, - cert, - dss, - &self - .0 - .signature_verification_algorithms, - ) - } - - fn supported_verify_schemes(&self) -> Vec { - self.0 - .signature_verification_algorithms - .supported_schemes() - } - } -} diff --git a/deboa-smol/src/client/http/http1.rs b/deboa-smol/src/client/http/http1.rs index 99aa3db7..f0acceb9 100644 --- a/deboa-smol/src/client/http/http1.rs +++ b/deboa-smol/src/client/http/http1.rs @@ -1,19 +1,15 @@ -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::alpn; -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::client::http::conn::stream::tls_connection; use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::plain_connection, BaseHttpConnection, Http1Connection}, + client::http::conn::{BaseHttpConnection, Http1Connection}, + rt::stream::SmolStream, }; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, + errors::{ConnectionError, DeboaError}, request::Http1Request, Result, }; use http::version::Version; use hyper::client::conn::http1::handshake; -use hyper_body_utils::HttpBody; use smol_hyper::rt::FuturesIo; impl HttpConnection for Http1Connection { @@ -24,46 +20,20 @@ impl HttpConnection for Http1Connection { } impl ProtoConnection for Http1Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http1Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = SmolStream; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_11 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - #[cfg(any(feature = "rust-tls", feature = "native-tls"))] - let stream = if config.is_secure() { - tls_connection( - *config.ip(), - config.host(), - config.port(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - ) + async fn connect(stream: Self::RuntimeStream) -> Result { + let (sender, conn) = handshake(FuturesIo::new(stream)) .await - } else { - plain_connection(*config.ip(), config.host(), config.port()).await - }; - - #[cfg(not(any(feature = "rust-tls", feature = "native-tls")))] - let stream = plain_connection(config.host(), config.port()).await; - - if let Err(e) = stream { - return Err(e); - } - - let result = handshake(FuturesIo::new(stream.unwrap())).await; - - let (sender, conn) = result.unwrap(); + .map_err(|e| { + DeboaError::Connection(ConnectionError::Handshake { message: e.to_string() }) + })?; smol::spawn(async move { match conn @@ -71,7 +41,9 @@ impl ProtoConnection for Http1Connection { .await { Ok(_) => (), - Err(_err) => {} + Err(err) => { + log::error!("Error: {:#}", err) + } }; }) .detach(); diff --git a/deboa-smol/src/client/http/http2.rs b/deboa-smol/src/client/http/http2.rs index 5d9f18cc..99f2927f 100644 --- a/deboa-smol/src/client/http/http2.rs +++ b/deboa-smol/src/client/http/http2.rs @@ -1,20 +1,15 @@ -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::alpn; -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::client::http::conn::stream::tls_connection; use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::plain_connection, BaseHttpConnection, Http2Connection}, - rt::executor::SmolExecutor, + client::http::conn::{BaseHttpConnection, Http2Connection}, + rt::{executor::SmolExecutor, stream::SmolStream}, }; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, + errors::{ConnectionError, DeboaError}, request::Http2Request, Result, }; use http::version::Version; use hyper::client::conn::http2::handshake; -use hyper_body_utils::HttpBody; use smol_hyper::rt::FuturesIo; impl HttpConnection for Http2Connection { @@ -25,54 +20,27 @@ impl HttpConnection for Http2Connection { } impl ProtoConnection for Http2Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http2Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = SmolStream; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_2 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - #[cfg(any(feature = "rust-tls", feature = "native-tls"))] - let stream = if config.is_secure() { - tls_connection( - *config.ip(), - config.host(), - config.port(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - ) + async fn connect(stream: Self::RuntimeStream) -> Result { + let (sender, conn) = handshake(SmolExecutor::new(), FuturesIo::new(stream)) .await - } else { - plain_connection(*config.ip(), config.host(), config.port()).await - }; - - #[cfg(not(any(feature = "rust-tls", feature = "native-tls")))] - let stream = plain_connection(*config.ip(), config.host(), config.port()).await; - - if let Err(e) = stream { - return Err(e); - } - - let result = handshake(SmolExecutor::new(), FuturesIo::new(stream.unwrap())).await; - - let (sender, conn) = result.unwrap(); + .map_err(|e| { + DeboaError::Connection(ConnectionError::Handshake { message: e.to_string() }) + })?; smol::spawn(async move { match conn.await { Ok(_) => (), Err(err) => { - println!("Error: {:#}", err) + log::error!("Error: {:#}", err) } - _ => {} }; }) .detach(); diff --git a/deboa-smol/src/client/http/http3.rs b/deboa-smol/src/client/http/http3.rs index 9aabdb0b..024a4f75 100644 --- a/deboa-smol/src/client/http/http3.rs +++ b/deboa-smol/src/client/http/http3.rs @@ -1,65 +1,13 @@ -use crate::{ - alpn, - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{BaseHttpConnection, Http3Connection}, -}; +use crate::client::http::conn::{BaseHttpConnection, Http3Connection}; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, errors::{ConnectionError, DeboaError}, Result, }; use deboa_h3::generic::{Http3Request, SendRequest}; use futures::future; +use h3_quinn::Connection; use http::version::Version; -use hyper_body_utils::HttpBody; -use quinn::{crypto::rustls::QuicClientConfig, Endpoint}; -use std::{ - net::{IpAddr, SocketAddr}, - sync::Arc, -}; - -async fn lookup_and_connect( - ip: IpAddr, - host: &str, - port: u16, - client_endpoint: &Endpoint, -) -> std::result::Result { - let conn = client_endpoint.connect(SocketAddr::new(ip, port), host); - - let conn = match conn { - Ok(conn) => conn, - Err(e) => { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })) - } - }; - - let conn = conn.await; - - let conn = match conn { - Ok(conn) => conn, - Err(e) => match e { - quinn::ConnectionError::TransportError(e) => { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })) - } - _ => { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })) - } - }, - }; - - let quinn_conn: h3_quinn::Connection = h3_quinn::Connection::new(conn); - - Ok(quinn_conn) -} impl HttpConnection for Http3Connection { type Sender = Http3Request; @@ -69,77 +17,18 @@ impl HttpConnection for Http3Connection { } impl ProtoConnection for Http3Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http3Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = Connection; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_3 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - let client_endpoint = Endpoint::client(SocketAddr::new(*config.client_bind_addr(), 0)); - - if let Err(e) = client_endpoint { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let mut client_endpoint = client_endpoint.unwrap(); - - let tls_config = setup_rust_tls( - config.host(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - )?; - - let quic_config = QuicClientConfig::try_from(tls_config); - if let Err(e) = quic_config { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let quic_config = quic_config.unwrap(); - - let client_config = quinn::ClientConfig::new(Arc::new(quic_config)); - client_endpoint.set_default_client_config(client_config); - - let result = - lookup_and_connect(*config.ip(), config.host(), config.port(), &client_endpoint).await; - - if let Err(e) = result { - return Err(e); - } - - let conn = result.unwrap(); - - let client = h3::client::new(conn).await; - - if let Err(e) = client { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let (mut conn, sender) = client.unwrap(); + async fn connect(stream: Self::RuntimeStream) -> Result { + let (mut conn, sender) = h3::client::new(stream) + .await + .map_err(|e| DeboaError::Connection(ConnectionError::Udp { message: e.to_string() }))?; smol::spawn(async move { future::poll_fn(|cx| conn.poll_close(cx)).await; diff --git a/deboa-smol/src/client/mod.rs b/deboa-smol/src/client/mod.rs index af0eb4d5..988b235d 100644 --- a/deboa-smol/src/client/mod.rs +++ b/deboa-smol/src/client/mod.rs @@ -3,5 +3,4 @@ /// This module provides DNS resolution functionality for the Deboa HTTP client.pub(crate) mod dns; pub mod dns; pub mod http; -#[cfg(feature = "websockets")] -pub mod ws; +pub mod tls; diff --git a/deboa-smol/src/client/tls/mod.rs b/deboa-smol/src/client/tls/mod.rs new file mode 100644 index 00000000..afc96df1 --- /dev/null +++ b/deboa-smol/src/client/tls/mod.rs @@ -0,0 +1,9 @@ +//! TLS transport implementations for the Deboa HTTP client. +//! +//! This module provides TLS functionality for secure HTTP connections. +//! It supports both native-tls and rustls backends. + +#[cfg(feature = "native-tls")] +pub mod native; +#[cfg(feature = "rust-tls")] +pub mod rustls; diff --git a/deboa-smol/src/client/tls/native.rs b/deboa-smol/src/client/tls/native.rs new file mode 100644 index 00000000..4e06feab --- /dev/null +++ b/deboa-smol/src/client/tls/native.rs @@ -0,0 +1,125 @@ +//! TLS implementation using native-tls + +use crate::cert::{DeboaCertificate, DeboaIdentity}; +use async_native_tls::{Certificate, Identity, TlsConnector, TlsStream}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, +}; +use smol::net::TcpStream; + +#[inline] +pub(crate) fn alpn() -> &'static [&'static str] { + &[ + #[cfg(feature = "http3")] + "h3", + #[cfg(feature = "http2")] + "h2", + #[cfg(feature = "http1")] + "http/1.1", + ] +} + +/// Builder for TLS connections using native-tls +pub struct TlsConnectionBuilder<'a> { + tcp_stream: TcpStream, + host: &'a str, + identity: Option<&'a DeboaIdentity>, + certificate: Option<&'a DeboaCertificate>, + skip_server_verification: bool, + alpn: &'a [&'a str], +} + +impl<'a> TlsConnectionBuilder<'a> { + /// Creates a new TLS connection builder + pub fn new(tcp_stream: TcpStream, host: &'a str) -> Self { + Self { + tcp_stream, + host, + identity: None, + certificate: None, + skip_server_verification: false, + alpn: alpn(), + } + } + + /// Sets the client identity for mutual TLS authentication + pub fn identity(mut self, identity: Option<&'a DeboaIdentity>) -> Self { + self.identity = identity; + self + } + + /// Sets the server certificate for verification + pub fn certificate(mut self, certificate: Option<&'a DeboaCertificate>) -> Self { + self.certificate = certificate; + self + } + + /// Skips server certificate verification (use with caution) + pub fn skip_server_verification(mut self, skip_server_verification: bool) -> Self { + self.skip_server_verification = skip_server_verification; + self + } + + /// Sets the ALPN protocols to use + pub fn alpn(mut self, alpn: &'a [&str]) -> Self { + self.alpn = alpn; + self + } + + /// Establishes the TLS connection + pub async fn connect(self) -> Result> { + let builder = TlsConnector::new(); + + let builder = if self.skip_server_verification { + builder + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + } else { + builder + }; + + let builder = builder.request_alpns(self.alpn); + + let builder = if let Some(ca) = self.certificate { + let cert: Certificate = ca + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid CA certificate: {}", e), + }) + })?; + builder.add_root_certificate(cert) + } else { + builder + }; + + let builder = if let Some(identity) = self.identity { + let ident: Identity = identity + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid client identity: {}", e), + }) + })?; + builder.identity(ident) + } else { + builder + }; + + let stream = builder + .connect( + self.host + .to_string(), + self.tcp_stream, + ) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not connect to server: {}", e), + }) + }); + + stream + } +} diff --git a/deboa-smol/src/client/tls/rustls.rs b/deboa-smol/src/client/tls/rustls.rs new file mode 100644 index 00000000..a82bea17 --- /dev/null +++ b/deboa-smol/src/client/tls/rustls.rs @@ -0,0 +1,257 @@ +//! TLS implementation using rustls +use crate::cert::{DeboaCertificate, DeboaIdentity}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, +}; +use rustls::{ + crypto::CryptoProvider, + pki_types::{CertificateDer, PrivateKeyDer}, + ClientConfig, +}; + +pub(crate) fn default_provider() -> CryptoProvider { + #[cfg(feature = "__rustls_aws_lc_rs")] + return rustls::crypto::aws_lc_rs::default_provider(); + #[cfg(feature = "__rustls_ring")] + return rustls::crypto::ring::default_provider(); +} + +#[inline] +pub(crate) fn alpn() -> Vec> { + vec![ + #[cfg(feature = "http3")] + b"h3".to_vec(), + #[cfg(feature = "http2")] + b"h2".to_vec(), + #[cfg(feature = "http1")] + b"http/1.1".to_vec(), + ] +} + +/// Builder for TLS connections using rustls +pub struct TlsConnectionBuilder<'a> { + identity: Option<&'a DeboaIdentity>, + certificate: Option<&'a DeboaCertificate>, + skip_server_verification: bool, + alpn: Vec>, + provider: CryptoProvider, +} + +impl Default for TlsConnectionBuilder<'_> { + fn default() -> Self { + Self { + identity: None, + certificate: None, + skip_server_verification: false, + alpn: alpn(), + provider: default_provider(), + } + } +} + +impl<'a> TlsConnectionBuilder<'a> { + /// Set the identity to use for the connection + pub fn identity(mut self, identity: Option<&'a DeboaIdentity>) -> Self { + self.identity = identity; + self + } + + /// Set the certificate to use for the connection + pub fn certificate(mut self, certificate: Option<&'a DeboaCertificate>) -> Self { + self.certificate = certificate; + self + } + + /// Skip server verification + pub fn skip_server_verification(mut self, skip_server_verification: bool) -> Self { + self.skip_server_verification = skip_server_verification; + self + } + + /// Set the ALPN protocols to use for the connection + pub fn alpn(mut self, alpn: Vec>) -> Self { + self.alpn = alpn; + self + } + + /// Build the TLS client configuration + pub fn build_config(self) -> Result { + let client_config = { + if self.skip_server_verification { + ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier( + deboa_tls::rust::verify::SkipServerVerification::new(self.provider), + ) + .with_no_client_auth() + } else { + #[cfg(feature = "__webpki_rustls_verifier")] + let config = { + let config = ClientConfig::builder_with_provider(self.provider.into()) + .with_protocol_versions(rustls::ALL_VERSIONS) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set TLS version: {}", e), + }) + })?; + + let mut root_store = + rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; + let config = if let Some(ca) = self.certificate { + let cert = ca + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid CA certificate: {}", e), + }) + })?; + + root_store + .add(cert) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!( + "Could not add CA certificate to the store: {}", + e + ), + }) + })?; + + config.with_root_certificates(root_store) + } else { + config.with_root_certificates(root_store) + }; + + config + }; + + #[cfg(feature = "__platform_rustls_verifier")] + let config = { + use rustls_platform_verifier::BuilderVerifierExt; + rustls::ClientConfig::builder_with_provider(default_provider()) + .with_protocol_versions(rustls::ALL_VERSIONS) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set TLS version: {}", e), + }) + })? + .with_platform_verifier() + }; + + let mut config = if let Some(id) = self.identity { + let pair: (CertificateDer<'_>, PrivateKeyDer<'_>) = id + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid client identity: {}", e), + }) + })?; + + config + .with_client_auth_cert(vec![pair.0], pair.1) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set client identity: {}", e), + }) + })? + } else { + config.with_no_client_auth() + }; + + config.enable_early_data = true; + + config.alpn_protocols = self.alpn; + + config + } + }; + + Ok(client_config) + } +} + +#[cfg(any(feature = "http1", feature = "http2"))] +/// TCP connection module for TLS +pub mod tcp { + use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, + }; + use futures_rustls::{client::TlsStream, TlsConnector}; + use rustls::ClientConfig; + use rustls_pki_types::ServerName; + use smol::net::TcpStream; + use std::sync::Arc; + + /// Establish a TLS connection over TCP + pub async fn connect( + config: ClientConfig, + inner_stream: TcpStream, + host: &str, + ) -> Result> { + let connector = TlsConnector::from(Arc::new(config)); + + let hostname = ServerName::try_from(host.to_string()) + .map_err(|e| DeboaError::Connection(ConnectionError::Tls { message: e.to_string() }))?; + + connector + .connect(hostname, inner_stream) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not connect to server: {}", e), + }) + }) + } +} + +#[cfg(feature = "http3")] +/// UDP connection module for TLS +pub mod udp { + use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, + }; + use h3_quinn::Connection; + use quinn::{crypto::rustls::QuicClientConfig, Endpoint}; + use rustls::ClientConfig; + use std::{net::SocketAddr, sync::Arc}; + + /// Establish a TLS connection over UDP + pub async fn connect( + config: ClientConfig, + endpoint: &mut Endpoint, + socket_addr: SocketAddr, + host: &str, + ) -> Result { + let quic_config = QuicClientConfig::try_from(config).map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not create QUIC client config: {}", e), + }) + })?; + + let client_config = quinn::ClientConfig::new(Arc::new(quic_config)); + endpoint.set_default_client_config(client_config); + + let conn = endpoint + .connect(socket_addr, host) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: format!("Could not connect to server: {}", e), + }) + })?; + + let conn = conn + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: format!("Could not connect to server: {}", e), + }) + })?; + + let quinn_conn = h3_quinn::Connection::new(conn); + + Ok(quinn_conn) + } +} diff --git a/deboa-smol/src/client/ws/io/mod.rs b/deboa-smol/src/client/ws/io/mod.rs deleted file mode 100644 index d22cc845..00000000 --- a/deboa-smol/src/client/ws/io/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod socket; diff --git a/deboa-smol/src/client/ws/io/socket.rs b/deboa-smol/src/client/ws/io/socket.rs deleted file mode 100644 index 8c7d8388..00000000 --- a/deboa-smol/src/client/ws/io/socket.rs +++ /dev/null @@ -1,386 +0,0 @@ -use crate::{ - errors::{DeboaExtrasError, WebSocketError}, - ws::protocol::Message, -}; -use hyper::upgrade::Upgraded; -use pin_project_lite::pin_project; -use smol::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use smol_hyper::rt::FuturesIo; -use std::{ - future::Future, - io, - pin::Pin, - task::{Context, Poll}, -}; -use ws_framer::{WsFrame, WsRxFramer, WsTxFramer}; - -pub type UpgradedIo = FuturesIo; - -pub trait DeboaWebSocket { - type Stream; - - fn new(stream: Self::Stream) -> Self; - fn read_message(&mut self) -> impl Future, DeboaExtrasError>>; - fn write_message( - &mut self, - message: Message, - ) -> impl Future>; - fn send_close( - &mut self, - code: u16, - reason: &str, - ) -> impl Future>; - fn send_text(&mut self, message: &str) -> impl Future>; - fn send_binary(&mut self, message: &[u8]) - -> impl Future>; - fn send_ping(&mut self, message: &[u8]) -> impl Future>; - fn send_pong(&mut self, message: &[u8]) -> impl Future>; -} - -pin_project! { - /// WebSocket struct - pub struct WebSocket - { - #[pin] - stream: T, - } -} - -impl DeboaWebSocket for WebSocket { - type Stream = UpgradedIo; - - /// new method - /// - /// # Arguments - /// - /// * `stream` - A string slice that holds the stream data. - /// - /// # Returns - /// - /// A WebSocket struct. - /// - fn new(stream: Self::Stream) -> Self { - Self { stream } - } - - /// Reads a message from the WebSocket. - /// - /// # Returns - /// - /// A Result containing an Option or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// while let Some(message) = websocket.read_message().await { - /// println!("message: {}", message); - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn read_message(&mut self) -> Result, DeboaExtrasError> { - let mut rx_buf = vec![0; 10240]; - let mut rx_framer = WsRxFramer::new(&mut rx_buf); - - let bytes_read = self - .stream - .read(rx_framer.mut_buf()) - .await; - if bytes_read.is_err() { - return Err(DeboaExtrasError::WebSocket(WebSocketError::ReceiveMessage { - message: "Failed to read message".to_string(), - })); - } - - let bytes_read = bytes_read.unwrap(); - rx_framer.revolve_write_offset(bytes_read); - let res = rx_framer.process_data(); - let message = if let Some(frame) = res { - #[allow(clippy::collapsible_match)] - match frame { - WsFrame::Text(data) => Some(Message::Text(data.to_string())), - WsFrame::Binary(data) => Some(Message::Binary(data.to_vec())), - WsFrame::Close(code, reason) => Some(Message::Close(code, reason.to_string())), - WsFrame::Ping(data) => Some(Message::Ping(data.to_vec())), - _ => None, - } - } else { - None - }; - - Ok(message) - } - - /// Writes a message to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The message to write. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket - /// .write_message(protocol::Message::Text(message.to_string())) - /// .await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - /// - async fn write_message(&mut self, message: Message) -> Result<(), DeboaExtrasError> { - let mut tx_buf = vec![0; 10240]; - let mut tx_framer = WsTxFramer::new(true, &mut tx_buf); - - let result = match message { - Message::Text(data) => { - self.write_all(tx_framer.frame(WsFrame::Text(&data))) - .await - } - Message::Binary(data) => { - self.write_all(tx_framer.frame(WsFrame::Binary(&data))) - .await - } - Message::Close(code, reason) => { - self.write_all(tx_framer.frame(WsFrame::Close(code, &reason))) - .await - } - Message::Ping(data) => { - self.write_all(tx_framer.frame(WsFrame::Ping(&data))) - .await - } - _ => Ok(()), - }; - - if result.is_err() { - return Err(DeboaExtrasError::WebSocket(WebSocketError::SendMessage { - message: "Failed to send frame".to_string(), - })); - } - - Ok(()) - } - - /// Sends a close frame to the WebSocket. - /// - /// # Arguments - /// - /// * `code` - The close code. - /// * `reason` - The close reason. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_close(1000, "Goodbye").await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_close(&mut self, code: u16, reason: &str) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Close(code, reason.to_string())) - .await - } - - /// Sends a text frame to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The text message to send. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_text("Hello").await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_text(&mut self, message: &str) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Text(message.to_string())) - .await - } - - /// Sends a binary frame to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The binary message to send. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_binary(&[0x00, 0x01, 0x02]).await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_binary(&mut self, message: &[u8]) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Binary(message.to_vec())) - .await - } - - /// Sends a ping frame to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The ping message to send. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_ping(&[0x00, 0x01, 0x02]).await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_ping(&mut self, message: &[u8]) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Ping(message.to_vec())) - .await - } - - /// Sends a pong frame to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The pong message to send. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_pong(&[0x00, 0x01, 0x02]).await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_pong(&mut self, message: &[u8]) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Pong(message.to_vec())) - .await - } -} - -impl AsyncRead for WebSocket> -where - T: hyper::rt::Read, -{ - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - Poll::Ready(Ok(0)) - } -} - -impl AsyncWrite for WebSocket> -where - T: hyper::rt::Write, -{ - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - hyper::rt::Write::poll_write( - self.project() - .stream - .get_pin_mut(), - cx, - buf, - ) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - hyper::rt::Write::poll_flush( - self.project() - .stream - .get_pin_mut(), - cx, - ) - } - - fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - hyper::rt::Write::poll_shutdown( - self.project() - .stream - .get_pin_mut(), - cx, - ) - } - - fn poll_write_vectored( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[std::io::IoSlice<'_>], - ) -> Poll> { - hyper::rt::Write::poll_write_vectored( - self.project() - .stream - .get_pin_mut(), - cx, - bufs, - ) - } -} diff --git a/deboa-smol/src/client/ws/mod.rs b/deboa-smol/src/client/ws/mod.rs deleted file mode 100644 index e3c72e99..00000000 --- a/deboa-smol/src/client/ws/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! -//! WebSocket support for deboa-extras. -//! Provides WebSocket client functionality with message encoding/decoding. -//! Supports text and binary message types with automatic serialization. -//! Includes support for various serialization formats through the serialization feature. -//! Requires the `websockets` feature to be enabled. -//! -//! -//! ## Example -//! ```rust, compile_fail -//! use deboa::{Client, Result, request::DeboaRequestBuilder}; -//! use deboa_extras::ws::{ -//! io::socket::DeboaWebSocket, -//! protocol::{self}, -//! request::WebsocketRequestBuilder, -//! response::IntoWebSocket, -//! }; -//! -//! let mut client = Client::new(); -//! -//! let websocket = DeboaRequestBuilder::websocket("wss://echo.websocket.org")? -//! .send_with(&mut client) -//! .await? -//! .into_websocket() -//! .await; -//! -//! loop { -//! select! { -//! outgoing_message = websocket.read_message() => { -//! if let Err(message) = outgoing_message { -//! println!("Failed to read message from echo server: {}", message); -//! -//! output.send(Event::Disconnected).await; -//! break; -//! } -//! -//! match outgoing_message.unwrap() { -//! Some(message) => { -//! if let protocol::Message::Text(message) = message { -//! output -//! .send(Event::MessageReceived(Message::User( -//! format!("Server: {}", message).to_string(), -//! ))) -//! .await; -//! } -//! } -//! None => { -//! output.send(Event::Disconnected).await; -//! break; -//! } -//! } -//! } -//! -//! incoming_message = input.next() => { -//! if let Some(message) = incoming_message { -//! let result = websocket -//! .write_message(protocol::Message::Text(message.to_string())) -//! .await; -//! if result.is_err() { -//! output.send(Event::Disconnected).await; -//! break; -//! } -//! } -//! } -//! } -//! } -//! ``` -//! -//! ## Modules -//! -//! * `io` - WebSocket I/O operations -//! * `protocol` - WebSocket protocol handling -//! * `request` - WebSocket request building -//! * `response` - WebSocket response parsing -//! -pub mod io; -pub mod protocol; -pub mod request; -pub mod response; diff --git a/deboa-smol/src/client/ws/protocol.rs b/deboa-smol/src/client/ws/protocol.rs deleted file mode 100644 index 0f8131c7..00000000 --- a/deboa-smol/src/client/ws/protocol.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// Message enum -/// -/// # Variants -/// -/// * `Text(String)` - A text message. -/// * `Binary(Vec)` - A binary message. -/// * `Close(u16, String)` - A close message. -/// * `Ping(Vec)` - A ping message. -/// * `Pong(Vec)` - A pong message. -#[derive(Clone)] -pub enum Message { - Text(String), - Binary(Vec), - Close(u16, String), - Ping(Vec), - Pong(Vec), -} diff --git a/deboa-smol/src/client/ws/request.rs b/deboa-smol/src/client/ws/request.rs deleted file mode 100644 index 363e0f8a..00000000 --- a/deboa-smol/src/client/ws/request.rs +++ /dev/null @@ -1,51 +0,0 @@ -use base64::engine::general_purpose::STANDARD; -use base64::Engine; -use deboa::{ - request::{DeboaRequest, DeboaRequestBuilder}, - url::IntoUrl, - Result, -}; -use http::{header, Method}; - -/// Trait for building websocket requests -pub trait WebsocketRequestBuilder { - /// Creates a websocket request - /// - /// # Arguments - /// - /// * `url` - The URL to connect to - /// - /// # Returns - /// - /// A Result containing the DeboaRequestBuilder - /// - /// # Example - /// - /// ``` compile_fail - /// use deboa::{Client, Result, request::{IntoUrl, DeboaRequestBuilder}}; - /// use deboa_extras::http::ws::request::{WebsocketRequestBuilder}; - /// - /// let mut client = Client::new(); - /// let request = DeboaRequestBuilder::websocket("ws://example.com").unwrap(); - /// let response = request.send_with(&mut client).await.unwrap(); - /// let ws = response.into_websocket().unwrap(); - /// loop { - /// if let Ok(Some(message)) = ws.read_message().await { - /// println!("message: {}", message); - /// } - /// } - /// ``` - fn websocket(url: T) -> Result; -} - -impl WebsocketRequestBuilder for DeboaRequestBuilder { - fn websocket(url: T) -> Result { - let rnd: [u8; 16] = rand::random(); - let key = STANDARD.encode(rnd); - Ok(DeboaRequest::at(url, Method::GET)? - .header(header::UPGRADE, "websocket") - .header(header::CONNECTION, "Upgrade") - .header(header::SEC_WEBSOCKET_KEY, &key) - .header(header::SEC_WEBSOCKET_VERSION, "13")) - } -} diff --git a/deboa-smol/src/client/ws/response.rs b/deboa-smol/src/client/ws/response.rs deleted file mode 100644 index 3ab71f48..00000000 --- a/deboa-smol/src/client/ws/response.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::future::Future; - -use crate::ws::io::socket::{DeboaWebSocket, UpgradedIo, WebSocket}; -use deboa::{response::DeboaResponse, Result}; - -/// Trait for converting a DeboaResponse into a WebSocket -pub trait IntoWebSocket { - /// Converts a DeboaResponse into a WebSocket - /// - /// # Arguments - /// - /// * `self` - The DeboaResponse to convert - /// - /// # Returns - /// - /// A Result containing the WebSocket - /// - /// # Example - /// - /// ``` compile_fail - /// use deboa::{Client, Result, request::{IntoUrl, DeboaRequestBuilder}}; - /// use deboa_extras::http::ws::request::{WebsocketRequestBuilder}; - /// - /// let mut client = Client::new(); - /// let builder = DeboaRequestBuilder::websocket("ws://example.com").unwrap(); - /// let response = builder - /// .send_with(&mut client) - /// .await - /// .unwrap(); - /// let websocket = response.into_websocket().unwrap(); - /// - /// loop { - /// if let Ok(Some(message)) = websocket.read_message().await { - /// println!("message: {}", message); - /// } - /// } - /// ``` - fn into_websocket(self) -> impl Future>>; -} - -impl IntoWebSocket for DeboaResponse { - async fn into_websocket(self) -> Result> { - let upgraded = self - .upgrade() - .await?; - Ok(WebSocket::new(upgraded)) - } -} diff --git a/deboa-smol/src/lib.rs b/deboa-smol/src/lib.rs index e899b76f..0ea64a39 100644 --- a/deboa-smol/src/lib.rs +++ b/deboa-smol/src/lib.rs @@ -24,6 +24,9 @@ compile_error!( #[cfg(all(feature = "native-tls", feature = "rust-tls"))] compile_error!("You cannot enable native-tls and rust-tls features at the same time."); +#[cfg(all(not(any(feature = "native-tls", feature = "rust-tls")), feature = "http2"))] +compile_error!("HTTP2 requires native-tls or rust-tls support."); + #[cfg(all(feature = "native-tls", feature = "http3"))] compile_error!("HTTP3 is not supported within native-tls runtime."); @@ -37,32 +40,6 @@ use crate::{ client::{dns::DefaultDnsResolver, http::conn::pool::HttpConnectionPool}, }; -#[cfg(feature = "rust-tls")] -#[inline] -pub(crate) fn alpn() -> Vec> { - vec![ - #[cfg(feature = "http2")] - b"h2".to_vec(), - #[cfg(feature = "http1")] - b"http/1.1".to_vec(), - #[cfg(feature = "http3")] - b"h3".to_vec(), - ] -} - -#[cfg(feature = "native-tls")] -#[inline] -pub(crate) fn alpn() -> &'static [&'static str] { - &[ - #[cfg(feature = "http2")] - "h2", - #[cfg(feature = "http1")] - "http/1.1", - #[cfg(feature = "http3")] - "h3", - ] -} - /// Certificate management module for handling SSL/TLS certificates. pub mod cert; /// Internal module for HTTP and Websockets clients implementations. diff --git a/deboa-smol/src/rt/stream.rs b/deboa-smol/src/rt/stream.rs index fca640de..8a7fd537 100644 --- a/deboa-smol/src/rt/stream.rs +++ b/deboa-smol/src/rt/stream.rs @@ -12,7 +12,7 @@ use std::{ }; /// A stream that can be either plain TCP or TLS-secured. -pub(crate) enum SmolStream { +pub enum SmolStream { /// A plain TCP connection. Plain(TcpStream), diff --git a/deboa-smol/tests/base/get.rs b/deboa-smol/tests/base/get.rs index 9b478b26..4e8b574d 100644 --- a/deboa-smol/tests/base/get.rs +++ b/deboa-smol/tests/base/get.rs @@ -32,12 +32,19 @@ async fn test_get_http( #[rstest] #[test_attr(apply(test))] async fn test_get_http_skip_verification( - create_client: Client, #[future] create_server: EasyHttpMock, protocol_version: http::Version, ) -> TestResult<()> { + let client = Client::builder() + .certificate(DeboaCertificate::from_slice( + deboa_test_utils::common::helpers::CA_CERT, + ContentEncoding::DER, + )) + .skip_cert_verification(true) + .build(); + deboa_test_utils::base::get::test_skip_cert_verification( - &create_client, + &client, &mut create_server.await, protocol_version, true, @@ -48,12 +55,15 @@ async fn test_get_http_skip_verification( #[rstest] #[test_attr(apply(test))] async fn test_get_http_verify( - create_client: Client, #[future] create_server: EasyHttpMock, protocol_version: http::Version, ) -> TestResult<()> { + let client = Client::builder() + .skip_cert_verification(false) + .build(); + deboa_test_utils::base::get::test_skip_cert_verification( - &create_client, + &client, &mut create_server.await, protocol_version, false, diff --git a/deboa-smol/tests/common/helpers.rs b/deboa-smol/tests/common/helpers.rs index eb6032f0..b72281e1 100644 --- a/deboa-smol/tests/common/helpers.rs +++ b/deboa-smol/tests/common/helpers.rs @@ -74,9 +74,13 @@ pub async fn tls_mock_server() -> EasyHttpMock { let vetis_adapter_config = VetisAdapterConfig::builder() .hostname(&hostname) - .interface(&interface) - .protocol_version(protocol_version()) - .with_random_port() + .interface( + interface + .parse() + .unwrap(), + ) + .protos(vec![protocol_version()]) + .port(free_port(&interface)) .cert(server_cert.to_vec()) .key(server_key.to_vec()) .ca(CA_CERT.to_vec()) @@ -104,7 +108,11 @@ pub async fn plain_mock_server() -> EasyHttpMock { let vetis_adapter_config = VetisAdapterConfig::builder() .hostname(&hostname) - .interface(&interface) + .interface( + interface + .parse() + .unwrap(), + ) .protocol_version(protocol_version()) .with_random_port() .build(); @@ -131,3 +139,26 @@ pub async fn create_server() -> EasyHttpMock { #[cfg(not(any(feature = "rust-tls", feature = "native-tls")))] return plain_mock_server().await; } + +/// An ephemeral port the OS says is free, rather than a guessed one. +/// +/// `with_random_port()` picks `rand::random_range(9000..65535)` and +/// de-duplicates against a process-local set. Under `cargo test` that mostly +/// holds, because every test shares one process and therefore one set. Under +/// `cargo nextest run` — which is what CI uses — each test is its own process, +/// so the set protects nothing and two tests eventually roll the same number. +/// The loser dies with `Address already in use (os error 98)` before its body +/// runs, which is why the failing test is a different one each time. +/// +/// Binding port 0 asks the kernel for a port it knows is free. There is still a +/// window between dropping this listener and the server binding, but the kernel +/// will not hand the same ephemeral port to someone else inside it, which is +/// the part guessing cannot promise. +fn free_port(interface: &str) -> u16 { + let listener = std::net::TcpListener::bind((interface, 0)) + .expect("bind an ephemeral port to ask the OS for a free one"); + listener + .local_addr() + .expect("read back the bound port") + .port() +} diff --git a/deboa-test-utils/Cargo.toml b/deboa-test-utils/Cargo.toml index 2c1b1db3..ca31ffdb 100644 --- a/deboa-test-utils/Cargo.toml +++ b/deboa-test-utils/Cargo.toml @@ -10,8 +10,8 @@ license.workspace = true rust-version.workspace = true [dependencies] -bytes = "1.12.1" -caramelo = "0.1.2" +bytes = { workspace = true } +caramelo = { workspace = true } ciborium = "0.2.2" deboa.workspace = true deboa-extras = { workspace = true, default-features = false, features = [ @@ -23,21 +23,18 @@ easyhttpmock = "0.1.3" env_logger = "0.11.11" fory = "1.5.0" fory-core = "1.5.0" -futures-util = "0.3.33" -http = "1.5.0" -http-body-util = "0.1" +futures-util = { workspace = true } +http = { workspace = true } +http-body-util = { workspace = true } mime = { version = "0.3.17" } multer = "3.1.0" once_cell = "1.21.4" -rand = "0.10.2" -serde = "1.0.229" -tackle = "0.1.3" -url = "2.5.8" +rand = { workspace = true } +serde = { workspace = true } +tackle = { workspace = true } +url = { workspace = true } vamo = { workspace = true, default-features = false } vamo-macros = { workspace = true, features = [ "json", ], default-features = false } vetis = "0.1.4" - -[dev-dependencies] -caramelo = "0.1.2" diff --git a/deboa-test-utils/src/base/get.rs b/deboa-test-utils/src/base/get.rs index 52229c5b..cc13a4cc 100644 --- a/deboa-test-utils/src/base/get.rs +++ b/deboa-test-utils/src/base/get.rs @@ -60,7 +60,7 @@ where } pub async fn test_skip_cert_verification( - _client: &Client>, + client: &Client>, server: &mut EasyHttpMock, protocol_version: http::Version, skip: bool, @@ -83,9 +83,6 @@ where server .register_mock(mock) .await?; - let client: Client> = Client::builder() - .skip_cert_verification(skip) - .build(); let request = DeboaRequest::get(server.url("/posts/1"))? .version(protocol_version) @@ -107,7 +104,6 @@ where } Version::HTTP_3 => { let error = DeboaError::Connection(ConnectionError::Udp { - host: "localhost".to_string(), message: "Could not connect to server: aborted by peer: the cryptographic handshake failed: error 120: peer doesn't support any known protocol".to_string(), }); expect(response.unwrap_err()).to_be(eq(error)); diff --git a/deboa-tls/Cargo.toml b/deboa-tls/Cargo.toml new file mode 100644 index 00000000..5fddba64 --- /dev/null +++ b/deboa-tls/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "deboa-tls" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +rust-version.workspace = true + +[features] +default = [ + "rust-tls", + "default-rustls-provider", + "default-rustls-verifier", +] + +rust-tls = [ + "rustls/tls12", + "dep:rustls-native-certs", + "dep:futures-rustls", + "dep:rustls-pki-types", +] +native-tls= [] + +default-rustls-verifier = ["__webpki_rustls_verifier"] +webpki-rustls-verifier = ["__webpki_rustls_verifier"] +platform-rustls-verifier = ["__platform_rustls_verifier"] +__webpki_rustls_verifier = ["dep:webpki-roots"] +__platform_rustls_verifier = ["dep:rustls-platform-verifier"] + +# rustls providers +default-rustls-provider = ["__rustls_aws_lc_rs"] +no-provider = [] +aws-lc-rustls-provider = ["__rustls_aws_lc_rs"] +ring-rustls-provider = ["__rustls_ring"] + +__rustls_aws_lc_rs = ["rustls/aws-lc-rs"] +__rustls_ring = ["rustls/ring"] + +tcp = [] +quic = [] + +[dependencies] +deboa = { workspace = true } +futures-rustls = { workspace = true, optional = true } +rustls = { workspace = true, optional = true } +rustls-native-certs = { workspace = true, optional = true } +rustls-pki-types = { workspace = true, optional = true } +rustls-platform-verifier = { workspace = true, optional = true } +webpki-roots = { workspace = true, optional = true } diff --git a/deboa-tls/src/lib.rs b/deboa-tls/src/lib.rs new file mode 100644 index 00000000..3fe4da73 --- /dev/null +++ b/deboa-tls/src/lib.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "rust-tls")] +pub mod rust; diff --git a/deboa-tls/src/rust/mod.rs b/deboa-tls/src/rust/mod.rs new file mode 100644 index 00000000..aa50c262 --- /dev/null +++ b/deboa-tls/src/rust/mod.rs @@ -0,0 +1,68 @@ +pub mod verify { + use rustls::{ + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::CryptoProvider, + pki_types::{CertificateDer, ServerName, UnixTime}, + }; + use std::sync::Arc; + + #[derive(Debug)] + pub struct SkipServerVerification(CryptoProvider); + + impl SkipServerVerification { + pub fn new(provider: CryptoProvider) -> Arc { + Arc::new(Self(provider)) + } + } + + impl ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> std::result::Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self + .0 + .signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self + .0 + .signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0 + .signature_verification_algorithms + .supported_schemes() + } + } +} diff --git a/deboa-tokio/Cargo.toml b/deboa-tokio/Cargo.toml index 4b369923..843ff908 100644 --- a/deboa-tokio/Cargo.toml +++ b/deboa-tokio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deboa-tokio" -version = "0.1.2" +version = "0.1.3" edition.workspace = true authors.workspace = true repository.workspace = true @@ -16,30 +16,31 @@ rust-version.workspace = true features = [ "http1", "http2", - "http3", "rust-tls", - "native-tls", - "websockets", "default-rustls-provider", "default-rustls-verifier", ] [features] default = [ + "http1", "http2", + #"http3", "rust-tls", "default-rustls-provider", "default-rustls-verifier", + "websockets" ] # tls rust-tls = [ - "dep:rustls", + "rustls/tls12", "dep:rustls-native-certs", "dep:tokio-rustls", "dep:rustls-pki-types", ] +# rustls verifiers webpki-rustls-verifier = ["__webpki_rustls_verifier"] platform-rustls-verifier = ["__platform_rustls_verifier"] @@ -53,99 +54,95 @@ aws-lc-rustls-provider = ["__rustls_aws_lc_rs"] ring-rustls-provider = ["__rustls_ring"] default-rustls-provider = ["__rustls_aws_lc_rs"] -__rustls_aws_lc_rs = [ - "futures-rustls/aws-lc-rs", - "quinn/rustls-aws-lc-rs", -] -__rustls_ring = [ - "futures-rustls/ring", - "quinn/rustls-ring", -] +__rustls_aws_lc_rs = ["rustls/aws-lc-rs", "quinn/rustls-aws-lc-rs"] +__rustls_ring = ["rustls/ring", "quinn/rustls-ring"] -# tls implementations +# native-tls implementations native-tls = ["async-native-tls/runtime-tokio"] # protocols -http1 = ["hyper/http1", "hyper-util/http1"] -http2 = ["hyper/http2", "hyper-util/http2"] +http1 = ["rustls/tls12", "hyper/http1", "hyper-util/http1"] +http2 = ["rustls/tls12", "hyper/http2", "hyper-util/http2"] http3 = [ "deboa-h3/generic", + "dep:futures", "dep:h3", "dep:h3-quinn", "quinn/runtime-tokio", - "hyper-body-utils/http3", "hyper-body-utils/generic", + "hyper-body-utils/generic-h3", ] -websockets = ["ws-framer/http", "ws-framer/alloc"] +websockets = ["deboa-ws/tokio"] [dependencies] -async-executor = { version = "1.13.3", optional = true, default-features = false } -async-lock = "3.4.2" -async-native-tls = { version = "0.6.0", optional = true, default-features = false } -base64 = {version = "0.23.0" } -bytes = { version = "1.11", default-features = false } -cookie = { version = "0.18.1", default-features = false } +async-executor = { workspace = true, optional = true } +async-lock = { workspace = true } +async-native-tls = { workspace = true, optional = true } +base64 = { workspace = true } +bytes = { workspace = true } +cookie = { workspace = true } deboa = { workspace = true } deboa-h3 = { workspace = true, optional = true } -futures = "0.3.31" -futures-rustls = { version = "0.26.0", optional = true, default-features = false } -futures-util = { version = "0.3.31", optional = true, default-features = false } -h3 = { version = "0.0.8", optional = true, default-features = false } -h3-quinn = { version = "0.0.10", optional = true, default-features = false } -hashbrown = "0.17.1" -http = "1" -http-body = "1" -http-body-util = "0.1" -hyper = { version = "1.10.1", features = ["client"], default-features = false } +deboa-tls = { workspace = true } +deboa-ws = { workspace = true, optional = true } +futures = { workspace = true, optional = true } +futures-util = { workspace = true, optional = true } +h3 = { workspace = true, optional = true } +h3-quinn = { workspace = true, optional = true } +hashbrown = { workspace = true } +http = { workspace = true } +http-body = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true } hyper-body-utils = { workspace = true, default-features = false } -hyper-util = { version = "0.1.20", features = [ - "client", - "client-legacy", - "tokio", -], default-features = false } +hyper-util = { workspace = true } indexmap = "2.11.4" -log = "0.4.32" -minimime = "1.0.0" -mockall = "0.15.0" +log = { workspace = true } +minimime = { workspace = true } quinn = { version = "0.11.7", optional = true, features = [ "runtime-tokio", ], default-features = false } -rand = { version = "0.10.1", default-features = false } -regex = { version = "1.12.4", default-features = false } -rustls = { version = "0.23.36", optional = true, default-features = false } -rustls-native-certs = { version = "0.8.4", optional = true, default-features = false } -rustls-pki-types = { version = "1.14.1", optional = true, default-features = false } -rustls-platform-verifier = { version = "0.7.0", optional = true, default-features = false } -serde = { version = "1.0.217", features = ["derive"] } -tackle = { version = "0.1.1"} -thiserror = "2.0.17" -time = { version = "0.3.51", default-features = false } -tokio = { version = "1.38.1", features = [ +rand = { workspace = true } +regex = { workspace = true } +rustls = { workspace = true, optional = true } +rustls-native-certs = { workspace = true, optional = true } +rustls-pki-types = { workspace = true, optional = true } +rustls-platform-verifier = { workspace = true, optional = true } +serde = { workspace = true , features = ["derive"] } +tackle = { workspace = true } +thiserror = { workspace = true } +tokio = { version = "1.53.1", features = [ "macros", "fs", + "time", ], default-features = false } tokio-rustls = { version = "0.26.4", optional = true, default-features = false } tokio-util = { version = "0.7.11", features = ["io"], default-features = false } -url = "2.5.8" -urlencoding = "2.1.3" -webpki-roots = { version = "1.0.6", optional = true, default-features = false } -ws-framer = { version = "0.3.1", optional = true, default-features = false } +url = { workspace = true } +urlencoding = { workspace = true } +webpki-roots = { workspace = true, optional = true, default-features = false } + [dev-dependencies] -caramelo = "0.1.2" +caramelo = { workspace = true } criterion = { version = "0.8.2", features = [ "html_reports", "async", "async_tokio", ] } deboa-test-utils = { workspace = true } -easyhttpmock-vetis-tokio = { version = "0.1.0", features = ["http2", "rust-tls"], default-features = false } -futures-util = "0.3.31" -multer = "3.1.0" -rstest = "0.26.1" -tokio = { version = "1.38.1", features = [ +easyhttpmock-vetis-tokio = { version = "0.1.1", features = [ + "http1", + "http2", + #"http3", + "rust-tls", +], default-features = false } +futures-util = { workspace = true } +multer = { workspace = true } +rstest = { workspace = true } +tokio = { version = "1.53.1", features = [ "macros", "fs", - "net" + "net", ], default-features = false } diff --git a/deboa-tokio/README.md b/deboa-tokio/README.md index ac80e516..77c03735 100644 --- a/deboa-tokio/README.md +++ b/deboa-tokio/README.md @@ -61,7 +61,7 @@ http = "1.3.1" ## Usage -```rust +```rust, ignore use deboa::{ HttpClient, request::{DeboaRequest, FetchWith, get}, diff --git a/deboa-tokio/src/cert.rs b/deboa-tokio/src/cert.rs index 9f6700c9..2d8ff0ac 100644 --- a/deboa-tokio/src/cert.rs +++ b/deboa-tokio/src/cert.rs @@ -92,7 +92,7 @@ impl deboa::cert::Identity for DeboaIdentity { &self.cert } - fn ket(&self) -> &Option> { + fn key(&self) -> &Option> { &self.key } diff --git a/deboa-tokio/src/client/dns.rs b/deboa-tokio/src/client/dns.rs index 264983ec..55b4bf57 100644 --- a/deboa-tokio/src/client/dns.rs +++ b/deboa-tokio/src/client/dns.rs @@ -1,3 +1,7 @@ +//! DNS resolution for the Deboa HTTP client. +//! +//! This module provides DNS resolution functionality for the Deboa HTTP client. + use deboa::{ dns::DnsResolver, errors::{DeboaError::Dns, DnsError}, diff --git a/deboa-tokio/src/client/http/conn/mod.rs b/deboa-tokio/src/client/http/conn/mod.rs index 5860cac5..d6a16ce7 100644 --- a/deboa-tokio/src/client/http/conn/mod.rs +++ b/deboa-tokio/src/client/http/conn/mod.rs @@ -16,13 +16,16 @@ //! - Thread-safe connection handling //! ``` use crate::cert::{DeboaCertificate, DeboaIdentity}; +#[cfg(any(feature = "http1", feature = "http2"))] +use crate::rt::stream::TokioStream; #[cfg(feature = "http1")] use deboa::request::Http1Request; #[cfg(feature = "http2")] use deboa::request::Http2Request; use deboa::{ conn::{ConnectionConfig, HttpConnectionDispatcher, ProtoConnection}, - errors::{DeboaError, RequestError}, + dns::DnsResolver, + errors::{ConnectionError, DeboaError, RequestError}, response::DeboaResponse, Result, }; @@ -30,7 +33,10 @@ use deboa::{ use deboa_h3::generic::Http3Request; use http::{Request, Version}; use hyper_body_utils::HttpBody; -use std::marker::PhantomData; +use log::info; +use std::{borrow::Cow, marker::PhantomData, time::Duration}; +#[cfg(any(feature = "http1", feature = "http2"))] +use tokio::net::TcpStream; /// Connection pooling for efficient HTTP connections. /// @@ -45,9 +51,6 @@ use std::marker::PhantomData; /// - Configurable pool size (coming soon) pub mod pool; -/// Stream module for runtime-specific stream implementations. -pub(crate) mod stream; - #[cfg(feature = "http1")] pub(crate) type Http1Connection = BaseHttpConnection; #[cfg(feature = "http2")] @@ -92,20 +95,8 @@ impl DeboaConnection { pub fn http3(conn: Http3Connection) -> Self { DeboaConnection::Http3(Box::new(conn)) } -} -impl HttpConnectionDispatcher for DeboaConnection { - /// Send a request over the connection. - /// - /// # Arguments - /// - /// * `url` - The URL to send the request to. - /// * `request` - The request to send. - /// - /// # Returns - /// - /// * `Result` - The response or error. - async fn send_request(&mut self, request: Request) -> Result { + async fn send(&mut self, request: Request) -> Result { match self { #[cfg(feature = "http1")] DeboaConnection::Http1(ref mut conn) => { @@ -159,6 +150,30 @@ impl HttpConnectionDispatcher for DeboaConnection { } } +impl HttpConnectionDispatcher for DeboaConnection { + /// Send a request over the connection. + /// + /// # Arguments + /// + /// * `url` - The URL to send the request to. + /// * `request` - The request to send. + /// + /// # Returns + /// + /// * `Result` - The response or error. + async fn send_request( + &mut self, + request: Request, + timeout: Duration, + ) -> Result { + tokio::time::timeout(timeout, self.send(request)) + .await + .map_err(|_| { + DeboaError::Request(RequestError::Send { message: "Request timed out".to_string() }) + })? + } +} + /// Struct that represents the connection. /// /// # Fields @@ -176,29 +191,189 @@ impl BaseHttpConnection { } } +#[cfg(feature = "rust-tls")] +async fn connect_with_rustls<'a>( + tcp_stream: TcpStream, + config: &ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, +) -> Result<(Version, TokioStream)> { + use crate::client::tls::rustls::{tcp::connect, TlsConnectionBuilder}; + let tls_config = TlsConnectionBuilder::default() + .certificate(config.certificate()) + .identity(config.identity()) + .build_config()?; + + let stream = Box::new(connect(tls_config, tcp_stream, config.host()).await?); + + if let Some(alpn) = stream + .get_ref() + .1 + .alpn_protocol() + { + let Cow::Borrowed(alpn_code) = String::from_utf8_lossy(alpn) else { + return Err(DeboaError::Connection(ConnectionError::Tcp { + message: "Invalid ALPN code".to_string(), + })); + }; + + let version = match alpn_code { + "http1.1" => Version::HTTP_11, + "h2" => Version::HTTP_2, + "h3" => Version::HTTP_3, + _ => panic!("Unsupported protocol"), + }; + + info!("ALPN info found, switching connection to {:?}", version); + Ok((version, TokioStream::Tls(stream))) + } else { + info!("No ALPN info available, falling back to HTTP/1.1"); + Ok((Version::HTTP_11, TokioStream::Tls(stream))) + } +} + +#[cfg(feature = "native-tls")] +async fn connect_with_nativetls<'a>( + tcp_stream: TcpStream, + config: &ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, +) -> Result<(Version, TokioStream)> { + use crate::client::tls::native::TlsConnectionBuilder; + let stream = TlsConnectionBuilder::new(tcp_stream, config.host()) + .certificate(config.certificate()) + .identity(config.identity()) + .connect() + .await?; + + if let Some(alpn) = stream + .get_ref() + .alpn_protocol() + { + let Cow::Borrowed(alpn_code) = String::from_utf8_lossy(alpn) else { + return Err(DeboaError::Connection(ConnectionError::Tcp { + message: "Invalid ALPN code".to_string(), + })); + }; + + let version = match alpn_code { + "http1.1" => Version::HTTP_11, + "h2" => Version::HTTP_2, + "h3" => Version::HTTP_3, + _ => panic!("Unsupported protocol"), + }; + + info!("ALPN info found, switching connection to {:?}", version); + Ok((version, TokioStream::Tls(stream))) + } else { + info!("No ALPN info available, falling back to HTTP/1.1"); + Ok((Version::HTTP_11, TokioStream::Tls(stream))) + } +} + /// Factory for creating connections. pub(crate) struct ConnectionFactory {} impl ConnectionFactory { /// Create a new connection. - pub async fn create_connection<'a>( - protocol: &Version, + pub async fn create_connection<'a, D>( config: &'a ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>, - ) -> Result { - let conn = match protocol { + dns_resolver: &D, + ) -> Result + where + D: DnsResolver, + { + //TODO: consider add support to DNS HTTPS record + + let ips = dns_resolver + .resolve( + config + .host() + .to_string(), + config.port(), + ) + .await?; + let ips = if config + .client_bind_addr() + .is_ipv4() + { + ips.into_iter() + .filter(|ip| ip.is_ipv4()) + .collect::>() + } else { + ips.into_iter() + .filter(|ip| ip.is_ipv6()) + .collect::>() + }; + + let Some(ip) = ips.first() else { + return Err(DeboaError::Request(RequestError::Send { + message: format!("No IP addresses found for hostname: {}", config.host()), + })); + }; + + #[cfg(any(feature = "http1", feature = "http2"))] + let conn_pair = { + let tcp_stream = TcpStream::connect(format!("{}:{}", ip, config.port())) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tcp { message: e.to_string() }) + })?; + let use_tls = config.scheme() == "https" || config.scheme() == "wss"; + if !use_tls { + (Version::HTTP_11, TokioStream::Plain(tcp_stream)) + } else { + #[cfg(feature = "rust-tls")] + { + connect_with_rustls(tcp_stream, config).await? + } + + #[cfg(feature = "native-tls")] + { + connect_with_nativels(tcp_stream, config).await? + } + } + }; + + let conn = match conn_pair.0 { #[cfg(feature = "http1")] - &Version::HTTP_11 => { - let conn = Http1Connection::connect(config).await?; + Version::HTTP_11 => { + let conn = Http1Connection::connect(conn_pair.1).await?; DeboaConnection::http1(conn) } #[cfg(feature = "http2")] - &Version::HTTP_2 => { - let conn = Http2Connection::connect(config).await?; + Version::HTTP_2 => { + let conn = Http2Connection::connect(conn_pair.1).await?; DeboaConnection::http2(conn) } #[cfg(feature = "http3")] - &Version::HTTP_3 => { - let conn = Http3Connection::connect(&config).await?; + Version::HTTP_3 => { + let stream = { + use crate::client::tls::rustls::udp::connect; + #[cfg(feature = "rust-tls")] + use crate::client::tls::rustls::TlsConnectionBuilder; + use quinn::Endpoint; + use std::net::SocketAddr; + + let mut client_endpoint = Endpoint::client(SocketAddr::new( + *config.client_bind_addr(), + 0, + )) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { message: e.to_string() }) + })?; + + let tls_config = TlsConnectionBuilder::default() + .certificate(config.certificate()) + .identity(config.identity()) + .build_config()?; + + connect( + tls_config, + &mut client_endpoint, + SocketAddr::new(*ip, config.port()), + config.host(), + ) + .await? + }; + + let conn = Http3Connection::connect(stream).await?; DeboaConnection::http3(conn) } _ => { diff --git a/deboa-tokio/src/client/http/conn/pool.rs b/deboa-tokio/src/client/http/conn/pool.rs index bfaa06dd..f4156650 100644 --- a/deboa-tokio/src/client/http/conn/pool.rs +++ b/deboa-tokio/src/client/http/conn/pool.rs @@ -2,9 +2,13 @@ use crate::{ cert::{DeboaCertificate, DeboaIdentity}, client::http::conn::{ConnectionConfig, ConnectionFactory, DeboaConnection}, }; -use deboa::Result; -use std::collections::HashMap; -use time::Duration; +use deboa::{ + dns::DnsResolver, + errors::{ConnectionError, DeboaError}, + Result, +}; +use hashbrown::HashMap; +use std::time::Duration; /// Struct that represents the HTTP connection pool. /// @@ -27,7 +31,7 @@ impl Default for HttpConnectionPool { fn default() -> Self { Self { max_idle_connections: 5, - keep_alive_duration: Duration::minutes(5), + keep_alive_duration: Duration::from_mins(5), connections: HashMap::new(), } } @@ -76,36 +80,48 @@ impl deboa::conn::HttpConnectionPool for HttpConnectionPool { .len() as u32 } - async fn create_connection<'a>( - &'a mut self, + async fn create_connection<'a, D>( + &mut self, config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result<&'a mut DeboaConnection> { - if self.max_idle_connections == 0 { - self.connections - .clear(); - } - - let host = config.host(); + dns_resolver: &D, + ) -> Result<&mut DeboaConnection> + where + D: DnsResolver, + { + let key = format!("{}:{}", config.host(), config.port()); if self .connections - .contains_key(host) + .contains_key(&key) { - log::debug!("Connection already exists for {}, reusing.", host); + log::debug!("Connection already exists for {}, reusing.", key); return Ok(self .connections - .get_mut(host) + .get_mut(&key) .unwrap()); } - log::debug!("Creating new connection for {}", host); - let connection = - ConnectionFactory::create_connection(config.protocol_version(), config).await?; + log::debug!("Creating new connection for {}", key); + let connection = tokio::time::timeout( + config.connection_timeout(), + ConnectionFactory::create_connection(config, dns_resolver), + ) + .await + .map_err(|_| { + DeboaError::Connection(ConnectionError::Timeout { + message: format!( + "Connection to {} timed out after {:?}", + key, + config.connection_timeout() + ), + }) + })??; self.connections - .insert(host.to_string(), connection); + .insert(key.clone(), connection); + Ok(self .connections - .get_mut(host) + .get_mut(&key) .unwrap()) } } diff --git a/deboa-tokio/src/client/http/conn/stream/mod.rs b/deboa-tokio/src/client/http/conn/stream/mod.rs deleted file mode 100644 index e735122d..00000000 --- a/deboa-tokio/src/client/http/conn/stream/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// Plain module for runtime-specific plain implementations. -pub(crate) mod plain; -pub(crate) use plain::*; - -/// TLS module for runtime-specific TLS implementations. -#[cfg(all( - any(feature = "rust-tls", feature = "native-tls"), - any(feature = "http1", feature = "http2", feature = "http3") -))] -pub(crate) mod tls; -#[cfg(all( - any(feature = "rust-tls", feature = "native-tls"), - any(feature = "http1", feature = "http2", feature = "http3") -))] -pub(crate) use tls::*; diff --git a/deboa-tokio/src/client/http/conn/stream/plain.rs b/deboa-tokio/src/client/http/conn/stream/plain.rs deleted file mode 100644 index 1c852cce..00000000 --- a/deboa-tokio/src/client/http/conn/stream/plain.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::net::IpAddr; - -use crate::rt::stream::TokioStream; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; -use tokio::net::TcpStream; - -pub(crate) async fn create_stream(addr: IpAddr, host: &str, port: u16) -> Result { - let tcp_stream = TcpStream::connect((addr, port)).await; - let tcp_stream = match tcp_stream { - Ok(tcp_stream) => tcp_stream, - Err(e) => { - return Err(DeboaError::Connection(ConnectionError::Tcp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - }; - - Ok(tcp_stream) -} - -pub(crate) async fn plain_connection(addr: IpAddr, host: &str, port: u16) -> Result { - let stream = create_stream(addr, host, port).await?; - Ok(TokioStream::Plain(stream)) -} diff --git a/deboa-tokio/src/client/http/conn/stream/tls/mod.rs b/deboa-tokio/src/client/http/conn/stream/tls/mod.rs deleted file mode 100644 index 3497239f..00000000 --- a/deboa-tokio/src/client/http/conn/stream/tls/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -#[cfg(feature = "native-tls")] -mod native; - -#[cfg(feature = "rust-tls")] -/// Internal stream handling utilities for connection establishment. -/// Provides low-level connection creation function for secure connections . -/// Used internally by the HTTP connection implementations. -/// -/// # Modules -/// -/// - `tls_connection`: Creates TLS-encrypted connections with optional client certificates -/// -/// # Examples -/// -/// ```compile_fail, rust -/// use deboa::client::conn::stream::tls_connection; -/// -/// // Create a TLS connection -/// let stream = tls_connection("example.com:443", None).await?; -/// ``` -mod rustls; - -#[cfg(feature = "rust-tls")] -pub(crate) use rustls::*; - -#[cfg(feature = "native-tls")] -pub(crate) use native::*; diff --git a/deboa-tokio/src/client/http/conn/stream/tls/native.rs b/deboa-tokio/src/client/http/conn/stream/tls/native.rs deleted file mode 100644 index c0028131..00000000 --- a/deboa-tokio/src/client/http/conn/stream/tls/native.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{ - cert::{Certificate as DeboaCertificate, Identity as DeboaIdentity}, - client::http::conn::stream::create_stream, - rt::stream::TokioStream, -}; -use async_native_tls::{Certificate, Identity, TlsConnector}; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; -use std::net::IpAddr; - -pub(crate) async fn tls_connection( - ip: IpAddr, - host: &str, - port: u16, - identity: &Option, - certificate: &Option, - skip_server_verification: bool, - alpn: &[&str], -) -> Result { - let socket = create_stream(ip, host, port).await?; - let builder = TlsConnector::new(); - - let builder = if skip_server_verification { - builder - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - } else { - builder - }; - - let builder = builder.request_alpns(alpn); - - let builder = if let Some(ca) = certificate { - let cert: std::result::Result = ca.try_into(); - if let Err(e) = cert { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid CA certificate: {}", e), - })); - } - - builder.add_root_certificate(cert.unwrap()) - } else { - builder - }; - - let builder = if let Some(identity) = identity { - let ident: std::result::Result = identity.try_into(); - if let Err(e) = ident { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid client identity: {}", e), - })); - } - builder.identity(ident.unwrap()) - } else { - builder - }; - - let stream = builder - .connect(host.to_string(), socket) - .await; - - if let Err(e) = stream { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - - let stream = stream.unwrap(); - Ok(TokioStream::Tls(stream)) -} diff --git a/deboa-tokio/src/client/http/conn/stream/tls/rustls.rs b/deboa-tokio/src/client/http/conn/stream/tls/rustls.rs deleted file mode 100644 index a5714ce0..00000000 --- a/deboa-tokio/src/client/http/conn/stream/tls/rustls.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::{ - cert::{Certificate as DeboaCertificate, Identity as DeboaIdentity}, - client::http::conn::stream::create_stream, - rt::stream::TokioStream, -}; -use deboa::{ - errors::{ConnectionError, DeboaError}, - Result, -}; -use rustls::pki_types::ServerName; -use rustls::pki_types::{CertificateDer, PrivateKeyDer}; -use rustls::ClientConfig; -use std::{net::IpAddr, sync::Arc}; -use tokio_rustls::TlsConnector; - -pub(crate) async fn tls_connection( - ip: IpAddr, - host: &str, - port: u16, - identity: &Option, - certificate: &Option, - skip_server_verification: bool, - alpn: Vec>, -) -> Result { - let socket = create_stream(ip, host, port).await?; - let config = setup_rust_tls(host, identity, certificate, skip_server_verification, alpn)?; - let connector = TlsConnector::from(Arc::new(config)); - let hostname = ServerName::try_from(host.to_string()); - - if let Err(e) = hostname { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: e.to_string(), - })); - } - - let stream = connector - .connect(hostname.unwrap(), socket) - .await; - - if let Err(e) = stream { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })); - } - - let stream = stream.unwrap(); - Ok(TokioStream::Tls(Box::new(stream))) -} - -pub(crate) fn default_provider() -> Arc { - #[cfg(feature = "__rustls_aws_lc_rs")] - let provider = rustls::crypto::aws_lc_rs::default_provider(); - #[cfg(feature = "__rustls_ring")] - let provider = rustls::crypto::ring::default_provider(); - Arc::new(provider) -} - -pub fn setup_rust_tls( - host: &str, - identity: &Option, - certificate: &Option, - skip_server_verification: bool, - alpn: Vec>, -) -> Result { - let provider = default_provider(); - - if skip_server_verification { - use verify::SkipServerVerification; - let config = rustls::ClientConfig::builder_with_provider(provider) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version") - .dangerous() - .with_custom_certificate_verifier(SkipServerVerification::new()) - .with_no_client_auth(); - return Ok(config); - } - - #[cfg(feature = "__webpki_rustls_verifier")] - let config = { - let config = rustls::ClientConfig::builder_with_provider(provider) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version"); - - let mut root_store = - rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; - if let Some(ca) = certificate { - let cert = ca.try_into(); - if let Err(e) = cert { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid CA certificate: {}", e), - })); - } - - let result = root_store.add(cert.unwrap()); - if let Err(e) = result { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not add CA certificate to the store: {}", e), - })); - } - - config.with_root_certificates(root_store) - } else { - config.with_root_certificates(root_store) - } - }; - - #[cfg(feature = "__platform_rustls_verifier")] - let config = { - use rustls_platform_verifier::Verifier; - let verifier = Verifier::new(provider).expect("Failed to create platform verifier"); - rustls::ClientConfig::builder_with_provider(default_provider()) - .with_protocol_versions(&[&rustls::version::TLS13]) - .expect("Failed to set TLS version") - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - }; - - let mut config = if let Some(id) = identity { - let pair: std::result::Result< - (CertificateDer<'static>, PrivateKeyDer<'static>), - std::io::Error, - > = id.try_into(); - if let Err(e) = pair { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Invalid client identity: {}", e), - })); - } - - let pair = pair.unwrap(); - - config - .with_client_auth_cert(vec![pair.0], pair.1) - .expect("Failed to set client identity") - } else { - config.with_no_client_auth() - }; - - config.enable_early_data = true; - - config.alpn_protocols = alpn; - - Ok(config) -} - -pub(crate) mod verify { - use rustls::{ - client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, - pki_types::{CertificateDer, ServerName, UnixTime}, - }; - use std::sync::Arc; - - #[derive(Debug)] - pub(crate) struct SkipServerVerification(Arc); - - impl SkipServerVerification { - pub(crate) fn new() -> Arc { - let provider = super::default_provider(); - Arc::new(Self(provider)) - } - } - - impl ServerCertVerifier for SkipServerVerification { - fn verify_server_cert( - &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp: &[u8], - _now: UnixTime, - ) -> std::result::Result { - Ok(ServerCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &rustls::DigitallySignedStruct, - ) -> std::result::Result { - rustls::crypto::verify_tls12_signature( - message, - cert, - dss, - &self - .0 - .signature_verification_algorithms, - ) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &rustls::DigitallySignedStruct, - ) -> std::result::Result { - rustls::crypto::verify_tls13_signature( - message, - cert, - dss, - &self - .0 - .signature_verification_algorithms, - ) - } - - fn supported_verify_schemes(&self) -> Vec { - self.0 - .signature_verification_algorithms - .supported_schemes() - } - } -} diff --git a/deboa-tokio/src/client/http/http1.rs b/deboa-tokio/src/client/http/http1.rs index 484b0f6e..6dc11dce 100644 --- a/deboa-tokio/src/client/http/http1.rs +++ b/deboa-tokio/src/client/http/http1.rs @@ -1,17 +1,15 @@ -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::{alpn, client::http::conn::stream::tls_connection}; use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::plain_connection, BaseHttpConnection, Http1Connection}, + client::http::conn::{BaseHttpConnection, Http1Connection}, + rt::stream::TokioStream, }; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, + errors::{ConnectionError, DeboaError}, request::Http1Request, Result, }; use http::version::Version; use hyper::client::conn::http1::handshake; -use hyper_body_utils::HttpBody; use hyper_util::rt::TokioIo; impl HttpConnection for Http1Connection { @@ -22,46 +20,20 @@ impl HttpConnection for Http1Connection { } impl ProtoConnection for Http1Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http1Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = TokioStream; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_11 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - #[cfg(any(feature = "rust-tls", feature = "native-tls"))] - let stream = if config.is_secure() { - tls_connection( - *config.ip(), - config.host(), - config.port(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - ) + async fn connect(stream: Self::RuntimeStream) -> Result { + let (sender, conn) = handshake(TokioIo::new(stream)) .await - } else { - plain_connection(*config.ip(), config.host(), config.port()).await - }; - - #[cfg(not(any(feature = "rust-tls", feature = "native-tls")))] - let stream = plain_connection(config.host(), config.port()).await; - - if let Err(e) = stream { - return Err(e); - } - - let result = handshake(TokioIo::new(stream.unwrap())).await; - - let (sender, conn) = result.unwrap(); + .map_err(|e| { + DeboaError::Connection(ConnectionError::Handshake { message: e.to_string() }) + })?; tokio::spawn(async move { match conn diff --git a/deboa-tokio/src/client/http/http2.rs b/deboa-tokio/src/client/http/http2.rs index a2b37c71..069a2238 100644 --- a/deboa-tokio/src/client/http/http2.rs +++ b/deboa-tokio/src/client/http/http2.rs @@ -1,17 +1,15 @@ -#[cfg(any(feature = "rust-tls", feature = "native-tls"))] -use crate::{alpn, client::http::conn::stream::tls_connection}; use crate::{ - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::plain_connection, BaseHttpConnection, Http2Connection}, + client::http::conn::{BaseHttpConnection, Http2Connection}, + rt::stream::TokioStream, }; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, + errors::{ConnectionError, DeboaError}, request::Http2Request, Result, }; use http::version::Version; use hyper::client::conn::http2::handshake; -use hyper_body_utils::HttpBody; use hyper_util::rt::{TokioExecutor, TokioIo}; impl HttpConnection for Http2Connection { @@ -22,46 +20,20 @@ impl HttpConnection for Http2Connection { } impl ProtoConnection for Http2Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http2Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = TokioStream; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_2 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - #[cfg(any(feature = "rust-tls", feature = "native-tls"))] - let stream = if config.is_secure() { - tls_connection( - *config.ip(), - config.host(), - config.port(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - ) + async fn connect(stream: Self::RuntimeStream) -> Result { + let (sender, conn) = handshake(TokioExecutor::new(), TokioIo::new(stream)) .await - } else { - plain_connection(*config.ip(), config.host(), config.port()).await - }; - - #[cfg(not(any(feature = "rust-tls", feature = "native-tls")))] - let stream = plain_connection(config.host(), config.port()).await; - - if let Err(e) = stream { - return Err(e); - } - - let result = handshake(TokioExecutor::new(), TokioIo::new(stream.unwrap())).await; - - let (sender, conn) = result.unwrap(); + .map_err(|e| { + DeboaError::Connection(ConnectionError::Handshake { message: e.to_string() }) + })?; tokio::spawn(async move { match conn.await { @@ -69,7 +41,6 @@ impl ProtoConnection for Http2Connection { Err(err) => { println!("Error: {:#}", err) } - _ => {} }; }); diff --git a/deboa-tokio/src/client/http/http3.rs b/deboa-tokio/src/client/http/http3.rs index 0c09ddb3..9235e040 100644 --- a/deboa-tokio/src/client/http/http3.rs +++ b/deboa-tokio/src/client/http/http3.rs @@ -1,65 +1,13 @@ -use crate::{ - alpn, - cert::{DeboaCertificate, DeboaIdentity}, - client::http::conn::{stream::setup_rust_tls, BaseHttpConnection, Http3Connection}, -}; +use crate::client::http::conn::{BaseHttpConnection, Http3Connection}; use deboa::{ - conn::{ConnectionConfig, HttpConnection, ProtoConnection}, + conn::{HttpConnection, ProtoConnection}, errors::{ConnectionError, DeboaError}, Result, }; use deboa_h3::generic::{Http3Request, SendRequest}; use futures::future; +use h3_quinn::Connection; use http::version::Version; -use hyper_body_utils::HttpBody; -use quinn::{crypto::rustls::QuicClientConfig, Endpoint}; -use std::{ - net::{IpAddr, SocketAddr}, - sync::Arc, -}; - -async fn lookup_and_connect( - ip: IpAddr, - host: &str, - port: u16, - client_endpoint: &Endpoint, -) -> std::result::Result { - let conn = client_endpoint.connect(SocketAddr::new(ip, port), host); - - let conn = match conn { - Ok(conn) => conn, - Err(e) => { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })) - } - }; - - let conn = conn.await; - - let conn = match conn { - Ok(conn) => conn, - Err(e) => match e { - quinn::ConnectionError::TransportError(e) => { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })) - } - _ => { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: host.to_string(), - message: format!("Could not connect to server: {}", e), - })) - } - }, - }; - - let quinn_conn: h3_quinn::Connection = h3_quinn::Connection::new(conn); - - Ok(quinn_conn) -} impl HttpConnection for Http3Connection { type Sender = Http3Request; @@ -69,77 +17,18 @@ impl HttpConnection for Http3Connection { } impl ProtoConnection for Http3Connection { - type ReqBody = HttpBody; - type ResBody = HttpBody; type Connection = Http3Connection; - type Identity = DeboaIdentity; - type Certificate = DeboaCertificate; + type RuntimeStream = Connection; #[inline] fn protocol_version(&self) -> Version { Version::HTTP_3 } - async fn connect<'a>( - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> Result { - let client_endpoint = Endpoint::client(SocketAddr::new(*config.client_bind_addr(), 0)); - - if let Err(e) = client_endpoint { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let mut client_endpoint = client_endpoint.unwrap(); - - let tls_config = setup_rust_tls( - config.host(), - config.identity(), - config.certificate(), - config.skip_cert_verification(), - alpn(), - )?; - - let quic_config = QuicClientConfig::try_from(tls_config); - if let Err(e) = quic_config { - return Err(DeboaError::Connection(ConnectionError::Tls { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let quic_config = quic_config.unwrap(); - - let client_config = quinn::ClientConfig::new(Arc::new(quic_config)); - client_endpoint.set_default_client_config(client_config); - - let result = - lookup_and_connect(*config.ip(), config.host(), config.port(), &client_endpoint).await; - - if let Err(e) = result { - return Err(e); - } - - let conn = result.unwrap(); - - let client = h3::client::new(conn).await; - - if let Err(e) = client { - return Err(DeboaError::Connection(ConnectionError::Udp { - host: config - .host() - .to_string(), - message: e.to_string(), - })); - } - - let (mut conn, sender) = client.unwrap(); + async fn connect(conn: Self::RuntimeStream) -> Result { + let (mut conn, sender) = h3::client::new(conn) + .await + .map_err(|e| DeboaError::Connection(ConnectionError::Udp { message: e.to_string() }))?; tokio::spawn(async move { future::poll_fn(|cx| conn.poll_close(cx)).await; diff --git a/deboa-tokio/src/client/mod.rs b/deboa-tokio/src/client/mod.rs index 2fe73861..992b077a 100644 --- a/deboa-tokio/src/client/mod.rs +++ b/deboa-tokio/src/client/mod.rs @@ -1,7 +1,7 @@ -/// DNS resolution for the Deboa HTTP client. -/// -/// This module provides DNS resolution functionality for the Deboa HTTP client. +//! Deboa HTTP client modules. +//! +//! This module provides DNS resolution, HTTP, TCP, and TLS transport functionality for the Deboa HTTP client. + pub mod dns; pub mod http; -#[cfg(feature = "websockets")] -pub mod ws; +pub mod tls; diff --git a/deboa-tokio/src/client/tls/mod.rs b/deboa-tokio/src/client/tls/mod.rs new file mode 100644 index 00000000..afc96df1 --- /dev/null +++ b/deboa-tokio/src/client/tls/mod.rs @@ -0,0 +1,9 @@ +//! TLS transport implementations for the Deboa HTTP client. +//! +//! This module provides TLS functionality for secure HTTP connections. +//! It supports both native-tls and rustls backends. + +#[cfg(feature = "native-tls")] +pub mod native; +#[cfg(feature = "rust-tls")] +pub mod rustls; diff --git a/deboa-tokio/src/client/tls/native.rs b/deboa-tokio/src/client/tls/native.rs new file mode 100644 index 00000000..3177eefe --- /dev/null +++ b/deboa-tokio/src/client/tls/native.rs @@ -0,0 +1,125 @@ +//! TLS implementation using native-tls +//! +use crate::cert::{DeboaCertificate, DeboaIdentity}; +use async_native_tls::{Certificate, Identity, TlsConnector, TlsStream}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, +}; +use tokio::net::TcpStream; + +#[inline] +pub(crate) fn alpn() -> &'static [&'static str] { + &[ + #[cfg(feature = "http3")] + "h3", + #[cfg(feature = "http2")] + "h2", + #[cfg(feature = "http1")] + "http/1.1", + ] +} + +/// Builder for TLS connections using native-tls +pub struct TlsConnectionBuilder<'a> { + tcp_stream: TcpStream, + host: &'a str, + identity: Option<&'a DeboaIdentity>, + certificate: Option<&'a DeboaCertificate>, + skip_server_verification: bool, + alpn: &'a [&'a str], +} + +impl<'a> TlsConnectionBuilder<'a> { + /// Creates a new TLS connection builder + pub fn new(tcp_stream: TcpStream, host: &'a str) -> Self { + Self { + tcp_stream, + host, + identity: None, + certificate: None, + skip_server_verification: false, + alpn: alpn(), + } + } + + /// Sets the client identity for mutual TLS authentication + pub fn identity(mut self, identity: Option<&'a DeboaIdentity>) -> Self { + self.identity = identity; + self + } + + /// Sets the server certificate for verification + pub fn certificate(mut self, certificate: Option<&'a DeboaCertificate>) -> Self { + self.certificate = certificate; + self + } + + /// Skips server certificate verification (use with caution) + pub fn skip_server_verification(mut self, skip_server_verification: bool) -> Self { + self.skip_server_verification = skip_server_verification; + self + } + + /// Sets the ALPN protocols to use + pub fn alpn(mut self, alpn: &'a [&str]) -> Self { + self.alpn = alpn; + self + } + + /// Establishes the TLS connection + pub async fn connect(self) -> Result> { + let builder = TlsConnector::new(); + + let builder = if self.skip_server_verification { + builder + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + } else { + builder + }; + + let builder = builder.request_alpns(self.alpn); + + let builder = if let Some(ca) = self.certificate { + let cert: Certificate = ca + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid CA certificate: {}", e), + }) + })?; + builder.add_root_certificate(cert) + } else { + builder + }; + + let builder = if let Some(identity) = self.identity { + let ident: Identity = identity + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid client identity: {}", e), + }) + })?; + builder.identity(ident) + } else { + builder + }; + + let stream = builder + .connect( + self.host + .to_string(), + self.tcp_stream, + ) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not connect to server: {}", e), + }) + }); + + stream + } +} diff --git a/deboa-tokio/src/client/tls/rustls.rs b/deboa-tokio/src/client/tls/rustls.rs new file mode 100644 index 00000000..f1e8f659 --- /dev/null +++ b/deboa-tokio/src/client/tls/rustls.rs @@ -0,0 +1,258 @@ +//! TLS implementation using rustls + +use crate::cert::{DeboaCertificate, DeboaIdentity}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, +}; +use rustls::{ + crypto::CryptoProvider, + pki_types::{CertificateDer, PrivateKeyDer}, + ClientConfig, +}; + +pub(crate) fn default_provider() -> CryptoProvider { + #[cfg(feature = "__rustls_aws_lc_rs")] + return rustls::crypto::aws_lc_rs::default_provider(); + #[cfg(feature = "__rustls_ring")] + return rustls::crypto::ring::default_provider(); +} + +#[inline] +pub(crate) fn alpn() -> Vec> { + vec![ + #[cfg(feature = "http3")] + b"h3".to_vec(), + #[cfg(feature = "http2")] + b"h2".to_vec(), + #[cfg(feature = "http1")] + b"http/1.1".to_vec(), + ] +} + +/// Builder for TLS connections using rustls +pub struct TlsConnectionBuilder<'a> { + identity: Option<&'a DeboaIdentity>, + certificate: Option<&'a DeboaCertificate>, + skip_server_verification: bool, + alpn: Vec>, + provider: CryptoProvider, +} + +impl Default for TlsConnectionBuilder<'_> { + fn default() -> Self { + Self { + identity: None, + certificate: None, + skip_server_verification: false, + alpn: alpn(), + provider: default_provider(), + } + } +} + +impl<'a> TlsConnectionBuilder<'a> { + /// Set the identity to use for the connection + pub fn identity(mut self, identity: Option<&'a DeboaIdentity>) -> Self { + self.identity = identity; + self + } + + /// Set the certificate to use for the connection + pub fn certificate(mut self, certificate: Option<&'a DeboaCertificate>) -> Self { + self.certificate = certificate; + self + } + + /// Skip server verification + pub fn skip_server_verification(mut self, skip_server_verification: bool) -> Self { + self.skip_server_verification = skip_server_verification; + self + } + + /// Set the ALPN protocols to use for the connection + pub fn alpn(mut self, alpn: Vec>) -> Self { + self.alpn = alpn; + self + } + + /// Build the TLS client configuration + pub fn build_config(self) -> Result { + let client_config = { + if self.skip_server_verification { + ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier( + deboa_tls::rust::verify::SkipServerVerification::new(self.provider), + ) + .with_no_client_auth() + } else { + #[cfg(feature = "__webpki_rustls_verifier")] + let config = { + let config = ClientConfig::builder_with_provider(self.provider.into()) + .with_protocol_versions(rustls::ALL_VERSIONS) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set TLS version: {}", e), + }) + })?; + + let mut root_store = + rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; + let config = if let Some(ca) = self.certificate { + let cert = ca + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid CA certificate: {}", e), + }) + })?; + + root_store + .add(cert) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!( + "Could not add CA certificate to the store: {}", + e + ), + }) + })?; + + config.with_root_certificates(root_store) + } else { + config.with_root_certificates(root_store) + }; + + config + }; + + #[cfg(feature = "__platform_rustls_verifier")] + let config = { + use rustls_platform_verifier::BuilderVerifierExt; + rustls::ClientConfig::builder_with_provider(default_provider()) + .with_protocol_versions(rustls::ALL_VERSIONS) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set TLS version: {}", e), + }) + })? + .with_platform_verifier() + }; + + let mut config = if let Some(id) = self.identity { + let pair: (CertificateDer<'_>, PrivateKeyDer<'_>) = id + .try_into() + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Invalid client identity: {}", e), + }) + })?; + + config + .with_client_auth_cert(vec![pair.0], pair.1) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Failed to set client identity: {}", e), + }) + })? + } else { + config.with_no_client_auth() + }; + + config.enable_early_data = true; + + config.alpn_protocols = self.alpn; + + config + } + }; + + Ok(client_config) + } +} + +#[cfg(any(feature = "http1", feature = "http2"))] +/// TCP connection module for TLS +pub mod tcp { + use deboa::{ + errors::{ConnectionError, DeboaError}, + Result, + }; + use rustls::ClientConfig; + use rustls_pki_types::ServerName; + use std::sync::Arc; + use tokio::net::TcpStream; + use tokio_rustls::{client::TlsStream, TlsConnector}; + + /// Establish a TLS connection over TCP + pub async fn connect( + config: ClientConfig, + inner_stream: TcpStream, + host: &str, + ) -> Result> { + let connector = TlsConnector::from(Arc::new(config)); + + let hostname = ServerName::try_from(host.to_string()) + .map_err(|e| DeboaError::Connection(ConnectionError::Tls { message: e.to_string() }))?; + + connector + .connect(hostname, inner_stream) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not connect to server: {}", e), + }) + }) + } +} + +#[cfg(feature = "http3")] +/// UDP connection module for TLS +pub mod udp { + use deboa::{ + errors::{http::ConnectionError, DeboaError}, + Result, + }; + use h3_quinn::Connection; + use quinn::{crypto::rustls::QuicClientConfig, Endpoint}; + use rustls::ClientConfig; + use std::{net::SocketAddr, sync::Arc}; + + /// Establish a TLS connection over UDP + pub async fn connect( + config: ClientConfig, + endpoint: &mut Endpoint, + socket_addr: SocketAddr, + host: &str, + ) -> Result { + let quic_config = QuicClientConfig::try_from(config).map_err(|e| { + DeboaError::Connection(ConnectionError::Tls { + message: format!("Could not create QUIC client config: {}", e), + }) + })?; + + let client_config = quinn::ClientConfig::new(Arc::new(quic_config)); + endpoint.set_default_client_config(client_config); + + let conn = endpoint + .connect(socket_addr, host) + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: format!("Could not connect to server: {}", e), + }) + })?; + + let conn = conn + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Udp { + message: format!("Could not connect to server: {}", e), + }) + })?; + + let quinn_conn = h3_quinn::Connection::new(conn); + + Ok(quinn_conn) + } +} diff --git a/deboa-tokio/src/client/ws/io/mod.rs b/deboa-tokio/src/client/ws/io/mod.rs deleted file mode 100644 index d22cc845..00000000 --- a/deboa-tokio/src/client/ws/io/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod socket; diff --git a/deboa-tokio/src/client/ws/io/socket.rs b/deboa-tokio/src/client/ws/io/socket.rs deleted file mode 100644 index 14dd0f37..00000000 --- a/deboa-tokio/src/client/ws/io/socket.rs +++ /dev/null @@ -1,383 +0,0 @@ -use crate::{ - errors::{DeboaExtrasError, WebSocketError}, - ws::protocol::Message, -}; -use hyper::upgrade::Upgraded; -use hyper_util::rt::TokioIo; -use pin_project_lite::pin_project; -use std::{ - future::Future, - io, - pin::Pin, - task::{Context, Poll}, -}; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; -use ws_framer::{WsFrame, WsRxFramer, WsTxFramer}; - -pub type UpgradedIo = TokioIo; - -pub trait DeboaWebSocket { - type Stream; - - fn new(stream: Self::Stream) -> Self; - fn read_message(&mut self) -> impl Future, DeboaExtrasError>>; - fn write_message( - &mut self, - message: Message, - ) -> impl Future>; - fn send_close( - &mut self, - code: u16, - reason: &str, - ) -> impl Future>; - fn send_text(&mut self, message: &str) -> impl Future>; - fn send_binary(&mut self, message: &[u8]) - -> impl Future>; - fn send_ping(&mut self, message: &[u8]) -> impl Future>; - fn send_pong(&mut self, message: &[u8]) -> impl Future>; -} - -pin_project! { - /// WebSocket struct - pub struct WebSocket - { - #[pin] - stream: T, - } -} - -impl DeboaWebSocket for WebSocket { - type Stream = UpgradedIo; - - /// new method - /// - /// # Arguments - /// - /// * `stream` - A string slice that holds the stream data. - /// - /// # Returns - /// - /// A WebSocket struct. - /// - fn new(stream: Self::Stream) -> Self { - Self { stream } - } - - /// Reads a message from the WebSocket. - /// - /// # Returns - /// - /// A Result containing an Option or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// while let Some(message) = websocket.read_message().await { - /// println!("message: {}", message); - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn read_message(&mut self) -> Result, DeboaExtrasError> { - let mut rx_buf = vec![0; 10240]; - let mut rx_framer = WsRxFramer::new(&mut rx_buf); - - let bytes_read = self - .stream - .read(rx_framer.mut_buf()) - .await; - if bytes_read.is_err() { - return Err(DeboaExtrasError::WebSocket(WebSocketError::ReceiveMessage { - message: "Failed to read message".to_string(), - })); - } - - let bytes_read = bytes_read.unwrap(); - rx_framer.revolve_write_offset(bytes_read); - let res = rx_framer.process_data(); - let message = if let Some(frame) = res { - #[allow(clippy::collapsible_match)] - match frame { - WsFrame::Text(data) => Some(Message::Text(data.to_string())), - WsFrame::Binary(data) => Some(Message::Binary(data.to_vec())), - WsFrame::Close(code, reason) => Some(Message::Close(code, reason.to_string())), - WsFrame::Ping(data) => Some(Message::Ping(data.to_vec())), - _ => None, - } - } else { - None - }; - - Ok(message) - } - - /// Writes a message to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The message to write. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket - /// .write_message(protocol::Message::Text(message.to_string())) - /// .await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - /// - async fn write_message(&mut self, message: Message) -> Result<(), DeboaExtrasError> { - let mut tx_buf = vec![0; 10240]; - let mut tx_framer = WsTxFramer::new(true, &mut tx_buf); - - let result = match message { - Message::Text(data) => { - self.write_all(tx_framer.frame(WsFrame::Text(&data))) - .await - } - Message::Binary(data) => { - self.write_all(tx_framer.frame(WsFrame::Binary(&data))) - .await - } - Message::Close(code, reason) => { - self.write_all(tx_framer.frame(WsFrame::Close(code, &reason))) - .await - } - Message::Ping(data) => { - self.write_all(tx_framer.frame(WsFrame::Ping(&data))) - .await - } - _ => Ok(()), - }; - - if result.is_err() { - return Err(DeboaExtrasError::WebSocket(WebSocketError::SendMessage { - message: "Failed to send frame".to_string(), - })); - } - - Ok(()) - } - - /// Sends a close frame to the WebSocket. - /// - /// # Arguments - /// - /// * `code` - The close code. - /// * `reason` - The close reason. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_close(1000, "Goodbye").await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_close(&mut self, code: u16, reason: &str) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Close(code, reason.to_string())) - .await - } - - /// Sends a text frame to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The text message to send. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_text("Hello").await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_text(&mut self, message: &str) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Text(message.to_string())) - .await - } - - /// Sends a binary frame to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The binary message to send. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_binary(&[0x00, 0x01, 0x02]).await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_binary(&mut self, message: &[u8]) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Binary(message.to_vec())) - .await - } - - /// Sends a ping frame to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The ping message to send. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_ping(&[0x00, 0x01, 0x02]).await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_ping(&mut self, message: &[u8]) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Ping(message.to_vec())) - .await - } - - /// Sends a pong frame to the WebSocket. - /// - /// # Arguments - /// - /// * `message` - The pong message to send. - /// - /// # Returns - /// - /// A Result indicating success or a DeboaExtrasError. - /// - /// # Examples - /// - /// ```rust, compile_fail - /// let result = websocket.send_pong(&[0x00, 0x01, 0x02]).await; - /// if result.is_err() { - /// output.send(Event::Disconnected).await; - /// break; - /// } - /// ``` - /// - /// # Panics - /// - /// This function may panic if the WebSocket frame processing fails. - /// - async fn send_pong(&mut self, message: &[u8]) -> Result<(), DeboaExtrasError> { - self.write_message(Message::Pong(message.to_vec())) - .await - } -} - -impl AsyncRead for WebSocket { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - self.project() - .stream - .poll_read(cx, buf) - } -} - -impl AsyncWrite for WebSocket { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> std::task::Poll> { - self.project() - .stream - .poll_write(cx, buf) - } - - fn poll_flush( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - self.project() - .stream - .poll_flush(cx) - } - - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - self.project() - .stream - .poll_shutdown(cx) - } - - fn poll_write_vectored( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[std::io::IoSlice<'_>], - ) -> Poll> { - let buf = bufs - .iter() - .find(|b| !b.is_empty()) - .map_or(&[][..], |b| &**b); - self.project() - .stream - .poll_write(cx, buf) - } - - fn is_write_vectored(&self) -> bool { - self.stream - .is_write_vectored() - } -} diff --git a/deboa-tokio/src/client/ws/mod.rs b/deboa-tokio/src/client/ws/mod.rs deleted file mode 100644 index e3c72e99..00000000 --- a/deboa-tokio/src/client/ws/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! -//! WebSocket support for deboa-extras. -//! Provides WebSocket client functionality with message encoding/decoding. -//! Supports text and binary message types with automatic serialization. -//! Includes support for various serialization formats through the serialization feature. -//! Requires the `websockets` feature to be enabled. -//! -//! -//! ## Example -//! ```rust, compile_fail -//! use deboa::{Client, Result, request::DeboaRequestBuilder}; -//! use deboa_extras::ws::{ -//! io::socket::DeboaWebSocket, -//! protocol::{self}, -//! request::WebsocketRequestBuilder, -//! response::IntoWebSocket, -//! }; -//! -//! let mut client = Client::new(); -//! -//! let websocket = DeboaRequestBuilder::websocket("wss://echo.websocket.org")? -//! .send_with(&mut client) -//! .await? -//! .into_websocket() -//! .await; -//! -//! loop { -//! select! { -//! outgoing_message = websocket.read_message() => { -//! if let Err(message) = outgoing_message { -//! println!("Failed to read message from echo server: {}", message); -//! -//! output.send(Event::Disconnected).await; -//! break; -//! } -//! -//! match outgoing_message.unwrap() { -//! Some(message) => { -//! if let protocol::Message::Text(message) = message { -//! output -//! .send(Event::MessageReceived(Message::User( -//! format!("Server: {}", message).to_string(), -//! ))) -//! .await; -//! } -//! } -//! None => { -//! output.send(Event::Disconnected).await; -//! break; -//! } -//! } -//! } -//! -//! incoming_message = input.next() => { -//! if let Some(message) = incoming_message { -//! let result = websocket -//! .write_message(protocol::Message::Text(message.to_string())) -//! .await; -//! if result.is_err() { -//! output.send(Event::Disconnected).await; -//! break; -//! } -//! } -//! } -//! } -//! } -//! ``` -//! -//! ## Modules -//! -//! * `io` - WebSocket I/O operations -//! * `protocol` - WebSocket protocol handling -//! * `request` - WebSocket request building -//! * `response` - WebSocket response parsing -//! -pub mod io; -pub mod protocol; -pub mod request; -pub mod response; diff --git a/deboa-tokio/src/client/ws/protocol.rs b/deboa-tokio/src/client/ws/protocol.rs deleted file mode 100644 index 0f8131c7..00000000 --- a/deboa-tokio/src/client/ws/protocol.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// Message enum -/// -/// # Variants -/// -/// * `Text(String)` - A text message. -/// * `Binary(Vec)` - A binary message. -/// * `Close(u16, String)` - A close message. -/// * `Ping(Vec)` - A ping message. -/// * `Pong(Vec)` - A pong message. -#[derive(Clone)] -pub enum Message { - Text(String), - Binary(Vec), - Close(u16, String), - Ping(Vec), - Pong(Vec), -} diff --git a/deboa-tokio/src/client/ws/request.rs b/deboa-tokio/src/client/ws/request.rs deleted file mode 100644 index 363e0f8a..00000000 --- a/deboa-tokio/src/client/ws/request.rs +++ /dev/null @@ -1,51 +0,0 @@ -use base64::engine::general_purpose::STANDARD; -use base64::Engine; -use deboa::{ - request::{DeboaRequest, DeboaRequestBuilder}, - url::IntoUrl, - Result, -}; -use http::{header, Method}; - -/// Trait for building websocket requests -pub trait WebsocketRequestBuilder { - /// Creates a websocket request - /// - /// # Arguments - /// - /// * `url` - The URL to connect to - /// - /// # Returns - /// - /// A Result containing the DeboaRequestBuilder - /// - /// # Example - /// - /// ``` compile_fail - /// use deboa::{Client, Result, request::{IntoUrl, DeboaRequestBuilder}}; - /// use deboa_extras::http::ws::request::{WebsocketRequestBuilder}; - /// - /// let mut client = Client::new(); - /// let request = DeboaRequestBuilder::websocket("ws://example.com").unwrap(); - /// let response = request.send_with(&mut client).await.unwrap(); - /// let ws = response.into_websocket().unwrap(); - /// loop { - /// if let Ok(Some(message)) = ws.read_message().await { - /// println!("message: {}", message); - /// } - /// } - /// ``` - fn websocket(url: T) -> Result; -} - -impl WebsocketRequestBuilder for DeboaRequestBuilder { - fn websocket(url: T) -> Result { - let rnd: [u8; 16] = rand::random(); - let key = STANDARD.encode(rnd); - Ok(DeboaRequest::at(url, Method::GET)? - .header(header::UPGRADE, "websocket") - .header(header::CONNECTION, "Upgrade") - .header(header::SEC_WEBSOCKET_KEY, &key) - .header(header::SEC_WEBSOCKET_VERSION, "13")) - } -} diff --git a/deboa-tokio/src/client/ws/response.rs b/deboa-tokio/src/client/ws/response.rs deleted file mode 100644 index 601b7bed..00000000 --- a/deboa-tokio/src/client/ws/response.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::future::Future; - -use crate::client::ws::io::socket::{DeboaWebSocket, UpgradedIo, WebSocket}; -use deboa::{response::DeboaResponse, Result}; - -/// Trait for converting a DeboaResponse into a WebSocket -pub trait IntoWebSocket { - /// Converts a DeboaResponse into a WebSocket - /// - /// # Arguments - /// - /// * `self` - The DeboaResponse to convert - /// - /// # Returns - /// - /// A Result containing the WebSocket - /// - /// # Example - /// - /// ``` compile_fail - /// use deboa::{Client, Result, request::{IntoUrl, DeboaRequestBuilder}}; - /// use deboa_extras::http::ws::request::{WebsocketRequestBuilder}; - /// - /// let mut client = Client::new(); - /// let builder = DeboaRequestBuilder::websocket("ws://example.com").unwrap(); - /// let response = builder - /// .send_with(&mut client) - /// .await - /// .unwrap(); - /// let websocket = response.into_websocket().unwrap(); - /// - /// loop { - /// if let Ok(Some(message)) = websocket.read_message().await { - /// println!("message: {}", message); - /// } - /// } - /// ``` - fn into_websocket(self) -> impl Future>>; -} - -impl IntoWebSocket for DeboaResponse { - async fn into_websocket(self) -> Result> { - let upgraded = self - .upgrade() - .await?; - Ok(WebSocket::new(upgraded)) - } -} diff --git a/deboa-tokio/src/lib.rs b/deboa-tokio/src/lib.rs index 6485cc8f..3e458c1b 100644 --- a/deboa-tokio/src/lib.rs +++ b/deboa-tokio/src/lib.rs @@ -30,38 +30,15 @@ compile_error!( #[cfg(all(feature = "native-tls", feature = "rust-tls"))] compile_error!("You cannot enable native-tls and rust-tls features at the same time."); +#[cfg(all(not(any(feature = "native-tls", feature = "rust-tls")), feature = "http2"))] +compile_error!("HTTP2 requires native-tls or rust-tls support."); + #[cfg(all(feature = "native-tls", feature = "http3"))] compile_error!("HTTP3 is not supported within native-tls runtime."); #[cfg(not(any(feature = "http1", feature = "http2", feature = "http3")))] compile_error!("At least one HTTP version feature must be enabled."); -#[cfg(feature = "rust-tls")] -#[inline] -pub(crate) fn alpn() -> Vec> { - vec![ - #[cfg(feature = "http2")] - b"h2".to_vec(), - #[cfg(feature = "http1")] - b"http/1.1".to_vec(), - #[cfg(feature = "http3")] - b"h3".to_vec(), - ] -} - -#[cfg(feature = "native-tls")] -#[inline] -pub(crate) fn alpn() -> &'static [&'static str] { - &[ - #[cfg(feature = "http2")] - "h2", - #[cfg(feature = "http1")] - "http/1.1", - #[cfg(feature = "http3")] - "h3", - ] -} - /// Certificate management module for handling SSL/TLS certificates. pub mod cert; /// Internal module for HTTP and Websockets clients implementations. diff --git a/deboa-tokio/src/rt/stream.rs b/deboa-tokio/src/rt/stream.rs index f1c77422..f4df8abd 100644 --- a/deboa-tokio/src/rt/stream.rs +++ b/deboa-tokio/src/rt/stream.rs @@ -12,7 +12,7 @@ use tokio::{ use tokio_rustls::client::TlsStream; /// Stream enum for runtime-specific stream implementations. -pub(crate) enum TokioStream { +pub enum TokioStream { /// A plain TCP connection. Plain(TcpStream), diff --git a/deboa-tokio/tests/base/get.rs b/deboa-tokio/tests/base/get.rs index 3ce9e230..aa35ef34 100644 --- a/deboa-tokio/tests/base/get.rs +++ b/deboa-tokio/tests/base/get.rs @@ -2,6 +2,8 @@ use crate::common::helpers::{create_client, create_server, protocol_version}; #[cfg(feature = "rust-tls")] use deboa::cert::IdentityExt as _; +#[cfg(feature = "native-tls")] +use deboa::cert::IdentityNativeExt as _; #[cfg(any(feature = "rust-tls", feature = "native-tls"))] use deboa::cert::{CertificateExt as _, ContentEncoding}; use deboa::TestResult; @@ -26,12 +28,26 @@ async fn test_get_http( #[rstest] #[tokio::test] async fn test_get_http_skip_verification( - create_client: Client, #[future] create_server: EasyHttpMock, protocol_version: http::Version, ) -> TestResult<()> { + let identity = DeboaIdentity::from_pkcs8( + deboa_test_utils::common::helpers::CLIENT_CERT, + deboa_test_utils::common::helpers::CLIENT_KEY, + ContentEncoding::DER, + ); + + let client = Client::builder() + .certificate(DeboaCertificate::from_slice( + deboa_test_utils::common::helpers::CA_CERT, + ContentEncoding::DER, + )) + .identity(identity) + .skip_cert_verification(true) + .build(); + deboa_test_utils::base::get::test_skip_cert_verification( - &create_client, + &client, &mut create_server.await, protocol_version, true, @@ -42,12 +58,15 @@ async fn test_get_http_skip_verification( #[rstest] #[tokio::test] async fn test_get_http_verify( - create_client: Client, #[future] create_server: EasyHttpMock, protocol_version: http::Version, ) -> TestResult<()> { + let client = Client::builder() + .skip_cert_verification(false) + .build(); + deboa_test_utils::base::get::test_skip_cert_verification( - &create_client, + &client, &mut create_server.await, protocol_version, false, @@ -62,18 +81,11 @@ async fn test_get_http_mutual_authentication( #[future] create_server: EasyHttpMock, protocol_version: http::Version, ) -> TestResult<()> { - let identity = DeboaIdentity::from_pkcs8( - deboa_test_utils::common::helpers::CLIENT_CERT, - deboa_test_utils::common::helpers::CLIENT_KEY, - ContentEncoding::DER, - ); - let client = Client::builder() .certificate(DeboaCertificate::from_slice( deboa_test_utils::common::helpers::CA_CERT, ContentEncoding::DER, )) - .identity(identity) .build(); deboa_test_utils::base::get::test_get_http_mutual_authentication( diff --git a/deboa-tokio/tests/base/mod.rs b/deboa-tokio/tests/base/mod.rs index e464b7f8..41f735fc 100644 --- a/deboa-tokio/tests/base/mod.rs +++ b/deboa-tokio/tests/base/mod.rs @@ -8,6 +8,7 @@ mod delete; mod form; #[cfg(test)] mod get; +#[cfg(test)] mod hook; #[cfg(test)] mod patch; diff --git a/deboa-tokio/tests/common/helpers.rs b/deboa-tokio/tests/common/helpers.rs index b0e540b4..c0d82678 100644 --- a/deboa-tokio/tests/common/helpers.rs +++ b/deboa-tokio/tests/common/helpers.rs @@ -75,9 +75,13 @@ pub async fn tls_mock_server() -> EasyHttpMock { let vetis_adapter_config = VetisAdapterConfig::builder() .hostname(&hostname) - .interface(&interface) - .protocol_version(protocol_version()) - .with_random_port() + .interface( + interface + .parse() + .unwrap(), + ) + .protos(vec![protocol_version()]) + .port(free_port(&interface)) .cert(server_cert.to_vec()) .key(server_key.to_vec()) .ca(CA_CERT.to_vec()) @@ -105,7 +109,11 @@ pub async fn plain_mock_server() -> EasyHttpMock { let vetis_adapter_config = VetisAdapterConfig::builder() .hostname(&hostname) - .interface(&interface) + .interface( + interface + .parse() + .unwrap(), + ) .protocol_version(protocol_version()) .with_random_port() .build(); @@ -132,3 +140,26 @@ pub async fn create_server() -> EasyHttpMock { #[cfg(not(any(feature = "rust-tls", feature = "native-tls")))] return plain_mock_server().await; } + +/// An ephemeral port the OS says is free, rather than a guessed one. +/// +/// `with_random_port()` picks `rand::random_range(9000..65535)` and +/// de-duplicates against a process-local set. Under `cargo test` that mostly +/// holds, because every test shares one process and therefore one set. Under +/// `cargo nextest run` — which is what CI uses — each test is its own process, +/// so the set protects nothing and two tests eventually roll the same number. +/// The loser dies with `Address already in use (os error 98)` before its body +/// runs, which is why the failing test is a different one each time. +/// +/// Binding port 0 asks the kernel for a port it knows is free. There is still a +/// window between dropping this listener and the server binding, but the kernel +/// will not hand the same ephemeral port to someone else inside it, which is +/// the part guessing cannot promise. +fn free_port(interface: &str) -> u16 { + let listener = std::net::TcpListener::bind((interface, 0)) + .expect("bind an ephemeral port to ask the OS for a free one"); + listener + .local_addr() + .expect("read back the bound port") + .port() +} diff --git a/deboa-ws/Cargo.toml b/deboa-ws/Cargo.toml new file mode 100644 index 00000000..3a0abb80 --- /dev/null +++ b/deboa-ws/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "deboa-ws" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +rust-version.workspace = true + +[features] + +default = ["tokio"] +tokio = ["dep:tokio", "hyper-util/tokio"] +smol = ["dep:smol", "dep:smol-hyper"] + +[dependencies] +base64 = { workspace = true } +deboa = { workspace = true } +futures-util = { workspace = true } +http = { workspace = true } +hyper = { workspace = true } +hyper-util = { workspace = true } +pin-project-lite = { workspace = true } +rand = { workspace = true } +smol = { version = "2.0.2", optional = true, default-features = false } +smol-hyper = { version = "0.1.0", optional = true, default-features = false } +thiserror = { workspace = true } +tokio = { version = "1.53.1", optional = true, features = ["io-std"] } +ws-framer = { workspace = true } diff --git a/deboa-ws/src/errors.rs b/deboa-ws/src/errors.rs new file mode 100644 index 00000000..b28fe0b5 --- /dev/null +++ b/deboa-ws/src/errors.rs @@ -0,0 +1,16 @@ +use thiserror::Error; + +#[derive(Debug, Clone, Error, PartialEq)] +pub enum WebSocketError { + #[error("Error receiving message: {message}")] + ReceiveMessage { + /// Error message + message: String, + }, + + #[error("Error sending message: {message}")] + SendMessage { + /// Error message + message: String, + }, +} diff --git a/deboa-ws/src/lib.rs b/deboa-ws/src/lib.rs new file mode 100644 index 00000000..29673f9d --- /dev/null +++ b/deboa-ws/src/lib.rs @@ -0,0 +1,369 @@ +//! WebSockets module +use std::future::Future; + +use base64::{engine::general_purpose::STANDARD, Engine}; +use deboa::{ + request::{DeboaRequest, DeboaRequestBuilder}, + url::IntoUrl, +}; +use http::{header, Method}; +use pin_project_lite::pin_project; + +use crate::errors::WebSocketError; + +pub mod errors; + +/// Smol runtime support +#[cfg(feature = "smol")] +pub mod smol; +/// Tokio runtime support +#[cfg(feature = "tokio")] +pub mod tokio; + +/// Result alias +pub type Result = std::result::Result; + +/// Message enum +/// +/// # Variants +/// +/// * `Text(String)` - A text message. +/// * `Binary(Vec)` - A binary message. +/// * `Close(u16, String)` - A close message. +/// * `Ping(Vec)` - A ping message. +/// * `Pong(Vec)` - A pong message. +#[derive(Clone)] +pub enum Message { + /// A text message + Text(String), + /// BBinary message + Binary(Vec), + /// Close message + Close(u16, String), + /// Ping message + Ping(Vec), + /// Pong reply message + Pong(Vec), +} + +/// Trait for building websocket requests +pub trait WebsocketRequestBuilder { + /// Creates a websocket request + /// + /// # Arguments + /// + /// * `url` - The URL to connect to + /// + /// # Returns + /// + /// A Result containing the DeboaRequestBuilder + /// + /// # Example + /// + /// ``` compile_fail + /// use deboa::{Client, Result, request::{IntoUrl, DeboaRequestBuilder}}; + /// use deboa_extras::http::ws::request::{WebsocketRequestBuilder}; + /// + /// let mut client = Client::new(); + /// let request = DeboaRequestBuilder::websocket("ws://example.com").unwrap(); + /// let response = request.send_with(&mut client).await.unwrap(); + /// let ws = response.into_websocket().unwrap(); + /// loop { + /// if let Ok(Some(message)) = ws.read_message().await { + /// println!("message: {}", message); + /// } + /// } + /// ``` + fn websocket(url: T) -> deboa::Result; +} + +impl WebsocketRequestBuilder for DeboaRequestBuilder { + fn websocket(url: T) -> deboa::Result { + let rnd: [u8; 16] = rand::random(); + let key = STANDARD.encode(rnd); + Ok(DeboaRequest::at(url, Method::GET)? + .header(header::UPGRADE, "websocket") + .header(header::CONNECTION, "Upgrade") + .header(header::SEC_WEBSOCKET_KEY, &key) + .header(header::SEC_WEBSOCKET_VERSION, "13")) + } +} + +/// Trait for converting a DeboaResponse into a WebSocket +pub trait IntoWebSocket { + type UpgradedIo; + /// Converts a DeboaResponse into a WebSocket + /// + /// # Arguments + /// + /// * `self` - The DeboaResponse to convert + /// + /// # Returns + /// + /// A Result containing the WebSocket + /// + /// # Example + /// + /// ``` compile_fail + /// use deboa::{Client, Result, request::{IntoUrl, DeboaRequestBuilder}}; + /// use deboa_smol::client::ws::request::{WebsocketRequestBuilder}; + /// + /// let mut client = Client::new(); + /// let builder = DeboaRequestBuilder::websocket("ws://example.com").unwrap(); + /// let response = builder + /// .send_with(&mut client) + /// .await + /// .unwrap(); + /// let websocket = response.into_websocket().unwrap(); + /// + /// loop { + /// if let Ok(Some(message)) = websocket.read_message().await { + /// println!("message: {}", message); + /// } + /// } + /// ``` + fn into_websocket(self) -> impl Future>>; +} + +pub trait WebSocketRead { + /// Reads a message from the WebSocket. + /// + /// # Returns + /// + /// A Result containing an Option or a DeboaExtrasError. + /// + /// # Examples + /// + /// ```rust, compile_fail + /// while let Some(message) = websocket.read_message().await { + /// println!("message: {}", message); + /// } + /// ``` + /// + /// # Panics + /// + /// This function may panic if the WebSocket frame processing fails. + /// + fn read_message(&mut self) -> impl Future>>; +} + +pub trait WebSocketWrite { + /// Writes a message to the WebSocket. + /// + /// # Arguments + /// + /// * `message` - The message to write. + /// + /// # Returns + /// + /// A Result indicating success or a DeboaExtrasError. + /// + /// # Examples + /// + /// ```rust, compile_fail + /// let result = websocket + /// .write_message(protocol::Message::Text(message.to_string())) + /// .await; + /// if result.is_err() { + /// output.send(Event::Disconnected).await; + /// break; + /// } + /// ``` + /// + /// # Panics + /// + /// This function may panic if the WebSocket frame processing fails. + /// + /// + fn write_message(&mut self, message: Message) -> impl Future>; +} + +/// Trait for WebSockets +pub trait WebSocketExt { + /// Close connection + fn send_close(&mut self, code: u16, reason: &str) -> impl Future>; + /// Send a text message + fn send_text(&mut self, message: &str) -> impl Future>; + /// Send binary content + fn send_binary(&mut self, message: &[u8]) -> impl Future>; + /// Send ping message + fn send_ping(&mut self, message: &[u8]) -> impl Future>; + /// Send pong message + fn send_pong(&mut self, message: &[u8]) -> impl Future>; +} + +pin_project! { + /// WebSocket struct + pub struct WebSocket + { + #[pin] + inner: T, + } +} + +impl WebSocket { + /// new method + /// + /// # Arguments + /// + /// * `inner` - A inner stream. + /// + /// # Returns + /// + /// A WebSocket struct. + /// + pub fn new(inner: T) -> Self { + Self { inner } + } +} + +impl WebSocketExt for WebSocket +where + Self: WebSocketRead + WebSocketWrite, +{ + /// Sends a close frame to the WebSocket. + /// + /// # Arguments + /// + /// * `code` - The close code. + /// * `reason` - The close reason. + /// + /// # Returns + /// + /// A Result indicating success or a DeboaExtrasError. + /// + /// # Examples + /// + /// ```rust, compile_fail + /// let result = websocket.send_close(1000, "Goodbye").await; + /// if result.is_err() { + /// output.send(Event::Disconnected).await; + /// break; + /// } + /// ``` + /// + /// # Panics + /// + /// This function may panic if the WebSocket frame processing fails. + /// + async fn send_close(&mut self, code: u16, reason: &str) -> Result<()> { + self.write_message(Message::Close(code, reason.to_string())) + .await + } + + /// Sends a text frame to the WebSocket. + /// + /// # Arguments + /// + /// * `message` - The text message to send. + /// + /// # Returns + /// + /// A Result indicating success or a DeboaExtrasError. + /// + /// # Examples + /// + /// ```rust, compile_fail + /// let result = websocket.send_text("Hello").await; + /// if result.is_err() { + /// output.send(Event::Disconnected).await; + /// break; + /// } + /// ``` + /// + /// # Panics + /// + /// This function may panic if the WebSocket frame processing fails. + /// + async fn send_text(&mut self, message: &str) -> Result<()> { + self.write_message(Message::Text(message.to_string())) + .await + } + + /// Sends a binary frame to the WebSocket. + /// + /// # Arguments + /// + /// * `message` - The binary message to send. + /// + /// # Returns + /// + /// A Result indicating success or a DeboaError. + /// + /// # Examples + /// + /// ```rust, compile_fail + /// let result = websocket.send_binary(&[0x00, 0x01, 0x02]).await; + /// if result.is_err() { + /// output.send(Event::Disconnected).await; + /// break; + /// } + /// ``` + /// + /// # Panics + /// + /// This function may panic if the WebSocket frame processing fails. + /// + async fn send_binary(&mut self, message: &[u8]) -> Result<()> { + self.write_message(Message::Binary(message.to_vec())) + .await + } + + /// Sends a ping frame to the WebSocket. + /// + /// # Arguments + /// + /// * `message` - The ping message to send. + /// + /// # Returns + /// + /// A Result indicating success or a DeboaError. + /// + /// # Examples + /// + /// ```rust, compile_fail + /// let result = websocket.send_ping(&[0x00, 0x01, 0x02]).await; + /// if result.is_err() { + /// output.send(Event::Disconnected).await; + /// break; + /// } + /// ``` + /// + /// # Panics + /// + /// This function may panic if the WebSocket frame processing fails. + /// + async fn send_ping(&mut self, message: &[u8]) -> Result<()> { + self.write_message(Message::Ping(message.to_vec())) + .await + } + + /// Sends a pong frame to the WebSocket. + /// + /// # Arguments + /// + /// * `message` - The pong message to send. + /// + /// # Returns + /// + /// A Result indicating success or a DeboaError. + /// + /// # Examples + /// + /// ```rust, compile_fail + /// let result = websocket.send_pong(&[0x00, 0x01, 0x02]).await; + /// if result.is_err() { + /// output.send(Event::Disconnected).await; + /// break; + /// } + /// ``` + /// + /// # Panics + /// + /// This function may panic if the WebSocket frame processing fails. + /// + async fn send_pong(&mut self, message: &[u8]) -> Result<()> { + self.write_message(Message::Pong(message.to_vec())) + .await + } +} diff --git a/deboa-ws/src/smol.rs b/deboa-ws/src/smol.rs new file mode 100644 index 00000000..b14e596b --- /dev/null +++ b/deboa-ws/src/smol.rs @@ -0,0 +1,179 @@ +use crate::{ + errors::WebSocketError, IntoWebSocket, Message, Result, WebSocket, WebSocketRead, + WebSocketWrite, +}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + response::DeboaResponse, +}; +use hyper::{ + rt::ReadBuf, + upgrade::{on, Upgraded}, +}; +use smol::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use smol_hyper::rt::FuturesIo; +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; +use ws_framer::{WsFrame, WsRxFramer, WsTxFramer}; + +impl IntoWebSocket for DeboaResponse { + type UpgradedIo = FuturesIo; + async fn into_websocket(self) -> deboa::Result> { + let upgraded = on(self.into_inner()) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Upgrade { message: e.to_string() }) + })?; + Ok(WebSocket::new(FuturesIo::new(upgraded))) + } +} + +impl WebSocketRead for WebSocket> { + async fn read_message(&mut self) -> Result> { + let mut rx_buf = vec![0; 10240]; + let mut rx_framer = WsRxFramer::new(&mut rx_buf); + + let bytes_read = self + .read(rx_framer.mut_buf()) + .await; + if bytes_read.is_err() { + return Err(WebSocketError::ReceiveMessage { + message: "Failed to read message".to_string(), + }); + } + + let bytes_read = bytes_read.unwrap(); + rx_framer.revolve_write_offset(bytes_read); + let res = rx_framer.process_data(); + let message = if let Some(frame) = res { + #[allow(clippy::collapsible_match)] + match frame { + WsFrame::Text(data) => Some(Message::Text(data.to_string())), + WsFrame::Binary(data) => Some(Message::Binary(data.to_vec())), + WsFrame::Close(code, reason) => Some(Message::Close(code, reason.to_string())), + WsFrame::Ping(data) => Some(Message::Ping(data.to_vec())), + _ => None, + } + } else { + None + }; + + Ok(message) + } +} + +impl WebSocketWrite for &mut WebSocket> { + async fn write_message(&mut self, message: Message) -> Result<()> { + let mut tx_buf = vec![0; 10240]; + let mut tx_framer = WsTxFramer::new(true, &mut tx_buf); + + let result = match message { + Message::Text(data) => { + self.write_all(tx_framer.frame(WsFrame::Text(&data))) + .await + } + Message::Binary(data) => { + self.write_all(tx_framer.frame(WsFrame::Binary(&data))) + .await + } + Message::Close(code, reason) => { + self.write_all(tx_framer.frame(WsFrame::Close(code, &reason))) + .await + } + Message::Ping(data) => { + self.write_all(tx_framer.frame(WsFrame::Ping(&data))) + .await + } + _ => Ok(()), + }; + + if result.is_err() { + return Err(WebSocketError::SendMessage { + message: "Failed to send frame".to_string(), + }); + } + + Ok(()) + } +} + +impl AsyncRead for WebSocket> +where + T: hyper::rt::Read, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let mut chunk = ReadBuf::new(buf); + let buf = chunk.unfilled(); + + let n = match self + .project() + .inner + .get_pin_mut() + .poll_read(cx, buf) + { + Poll::Ready(Ok(())) => chunk.filled().len(), + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Pending => return Poll::Pending, + }; + + Poll::Ready(Ok(n)) + } +} + +impl AsyncWrite for WebSocket> +where + T: hyper::rt::Write, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + hyper::rt::Write::poll_write( + self.project() + .inner + .get_pin_mut(), + cx, + buf, + ) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + hyper::rt::Write::poll_flush( + self.project() + .inner + .get_pin_mut(), + cx, + ) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + hyper::rt::Write::poll_shutdown( + self.project() + .inner + .get_pin_mut(), + cx, + ) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + hyper::rt::Write::poll_write_vectored( + self.project() + .inner + .get_pin_mut(), + cx, + bufs, + ) + } +} diff --git a/deboa-ws/src/tokio.rs b/deboa-ws/src/tokio.rs new file mode 100644 index 00000000..22a5aff9 --- /dev/null +++ b/deboa-ws/src/tokio.rs @@ -0,0 +1,154 @@ +use crate::{ + errors::WebSocketError, IntoWebSocket, Message, Result, WebSocket, WebSocketRead, + WebSocketWrite, +}; +use deboa::{ + errors::{ConnectionError, DeboaError}, + response::DeboaResponse, +}; +use hyper::upgrade::{on, Upgraded}; +use hyper_util::rt::TokioIo; +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _, ReadBuf}; +use ws_framer::{WsFrame, WsRxFramer, WsTxFramer}; + +impl IntoWebSocket for DeboaResponse { + type UpgradedIo = TokioIo; + async fn into_websocket(self) -> deboa::Result> { + let upgraded = on(self.into_inner()) + .await + .map_err(|e| { + DeboaError::Connection(ConnectionError::Upgrade { message: e.to_string() }) + })?; + Ok(WebSocket::new(TokioIo::new(upgraded))) + } +} + +impl WebSocketRead for WebSocket> { + async fn read_message(&mut self) -> Result> { + let mut rx_buf = vec![0; 10240]; + let mut rx_framer = WsRxFramer::new(&mut rx_buf); + + let bytes_read = self + .inner + .read(rx_framer.mut_buf()) + .await; + if bytes_read.is_err() { + return Err(WebSocketError::ReceiveMessage { + message: "Failed to read message".to_string(), + }); + } + + let bytes_read = bytes_read.unwrap(); + rx_framer.revolve_write_offset(bytes_read); + let res = rx_framer.process_data(); + let message = if let Some(frame) = res { + #[allow(clippy::collapsible_match)] + match frame { + WsFrame::Text(data) => Some(Message::Text(data.to_string())), + WsFrame::Binary(data) => Some(Message::Binary(data.to_vec())), + WsFrame::Close(code, reason) => Some(Message::Close(code, reason.to_string())), + WsFrame::Ping(data) => Some(Message::Ping(data.to_vec())), + _ => None, + } + } else { + None + }; + + Ok(message) + } +} + +impl WebSocketWrite for &mut WebSocket> { + async fn write_message(&mut self, message: Message) -> Result<()> { + let mut tx_buf = vec![0; 10240]; + let mut tx_framer = WsTxFramer::new(true, &mut tx_buf); + + let result = match message { + Message::Text(data) => { + self.write_all(tx_framer.frame(WsFrame::Text(&data))) + .await + } + Message::Binary(data) => { + self.write_all(tx_framer.frame(WsFrame::Binary(&data))) + .await + } + Message::Close(code, reason) => { + self.write_all(tx_framer.frame(WsFrame::Close(code, &reason))) + .await + } + Message::Ping(data) => { + self.write_all(tx_framer.frame(WsFrame::Ping(&data))) + .await + } + _ => Ok(()), + }; + + if result.is_err() { + return Err(WebSocketError::SendMessage { + message: "Failed to send frame".to_string(), + }); + } + + Ok(()) + } +} + +impl AsyncRead for WebSocket> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.project() + .inner + .poll_read(cx, buf) + } +} + +impl AsyncWrite for WebSocket> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.project() + .inner + .poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project() + .inner + .poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project() + .inner + .poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + let buf = bufs + .iter() + .find(|b| !b.is_empty()) + .map_or(&[][..], |b| &**b); + self.project() + .inner + .poll_write(cx, buf) + } + + fn is_write_vectored(&self) -> bool { + self.inner + .is_write_vectored() + } +} diff --git a/deboa/Cargo.toml b/deboa/Cargo.toml index 0773b11f..e3663b37 100644 --- a/deboa/Cargo.toml +++ b/deboa/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deboa" -version = "0.1.2" +version = "0.1.3" edition.workspace = true authors.workspace = true repository.workspace = true @@ -42,12 +42,11 @@ regex = { version = "1.12.4", default-features = false } serde = { version = "1.0.217", default-features = false, features = ["derive"] } tackle = { version = "0.1.1"} thiserror = "2.0.17" -time = { version = "0.3.53", default-features = false } url = "2.5.8" urlencoding = "2.1.3" [dev-dependencies] -caramelo = "0.1.1-beta.3" +caramelo = "0.1.2" criterion = { version = "0.8.2", features = [ "html_reports", "async", @@ -56,3 +55,4 @@ criterion = { version = "0.8.2", features = [ ] } mime = { version = "0.3.17" } multer = "3.1.0" +time = { version = "0.3.53" } diff --git a/deboa/src/cert.rs b/deboa/src/cert.rs index cc0f40ae..1812dfac 100644 --- a/deboa/src/cert.rs +++ b/deboa/src/cert.rs @@ -95,7 +95,7 @@ pub trait Identity { /// # Returns /// /// * `&Option>` - The private key - fn ket(&self) -> &Option>; + fn key(&self) -> &Option>; /// Get the encoding /// /// # Returns diff --git a/deboa/src/conn.rs b/deboa/src/conn.rs index d4f3e5c2..5efc1ca9 100644 --- a/deboa/src/conn.rs +++ b/deboa/src/conn.rs @@ -3,24 +3,24 @@ //! This module provides functionality for managing HTTP connections. use crate::{ cert::{Certificate, Identity}, + dns::DnsResolver, response::DeboaResponse, Result, }; use http::{Request, Version}; -use http_body::Body; use hyper_body_utils::HttpBody; +use std::time::Duration; use std::{future::Future, net::IpAddr}; -use time::Duration; /// Builder for connection configuration. pub struct ConnectionConfigBuilder<'a, I, C> { - is_secure: bool, - ip: IpAddr, + scheme: &'a str, host: &'a str, port: u16, protocol_version: Version, - identity: Option, - certificate: Option, + connection_timeout: Duration, + identity: Option<&'a I>, + certificate: Option<&'a C>, skip_cert_verification: bool, client_bind_addr: IpAddr, } @@ -34,13 +34,11 @@ where #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { - is_secure: false, - ip: "127.0.0.1" - .parse::() - .unwrap(), + scheme: "http", host: "", port: 80, protocol_version: Version::HTTP_2, + connection_timeout: Duration::from_secs(30), identity: None, certificate: None, skip_cert_verification: false, @@ -50,15 +48,9 @@ where } } - /// Set whether the connection is secure. - pub fn is_secure(mut self, is_secure: bool) -> Self { - self.is_secure = is_secure; - self - } - - /// Set the IP address for the connection. - pub fn ip(mut self, ip: IpAddr) -> Self { - self.ip = ip; + /// Set the scheme for the connection. + pub fn scheme(mut self, scheme: &'a str) -> Self { + self.scheme = scheme; self } @@ -80,14 +72,20 @@ where self } + /// Set the connection timeout for the connection. + pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self { + self.connection_timeout = connection_timeout; + self + } + /// Set the identity for the connection. - pub fn identity(mut self, identity: Option) -> Self { + pub fn identity(mut self, identity: Option<&'a I>) -> Self { self.identity = identity; self } /// Set the certificate for the connection. - pub fn certificate(mut self, certificate: Option) -> Self { + pub fn certificate(mut self, certificate: Option<&'a C>) -> Self { self.certificate = certificate; self } @@ -107,11 +105,11 @@ where /// Build the connection configuration. pub fn build(self) -> ConnectionConfig<'a, I, C> { ConnectionConfig { - is_secure: self.is_secure, - ip: self.ip, + scheme: self.scheme, host: self.host, port: self.port, protocol_version: self.protocol_version, + connection_timeout: self.connection_timeout, identity: self.identity, certificate: self.certificate, skip_cert_verification: self.skip_cert_verification, @@ -122,13 +120,13 @@ where /// Connection configuration. pub struct ConnectionConfig<'a, I, C> { - is_secure: bool, - ip: IpAddr, + scheme: &'a str, host: &'a str, port: u16, protocol_version: Version, - identity: Option, - certificate: Option, + connection_timeout: Duration, + identity: Option<&'a I>, + certificate: Option<&'a C>, skip_cert_verification: bool, client_bind_addr: IpAddr, } @@ -143,14 +141,9 @@ where ConnectionConfigBuilder::new() } - /// Get whether the connection is secure. - pub fn is_secure(&self) -> bool { - self.is_secure - } - - /// Get the IP address for the connection. - pub fn ip(&self) -> &IpAddr { - &self.ip + /// Get the scheme for the connection. + pub fn scheme(&self) -> &str { + self.scheme } /// Get the host for the connection. @@ -168,14 +161,19 @@ where &self.protocol_version } + /// Get the connection timeout for the connection. + pub fn connection_timeout(&self) -> Duration { + self.connection_timeout + } + /// Get the identity for the connection. - pub fn identity(&self) -> &Option { - &self.identity + pub fn identity(&self) -> Option<&I> { + self.identity } /// Get the certificate for the connection. - pub fn certificate(&self) -> &Option { - &self.certificate + pub fn certificate(&self) -> Option<&C> { + self.certificate } /// Get whether to skip certificate verification. @@ -237,18 +235,20 @@ pub trait HttpConnectionPool { /// /// # Arguments /// - /// * `url` - The url to connect. - /// * `protocol` - The protocol to use. - /// * `retries` - The number of retries. + /// * `config` - The connection configuration. + /// * `dns_resolver` - The DNS resolver to use. /// /// # Returns /// /// * `Result<&mut Self::ConnectionDispather>` - The connection or error. /// - fn create_connection<'a>( - &'a mut self, - config: &ConnectionConfig<'a, Self::Identity, Self::Certificate>, - ) -> impl Future>; + fn create_connection( + &mut self, + config: &ConnectionConfig, + dns_resolver: &D, + ) -> impl Future> + where + D: DnsResolver; } /// Trait that represents the HTTP connection dispatcher. @@ -265,6 +265,7 @@ pub trait HttpConnectionDispatcher { fn send_request( &mut self, request: Request, + timeout: Duration, ) -> impl Future>; } @@ -272,32 +273,20 @@ pub trait HttpConnectionDispatcher { /// /// # Type Parameters /// -/// * `Sender` - The sender to use. -/// * `ReqBody` - The request body type. -/// * `ResBody` - The response body type. +/// * `Connection` - The connection type. +/// * `RuntimeStream` - The runtime stream type. /// pub trait ProtoConnection { - /// The request body type. - type ReqBody: Body + Unpin; - /// The response body type. - type ResBody: Body + Unpin; /// The connection type. type Connection: HttpConnection; - /// The identity type. - type Identity: crate::cert::Identity; - /// The certificate type. - type Certificate: crate::cert::Certificate; + /// The runtime stream type. + type RuntimeStream; /// Create a new connection. /// /// # Arguments /// - /// * `is_secure` - Whether the connection is secure. - /// * `host` - The host to connect. - /// * `port` - The port to connect. - /// * `identity` - The identity to use. - /// * `certificate` - The certificate to use. - /// * `skip_cert_verification` - Whether to skip certificate verification. + /// * `stream` - The runtime stream to use. /// /// # Errors /// @@ -307,9 +296,7 @@ pub trait ProtoConnection { /// /// * `Result` - The connection or error. /// - fn connect( - config: &ConnectionConfig, - ) -> impl Future>; + fn connect(stream: Self::RuntimeStream) -> impl Future>; /// Get connection protocol. /// @@ -319,9 +306,3 @@ pub trait ProtoConnection { /// fn protocol_version(&self) -> Version; } - -/// Common interface for Plain and TLS stream connections -pub trait StreamConnector { - /// Connect using ip and port - fn connect(ip: IpAddr, port: u16); -} diff --git a/deboa/src/cookie.rs b/deboa/src/cookie.rs index a6868322..25df19b7 100644 --- a/deboa/src/cookie.rs +++ b/deboa/src/cookie.rs @@ -42,12 +42,9 @@ //! assert_eq!(cookie.path(), Some(&"/".to_string())); //! assert_eq!(cookie.secure(), Some(true)); //! ``` - -use std::fmt; - -use cookie::{Cookie, Expiration}; - use crate::{errors::DeboaError, Result}; +use cookie::{Cookie, Expiration}; +use std::fmt; /// Represents an HTTP cookie with all its attributes. /// diff --git a/deboa/src/errors.rs b/deboa/src/errors.rs index fbad6d43..3e3d8fa2 100644 --- a/deboa/src/errors.rs +++ b/deboa/src/errors.rs @@ -44,8 +44,7 @@ //! } //! Ok(()) //! } -//! ``` - +//! `` use http::StatusCode; use thiserror::Error; @@ -158,43 +157,42 @@ pub enum RequestError { /// Error message message: String, }, + + /// Request timeout error + #[error("Request timeout: {message}")] + Timeout { + /// Error message + message: String, + }, } /// Connection error #[derive(Debug, Clone, Error, PartialEq)] pub enum ConnectionError { /// Tcp connection error - #[error("Tcp connection error: {host} {message}")] + #[error("Tcp error: {message}")] Tcp { - /// Host - host: String, /// Error message message: String, }, /// Tls connection error - #[error("Tls connection error: {host} {message}")] + #[error("Tls error: {message}")] Tls { - /// Host - host: String, /// Error message message: String, }, /// Udp connection error - #[error("Udp connection error: {message}")] + #[error("Udp error: {message}")] Udp { - /// Host - host: String, /// Error message message: String, }, /// Connection handshake error - #[error("Connection handshake error: {host} {message}")] + #[error("Handshake error: {message}")] Handshake { - /// Host - host: String, /// Error message message: String, }, @@ -206,6 +204,13 @@ pub enum ConnectionError { message: String, }, + /// Connection timeout error + #[error("Connection timeout: {message}")] + Timeout { + /// Error message + message: String, + }, + /// Unsupported scheme error #[error("Unsupported scheme: {message}")] UnsupportedScheme { @@ -232,6 +237,24 @@ pub enum ContentError { }, } +/// WebSocket errors +#[derive(Debug, Clone, Error, PartialEq)] +pub enum WebSocketError { + /// Failed to send message + #[error("Failed to send message: {message}")] + SendMessage { + /// The error message + message: String, + }, + + /// Failed to receive message + #[error("Failed to receive message: {message}")] + ReceiveMessage { + /// The error message + message: String, + }, +} + /// Connection error #[derive(Debug, Clone, Error, PartialEq)] pub enum DnsError { diff --git a/deboa/src/lib.rs b/deboa/src/lib.rs index 962e2228..9380dfe7 100644 --- a/deboa/src/lib.rs +++ b/deboa/src/lib.rs @@ -14,7 +14,7 @@ use std::{ future::Future, net::{IpAddr, Ipv4Addr}, ops::Shl, - sync::Arc, + time::Duration, }; use tackle::{Chain, Hook, HookFn}; @@ -109,12 +109,19 @@ where } /// Set connection timeout - pub fn connection_timeout(mut self, connection_timeout: u64) -> Self { + pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self { self.inner .connection_timeout = connection_timeout; self } + /// Set request timeout + pub fn request_timeout(mut self, request_timeout: Duration) -> Self { + self.inner + .request_timeout = request_timeout; + self + } + /// Set certificate pub fn certificate(mut self, certificate: C) -> Self { self.inner @@ -137,7 +144,7 @@ where /// Set dns resolver pub fn dns_resolver(mut self, dns_resolver: R) -> Self { self.inner - .dns_resolver = Arc::new(dns_resolver); + .dns_resolver = dns_resolver; self } @@ -312,13 +319,13 @@ where /// - Automatic connection reuse when possible /// - Configurable timeouts prevent hanging requests pub struct InnerClient { - connection_timeout: u64, - request_timeout: u64, + connection_timeout: Duration, + request_timeout: Duration, identity: Option, certificate: Option, skip_cert_verification: bool, pool: RwLock

, - dns_resolver: Arc, + dns_resolver: R, bind_addr: IpAddr, } @@ -338,12 +345,23 @@ impl InnerClient { /// /// # Returns /// - /// * `u64` - The timeout. + /// * `Duration` - The timeout. /// - pub fn connection_timeout(&self) -> u64 { + pub fn connection_timeout(&self) -> Duration { self.connection_timeout } + #[inline] + /// Allow get request request timeout at any time. + /// + /// # Returns + /// + /// * `Duration` - The timeout. + /// + pub fn request_timeout(&self) -> Duration { + self.request_timeout + } + /// Allow get connection pool at any time. /// /// # Returns @@ -362,7 +380,7 @@ impl InnerClient { /// * `Arc` - The DNS resolver. /// #[inline] - pub fn dns_resolver(&self) -> &Arc { + pub fn dns_resolver(&self) -> &R { &self.dns_resolver } @@ -377,17 +395,6 @@ impl InnerClient { self.bind_addr } - /// Allow get request request timeout at any time. - /// - /// # Returns - /// - /// * `u64` - The timeout. - /// - #[inline] - pub fn request_timeout(&self) -> u64 { - self.request_timeout - } - /// Allow get certificate at any time. /// /// # Returns @@ -420,13 +427,13 @@ where fn default() -> Self { Self { bind_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), - connection_timeout: 30, - request_timeout: 30, + connection_timeout: Duration::from_secs(30), + request_timeout: Duration::from_secs(30), identity: None, certificate: None, skip_cert_verification: false, pool: RwLock::new(P::default()), - dns_resolver: Arc::new(R::default()), + dns_resolver: R::default(), } } } @@ -442,74 +449,46 @@ where type Error = DeboaError; async fn call(&self, request: DeboaRequest) -> Result { - let resolver = self - .dns_resolver - .clone(); + info!("Building request: {} {}", request.method(), request.uri()); let uri = request .uri() .clone(); - let method = request.method(); - let host = uri - .host() - .unwrap_or("localhost"); - let port = if let Some(port) = uri.port() { port.as_u16() } else { 80u16 }; - info!("Building request: {} {}", method, uri); - let request = request.body(); - let ips = resolver - .resolve(host.to_string(), port) - .await?; - - let ips = if self - .bind_addr - .is_ipv4() - { - ips.into_iter() - .filter(|ip| ip.is_ipv4()) - .collect::>() - } else { - ips.into_iter() - .filter(|ip| ip.is_ipv6()) - .collect::>() + let Some(scheme) = uri.scheme_str() else { + return Err(DeboaError::Request(RequestError::Send { + message: "Missing scheme".to_string(), + })); }; - let Some(ip) = ips.first() else { + let Some(host) = uri.host() else { return Err(DeboaError::Request(RequestError::Send { - message: format!("No IP addresses found for hostname: {}", host), + message: "Missing host".to_string(), })); }; - let uri = request.uri(); - let scheme = uri - .scheme_str() - .unwrap_or("http"); - let (port, is_secure) = match scheme { - "https" | "wss" => ( - uri.port_u16() - .unwrap_or(443), - true, - ), - _ => ( - uri.port_u16() - .unwrap_or(80), - false, - ), - }; + let port = uri + .port_u16() + .unwrap_or({ + match scheme { + "http" => 80, + "https" => 443, + _ => 80, + } + }); let config = ConnectionConfig::builder() - .is_secure(is_secure) - .ip(*ip) + .scheme(scheme) .host(host) .port(port) .protocol_version(request.version()) .identity( self.identity - .clone(), + .as_ref(), ) .certificate( self.certificate - .clone(), + .as_ref(), ) .skip_cert_verification(self.skip_cert_verification) .client_bind_addr(self.bind_addr) @@ -521,11 +500,13 @@ where .await; let conn = pool - .create_connection(&config) + .create_connection(&config, &self.dns_resolver) .await?; + let request = request.body(); + let response = conn - .send_request(request) + .send_request(request, self.request_timeout) .await?; Ok(response) diff --git a/deboa/src/request.rs b/deboa/src/request.rs index 37e64133..5331c37b 100644 --- a/deboa/src/request.rs +++ b/deboa/src/request.rs @@ -240,6 +240,7 @@ impl MethodExt for Method { match self { Method::GET => DeboaRequest::get(url), Method::POST => DeboaRequest::post(url), + Method::QUERY => DeboaRequest::query(url), Method::PUT => DeboaRequest::put(url), Method::DELETE => DeboaRequest::delete(url), Method::PATCH => DeboaRequest::patch(url), @@ -258,6 +259,7 @@ impl MethodExt for &str { match self { "GET" | "get" => DeboaRequest::get(url), "POST" | "post" => DeboaRequest::post(url), + "QUERY" | "query" => DeboaRequest::query(url), "PUT" | "put" => DeboaRequest::put(url), "DELETE" | "delete" => DeboaRequest::delete(url), "PATCH" | "patch" => DeboaRequest::patch(url), @@ -434,6 +436,36 @@ pub fn post(url: T) -> Result { DeboaRequest::post(url) } +/// A utility function to create a QUERY request within DeboaRequest. +/// +/// # Arguments +/// +/// * `url` - The url to connect. +/// +/// # Returns +/// +/// * `Result` - The request builder. +/// +/// # Examples +/// +/// ```rust,compile_fail +/// use deboa::{request::query}; +/// use deboa_tokio::Client; +/// +/// let client = Client::new(); +/// +/// let request = query("https://jsonplaceholder.typicode.com/posts")? +/// .raw_body(b"{\"title\": \"foo\", \"body\": \"bar\", \"userId\": 1}") +/// .build()?; +/// let response = request.send_with(&client).await?; +/// assert_eq!(response.status(), 201); +/// ``` +/// +#[inline] +pub fn query(url: T) -> Result { + DeboaRequest::query(url) +} + /// A utility function to create a PUT request within DeboaRequest. /// /// # Arguments @@ -1050,7 +1082,7 @@ impl FromStr for DeboaRequest { let mut is_reading_body = false; let method_url_regex = - Regex::new(r"(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(https?://[^\s]+)"); + Regex::new(r"(GET|POST|QUERY|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(https?://[^\s]+)"); if let Err(e) = method_url_regex { error!("Failed to parse request: {}", e); return Err(DeboaError::Request(RequestError::Parse { message: e.to_string() })); @@ -1220,20 +1252,12 @@ impl DeboaRequest { /// #[inline] pub fn at(url: T, method: http::Method) -> Result { - let parsed_url = url - .into_url() - .map_err(|e| { - error!("Failed to parse url: {}", e); - DeboaError::Request(RequestError::UrlParse { message: e.to_string() }) - })?; + let parsed_url = url.into_url()?; let uri = parsed_url .to_string() .parse::() - .map_err(|e| { - error!("Failed to parse uri: {}", e); - DeboaError::Request(RequestError::UrlParse { message: e.to_string() }) - })?; + .map_err(|e| DeboaError::Request(RequestError::UrlParse { message: e.to_string() }))?; let request = Request::builder() .method(method) @@ -1322,6 +1346,25 @@ impl DeboaRequest { Ok(DeboaRequest::to(url)?.method(Method::POST)) } + /// Allow make a QUERY request. + /// + /// # Arguments + /// + /// * `url` - The url to be requested. + /// + /// # Returns + /// + /// * `DeboaRequestBuilder` - The request builder. + /// + /// # Panics + /// + /// * If URL is invalid + /// + #[inline] + pub fn query(url: T) -> Result { + Ok(DeboaRequest::to(url)?.method(Method::QUERY)) + } + /// Allow make a PUT request. /// /// # Arguments @@ -1379,6 +1422,39 @@ impl DeboaRequest { Ok(DeboaRequest::from(url)?.method(Method::DELETE)) } + /// Create a request from parts and body. + /// + /// # Arguments + /// + /// * `parts` - The request parts. + /// * `body` - The request body. + /// + /// # Returns + /// + /// * `DeboaRequest` - The request. + /// + /// # Errors + /// + /// * `DeboaError` - If the request is invalid. + /// + #[inline] + pub fn from_parts(parts: http::request::Parts, body: HttpBody) -> Result { + let request = http::Request::from_parts(parts, body); + Ok(DeboaRequest { inner: request }) + } + + /// Convert the request into parts and body. + /// + /// # Returns + /// + /// * `(http::request::Parts, HttpBody)` - The request parts and body. + /// + #[inline] + pub fn into_parts(self) -> (http::request::Parts, HttpBody) { + self.inner + .into_parts() + } + /// Get request version at any time. /// /// # Returns diff --git a/deboa/src/response.rs b/deboa/src/response.rs index 4b992c20..e859901c 100644 --- a/deboa/src/response.rs +++ b/deboa/src/response.rs @@ -503,6 +503,17 @@ impl DeboaResponse { } } + /// Allow get inner response at any time. + /// + /// # Returns + /// + /// * `DeboaBody` - The inner response. + /// + #[inline] + pub fn into_inner(self) -> Response { + self.inner + } + /// Allow get inner response body at any time. /// /// # Returns