From dee7ae37d4230110718794202512d78634c443b0 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 07:28:14 -0400 Subject: [PATCH 01/66] Add dynamic-proxy --- dynamic-proxy/.gitignore | 1 + dynamic-proxy/Cargo.lock | 2030 +++++++++++++++++ dynamic-proxy/Cargo.toml | 29 + dynamic-proxy/src/body.rs | 23 + dynamic-proxy/src/graceful_shutdown.rs | 100 + dynamic-proxy/src/lib.rs | 6 + dynamic-proxy/src/proxy.rs | 151 ++ dynamic-proxy/src/request.rs | 70 + dynamic-proxy/src/server.rs | 204 ++ dynamic-proxy/src/upgrade.rs | 60 + dynamic-proxy/tests/common/cert.rs | 48 + .../tests/common/hello_world_service.rs | 27 + dynamic-proxy/tests/common/mod.rs | 5 + .../tests/common/simple_axum_server.rs | 76 + .../tests/common/simple_upgrade_service.rs | 68 + .../tests/common/websocket_echo_server.rs | 60 + dynamic-proxy/tests/graceful.rs | 60 + dynamic-proxy/tests/graceful_https.rs | 72 + dynamic-proxy/tests/hello_world_http.rs | 23 + dynamic-proxy/tests/https_test.rs | 36 + dynamic-proxy/tests/test_http_versions.rs | 44 + dynamic-proxy/tests/test_proxy_request.rs | 137 ++ dynamic-proxy/tests/test_proxy_websocket.rs | 105 + dynamic-proxy/tests/test_upgrade.rs | 67 + 24 files changed, 3502 insertions(+) create mode 100644 dynamic-proxy/.gitignore create mode 100644 dynamic-proxy/Cargo.lock create mode 100644 dynamic-proxy/Cargo.toml create mode 100644 dynamic-proxy/src/body.rs create mode 100644 dynamic-proxy/src/graceful_shutdown.rs create mode 100644 dynamic-proxy/src/lib.rs create mode 100644 dynamic-proxy/src/proxy.rs create mode 100644 dynamic-proxy/src/request.rs create mode 100644 dynamic-proxy/src/server.rs create mode 100644 dynamic-proxy/src/upgrade.rs create mode 100644 dynamic-proxy/tests/common/cert.rs create mode 100644 dynamic-proxy/tests/common/hello_world_service.rs create mode 100644 dynamic-proxy/tests/common/mod.rs create mode 100644 dynamic-proxy/tests/common/simple_axum_server.rs create mode 100644 dynamic-proxy/tests/common/simple_upgrade_service.rs create mode 100644 dynamic-proxy/tests/common/websocket_echo_server.rs create mode 100644 dynamic-proxy/tests/graceful.rs create mode 100644 dynamic-proxy/tests/graceful_https.rs create mode 100644 dynamic-proxy/tests/hello_world_http.rs create mode 100644 dynamic-proxy/tests/https_test.rs create mode 100644 dynamic-proxy/tests/test_http_versions.rs create mode 100644 dynamic-proxy/tests/test_proxy_request.rs create mode 100644 dynamic-proxy/tests/test_proxy_websocket.rs create mode 100644 dynamic-proxy/tests/test_upgrade.rs diff --git a/dynamic-proxy/.gitignore b/dynamic-proxy/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/dynamic-proxy/.gitignore @@ -0,0 +1 @@ +/target diff --git a/dynamic-proxy/Cargo.lock b/dynamic-proxy/Cargo.lock new file mode 100644 index 000000000..a54b99d35 --- /dev/null +++ b/dynamic-proxy/Cargo.lock @@ -0,0 +1,2030 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "async-trait" +version = "0.1.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "aws-lc-rs" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + +[[package]] +name = "axum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.7", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper 1.0.1", + "tokio", + "tokio-tungstenite 0.23.1", + "tower 0.5.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cc" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dynamic-proxy" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "rcgen", + "reqwest", + "rustls", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-tungstenite 0.24.0", + "tracing", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower 0.4.13", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rcgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.23.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.24.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/dynamic-proxy/Cargo.toml b/dynamic-proxy/Cargo.toml new file mode 100644 index 000000000..c6acaf92b --- /dev/null +++ b/dynamic-proxy/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "dynamic-proxy" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.89" +bytes = "1.7.2" +http = "1.1.0" +http-body = "1.0.1" +http-body-util = "0.1.2" +hyper = "1.4.1" +hyper-util = { version = "0.1.8", features = ["http1", "http2", "server", "server-graceful", "server-auto", "client", "client-legacy"] } +pin-project-lite = "0.2.14" +rustls = { version = "0.23.13", features = ["ring"] } +thiserror = "1.0.63" +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } +tokio-rustls = "0.26.0" +tracing = "0.1.40" + +[dev-dependencies] +axum = { version = "0.7.6", features = ["http2", "ws"] } +futures-util = "0.3.30" +http = "1.1.0" +rcgen = "0.13.1" +reqwest = { version = "0.12.7", features = ["http2", "stream"] } +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" +tokio-tungstenite = "0.24.0" diff --git a/dynamic-proxy/src/body.rs b/dynamic-proxy/src/body.rs new file mode 100644 index 000000000..def167013 --- /dev/null +++ b/dynamic-proxy/src/body.rs @@ -0,0 +1,23 @@ +use bytes::Bytes; +use http_body::Body; +use http_body_util::combinators::BoxBody; +use http_body_util::{BodyExt, Empty}; + +pub type BoxedError = Box; + +pub type SimpleBody = BoxBody; + +pub fn to_simple_body(body: B) -> SimpleBody +where + B: Body + Send + Sync + 'static, + B::Error: Into>, +{ + body.map_err(|e| e.into() as Box) + .boxed() +} + +pub fn simple_empty_body() -> SimpleBody { + Empty::::new() + .map_err(|_| unreachable!("Infallable")) + .boxed() +} diff --git a/dynamic-proxy/src/graceful_shutdown.rs b/dynamic-proxy/src/graceful_shutdown.rs new file mode 100644 index 000000000..6083d6ab2 --- /dev/null +++ b/dynamic-proxy/src/graceful_shutdown.rs @@ -0,0 +1,100 @@ +//! Near-identical copy of hyper_util::server::graceful::GracefulShutdown +//! that derives `Clone` and adds a `subscribe` method. +//! https://github.com/hyperium/hyper-util/blob/master/src/server/graceful.rs + +use hyper_util::server::graceful::GracefulConnection; +use pin_project_lite::pin_project; +use std::{ + fmt::{self, Debug}, + future::Future, + pin::Pin, + task::{self, Poll}, +}; +use tokio::sync::watch; + +#[derive(Clone)] +pub struct GracefulShutdown { + tx: watch::Sender<()>, +} + +impl GracefulShutdown { + /// Create a new graceful shutdown helper. + pub fn new() -> Self { + let (tx, _) = watch::channel(()); + Self { tx } + } + + /// Wrap a future for graceful shutdown watching. + pub fn watch(&self, conn: C) -> impl Future { + let mut rx = self.tx.subscribe(); + GracefulConnectionFuture::new(conn, async move { + let _ = rx.changed().await; + // hold onto the rx until the watched future is completed + rx + }) + } + + pub fn subscribe(&self) -> watch::Receiver<()> { + self.tx.subscribe() + } + + /// Signal shutdown for all watched connections. + /// + /// This returns a `Future` which will complete once all watched + /// connections have shutdown. + pub async fn shutdown(self) { + let Self { tx } = self; + + // signal all the watched futures about the change + let _ = tx.send(()); + // and then wait for all of them to complete + tx.closed().await; + } +} + +pin_project! { + struct GracefulConnectionFuture { + #[pin] + conn: C, + #[pin] + cancel: F, + #[pin] + // If cancelled, this is held until the inner conn is done. + cancelled_guard: Option, + } +} + +impl GracefulConnectionFuture { + fn new(conn: C, cancel: F) -> Self { + Self { + conn, + cancel, + cancelled_guard: None, + } + } +} + +impl Debug for GracefulConnectionFuture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GracefulConnectionFuture").finish() + } +} + +impl Future for GracefulConnectionFuture +where + C: GracefulConnection, + F: Future, +{ + type Output = C::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { + let mut this = self.project(); + if this.cancelled_guard.is_none() { + if let Poll::Ready(guard) = this.cancel.poll(cx) { + this.cancelled_guard.set(Some(guard)); + this.conn.as_mut().graceful_shutdown(); + } + } + this.conn.poll(cx) + } +} diff --git a/dynamic-proxy/src/lib.rs b/dynamic-proxy/src/lib.rs new file mode 100644 index 000000000..c2ca86831 --- /dev/null +++ b/dynamic-proxy/src/lib.rs @@ -0,0 +1,6 @@ +pub mod body; +mod graceful_shutdown; +pub mod proxy; +pub mod request; +pub mod server; +mod upgrade; diff --git a/dynamic-proxy/src/proxy.rs b/dynamic-proxy/src/proxy.rs new file mode 100644 index 000000000..c6adea225 --- /dev/null +++ b/dynamic-proxy/src/proxy.rs @@ -0,0 +1,151 @@ +use crate::{ + body::{simple_empty_body, to_simple_body, SimpleBody}, + request::should_upgrade, + upgrade::{split_request, split_response, UpgradeHandler}, +}; +use http::StatusCode; +use hyper::{Request, Response}; +use hyper_util::{ + client::legacy::{connect::HttpConnector, Client}, + rt::TokioExecutor, +}; +use std::time::Duration; + +pub struct ProxyClient { + client: Client, + timeout: Duration, +} + +impl Clone for ProxyClient { + fn clone(&self) -> Self { + Self { + client: self.client.clone(), + timeout: self.timeout, + } + } +} + +impl Default for ProxyClient { + fn default() -> Self { + Self::new() + } +} + +impl ProxyClient { + pub fn new() -> Self { + let client = Client::builder(TokioExecutor::new()).build(HttpConnector::new()); + Self { + client, + timeout: Duration::from_secs(10), + } + } + + pub async fn request( + &self, + request: Request, + ) -> Result< + (Response, Option), + Box, + > { + let url = request.uri().to_string(); + + let res = self.handle_request(request).await; + + let res = match res { + Ok(res) => res, + Err(ProxyError::Timeout) => { + tracing::error!(url, "Upstream request failed"); + return Ok(( + Response::builder() + .status(StatusCode::GATEWAY_TIMEOUT) + .body(simple_empty_body()) + .unwrap(), + None, + )); + } + Err(e) => { + tracing::error!(url, ?e, "Upstream request failed"); + return Ok(( + Response::builder() + .status(StatusCode::BAD_GATEWAY) + .body(simple_empty_body()) + .unwrap(), + None, + )); + } + }; + + let (res, upgrade_handler) = res; + let (parts, body) = res.into_parts(); + let res = Response::from_parts(parts, to_simple_body(body)); + + Ok((res, upgrade_handler)) + } + + async fn handle_request( + &self, + request: Request, + ) -> Result<(Response, Option), ProxyError> { + if should_upgrade(&request) { + println!("Upgrading request"); + let (response, upgrade_handler) = self.handle_upgrade(request).await?; + println!("Upgraded request"); + Ok((response, Some(upgrade_handler))) + } else { + let result = self.upstream_request(request).await?; + Ok((result, None)) + } + } + + async fn handle_upgrade( + &self, + request: Request, + ) -> Result<(Response, UpgradeHandler), ProxyError> { + let (upstream_request, request_with_body) = split_request(request); + let res = self.upstream_request(upstream_request).await?; + let (upstream_response, response_with_body) = split_response(res); + + let upgrade_handler = UpgradeHandler::new(request_with_body, response_with_body); + + Ok((upstream_response, upgrade_handler)) + } + + async fn upstream_request( + &self, + request: Request, + ) -> Result, ProxyError> { + let url = request.uri().to_string(); + + let res = match tokio::time::timeout(self.timeout, self.client.request(request)).await { + Ok(Ok(res)) => res, + Err(_) => { + tracing::warn!(url, "Upstream request timed out."); + return Err(ProxyError::Timeout); + } + Ok(Err(e)) => { + tracing::error!(url, "Upstream request failed: {}", e); + return Err(ProxyError::RequestFailed(e.into())); + } + }; + + let (parts, body) = res.into_parts(); + let res = Response::from_parts(parts, to_simple_body(body)); + + Ok(res) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum ProxyError { + #[error("Upstream request timed out.")] + Timeout, + + #[error("Upstream request failed: {0}")] + RequestFailed(#[from] Box), + + #[error("Failed to upgrade response: {0}")] + UpgradeError(#[from] hyper::Error), + + #[error("IO error: {0}")] + IoError(#[from] tokio::io::Error), +} diff --git a/dynamic-proxy/src/request.rs b/dynamic-proxy/src/request.rs new file mode 100644 index 000000000..af9deedf7 --- /dev/null +++ b/dynamic-proxy/src/request.rs @@ -0,0 +1,70 @@ +use bytes::Bytes; +use http::{ + request::Parts, + uri::{Authority, Scheme}, + HeaderName, HeaderValue, Request, Uri, +}; +use http_body::Body; +use std::{net::SocketAddr, str::FromStr}; + +use crate::body::{to_simple_body, SimpleBody}; + +pub struct MutableRequest +where + T: Body, + T::Error: Into>, +{ + parts: Parts, + body: T, +} + +impl MutableRequest +where + T: Body + Send + Sync + 'static, + T::Error: Into>, +{ + pub fn from_request(request: Request) -> Self { + let (parts, body) = request.into_parts(); + Self { parts, body } + } + + pub fn into_request(self) -> Request { + Request::from_parts(self.parts, self.body) + } + + pub fn into_request_with_simple_body(self) -> Request { + Request::from_parts(self.parts, to_simple_body(self.body)) + } + + pub fn set_upstream_address(&mut self, address: SocketAddr) { + let uri = std::mem::take(&mut self.parts.uri); + let mut uri_parts = uri.into_parts(); + uri_parts.scheme = Some(Scheme::HTTP); + uri_parts.authority = Some( + Authority::try_from(address.to_string()) + .expect("SocketAddr should always be a valid authority."), + ); + self.parts.uri = Uri::from_parts(uri_parts).expect("URI should always be valid."); + } + + pub fn add_header(&mut self, key: &str, value: &str) { + let key = HeaderName::from_str(key).unwrap(); + let value = HeaderValue::from_str(value).unwrap(); + self.parts.headers.append(key, value); + } +} + +pub fn should_upgrade(request: &Request) -> bool { + let Some(conn_header) = request.headers().get("connection") else { + return false; + }; + + let Ok(conn_header) = conn_header.to_str() else { + return false; + }; + + conn_header + .to_lowercase() + .split(',') + .any(|s| s.trim() == "upgrade") +} diff --git a/dynamic-proxy/src/server.rs b/dynamic-proxy/src/server.rs new file mode 100644 index 000000000..c9fc7cc74 --- /dev/null +++ b/dynamic-proxy/src/server.rs @@ -0,0 +1,204 @@ +use crate::{body::SimpleBody, graceful_shutdown::GracefulShutdown}; +use anyhow::Result; +use hyper::{body::Incoming, service::Service, Request, Response}; +use hyper_util::{ + rt::{TokioExecutor, TokioIo}, + server::conn::auto::Builder as ServerBuilder, +}; +use rustls::{server::ResolvesServerCert, ServerConfig}; +use std::{sync::Arc, time::Duration}; +use tokio::{net::TcpListener, select, task::JoinSet}; +use tokio_rustls::TlsAcceptor; + +pub struct SimpleHttpServer { + handle: tokio::task::JoinHandle>, + graceful_shutdown: Option, +} + +async fn listen_loop( + listener: TcpListener, + service: S, + graceful_shutdown: GracefulShutdown, +) -> JoinSet<()> +where + S: Service, Response = Response> + Clone + Send + 'static, + S::Future: Send + 'static, + S::Error: Into>, +{ + let mut recv = graceful_shutdown.subscribe(); + let mut join_set = JoinSet::new(); + + loop { + let stream = select! { + stream = listener.accept() => stream, + _ = recv.changed() => break, + }; + + let stream = match stream { + Ok((stream, _)) => stream, + Err(e) => { + tracing::warn!(?e, "Failed to accept connection."); + continue; + } + }; + let service = service.clone(); + + let server = ServerBuilder::new(TokioExecutor::new()); + let io = TokioIo::new(stream); + let conn = server.serve_connection_with_upgrades(io, service); + + let conn = graceful_shutdown.watch(conn.into_owned()); + join_set.spawn(async { + if let Err(e) = conn.await { + tracing::warn!(?e, "Failed to serve connection."); + } + }); + } + + // Even though join_set is never used, we return it to keep it from being dropped + // until the graceful shutdown (or timeout) is complete. Otherwise, the tasks we started + // would be stopped as soon as the graceful shutdown is initiated. + join_set +} + +async fn listen_loop_tls( + listener: TcpListener, + service: S, + resolver: Arc, + graceful_shutdown: GracefulShutdown, +) -> JoinSet<()> +where + S: Service, Response = Response> + Clone + Send + 'static, + S::Future: Send + 'static, + S::Error: Into>, +{ + let server_config = ServerConfig::builder() + .with_no_client_auth() + .with_cert_resolver(resolver); + let tls_acceptor = TlsAcceptor::from(Arc::new(server_config)); + let mut recv = graceful_shutdown.subscribe(); + let mut join_set = JoinSet::new(); + + loop { + let stream = select! { + stream = listener.accept() => stream, + _ = recv.changed() => break, + }; + + let stream = match stream { + Ok((stream, _)) => stream, + Err(e) => { + tracing::warn!(?e, "Failed to accept connection."); + continue; + } + }; + let service = service.clone(); + let tls_acceptor = tls_acceptor.clone(); + + let graceful_shutdown = graceful_shutdown.clone(); + join_set.spawn(async move { + let server = ServerBuilder::new(TokioExecutor::new()); + + let stream = match tls_acceptor.accept(stream).await { + Ok(stream) => stream, + Err(e) => { + tracing::warn!(?e, "Failed to accept TLS connection."); + return; + } + }; + let io = TokioIo::new(stream); + + let conn = server.serve_connection_with_upgrades(io, service); + let conn = graceful_shutdown.watch(conn.into_owned()); + + if let Err(e) = conn.await { + tracing::warn!(?e, "Failed to serve connection."); + } + }); + } + + // Even though join_set is never used, we return it to keep it from being dropped + // until the graceful shutdown (or timeout) is complete. Otherwise, the tasks we started + // would be stopped as soon as the graceful shutdown is initiated. + join_set +} + +pub enum HttpsConfig { + Http, + Https { + resolver: Arc, + }, +} + +impl HttpsConfig { + pub fn from_resolver(resolver: R) -> Self { + Self::Https { + resolver: Arc::new(resolver), + } + } + + pub fn http() -> Self { + Self::Http + } +} + +impl SimpleHttpServer { + pub fn new(service: S, listener: TcpListener, https_config: HttpsConfig) -> Result + where + S: Service, Response = Response> + Clone + Send + 'static, + S::Future: Send + 'static, + S::Error: Into>, + { + let graceful_shutdown = GracefulShutdown::new(); + + let handle = match https_config { + HttpsConfig::Http => { + tokio::spawn(listen_loop(listener, service, graceful_shutdown.clone())) + } + HttpsConfig::Https { resolver } => { + if rustls::crypto::ring::default_provider() + .install_default() + .is_err() + { + tracing::info!("Using already-installed crypto provider.") + } + + tokio::spawn(listen_loop_tls( + listener, + service, + resolver, + graceful_shutdown.clone(), + )) + } + }; + + Ok(Self { + handle, + graceful_shutdown: Some(graceful_shutdown), + }) + } + + pub async fn graceful_shutdown(mut self) { + let graceful_shutdown = self + .graceful_shutdown + .take() + .expect("self.graceful_shutdown is always set"); + graceful_shutdown.shutdown().await; + } + + pub async fn graceful_shutdown_with_timeout(mut self, timeout: Duration) { + let graceful_shutdown = self + .graceful_shutdown + .take() + .expect("self.graceful_shutdown is always set"); + tokio::time::timeout(timeout, graceful_shutdown.shutdown()) + .await + .unwrap(); + } +} + +impl Drop for SimpleHttpServer { + fn drop(&mut self) { + self.handle.abort(); + } +} diff --git a/dynamic-proxy/src/upgrade.rs b/dynamic-proxy/src/upgrade.rs new file mode 100644 index 000000000..9082d53d8 --- /dev/null +++ b/dynamic-proxy/src/upgrade.rs @@ -0,0 +1,60 @@ +use crate::{ + body::{simple_empty_body, SimpleBody}, + proxy::ProxyError, +}; +use http::{Request, Response}; +use hyper_util::rt::TokioIo; +use tokio::io::copy_bidirectional; + +/// Split a request into two requests. The first request has an empty body, +/// and the second request has the original body. +/// +/// The first request is forwarded on upstream. The second is used for bidirectional +/// communication after the connection has been upgraded. +pub fn split_request(request: Request) -> (Request, Request) { + let (parts, body) = request.into_parts(); + + let request1 = Request::from_parts(parts.clone(), simple_empty_body()); + let request2 = Request::from_parts(parts, body); + + (request1, request2) +} + +/// Clone a response, using an empty body. +pub fn split_response(response: Response) -> (Response, Response) { + let (parts, body) = response.into_parts(); + + let response1 = Response::from_parts(parts.clone(), simple_empty_body()); + let response2 = Response::from_parts(parts, body); + + (response1, response2) +} + +pub struct UpgradeHandler { + pub request: Request, + pub response: Response, +} + +impl UpgradeHandler { + pub fn new(request: Request, response: Response) -> Self { + Self { request, response } + } + + pub async fn run(self) -> Result<(), ProxyError> { + let response = hyper::upgrade::on(self.response) + .await + .map_err(ProxyError::UpgradeError)?; + let mut response = TokioIo::new(response); + + let request = hyper::upgrade::on(self.request) + .await + .map_err(ProxyError::UpgradeError)?; + let mut request = TokioIo::new(request); + + copy_bidirectional(&mut request, &mut response) + .await + .map_err(ProxyError::IoError)?; + + Ok(()) + } +} diff --git a/dynamic-proxy/tests/common/cert.rs b/dynamic-proxy/tests/common/cert.rs new file mode 100644 index 000000000..fc9784e44 --- /dev/null +++ b/dynamic-proxy/tests/common/cert.rs @@ -0,0 +1,48 @@ +use rcgen::generate_simple_self_signed; +use rustls::crypto::ring::sign::any_supported_type; +use rustls::server::{ClientHello, ResolvesServerCert}; +use rustls::{pki_types::PrivateKeyDer, sign::CertifiedKey}; +use std::sync::Arc; + +const CERTIFICATE_SUBJECT_ALT_NAME: &str = "plane.test"; + +#[derive(Debug)] +pub struct StaticCertificateResolver { + certified_key: Arc, +} + +#[allow(unused)] +impl StaticCertificateResolver { + pub fn new() -> Self { + let subject_alt_names = vec![CERTIFICATE_SUBJECT_ALT_NAME.to_string()]; + + let rcgen::CertifiedKey { cert, key_pair } = + generate_simple_self_signed(subject_alt_names).unwrap(); + + let key = PrivateKeyDer::try_from(key_pair.serialized_der()) + .expect("Could not convert key pair to der"); + + let key = any_supported_type(&key).expect("Could not convert key to supported type"); + + let cert = cert.der().clone(); + + let certified_key = Arc::new(CertifiedKey::new(vec![cert], key)); + + Self { certified_key } + } + + pub fn certificate(&self) -> reqwest::Certificate { + let der = &self.certified_key.cert[0]; + reqwest::Certificate::from_der(der).unwrap() + } + + pub fn hostname(&self) -> String { + CERTIFICATE_SUBJECT_ALT_NAME.to_string() + } +} + +impl ResolvesServerCert for StaticCertificateResolver { + fn resolve(&self, _client_hello: ClientHello) -> Option> { + Some(self.certified_key.clone()) + } +} diff --git a/dynamic-proxy/tests/common/hello_world_service.rs b/dynamic-proxy/tests/common/hello_world_service.rs new file mode 100644 index 000000000..1e0b3f858 --- /dev/null +++ b/dynamic-proxy/tests/common/hello_world_service.rs @@ -0,0 +1,27 @@ +use bytes::Bytes; +use dynamic_proxy::body::{to_simple_body, SimpleBody}; +use http_body_util::{BodyExt, Full}; +use hyper::{body::Incoming, service::Service, Request, Response}; +use std::{convert::Infallible, future::Future, pin::Pin}; + +#[derive(Clone)] +pub struct HelloWorldService; + +impl Service> for HelloWorldService { + type Response = Response; + type Error = Infallible; + type Future = Pin, Infallible>> + Send>>; + + fn call(&self, request: Request) -> Self::Future { + Box::pin(async { + let _ = request.collect().await.unwrap().to_bytes(); + + let response = Response::builder() + .status(200) + .body(to_simple_body(Full::new(Bytes::from("Hello, world!")))) + .unwrap(); + + Ok(response) + }) + } +} diff --git a/dynamic-proxy/tests/common/mod.rs b/dynamic-proxy/tests/common/mod.rs new file mode 100644 index 000000000..aff08bdab --- /dev/null +++ b/dynamic-proxy/tests/common/mod.rs @@ -0,0 +1,5 @@ +pub mod cert; +pub mod hello_world_service; +pub mod simple_axum_server; +pub mod simple_upgrade_service; +pub mod websocket_echo_server; diff --git a/dynamic-proxy/tests/common/simple_axum_server.rs b/dynamic-proxy/tests/common/simple_axum_server.rs new file mode 100644 index 000000000..999e0ca74 --- /dev/null +++ b/dynamic-proxy/tests/common/simple_axum_server.rs @@ -0,0 +1,76 @@ +use axum::{body::Body, extract::Request, routing::any, Json, Router}; +use http::Method; +use http_body_util::BodyExt; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, net::SocketAddr}; +use tokio::net::TcpListener; + +pub struct SimpleAxumServer { + handle: tokio::task::JoinHandle<()>, + addr: SocketAddr, +} + +#[allow(unused)] +impl SimpleAxumServer { + pub async fn new() -> Self { + let app = Router::new() + .route("/*path", any(return_request_info)) + .route("/", any(return_request_info)); + + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let tcp_listener = TcpListener::bind(addr).await.unwrap(); + let addr = tcp_listener.local_addr().unwrap(); + + let handle = tokio::spawn(async { + axum::serve(tcp_listener, app.into_make_service()) + .await + .unwrap(); + }); + + Self { handle, addr } + } + + pub fn addr(&self) -> SocketAddr { + self.addr + } +} + +impl Drop for SimpleAxumServer { + fn drop(&mut self) { + self.handle.abort(); + } +} + +// Handler function for the root route +async fn return_request_info(method: Method, request: Request) -> Json { + let method = method.to_string(); + + let path = request.uri().path().to_string(); + let query = request.uri().query().unwrap_or("").to_string(); + + let headers: HashMap = request + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string())) + .collect(); + + let body = request.into_body().collect().await.unwrap().to_bytes(); + let body = String::from_utf8(body.to_vec()).unwrap(); + + Json(RequestInfo { + path, + query, + method, + headers, + body, + }) +} + +#[derive(Serialize, Deserialize)] +pub struct RequestInfo { + pub path: String, + pub query: String, + pub method: String, + pub headers: HashMap, + pub body: String, +} diff --git a/dynamic-proxy/tests/common/simple_upgrade_service.rs b/dynamic-proxy/tests/common/simple_upgrade_service.rs new file mode 100644 index 000000000..ba1e5e5f6 --- /dev/null +++ b/dynamic-proxy/tests/common/simple_upgrade_service.rs @@ -0,0 +1,68 @@ +use bytes::Bytes; +use dynamic_proxy::body::{to_simple_body, SimpleBody}; +use http_body_util::{Empty, Full}; +use hyper::{ + body::Incoming, + header::{HeaderValue, UPGRADE}, + service::Service, + upgrade::Upgraded, + Request, Response, StatusCode, +}; +use hyper_util::rt::TokioIo; +use std::{convert::Infallible, future::Future, pin::Pin}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +#[derive(Clone)] +pub struct SimpleUpgradeService; + +impl Service> for SimpleUpgradeService { + type Response = Response; + type Error = Infallible; + type Future = Pin, Infallible>> + Send>>; + + fn call(&self, mut req: Request) -> Self::Future { + Box::pin(async move { + if req.headers().contains_key(UPGRADE) { + // Handle upgrade + let mut res = Response::new(to_simple_body(Empty::new())); + *res.status_mut() = StatusCode::SWITCHING_PROTOCOLS; + res.headers_mut() + .insert(UPGRADE, HeaderValue::from_static("websocket")); + + tokio::task::spawn(async move { + if let Ok(upgraded) = hyper::upgrade::on(&mut req).await { + if let Err(e) = handle_upgraded_connection(upgraded).await { + tracing::error!("Error handling upgraded connection: {}", e); + } + } + }); + + Ok(res) + } else { + // Regular response + let response = Response::builder() + .status(200) + .body(to_simple_body(Full::new(Bytes::from("Hello, world!")))) + .unwrap(); + + Ok(response) + } + }) + } +} + +async fn handle_upgraded_connection(upgraded: Upgraded) -> std::io::Result<()> { + let mut upgraded = TokioIo::new(upgraded); + + // echo message back to client + loop { + let mut buf = vec![0; 1024]; + let n = upgraded.read(&mut buf).await?; + if n == 0 { + break; + } + upgraded.write_all(&buf[..n]).await?; + } + + Ok(()) +} diff --git a/dynamic-proxy/tests/common/websocket_echo_server.rs b/dynamic-proxy/tests/common/websocket_echo_server.rs new file mode 100644 index 000000000..c84586c1a --- /dev/null +++ b/dynamic-proxy/tests/common/websocket_echo_server.rs @@ -0,0 +1,60 @@ +use axum::{ + extract::ws::{Message, WebSocket, WebSocketUpgrade}, + response::IntoResponse, + routing::get, + Router, +}; +use std::net::SocketAddr; +use tokio::net::TcpListener; + +pub struct WebSocketEchoServer { + handle: tokio::task::JoinHandle<()>, + addr: SocketAddr, +} + +#[allow(unused)] +impl WebSocketEchoServer { + pub async fn new() -> Self { + let app = Router::new().route("/ws", get(ws_handler)); + + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let tcp_listener = TcpListener::bind(addr).await.unwrap(); + let addr = tcp_listener.local_addr().unwrap(); + + let handle = tokio::spawn(async move { + axum::serve(tcp_listener, app.into_make_service()) + .await + .unwrap(); + }); + + Self { handle, addr } + } + + pub fn addr(&self) -> SocketAddr { + self.addr + } +} + +impl Drop for WebSocketEchoServer { + fn drop(&mut self) { + self.handle.abort(); + } +} + +async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse { + ws.on_upgrade(handle_socket) +} + +async fn handle_socket(mut socket: WebSocket) { + while let Some(msg) = socket.recv().await { + if let Ok(msg) = msg { + if let Message::Text(text) = msg { + if socket.send(Message::Text(text)).await.is_err() { + break; + } + } + } else { + break; + } + } +} diff --git a/dynamic-proxy/tests/graceful.rs b/dynamic-proxy/tests/graceful.rs new file mode 100644 index 000000000..5597fb28d --- /dev/null +++ b/dynamic-proxy/tests/graceful.rs @@ -0,0 +1,60 @@ +use bytes::Bytes; +use dynamic_proxy::body::to_simple_body; +use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; +use hyper::StatusCode; +use std::convert::Infallible; +use std::net::SocketAddr; +use tokio::net::TcpListener; +use tokio::time::Duration; + +mod common; + +// Ref: https://github.com/hyperium/hyper-util/blob/master/examples/server_graceful.rs + +#[tokio::test] +async fn test_graceful_shutdown() { + // Start the server + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = SimpleHttpServer::new( + hyper::service::service_fn(|_| async move { + tokio::time::sleep(Duration::from_secs(1)).await; // emulate slow request + let body = http_body_util::Full::::from("Hello, world!".to_owned()); + let body = to_simple_body(body); + Ok::<_, Infallible>(hyper::Response::new(body)) + }), + listener, + HttpsConfig::Http, + ) + .unwrap(); + + let url = format!("http://{}", addr); + + // Create a client and start a POST request without finishing the body + let client = reqwest::Client::new(); + + let _client = client.clone(); + let _url = url.clone(); + let response_handle = tokio::spawn(async move { _client.get(&_url).send().await.unwrap() }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + // Call server.graceful_shutdown() + let shutdown_task = tokio::spawn(async move { server.graceful_shutdown().await }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + let response = response_handle.await.unwrap(); + + // Wait for the shutdown task to complete. + shutdown_task.await.unwrap(); + + // Ensure that the result is as expected + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.text().await.unwrap(), "Hello, world!"); + + // Attempt to make another request, which should fail due to the server shutting down + let result = client.get(&url).send().await; + assert!(result.is_err()); +} diff --git a/dynamic-proxy/tests/graceful_https.rs b/dynamic-proxy/tests/graceful_https.rs new file mode 100644 index 000000000..0320214f7 --- /dev/null +++ b/dynamic-proxy/tests/graceful_https.rs @@ -0,0 +1,72 @@ +use bytes::Bytes; +use common::cert::StaticCertificateResolver; +use dynamic_proxy::body::to_simple_body; +use dynamic_proxy::server::HttpsConfig; +use dynamic_proxy::server::SimpleHttpServer; +use hyper::StatusCode; +use std::convert::Infallible; +use std::net::SocketAddr; +use tokio::net::TcpListener; +use tokio::time::Duration; + +mod common; + +// Ref: https://github.com/hyperium/hyper-util/blob/master/examples/server_graceful.rs + +#[tokio::test] +async fn test_graceful_shutdown_https() { + // Set up HTTPS configuration + let resolver = StaticCertificateResolver::new(); + let cert = resolver.certificate(); + let hostname = resolver.hostname(); + + // Start the server + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = SimpleHttpServer::new( + hyper::service::service_fn(|_| async move { + tokio::time::sleep(Duration::from_secs(1)).await; // emulate slow request + let body = http_body_util::Full::::from("Hello, world!".to_owned()); + let body = to_simple_body(body); + Ok::<_, Infallible>(hyper::Response::new(body)) + }), + listener, + HttpsConfig::from_resolver(resolver), + ) + .unwrap(); + + let url = format!("https://{}:{}", hostname, addr.port()); + + // Create a client with HTTPS configuration + let client = reqwest::Client::builder() + .https_only(true) + .add_root_certificate(cert) + .resolve(&hostname, addr /* port is ignored */) + .build() + .unwrap(); + + let _client = client.clone(); + let _url = url.clone(); + let response_handle = tokio::spawn(async move { _client.get(&_url).send().await.unwrap() }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + // Call server.graceful_shutdown() + let shutdown_task = tokio::spawn(async move { server.graceful_shutdown().await }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + let response = response_handle.await.unwrap(); + + // Wait for the shutdown task to complete. + shutdown_task.await.unwrap(); + + // Ensure that the result is as expected + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.text().await.unwrap(), "Hello, world!"); + + // Attempt to make another request, which should fail due to the server shutting down + let result = client.get(&url).send().await; + assert!(result.is_err()); +} diff --git a/dynamic-proxy/tests/hello_world_http.rs b/dynamic-proxy/tests/hello_world_http.rs new file mode 100644 index 000000000..2c79bac6b --- /dev/null +++ b/dynamic-proxy/tests/hello_world_http.rs @@ -0,0 +1,23 @@ +use common::hello_world_service::HelloWorldService; +use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; +use hyper::StatusCode; +use std::net::SocketAddr; +use tokio::net::TcpListener; + +mod common; + +#[tokio::test] +async fn test_hello_world_http() { + let service = HelloWorldService; + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + let _server = SimpleHttpServer::new(service, listener, HttpsConfig::Http).unwrap(); + + let url = format!("http://{}", addr); + + let client = reqwest::Client::new(); + let res = client.get(url).send().await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.text().await.unwrap(), "Hello, world!"); +} diff --git a/dynamic-proxy/tests/https_test.rs b/dynamic-proxy/tests/https_test.rs new file mode 100644 index 000000000..e9fa19467 --- /dev/null +++ b/dynamic-proxy/tests/https_test.rs @@ -0,0 +1,36 @@ +use common::{cert::StaticCertificateResolver, hello_world_service::HelloWorldService}; +use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; +use std::net::SocketAddr; +use tokio::net::TcpListener; + +mod common; + +#[tokio::test] +async fn test_https() { + let resolver = StaticCertificateResolver::new(); + let cert = resolver.certificate(); + let hostname = resolver.hostname(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + let _server = SimpleHttpServer::new( + HelloWorldService, + listener, + HttpsConfig::from_resolver(resolver), + ) + .unwrap(); + + let client = reqwest::Client::builder() + .https_only(true) + .add_root_certificate(cert) + .resolve(&hostname, addr /* port is ignored */) + .build() + .unwrap(); + + let url = format!("https://{}:{}", hostname, addr.port()); + + let res = client.get(&url).send().await.unwrap(); + assert!(res.status().is_success()); + assert_eq!(res.text().await.unwrap(), "Hello, world!"); +} diff --git a/dynamic-proxy/tests/test_http_versions.rs b/dynamic-proxy/tests/test_http_versions.rs new file mode 100644 index 000000000..7c5defe8e --- /dev/null +++ b/dynamic-proxy/tests/test_http_versions.rs @@ -0,0 +1,44 @@ +use common::hello_world_service::HelloWorldService; +use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; +use hyper::StatusCode; +use std::net::SocketAddr; +use tokio::net::TcpListener; + +mod common; + +#[tokio::test] +async fn test_http1() { + let service = HelloWorldService; + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + let _server = SimpleHttpServer::new(service, listener, HttpsConfig::Http).unwrap(); + + let url = format!("http://{}", addr); + + let client = reqwest::Client::builder().http1_only().build().unwrap(); + let res = client.get(url).send().await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.version(), reqwest::Version::HTTP_11); + assert_eq!(res.text().await.unwrap(), "Hello, world!"); +} + +#[tokio::test] +async fn test_http2() { + let service = HelloWorldService; + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + let _server = SimpleHttpServer::new(service, listener, HttpsConfig::Http).unwrap(); + + let url = format!("http://{}", addr); + + let client = reqwest::Client::builder() + .http2_prior_knowledge() + .build() + .unwrap(); + let res = client.get(url).send().await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.version(), reqwest::Version::HTTP_2); + assert_eq!(res.text().await.unwrap(), "Hello, world!"); +} diff --git a/dynamic-proxy/tests/test_proxy_request.rs b/dynamic-proxy/tests/test_proxy_request.rs new file mode 100644 index 000000000..410361552 --- /dev/null +++ b/dynamic-proxy/tests/test_proxy_request.rs @@ -0,0 +1,137 @@ +use crate::common::simple_axum_server::SimpleAxumServer; +use anyhow::Result; +use bytes::Bytes; +use common::simple_axum_server::RequestInfo; +use dynamic_proxy::{ + body::{simple_empty_body, to_simple_body, BoxedError}, + proxy::ProxyClient, + request::MutableRequest, +}; +use http::{Method, Request, StatusCode}; +use http_body_util::{combinators::BoxBody, BodyExt, Full}; +use std::net::SocketAddr; +use tokio::net::TcpListener; + +mod common; + +async fn make_request(req: Request>) -> Result { + let server = SimpleAxumServer::new().await; + let proxy_client = ProxyClient::new(); + + let mut req = MutableRequest::from_request(req); + req.set_upstream_address(server.addr()); + + let (res, upgrade_handler) = proxy_client.request(req.into_request()).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert!(upgrade_handler.is_none()); + + let body = res.into_body().collect().await.unwrap().to_bytes(); + let result: RequestInfo = serde_json::from_slice(&body).unwrap(); + + Ok(result) +} + +#[tokio::test] +async fn test_proxy_simple_request() { + let req = Request::builder() + .method(Method::GET) + .uri("http://foo.bar".to_string()) + .body(simple_empty_body()) + .unwrap(); + + let result = make_request(req).await.unwrap(); + + assert_eq!(result.path, "/"); + assert_eq!(result.method, "GET"); + assert_eq!(result.headers.len(), 1); + assert!(result.headers.contains_key("host")); +} + +#[tokio::test] +async fn test_proxy_simple_post_request() { + let req = Request::builder() + .method(Method::POST) + .uri("http://foo.bar".to_string()) + .body(simple_empty_body()) + .unwrap(); + + let result = make_request(req).await.unwrap(); + + assert_eq!(result.path, "/"); + assert_eq!(result.method, "POST"); + assert_eq!(result.headers.len(), 1); + assert!(result.headers.contains_key("host")); +} + +#[tokio::test] +async fn test_proxy_request_with_path_and_query_params() { + let req = Request::builder() + .method(Method::POST) + .uri("http://foo.bar/foo/bar?baz=1&qux=2".to_string()) + .body(simple_empty_body()) + .unwrap(); + + let result = make_request(req).await.unwrap(); + + assert_eq!(result.path, "/foo/bar"); + assert_eq!(result.query, "baz=1&qux=2"); + assert_eq!(result.method, "POST"); + assert_eq!(result.headers.len(), 1); + assert!(result.headers.contains_key("host")); +} + +#[tokio::test] +async fn test_proxy_request_with_headers() { + let req = Request::builder() + .method(Method::GET) + .uri("http://foo.bar/foo".to_string()) + .header("X-Test", "test") + .body(simple_empty_body()) + .unwrap(); + + let result = make_request(req).await.unwrap(); + + assert_eq!(result.path, "/foo"); + assert_eq!(result.method, "GET"); + assert_eq!(result.headers.len(), 2); + assert!(result.headers.contains_key("host")); + assert_eq!(result.headers.get("x-test").unwrap(), "test"); +} + +#[tokio::test] +async fn test_proxy_body() { + let req = Request::builder() + .method(Method::POST) + .uri("http://foo.bar/foo".to_string()) + .body(to_simple_body(Full::new("test".into()))) + .unwrap(); + + let result = make_request(req).await.unwrap(); + + assert_eq!(result.path, "/foo"); + assert_eq!(result.method, "POST"); + assert_eq!(result.headers.len(), 2); + assert!(result.headers.contains_key("host")); + assert!(result.headers.contains_key("content-length")); + assert_eq!(result.body, "test"); +} + +#[tokio::test] +async fn test_proxy_no_upstream() { + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let tcp_listener = TcpListener::bind(addr).await.unwrap(); + let addr = tcp_listener.local_addr().unwrap(); + + let req = Request::builder() + .method(Method::GET) + .uri(format!("http://{}", addr)) + .body(simple_empty_body()) + .unwrap(); + + let client = ProxyClient::new(); + let (result, upgrade_handler) = client.request(req).await.unwrap(); + + // expect error HTTP 502 after timeout + assert_eq!(result.status(), StatusCode::GATEWAY_TIMEOUT); + assert!(upgrade_handler.is_none()); +} diff --git a/dynamic-proxy/tests/test_proxy_websocket.rs b/dynamic-proxy/tests/test_proxy_websocket.rs new file mode 100644 index 000000000..0af9a3657 --- /dev/null +++ b/dynamic-proxy/tests/test_proxy_websocket.rs @@ -0,0 +1,105 @@ +use common::websocket_echo_server::WebSocketEchoServer; +use dynamic_proxy::{ + body::SimpleBody, + proxy::ProxyClient, + request::MutableRequest, + server::{HttpsConfig, SimpleHttpServer}, +}; +use futures_util::{SinkExt, StreamExt}; +use http::{Request, Response}; +use hyper::{body::Incoming, service::Service}; +use std::{future::Future, net::SocketAddr, pin::Pin}; +use tokio::net::TcpListener; +use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; + +mod common; + +#[derive(Clone)] +pub struct SimpleProxyService { + upstream: SocketAddr, + client: ProxyClient, +} + +impl SimpleProxyService { + pub fn new(upstream: SocketAddr) -> Self { + let client = ProxyClient::new(); + Self { upstream, client } + } +} + +impl Service> for SimpleProxyService { + type Response = Response; + type Error = Box; + type Future = Pin< + Box< + dyn Future< + Output = Result, Box>, + > + Send, + >, + >; + + fn call(&self, request: Request) -> Self::Future { + let mut request = MutableRequest::from_request(request); + request.set_upstream_address(self.upstream); + let request = request.into_request_with_simple_body(); + let client = self.client.clone(); + + Box::pin(async move { + let (res, upgrade_handler) = client.request(request).await.unwrap(); + + let upgrade_handler = upgrade_handler.unwrap(); + tokio::spawn(async move { + upgrade_handler.run().await.unwrap(); + }); + + Ok(res) + }) + } +} + +#[tokio::test] +async fn test_websocket_echo() { + // Start the WebSocket echo server + let server = WebSocketEchoServer::new().await; + let server_addr = server.addr(); + + // Start the proxy + let proxy_service = SimpleProxyService::new(server_addr); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind listener"); + let proxy_addr = listener.local_addr().expect("Failed to get proxy address"); + let _server = SimpleHttpServer::new(proxy_service, listener, HttpsConfig::Http).unwrap(); + + // Connect to the WebSocket server + let url = format!("ws://{}/ws", proxy_addr); + let (mut ws_stream, _) = connect_async(&url).await.expect("Failed to connect"); + + // Send a message + let message = "Hello, WebSocket!"; + ws_stream + .send(Message::Text(message.to_string())) + .await + .expect("Failed to send message"); + + // Receive the echoed message + if let Some(Ok(msg)) = ws_stream.next().await { + match msg { + Message::Text(received_text) => { + assert_eq!( + received_text, message, + "Received message doesn't match sent message" + ); + } + _ => panic!("Unexpected message type received"), + } + } else { + panic!("Failed to receive message"); + } + + // Close the connection + ws_stream + .close(None) + .await + .expect("Failed to close WebSocket"); +} diff --git a/dynamic-proxy/tests/test_upgrade.rs b/dynamic-proxy/tests/test_upgrade.rs new file mode 100644 index 000000000..7efbcdc6c --- /dev/null +++ b/dynamic-proxy/tests/test_upgrade.rs @@ -0,0 +1,67 @@ +use bytes::Bytes; +use common::simple_upgrade_service::SimpleUpgradeService; +use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; +use http_body_util::Empty; +use hyper::{ + header::{HeaderValue, UPGRADE}, + Request, StatusCode, +}; +use hyper_util::rt::TokioIo; +use std::net::SocketAddr; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; + +mod common; + +#[tokio::test] +async fn test_upgrade() { + let service = SimpleUpgradeService; + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + let _server = SimpleHttpServer::new(service, listener, HttpsConfig::Http).unwrap(); + + let url = format!("http://{}", addr); + + let req = Request::builder() + .uri(url) + .header(UPGRADE, "websocket") + .body(Empty::::new()) + .unwrap(); + + let stream = TcpStream::connect(addr).await.unwrap(); + let io = TokioIo::new(stream); + + let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap(); + + let handle = tokio::task::spawn(async move { + // conn.with_upgrades() will block until sender.send_request() is called. + // It's not clear to me why, but the example to run it in its own task + // comes from this example: + // https://github.com/hyperium/hyper/blob/master/examples/upgrades.rs + if let Err(err) = conn.with_upgrades().await { + Err(anyhow::anyhow!("Connection failed: {:?}", err)) + } else { + Ok(()) + } + }); + + let res = sender.send_request(req).await.unwrap(); + handle.await.unwrap().unwrap(); + + assert_eq!(res.status(), StatusCode::SWITCHING_PROTOCOLS); + assert_eq!( + res.headers().get(UPGRADE).unwrap(), + &HeaderValue::from_static("websocket") + ); + + let upgraded = hyper::upgrade::on(res).await.unwrap(); + let mut upgraded = TokioIo::new(upgraded); + upgraded.write_all(b"Hello from the client!").await.unwrap(); + + let mut buf = vec![0; 1024]; + let n = upgraded.read(&mut buf).await.unwrap(); + assert_eq!(&buf[..n], b"Hello from the client!"); + + upgraded.flush().await.unwrap(); +} From ecd45f81cf41e598e31b5277ae5f427063a19ae4 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 08:28:18 -0400 Subject: [PATCH 02/66] new proxy is integrated --- Cargo.lock | 765 ++++++++-- dynamic-proxy/Cargo.lock | 2030 --------------------------- dynamic-proxy/src/lib.rs | 4 + dynamic-proxy/src/request.rs | 4 +- plane/Cargo.toml | 1 + plane/src/proxy/cert_manager.rs | 14 +- plane/src/proxy/cert_pair.rs | 24 +- plane/src/proxy/mod.rs | 71 +- plane/src/proxy/proxy_connection.rs | 21 +- plane/src/proxy/subdomain.rs | 110 -- plane/src/proxy/tls.rs | 120 -- 11 files changed, 751 insertions(+), 2413 deletions(-) delete mode 100644 dynamic-proxy/Cargo.lock delete mode 100644 plane/src/proxy/subdomain.rs delete mode 100644 plane/src/proxy/tls.rs diff --git a/Cargo.lock b/Cargo.lock index 2c1bc94c0..705ef0c6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "data-encoding", "hyper 1.4.1", "openssl", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "thiserror", @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "asn1-rs" @@ -212,12 +212,45 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +[[package]] +name = "aws-lc-rs" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ddc4a5b231dd6958b140ff3151b6412b3f4321fab354f399eec8f14b06df62" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "axum" version = "0.6.20" @@ -247,20 +280,21 @@ dependencies = [ "sha1", "sync_wrapper 0.1.2", "tokio", - "tokio-tungstenite", - "tower", + "tokio-tungstenite 0.20.1", + "tower 0.4.13", "tower-layer", "tower-service", ] [[package]] name = "axum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" dependencies = [ "async-trait", - "axum-core 0.4.3", + "axum-core 0.4.4", + "base64 0.21.7", "bytes", "futures-util", "http 1.1.0", @@ -279,9 +313,11 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1", "sync_wrapper 1.0.1", "tokio", - "tower", + "tokio-tungstenite 0.23.1", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -306,9 +342,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00" dependencies = [ "async-trait", "bytes", @@ -319,7 +355,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -358,6 +394,29 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.5.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.60", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -440,15 +499,29 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cc" -version = "1.0.95" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -468,7 +541,18 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", ] [[package]] @@ -511,6 +595,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -743,6 +836,38 @@ dependencies = [ "tokio", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dynamic-proxy" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.6", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "rcgen", + "reqwest 0.12.7", + "rustls 0.23.13", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-rustls 0.26.0", + "tokio-tungstenite 0.24.0", + "tracing", +] + [[package]] name = "either" version = "1.13.0" @@ -870,6 +995,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.30" @@ -981,6 +1112,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.3.26" @@ -1000,6 +1137,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1160,7 +1316,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1183,6 +1339,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -1218,16 +1375,49 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.28", - "rustls", + "rustls 0.21.11", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", + "rustls 0.23.13", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", @@ -1238,7 +1428,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -1344,12 +1534,30 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1368,12 +1576,28 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.48.5", +] + [[package]] name = "libm" version = "0.2.8" @@ -1486,6 +1710,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1620,6 +1867,12 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "openssl-sys" version = "0.9.103" @@ -1772,6 +2025,7 @@ dependencies = [ "colored", "dashmap", "data-encoding", + "dynamic-proxy", "futures-util", "http-body 0.4.6", "hyper 0.14.28", @@ -1779,7 +2033,7 @@ dependencies = [ "openssl", "pem", "rand", - "reqwest", + "reqwest 0.11.27", "ring", "rusqlite", "rustls-pemfile 2.1.2", @@ -1791,15 +2045,15 @@ dependencies = [ "thiserror", "time", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-stream", - "tokio-tungstenite", - "tower", + "tokio-tungstenite 0.20.1", + "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", "trust-dns-server", - "tungstenite", + "tungstenite 0.20.1", "url", "valuable", "x509-parser", @@ -1827,14 +2081,14 @@ version = "0.4.12" dependencies = [ "anyhow", "async-trait", - "axum 0.7.5", + "axum 0.7.6", "bollard", "chrono", "futures-util", "hyper 0.14.28", "plane-dynamic", "plane-test-macro", - "reqwest", + "reqwest 0.11.27", "serde_json", "thiserror", "tokio", @@ -1856,6 +2110,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.60", +] + [[package]] name = "proc-macro2" version = "1.0.81" @@ -1904,6 +2168,19 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rcgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1968,11 +2245,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", - "hyper-rustls", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", @@ -1980,15 +2257,15 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.21.11", "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tower-service", "url", "wasm-bindgen", @@ -1998,6 +2275,51 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64 0.22.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-rustls 0.27.3", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.1.2", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + [[package]] name = "ring" version = "0.17.8" @@ -2054,6 +2376,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -2084,10 +2412,26 @@ checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -2109,9 +2453,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -2123,6 +2467,18 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.15" @@ -2135,6 +2491,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2151,20 +2516,43 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags 2.5.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" -version = "1.0.198" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -2173,11 +2561,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -2285,6 +2674,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2409,7 +2804,7 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", + "rustls 0.21.11", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -2632,6 +3027,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2653,7 +3051,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.5.0", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -2666,6 +3075,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -2680,18 +3099,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -2756,9 +3175,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -2782,13 +3201,34 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.11", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.13", + "rustls-pki-types", "tokio", ] @@ -2812,13 +3252,37 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", - "rustls", + "rustls 0.21.11", "tokio", - "tokio-rustls", - "tungstenite", + "tokio-rustls 0.24.1", + "tungstenite 0.20.1", "webpki-roots", ] +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.23.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.24.0", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -2883,6 +3347,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.4.4" @@ -2904,15 +3384,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -3081,13 +3561,49 @@ dependencies = [ "httparse", "log", "rand", - "rustls", + "rustls 0.21.11", "sha1", "thiserror", "url", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -3292,6 +3808,19 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" @@ -3308,6 +3837,18 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "whoami" version = "1.5.1" @@ -3346,7 +3887,37 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -3364,7 +3935,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -3384,18 +3964,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -3406,9 +3986,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -3418,9 +3998,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -3430,15 +4010,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -3448,9 +4028,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -3460,9 +4040,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -3472,9 +4052,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -3484,9 +4064,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -3524,6 +4104,15 @@ dependencies = [ "time", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/dynamic-proxy/Cargo.lock b/dynamic-proxy/Cargo.lock deleted file mode 100644 index a54b99d35..000000000 --- a/dynamic-proxy/Cargo.lock +++ /dev/null @@ -1,2030 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" - -[[package]] -name = "async-trait" -version = "0.1.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "aws-lc-rs" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" -dependencies = [ - "aws-lc-sys", - "mirai-annotations", - "paste", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", - "libc", - "paste", -] - -[[package]] -name = "axum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" -dependencies = [ - "async-trait", - "axum-core", - "base64 0.21.7", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper 1.0.1", - "tokio", - "tokio-tungstenite 0.23.1", - "tower 0.5.1", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.1", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bindgen" -version = "0.69.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", - "which", -] - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" - -[[package]] -name = "cc" -version = "1.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" -dependencies = [ - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "cmake" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" -dependencies = [ - "cc", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "data-encoding" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dynamic-proxy" -version = "0.1.0" -dependencies = [ - "anyhow", - "axum", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "pin-project-lite", - "rcgen", - "reqwest", - "rustls", - "serde", - "serde_json", - "thiserror", - "tokio", - "tokio-rustls", - "tokio-tungstenite 0.24.0", - "tracing", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "encoding_rs" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "h2" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "http" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "socket2", - "tokio", - "tower 0.4.13", - "tower-service", - "tracing", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "ipnet" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "libloading" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" -dependencies = [ - "cfg-if", - "windows-targets", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi", - "libc", - "wasi", - "windows-sys 0.52.0", -] - -[[package]] -name = "mirai-annotations" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" - -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "object" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "openssl" -version = "0.10.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pem" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" -dependencies = [ - "base64 0.22.1", - "serde", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rcgen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "yasna", -] - -[[package]] -name = "regex" -version = "1.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "reqwest" -version = "0.12.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 1.0.1", - "system-configuration", - "tokio", - "tokio-native-tls", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "windows-registry", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustix" -version = "0.38.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.23.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" -dependencies = [ - "base64 0.22.1", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "schannel" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "serde" -version = "1.0.210" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.210" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.128" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" -dependencies = [ - "futures-core", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" -dependencies = [ - "cfg-if", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "thiserror" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "tinyvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.23.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.24.0", -] - -[[package]] -name = "tokio-util" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 0.1.2", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand", - "sha1", - "thiserror", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand", - "sha1", - "thiserror", - "utf-8", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - -[[package]] -name = "unicode-ident" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" - -[[package]] -name = "wasm-streams" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - -[[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result", - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/dynamic-proxy/src/lib.rs b/dynamic-proxy/src/lib.rs index c2ca86831..f6bcd5d81 100644 --- a/dynamic-proxy/src/lib.rs +++ b/dynamic-proxy/src/lib.rs @@ -4,3 +4,7 @@ pub mod proxy; pub mod request; pub mod server; mod upgrade; + +pub use hyper; +pub use rustls; +pub use tokio_rustls; diff --git a/dynamic-proxy/src/request.rs b/dynamic-proxy/src/request.rs index af9deedf7..937fdbc8b 100644 --- a/dynamic-proxy/src/request.rs +++ b/dynamic-proxy/src/request.rs @@ -14,8 +14,8 @@ where T: Body, T::Error: Into>, { - parts: Parts, - body: T, + pub parts: Parts, + pub body: T, } impl MutableRequest diff --git a/plane/Cargo.toml b/plane/Cargo.toml index 4451f07ca..1f59ee183 100644 --- a/plane/Cargo.toml +++ b/plane/Cargo.toml @@ -21,6 +21,7 @@ clap = { version = "4.4.10", features = ["derive"] } colored = "2.0.4" dashmap = "5.5.3" data-encoding = "2.4.0" +dynamic-proxy = { path="../dynamic-proxy" } futures-util = "0.3.29" http-body = "0.4.6" hyper = { version = "0.14.27", features = ["server"] } diff --git a/plane/src/proxy/cert_manager.rs b/plane/src/proxy/cert_manager.rs index 3fc733511..b7e74cea5 100644 --- a/plane/src/proxy/cert_manager.rs +++ b/plane/src/proxy/cert_manager.rs @@ -10,6 +10,10 @@ use acme2_eab::{ }; use anyhow::{anyhow, Context, Result}; use chrono::Utc; +use dynamic_proxy::tokio_rustls::rustls::{ + server::{ClientHello, ResolvesServerCert}, + sign::CertifiedKey, +}; use std::{ ops::Sub, path::{Path, PathBuf}, @@ -20,10 +24,6 @@ use tokio::sync::{ broadcast, watch::{Receiver, Sender}, }; -use tokio_rustls::rustls::{ - server::{ClientHello, ResolvesServerCert}, - sign::CertifiedKey, -}; use valuable::Valuable; const DNS_01: &str = "dns-01"; @@ -87,6 +87,12 @@ impl CertWatcher { } } +impl std::fmt::Debug for CertWatcher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CertWatcher") + } +} + impl ResolvesServerCert for CertWatcher { fn resolve(&self, _client_hello: ClientHello<'_>) -> Option> { if self diff --git a/plane/src/proxy/cert_pair.rs b/plane/src/proxy/cert_pair.rs index 0066a8d40..19a1d439b 100644 --- a/plane/src/proxy/cert_pair.rs +++ b/plane/src/proxy/cert_pair.rs @@ -1,13 +1,15 @@ use crate::log_types::LoggableTime; use anyhow::{anyhow, Result}; +use dynamic_proxy::rustls::{ + crypto::aws_lc_rs::sign::any_supported_type, pki_types::PrivateKeyDer, +}; +use dynamic_proxy::tokio_rustls::rustls::sign::CertifiedKey; + use pem::Pem; -use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs1KeyDer}; +use rustls_pki_types::CertificateDer; +// use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs1KeyDer}; use serde::{Deserialize, Serialize}; use std::{fs::Permissions, io, os::unix::fs::PermissionsExt, path::Path}; -use tokio_rustls::rustls::{ - sign::{any_supported_type, CertifiedKey}, - Certificate, PrivateKey, -}; use x509_parser::{certificate::X509Certificate, oid_registry::asn1_rs::FromDer}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -68,13 +70,15 @@ impl CertificatePair { let validity_start = validity.not_before.to_datetime(); let validity_end = validity.not_after.to_datetime(); + // Convert the Vec> to a Vec> + // by copying the certificate data. let certs = certs .into_iter() - .map(|cert| Certificate(cert.to_vec())) + .map(|cert| CertificateDer::from(cert.to_vec())) .collect(); - let private_key = PrivateKey(key.secret_der().to_vec()); // NB. rustls 0.22 gets rid of this; the PrivateKeyDer is passed to any_supported_type directly. - let key = any_supported_type(&private_key)?; + // let private_key = PrivateKey(key.secret_der().to_vec()); // NB. rustls 0.22 gets rid of this; the PrivateKeyDer is passed to any_supported_type directly. + let key = any_supported_type(&key)?; let certified_key = CertifiedKey::new(certs, key); @@ -93,8 +97,8 @@ impl CertificatePair { .map(|cert_der| CertificateDer::from(cert_der.to_vec())) .collect(); - let key = PrivatePkcs1KeyDer::from(pkey_der.to_vec()); - let key: PrivateKeyDer = key.into(); + let key = PrivateKeyDer::try_from(pkey_der.to_vec()) + .map_err(|e| anyhow!("Error converting private key to der: {}", e))?; Self::new(&key, certs) } diff --git a/plane/src/proxy/mod.rs b/plane/src/proxy/mod.rs index 4e2a33cc6..f51d336a2 100644 --- a/plane/src/proxy/mod.rs +++ b/plane/src/proxy/mod.rs @@ -1,13 +1,14 @@ use self::proxy_connection::ProxyConnection; use crate::names::ProxyName; use crate::proxy::cert_manager::watcher_manager_pair; -use crate::proxy::proxy_service::ProxyMakeService; -use crate::proxy::shutdown_signal::ShutdownSignal; use crate::{client::PlaneClient, signals::wait_for_shutdown_signal, types::ClusterName}; use anyhow::Result; +use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; use serde::{Deserialize, Serialize}; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; +use std::time::Duration; +use tokio::net::TcpListener; use url::Url; pub mod cert_manager; @@ -15,12 +16,11 @@ mod cert_pair; pub mod command; mod connection_monitor; pub mod proxy_connection; -mod proxy_service; -mod rewriter; +// mod proxy_service; +mod proxy_server; +// mod rewriter; +mod request; mod route_map; -mod shutdown_signal; -mod subdomain; -mod tls; #[derive(Debug, Clone, Copy)] pub enum Protocol { @@ -37,19 +37,6 @@ impl Protocol { } } -/// Information about the incoming request that is forwarded to the request in -/// X-Forwarded-* headers. -#[derive(Debug, Clone, Copy)] -pub struct ForwardableRequestInfo { - /// The IP address of the client that made the request. - /// Forwarded as X-Forwarded-For. - ip: IpAddr, - - /// The protocol of the incoming request. - /// Forwarded as X-Forwarded-Proto. - protocol: Protocol, -} - #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct ServerPortConfig { pub http_port: u16, @@ -120,7 +107,6 @@ pub async fn run_proxy(config: ProxyConfig) -> Result<()> { .await?; let proxy_connection = ProxyConnection::new(config.name, client, config.cluster, cert_manager); - let shutdown_signal = ShutdownSignal::new(); let https_redirect = config.port_config.https_port.is_some(); @@ -128,35 +114,40 @@ pub async fn run_proxy(config: ProxyConfig) -> Result<()> { cert_watcher.wait_for_initial_cert().await?; } - let http_handle = ProxyMakeService { - state: proxy_connection.state(), - https_redirect, - root_redirect_url: config.root_redirect_url.clone(), - } - .serve_http(config.port_config.http_port, shutdown_signal.subscribe())?; + let tcp_addr = SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + config.port_config.http_port, + ); + let tcp_listener = TcpListener::bind(tcp_addr).await?; + let http_server = + SimpleHttpServer::new(proxy_connection.state(), tcp_listener, HttpsConfig::http())?; - let https_handle = if let Some(https_port) = config.port_config.https_port { + let https_server = if let Some(https_port) = config.port_config.https_port { tracing::info!("Waiting for initial certificate."); - let https_handle = ProxyMakeService { - state: proxy_connection.state(), - https_redirect: false, - root_redirect_url: config.root_redirect_url, - } - .serve_https(https_port, cert_watcher, shutdown_signal.subscribe())?; + let tcp_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), https_port); + let tcp_listener = TcpListener::bind(tcp_addr).await?; + let https_server = SimpleHttpServer::new( + proxy_connection.state(), + tcp_listener, + HttpsConfig::from_resolver(cert_watcher), + )?; - Some(https_handle) + Some(https_server) } else { None }; wait_for_shutdown_signal().await; - shutdown_signal.shutdown(); tracing::info!("Shutting down proxy server."); - http_handle.await?; - if let Some(https_handle) = https_handle { - https_handle.await?; + http_server + .graceful_shutdown_with_timeout(Duration::from_secs(10)) + .await; + if let Some(https_server) = https_server { + https_server + .graceful_shutdown_with_timeout(Duration::from_secs(10)) + .await; } Ok(()) diff --git a/plane/src/proxy/proxy_connection.rs b/plane/src/proxy/proxy_connection.rs index 18aac7646..827f279f1 100644 --- a/plane/src/proxy/proxy_connection.rs +++ b/plane/src/proxy/proxy_connection.rs @@ -1,4 +1,4 @@ -use super::{cert_manager::CertManager, proxy_service::ProxyState}; +use super::{cert_manager::CertManager, proxy_server::ProxyState}; use crate::{ client::PlaneClient, names::ProxyName, @@ -41,13 +41,16 @@ impl ProxyConnection { }); let sender = conn.sender(MessageFromProxy::RouteInfoRequest); - state.route_map.set_sender(move |m: RouteInfoRequest| { - if let Err(e) = sender.send(m) { - tracing::error!(?e, "Error sending route info request."); - } - }); + state + .inner + .route_map + .set_sender(move |m: RouteInfoRequest| { + if let Err(e) = sender.send(m) { + tracing::error!(?e, "Error sending route info request."); + } + }); let sender = conn.sender(MessageFromProxy::KeepAlive); - state.monitor.set_listener(move |backend| { + state.inner.monitor.set_listener(move |backend| { if let Err(err) = sender.send(backend.clone()) { tracing::error!(?err, "Error sending keepalive."); } @@ -56,7 +59,7 @@ impl ProxyConnection { while let Some(message) = conn.recv().await { match message { MessageToProxy::RouteInfoResponse(response) => { - state.route_map.receive(response); + state.inner.route_map.receive(response); } MessageToProxy::CertManagerResponse(response) => { tracing::info!( @@ -66,7 +69,7 @@ impl ProxyConnection { cert_manager.receive(response); } MessageToProxy::BackendRemoved { backend } => { - state.route_map.remove_backend(&backend); + state.inner.route_map.remove_backend(&backend); } } } diff --git a/plane/src/proxy/subdomain.rs b/plane/src/proxy/subdomain.rs deleted file mode 100644 index cf0db47b6..000000000 --- a/plane/src/proxy/subdomain.rs +++ /dev/null @@ -1,110 +0,0 @@ -use super::rewriter::RequestRewriterError; -use crate::types::ClusterName; - -// If a cluster name does not specify a port, :443 is implied. -// Most browsers will not specify it, but some (e.g. the `ws` websocket client in Node.js) -// will, so we strip it. -const HTTPS_PORT_SUFFIX: &str = ":443"; - -/// Returns Ok(Some(subdomain)) if a subdomain is found. -/// Returns Ok(None) if no subdomain is found, but the host header matches the cluster name. -/// Returns Err(RequestRewriterError::InvalidHostHeader) if the host header does not -/// match the cluster name. -pub fn subdomain_from_host<'a>( - host: &'a str, - cluster: &ClusterName, -) -> Result, RequestRewriterError> { - let host = if let Some(host) = host.strip_suffix(HTTPS_PORT_SUFFIX) { - host - } else { - host - }; - - if let Some(subdomain) = host.strip_suffix(cluster.as_str()) { - if subdomain.is_empty() { - // Subdomain exactly matches cluster name. - Ok(None) - } else if let Some(subdomain) = subdomain.strip_suffix('.') { - Ok(Some(subdomain)) - } else { - Err(RequestRewriterError::InvalidHostHeader) - } - } else { - tracing::warn!(host, "Host header does not end in cluster name."); - Err(RequestRewriterError::InvalidHostHeader) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::str::FromStr; - - #[test] - fn no_subdomains() { - let host = "foo.bar.baz"; - let cluster = ClusterName::from_str("foo.bar.baz").unwrap(); - assert_eq!(subdomain_from_host(host, &cluster), Ok(None)); - } - - #[test] - fn valid_subdomain() { - let host = "foobar.example.com"; - let cluster = ClusterName::from_str("example.com").unwrap(); - assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar"))); - } - - #[test] - fn valid_suffix_no_dot() { - let host = "foobarexample.com"; - let cluster = ClusterName::from_str("example.com").unwrap(); - assert_eq!( - subdomain_from_host(host, &cluster), - Err(RequestRewriterError::InvalidHostHeader) - ); - } - - #[test] - fn invalid_suffix() { - let host = "abc.abc.com"; - let cluster = ClusterName::from_str("example.com").unwrap(); - assert_eq!( - subdomain_from_host(host, &cluster), - Err(RequestRewriterError::InvalidHostHeader) - ); - } - - #[test] - fn allowed_port() { - let host = "foobar.myhost:8080"; - let cluster = ClusterName::from_str("myhost:8080").unwrap(); - assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar"))); - } - - #[test] - fn port_required() { - let host = "foobar.myhost"; - let cluster = ClusterName::from_str("myhost:8080").unwrap(); - assert_eq!( - subdomain_from_host(host, &cluster), - Err(RequestRewriterError::InvalidHostHeader) - ); - } - - #[test] - fn port_must_match() { - let host = "foobar.myhost:8080"; - let cluster = ClusterName::from_str("myhost").unwrap(); - assert_eq!( - subdomain_from_host(host, &cluster), - Err(RequestRewriterError::InvalidHostHeader) - ); - } - - #[test] - fn port_443_optional() { - let host = "foobar.myhost:443"; - let cluster = ClusterName::from_str("myhost").unwrap(); - assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar"))); - } -} diff --git a/plane/src/proxy/tls.rs b/plane/src/proxy/tls.rs deleted file mode 100644 index bc954a7a4..000000000 --- a/plane/src/proxy/tls.rs +++ /dev/null @@ -1,120 +0,0 @@ -use core::task::Context; -use futures_util::{ready, Future}; -use hyper::server::accept::Accept; -use hyper::server::conn::{AddrIncoming, AddrStream}; -use std::io; -use std::net::IpAddr; -use std::pin::Pin; -use std::sync::Arc; -use std::task::Poll; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use tokio_rustls::rustls::ServerConfig; - -// From: https://github.com/rustls/hyper-rustls/blob/main/examples/server.rs -pub struct TlsAcceptor { - config: Arc, - incoming: AddrIncoming, -} - -impl TlsAcceptor { - pub fn new(config: Arc, incoming: AddrIncoming) -> TlsAcceptor { - TlsAcceptor { config, incoming } - } -} - -impl Accept for TlsAcceptor { - type Conn = TlsStream; - type Error = io::Error; - - fn poll_accept( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - let pin = self.get_mut(); - match ready!(Pin::new(&mut pin.incoming).poll_accept(cx)) { - Some(Ok(sock)) => Poll::Ready(Some(Ok(TlsStream::new(sock, pin.config.clone())))), - Some(Err(e)) => Poll::Ready(Some(Err(e))), - None => Poll::Ready(None), - } - } -} - -impl AsyncRead for TlsStream { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context, - buf: &mut ReadBuf, - ) -> Poll> { - let pin = self.get_mut(); - match pin.state { - State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { - Ok(mut stream) => { - let result = Pin::new(&mut stream).poll_read(cx, buf); - pin.state = State::Streaming(stream); - result - } - Err(err) => Poll::Ready(Err(err)), - }, - State::Streaming(ref mut stream) => Pin::new(stream).poll_read(cx, buf), - } - } -} - -impl AsyncWrite for TlsStream { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - let pin = self.get_mut(); - match pin.state { - State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { - Ok(mut stream) => { - let result = Pin::new(&mut stream).poll_write(cx, buf); - pin.state = State::Streaming(stream); - result - } - Err(err) => Poll::Ready(Err(err)), - }, - State::Streaming(ref mut stream) => Pin::new(stream).poll_write(cx, buf), - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.state { - State::Handshaking(_) => Poll::Ready(Ok(())), - State::Streaming(ref mut stream) => Pin::new(stream).poll_flush(cx), - } - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.state { - State::Handshaking(_) => Poll::Ready(Ok(())), - State::Streaming(ref mut stream) => Pin::new(stream).poll_shutdown(cx), - } - } -} - -enum State { - Handshaking(tokio_rustls::Accept), - Streaming(tokio_rustls::server::TlsStream), -} - -// tokio_rustls::server::TlsStream doesn't expose constructor methods, -// so we have to TlsAcceptor::accept and handshake to have access to it -// TlsStream implements AsyncRead/AsyncWrite handshaking tokio_rustls::Accept first -pub struct TlsStream { - state: State, - pub remote_ip: IpAddr, -} - -impl TlsStream { - fn new(stream: AddrStream, config: Arc) -> TlsStream { - let remote_ip = stream.remote_addr().ip(); - let accept = tokio_rustls::TlsAcceptor::from(config).accept(stream); - TlsStream { - state: State::Handshaking(accept), - remote_ip, - } - } -} From bc40f2c6c3195887ee50f96ba2f5480713726534 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 08:30:50 -0400 Subject: [PATCH 03/66] remove extra imports --- Cargo.lock | 17 ++-- plane/Cargo.toml | 2 - plane/src/proxy/cert_pair.rs | 2 - plane/src/proxy/proxy_server.rs | 105 +++++++++++++++++++++++++ plane/src/proxy/request.rs | 133 ++++++++++++++++++++++++++++++++ 5 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 plane/src/proxy/proxy_server.rs create mode 100644 plane/src/proxy/request.rs diff --git a/Cargo.lock b/Cargo.lock index 705ef0c6f..5d44c0082 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,9 +812,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", @@ -1595,7 +1595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1755,11 +1755,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2034,7 +2033,6 @@ dependencies = [ "pem", "rand", "reqwest 0.11.27", - "ring", "rusqlite", "rustls-pemfile 2.1.2", "rustls-pki-types", @@ -2045,7 +2043,6 @@ dependencies = [ "thiserror", "time", "tokio", - "tokio-rustls 0.24.1", "tokio-stream", "tokio-tungstenite 0.20.1", "tower 0.4.13", @@ -3639,9 +3636,9 @@ checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unicode_categories" diff --git a/plane/Cargo.toml b/plane/Cargo.toml index 1f59ee183..3e3835249 100644 --- a/plane/Cargo.toml +++ b/plane/Cargo.toml @@ -30,7 +30,6 @@ openssl = "0.10.66" pem = "3.0.2" rand = "0.8.5" reqwest = { version = "0.11.22", features = ["json", "rustls-tls"], default-features = false } -ring = "0.17.5" rusqlite = { version = "0.31.0", features = ["bundled", "serde_json"] } rustls-pemfile = "2.0.0" rustls-pki-types = "1.0.0" @@ -41,7 +40,6 @@ sqlx = { version = "0.8.0", features = ["runtime-tokio", "tls-rustls", "postgres thiserror = "1.0.50" time = "0.3.30" tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread", "signal"] } -tokio-rustls = "0.24.1" tokio-stream = { version="0.1.14", features=["sync"] } tokio-tungstenite = { version = "0.20.1", features = ["rustls-tls-webpki-roots"] } tower = "0.4.13" diff --git a/plane/src/proxy/cert_pair.rs b/plane/src/proxy/cert_pair.rs index 19a1d439b..1a6713459 100644 --- a/plane/src/proxy/cert_pair.rs +++ b/plane/src/proxy/cert_pair.rs @@ -4,10 +4,8 @@ use dynamic_proxy::rustls::{ crypto::aws_lc_rs::sign::any_supported_type, pki_types::PrivateKeyDer, }; use dynamic_proxy::tokio_rustls::rustls::sign::CertifiedKey; - use pem::Pem; use rustls_pki_types::CertificateDer; -// use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs1KeyDer}; use serde::{Deserialize, Serialize}; use std::{fs::Permissions, io, os::unix::fs::PermissionsExt, path::Path}; use x509_parser::{certificate::X509Certificate, oid_registry::asn1_rs::FromDer}; diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs new file mode 100644 index 000000000..aa7ad0ff9 --- /dev/null +++ b/plane/src/proxy/proxy_server.rs @@ -0,0 +1,105 @@ +use super::{ + connection_monitor::ConnectionMonitorHandle, request::get_and_maybe_remove_bearer_token, + route_map::RouteMap, +}; +use dynamic_proxy::{ + body::SimpleBody, + hyper::{body::Incoming, service::Service, Request, Response, Uri}, + proxy::ProxyClient, + request::MutableRequest, +}; +use std::{future::Future, sync::atomic::AtomicBool}; +use std::{pin::Pin, sync::Arc}; + +pub struct ProxyStateInner { + pub route_map: RouteMap, + pub proxy_client: ProxyClient, + pub monitor: ConnectionMonitorHandle, + pub connected: AtomicBool, +} + +pub struct ProxyState { + pub inner: Arc, +} + +impl Default for ProxyState { + fn default() -> Self { + Self::new() + } +} + +impl ProxyState { + pub fn new() -> Self { + let inner = ProxyStateInner { + route_map: RouteMap::new(), + proxy_client: ProxyClient::new(), + monitor: ConnectionMonitorHandle::new(), + connected: AtomicBool::new(false), + }; + + Self { + inner: Arc::new(inner), + } + } + + pub fn set_connected(&self, connected: bool) { + self.inner + .connected + .store(connected, std::sync::atomic::Ordering::Relaxed); + } + + pub fn connected(&self) -> bool { + self.inner + .connected + .load(std::sync::atomic::Ordering::Relaxed) + } +} + +impl Service> for ProxyState { + type Response = Response; + type Error = Box; + type Future = Pin< + Box< + dyn Future< + Output = Result, Box>, + > + Send, + >, + >; + + fn call(&self, request: Request) -> Self::Future { + let mut request = MutableRequest::from_request(request); + + // extract the bearer token from the request + let mut uri_parts = request.parts.uri.clone().into_parts(); + let bearer_token = get_and_maybe_remove_bearer_token(&mut uri_parts); + + // TODO + let bearer_token = bearer_token.unwrap(); + + request.parts.uri = Uri::from_parts(uri_parts).unwrap(); + + let inner = self.inner.clone(); + + Box::pin(async move { + // look up the route info for the bearer token + let route_info = inner.route_map.lookup(&bearer_token).await; + + let Some(route_info) = route_info else { + // TODO + panic!("Route info not found for bearer token"); + }; + + request.set_upstream_address(route_info.address.0); + let request = request.into_request_with_simple_body(); + + let (res, upgrade_handler) = inner.proxy_client.request(request).await.unwrap(); + + let upgrade_handler = upgrade_handler.unwrap(); + tokio::spawn(async move { + upgrade_handler.run().await.unwrap(); + }); + + Ok(res) + }) + } +} diff --git a/plane/src/proxy/request.rs b/plane/src/proxy/request.rs new file mode 100644 index 000000000..e7bf2afcb --- /dev/null +++ b/plane/src/proxy/request.rs @@ -0,0 +1,133 @@ +use crate::types::{BearerToken, ClusterName}; +use dynamic_proxy::hyper::http::uri::{self, PathAndQuery}; +use std::str::FromStr; + +// If a cluster name does not specify a port, :443 is implied. +// Most browsers will not specify it, but some (e.g. the `ws` websocket client in Node.js) +// will, so we strip it. +const HTTPS_PORT_SUFFIX: &str = ":443"; + +/// Returns Ok(Some(subdomain)) if a subdomain is found. +/// Returns Ok(None) if no subdomain is found, but the host header matches the cluster name. +/// Returns Err(()) if the host header does not +/// match the cluster name. +pub fn subdomain_from_host<'a>( + host: &'a str, + cluster: &ClusterName, +) -> Result, ()> { + let host = if let Some(host) = host.strip_suffix(HTTPS_PORT_SUFFIX) { + host + } else { + host + }; + + if let Some(subdomain) = host.strip_suffix(cluster.as_str()) { + if subdomain.is_empty() { + // Subdomain exactly matches cluster name. + Ok(None) + } else if let Some(subdomain) = subdomain.strip_suffix('.') { + Ok(Some(subdomain)) + } else { + Err(()) + } + } else { + tracing::warn!(host, "Host header does not end in cluster name."); + Err(()) + } +} + +/// Removes a connection string from the URI and returns it. +/// If no connection string is found, returns None. +pub fn get_and_maybe_remove_bearer_token(parts: &mut uri::Parts) -> Option { + let path_and_query = parts.path_and_query.clone()?; + + let full_path = path_and_query.path().strip_prefix('/')?; + + // Split the incoming path into the token and the path to proxy to. If there is no slash, the token is + // the full incoming path, and the path to proxy to is just `/`. + let (token, path) = match full_path.split_once('/') { + Some((token, path)) => (token, path), + None => (full_path, "/"), + }; + + let token = BearerToken::from(token.to_string()); + + if token.is_static() { + // We don't rewrite the URL if using a static token. + return Some(token); + } + + let query = path_and_query + .query() + .map(|query| format!("?{}", query)) + .unwrap_or_default(); + + parts.path_and_query = Some( + PathAndQuery::from_str(format!("/{}{}", path, query).as_str()) + .expect("Path and query is valid."), + ); + + Some(token) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn no_subdomains() { + let host = "foo.bar.baz"; + let cluster = ClusterName::from_str("foo.bar.baz").unwrap(); + assert_eq!(subdomain_from_host(host, &cluster), Ok(None)); + } + + #[test] + fn valid_subdomain() { + let host = "foobar.example.com"; + let cluster = ClusterName::from_str("example.com").unwrap(); + assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar"))); + } + + #[test] + fn valid_suffix_no_dot() { + let host = "foobarexample.com"; + let cluster = ClusterName::from_str("example.com").unwrap(); + assert_eq!(subdomain_from_host(host, &cluster), Err(())); + } + + #[test] + fn invalid_suffix() { + let host = "abc.abc.com"; + let cluster = ClusterName::from_str("example.com").unwrap(); + assert_eq!(subdomain_from_host(host, &cluster), Err(())); + } + + #[test] + fn allowed_port() { + let host = "foobar.myhost:8080"; + let cluster = ClusterName::from_str("myhost:8080").unwrap(); + assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar"))); + } + + #[test] + fn port_required() { + let host = "foobar.myhost"; + let cluster = ClusterName::from_str("myhost:8080").unwrap(); + assert_eq!(subdomain_from_host(host, &cluster), Err(())); + } + + #[test] + fn port_must_match() { + let host = "foobar.myhost:8080"; + let cluster = ClusterName::from_str("myhost").unwrap(); + assert_eq!(subdomain_from_host(host, &cluster), Err(())); + } + + #[test] + fn port_443_optional() { + let host = "foobar.myhost:443"; + let cluster = ClusterName::from_str("myhost").unwrap(); + assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar"))); + } +} From f1a06499dfcefc42c6e0c3919cdca9a69ab01055 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 09:41:45 -0400 Subject: [PATCH 04/66] use appropriate imports --- plane/src/client/sse.rs | 12 +++++++----- plane/src/controller/backend_state.rs | 2 +- plane/src/typed_socket/client.rs | 14 +++++++++----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/plane/src/client/sse.rs b/plane/src/client/sse.rs index 81b97d88c..c62636175 100644 --- a/plane/src/client/sse.rs +++ b/plane/src/client/sse.rs @@ -1,7 +1,9 @@ use super::PlaneClientError; use crate::util::ExponentialBackoff; -use hyper::header::{ACCEPT, CONNECTION}; -use reqwest::{Client, Response}; +use reqwest::{ + header::{ACCEPT, CONNECTION}, + Client, Response, +}; use serde::de::DeserializeOwned; use std::marker::PhantomData; use tungstenite::http::HeaderValue; @@ -180,7 +182,7 @@ mod tests { Router, }; use futures_util::stream::Stream; - use hyper::HeaderMap; + use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; use std::{convert::Infallible, time::Duration}; use tokio::{sync::broadcast, task::JoinHandle, time::timeout}; @@ -192,7 +194,7 @@ mod tests { struct DemoSseServer { port: u16, - handle: Option>>, + handle: Option>>, disconnect_sender: broadcast::Sender<()>, } @@ -247,7 +249,7 @@ mod tests { let server = axum::Server::from_tcp(listener) .unwrap() .serve(app.into_make_service()); - let handle = tokio::spawn(server); + let handle = tokio::spawn(async move { server.await.map_err(anyhow::Error::new) }); Self { port, diff --git a/plane/src/controller/backend_state.rs b/plane/src/controller/backend_state.rs index ae169836d..3bff18389 100644 --- a/plane/src/controller/backend_state.rs +++ b/plane/src/controller/backend_state.rs @@ -5,6 +5,7 @@ use crate::{ }; use axum::{ extract::{Path, State}, + http::HeaderMap, response::{ sse::{Event, KeepAlive}, Response, Sse, @@ -12,7 +13,6 @@ use axum::{ Json, }; use futures_util::{Stream, StreamExt}; -use hyper::HeaderMap; use std::convert::Infallible; async fn backend_status( diff --git a/plane/src/typed_socket/client.rs b/plane/src/typed_socket/client.rs index 22bdc8727..10393c026 100644 --- a/plane/src/typed_socket/client.rs +++ b/plane/src/typed_socket/client.rs @@ -8,6 +8,10 @@ use std::marker::PhantomData; use tokio::net::TcpStream; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tungstenite::handshake::client::generate_key; +use tungstenite::http::{ + header::{HeaderValue, AUTHORIZATION}, + Method, Request, +}; use tungstenite::{error::ProtocolError, Message}; type Socket = WebSocketStream>; @@ -88,9 +92,9 @@ impl TypedSocketConnector { } /// Creates a WebSocket request from an AuthorizedAddress. -fn auth_url_to_request(addr: &AuthorizedAddress) -> Result, PlaneClientError> { - let mut request = hyper::Request::builder() - .method(hyper::Method::GET) +fn auth_url_to_request(addr: &AuthorizedAddress) -> Result, PlaneClientError> { + let mut request = Request::builder() + .method(Method::GET) .uri(addr.url.as_str()) .header( "Host", @@ -108,8 +112,8 @@ fn auth_url_to_request(addr: &AuthorizedAddress) -> Result, P if let Some(bearer_header) = addr.bearer_header() { request = request.header( - hyper::header::AUTHORIZATION, - hyper::header::HeaderValue::from_str(&bearer_header).expect("Bearer header is valid"), + AUTHORIZATION, + HeaderValue::from_str(&bearer_header).expect("Bearer header is valid"), ); } From 3aa829ab58d6a1b706cb7fdd7dd89299cd5ebdd2 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 10:37:33 -0400 Subject: [PATCH 05/66] basic proxy works --- dynamic-proxy/src/proxy.rs | 2 -- plane/src/proxy/proxy_server.rs | 9 +++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/dynamic-proxy/src/proxy.rs b/dynamic-proxy/src/proxy.rs index c6adea225..33625dde5 100644 --- a/dynamic-proxy/src/proxy.rs +++ b/dynamic-proxy/src/proxy.rs @@ -87,9 +87,7 @@ impl ProxyClient { request: Request, ) -> Result<(Response, Option), ProxyError> { if should_upgrade(&request) { - println!("Upgrading request"); let (response, upgrade_handler) = self.handle_upgrade(request).await?; - println!("Upgraded request"); Ok((response, Some(upgrade_handler))) } else { let result = self.upstream_request(request).await?; diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index aa7ad0ff9..a63fc0ce0 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -94,10 +94,11 @@ impl Service> for ProxyState { let (res, upgrade_handler) = inner.proxy_client.request(request).await.unwrap(); - let upgrade_handler = upgrade_handler.unwrap(); - tokio::spawn(async move { - upgrade_handler.run().await.unwrap(); - }); + if let Some(upgrade_handler) = upgrade_handler { + tokio::spawn(async move { + upgrade_handler.run().await.unwrap(); + }); + } Ok(res) }) From cb96f3177f772419234f6eb604ac986819f151c8 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 10:47:21 -0400 Subject: [PATCH 06/66] integrate connection monitor --- plane/src/proxy/proxy_server.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index a63fc0ce0..2fb297461 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -95,9 +95,21 @@ impl Service> for ProxyState { let (res, upgrade_handler) = inner.proxy_client.request(request).await.unwrap(); if let Some(upgrade_handler) = upgrade_handler { + let monitor = inner.monitor.monitor(); + monitor + .lock() + .expect("Monitor lock poisoned") + .inc_connection(&route_info.backend_id); tokio::spawn(async move { upgrade_handler.run().await.unwrap(); + + monitor + .lock() + .expect("Monitor lock poisoned") + .dec_connection(&route_info.backend_id); }); + } else { + inner.monitor.touch_backend(&route_info.backend_id); } Ok(res) From 8ccfb1b70bdbc7149f9bf5be23e9fd607cd2e930 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 14:40:18 -0400 Subject: [PATCH 07/66] redirect test work --- plane/src/proxy/mod.rs | 4 ++++ plane/src/proxy/proxy_redirects_http.rs | 0 2 files changed, 4 insertions(+) create mode 100644 plane/src/proxy/proxy_redirects_http.rs diff --git a/plane/src/proxy/mod.rs b/plane/src/proxy/mod.rs index f51d336a2..2f03d4031 100644 --- a/plane/src/proxy/mod.rs +++ b/plane/src/proxy/mod.rs @@ -39,7 +39,11 @@ impl Protocol { #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct ServerPortConfig { + /// The port to listen on for HTTP requests. + /// If https_port is provided, this pub http_port: u16, + + /// The port to listen on for HTTPS requests. pub https_port: Option, } diff --git a/plane/src/proxy/proxy_redirects_http.rs b/plane/src/proxy/proxy_redirects_http.rs new file mode 100644 index 000000000..e69de29bb From 768880c16ddf17894ed3631cf1d1cb9039e0f279 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 15:26:24 -0400 Subject: [PATCH 08/66] https redirect work --- dynamic-proxy/src/https_redirect.rs | 37 ++++++++++ dynamic-proxy/src/lib.rs | 1 + dynamic-proxy/tests/test_http_redirect.rs | 79 ++++++++++++++++++++++ plane/plane-tests/tests/common/test_env.rs | 13 ++-- 4 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 dynamic-proxy/src/https_redirect.rs create mode 100644 dynamic-proxy/tests/test_http_redirect.rs diff --git a/dynamic-proxy/src/https_redirect.rs b/dynamic-proxy/src/https_redirect.rs new file mode 100644 index 000000000..e0d1705e9 --- /dev/null +++ b/dynamic-proxy/src/https_redirect.rs @@ -0,0 +1,37 @@ +use crate::body::{simple_empty_body, SimpleBody}; +use http::{header, Request, Response, StatusCode}; +use hyper::{body::Incoming, service::Service}; +use std::{future::ready, pin::Pin, sync::Arc}; + +#[derive(Debug, Clone)] +pub struct HttpsRedirectService { + base_url: Arc, +} + +impl HttpsRedirectService { + pub fn new(base_url: String) -> Self { + Self { + base_url: Arc::new(base_url), + } + } +} + +impl Service> for HttpsRedirectService { + type Response = Response; + type Error = http::Error; + type Future = Pin, http::Error>>>>; + + fn call(&self, request: Request) -> Self::Future { + let request_uri = request.uri(); + let path_and_query = request_uri.path_and_query(); + let path = path_and_query.map(|pq| pq.as_str()).unwrap_or("/"); + + let base_url = self.base_url.as_str(); + let response = Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, format!("https://{base_url}{path}")) + .body(simple_empty_body()); + + Box::pin(ready(response)) + } +} diff --git a/dynamic-proxy/src/lib.rs b/dynamic-proxy/src/lib.rs index f6bcd5d81..d42c6e56b 100644 --- a/dynamic-proxy/src/lib.rs +++ b/dynamic-proxy/src/lib.rs @@ -1,5 +1,6 @@ pub mod body; mod graceful_shutdown; +pub mod https_redirect; pub mod proxy; pub mod request; pub mod server; diff --git a/dynamic-proxy/tests/test_http_redirect.rs b/dynamic-proxy/tests/test_http_redirect.rs new file mode 100644 index 000000000..c50a97398 --- /dev/null +++ b/dynamic-proxy/tests/test_http_redirect.rs @@ -0,0 +1,79 @@ +use dynamic_proxy::{ + https_redirect::HttpsRedirectService, + server::{HttpsConfig, SimpleHttpServer}, +}; +use http::{header, StatusCode}; +use reqwest::{Response, Url}; +use std::net::{IpAddr, SocketAddr}; +use tokio::net::TcpListener; + +const DOMAIN: &str = "foo.bar.baz"; + +fn get_client() -> reqwest::Client { + reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .resolve(DOMAIN, SocketAddr::new(IpAddr::from([127, 0, 0, 1]), 0)) + .build() + .unwrap() +} + +async fn do_request(url: &str) -> Response { + let service = HttpsRedirectService::new("foo.bar.baz".to_string()); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let https_config = HttpsConfig::http(); + let _server = SimpleHttpServer::new(service, listener, https_config); + + // url needs to have port + let mut url = Url::parse(url).unwrap(); + url.set_port(Some(port)).unwrap(); + + // Request to http://foo.bar.baz should redirect to https://foo.bar.baz + let response = get_client().get(url).send().await.unwrap(); + + response +} + +#[tokio::test] +async fn test_https_redirect() { + let response = do_request("http://foo.bar.baz").await; + + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!( + response.headers().get(header::LOCATION).unwrap(), + "https://foo.bar.baz/" + ); +} + +#[tokio::test] +async fn test_https_redirect_with_slash_path() { + let response = do_request("http://foo.bar.baz/").await; + + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!( + response.headers().get(header::LOCATION).unwrap(), + "https://foo.bar.baz/" + ); +} + +#[tokio::test] +async fn test_https_redirect_with_path() { + let response = do_request("http://foo.bar.baz/abc/123").await; + + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!( + response.headers().get(header::LOCATION).unwrap(), + "https://foo.bar.baz/abc/123" + ); +} + +#[tokio::test] +async fn test_https_redirect_with_query_params() { + let response = do_request("http://foo.bar.baz/?a=1&b=2").await; + + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!( + response.headers().get(header::LOCATION).unwrap(), + "https://foo.bar.baz/?a=1&b=2" + ); +} diff --git a/plane/plane-tests/tests/common/test_env.rs b/plane/plane-tests/tests/common/test_env.rs index 74c800bf7..15c76ef2a 100644 --- a/plane/plane-tests/tests/common/test_env.rs +++ b/plane/plane-tests/tests/common/test_env.rs @@ -4,18 +4,15 @@ use super::{ }; use chrono::Duration; use plane::{ + client::PlaneClient, controller::ControllerServer, database::PlaneDatabase, dns::run_dns_with_listener, - drone::{ - runtime::{ - docker::DockerRuntimeConfig, - unix_socket::{MessageToClient, MessageToServer, UnixSocketRuntimeConfig}, - }, - Drone, DroneConfig, ExecutorConfig, - }, - names::{AcmeDnsServerName, ControllerName, DroneName, Name}, + drone::runtime::unix_socket::{MessageToClient, MessageToServer, UnixSocketRuntimeConfig}, + drone::{runtime::docker::DockerRuntimeConfig, Drone, DroneConfig, ExecutorConfig}, + names::{AcmeDnsServerName, ControllerName, DroneName, Name, ProxyName}, proxy::AcmeEabConfiguration, + proxy::{cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection}, typed_unix_socket::{server::TypedUnixSocketServer, WrappedMessage}, types::{ClusterName, DronePoolName}, util::random_string, From b1d281a6264129870f8a929960e56d267517486d Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 16:01:02 -0400 Subject: [PATCH 09/66] proxy progress --- dynamic-proxy/Cargo.toml | 2 +- dynamic-proxy/src/https_redirect.rs | 49 ++++++++----- dynamic-proxy/src/server.rs | 83 +++++++++++++++++++++- dynamic-proxy/tests/test_http_redirect.rs | 3 +- plane/plane-tests/tests/common/test_env.rs | 4 +- plane/src/proxy/mod.rs | 53 ++++++-------- 6 files changed, 138 insertions(+), 56 deletions(-) diff --git a/dynamic-proxy/Cargo.toml b/dynamic-proxy/Cargo.toml index c6acaf92b..9429139c7 100644 --- a/dynamic-proxy/Cargo.toml +++ b/dynamic-proxy/Cargo.toml @@ -14,6 +14,7 @@ hyper-util = { version = "0.1.8", features = ["http1", "http2", "server", "serve pin-project-lite = "0.2.14" rustls = { version = "0.23.13", features = ["ring"] } thiserror = "1.0.63" +serde = { version = "1.0.210", features = ["derive"] } tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } tokio-rustls = "0.26.0" tracing = "0.1.40" @@ -24,6 +25,5 @@ futures-util = "0.3.30" http = "1.1.0" rcgen = "0.13.1" reqwest = { version = "0.12.7", features = ["http2", "stream"] } -serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" tokio-tungstenite = "0.24.0" diff --git a/dynamic-proxy/src/https_redirect.rs b/dynamic-proxy/src/https_redirect.rs index e0d1705e9..98faa2912 100644 --- a/dynamic-proxy/src/https_redirect.rs +++ b/dynamic-proxy/src/https_redirect.rs @@ -1,20 +1,14 @@ use crate::body::{simple_empty_body, SimpleBody}; -use http::{header, Request, Response, StatusCode}; +use http::{ + header, + uri::{Authority, Scheme}, + Request, Response, StatusCode, Uri, +}; use hyper::{body::Incoming, service::Service}; -use std::{future::ready, pin::Pin, sync::Arc}; +use std::{future::ready, pin::Pin, str::FromStr}; #[derive(Debug, Clone)] -pub struct HttpsRedirectService { - base_url: Arc, -} - -impl HttpsRedirectService { - pub fn new(base_url: String) -> Self { - Self { - base_url: Arc::new(base_url), - } - } -} +pub struct HttpsRedirectService; impl Service> for HttpsRedirectService { type Response = Response; @@ -22,14 +16,33 @@ impl Service> for HttpsRedirectService { type Future = Pin, http::Error>>>>; fn call(&self, request: Request) -> Self::Future { - let request_uri = request.uri(); - let path_and_query = request_uri.path_and_query(); - let path = path_and_query.map(|pq| pq.as_str()).unwrap_or("/"); + // Get the host header. + let hostname = request.headers().get(header::HOST).unwrap(); + // Parse the host header into an authority. + let authority = Authority::from_str(hostname.to_str().unwrap()) + .expect("Valid host is valid authority."); + // Strip the path. + let authority = + Authority::from_str(authority.host()).expect("Valid host is valid authority."); + + let request_uri = request.uri().clone(); + + // Set the scheme to HTTPS + let mut parts = request_uri.into_parts(); + parts.scheme = Some(Scheme::HTTPS); + + // Remove the port from the authority if it exists + // let authority = parts.authority.map(|d| d.host().to_string()).unwrap(); + // parts.authority = Some(Authority::from_str(authority).unwrap()); + + parts.authority = Some(authority); + + // Build the new URI + let new_uri = Uri::from_parts(parts).unwrap(); - let base_url = self.base_url.as_str(); let response = Response::builder() .status(StatusCode::FOUND) - .header(header::LOCATION, format!("https://{base_url}{path}")) + .header(header::LOCATION, new_uri.to_string()) .body(simple_empty_body()); Box::pin(ready(response)) diff --git a/dynamic-proxy/src/server.rs b/dynamic-proxy/src/server.rs index c9fc7cc74..dd1503e09 100644 --- a/dynamic-proxy/src/server.rs +++ b/dynamic-proxy/src/server.rs @@ -1,4 +1,6 @@ -use crate::{body::SimpleBody, graceful_shutdown::GracefulShutdown}; +use crate::{ + body::SimpleBody, graceful_shutdown::GracefulShutdown, https_redirect::HttpsRedirectService, +}; use anyhow::Result; use hyper::{body::Incoming, service::Service, Request, Response}; use hyper_util::{ @@ -6,7 +8,7 @@ use hyper_util::{ server::conn::auto::Builder as ServerBuilder, }; use rustls::{server::ResolvesServerCert, ServerConfig}; -use std::{sync::Arc, time::Duration}; +use std::{net::SocketAddr, sync::Arc, time::Duration}; use tokio::{net::TcpListener, select, task::JoinSet}; use tokio_rustls::TlsAcceptor; @@ -202,3 +204,80 @@ impl Drop for SimpleHttpServer { self.handle.abort(); } } + +pub struct ServerWithHttpRedirect { + http_server: SimpleHttpServer, + https_server: Option, +} + +pub struct ServerWithHttpRedirectHttpsConfig { + pub https_port: u16, + pub resolver: Arc, +} + +pub struct ServerWithHttpRedirectConfig { + pub http_port: u16, + pub https_config: Option, +} + +impl ServerWithHttpRedirect { + pub async fn new(service: S, server_config: ServerWithHttpRedirectConfig) -> Result + where + S: Service, Response = Response> + + Clone + + Send + + Sync + + 'static, + S::Future: Send + 'static, + S::Error: Into>, + { + if let Some(https_config) = server_config.https_config { + // Serve HTTPS + let https_listener = + TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], https_config.https_port))) + .await?; + let https_server = SimpleHttpServer::new( + service, + https_listener, + HttpsConfig::Https { + resolver: https_config.resolver, + }, + )?; + + // Redirect HTTP to HTTPS + let http_listener = + TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], server_config.http_port))) + .await?; + let http_server = + SimpleHttpServer::new(HttpsRedirectService, http_listener, HttpsConfig::Http)?; + + Ok(Self { + http_server, + https_server: Some(https_server), + }) + } else { + let listener = + TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], server_config.http_port))) + .await?; + let http_server = SimpleHttpServer::new(service, listener, HttpsConfig::Http)?; + + Ok(Self { + http_server, + https_server: None, + }) + } + } + + pub async fn graceful_shutdown_with_timeout(self, timeout: Duration) { + if let Some(https_server) = self.https_server { + tokio::join!( + self.http_server.graceful_shutdown_with_timeout(timeout), + https_server.graceful_shutdown_with_timeout(timeout) + ); + } else { + self.http_server + .graceful_shutdown_with_timeout(timeout) + .await; + } + } +} diff --git a/dynamic-proxy/tests/test_http_redirect.rs b/dynamic-proxy/tests/test_http_redirect.rs index c50a97398..d83569019 100644 --- a/dynamic-proxy/tests/test_http_redirect.rs +++ b/dynamic-proxy/tests/test_http_redirect.rs @@ -18,11 +18,10 @@ fn get_client() -> reqwest::Client { } async fn do_request(url: &str) -> Response { - let service = HttpsRedirectService::new("foo.bar.baz".to_string()); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); let https_config = HttpsConfig::http(); - let _server = SimpleHttpServer::new(service, listener, https_config); + let _server = SimpleHttpServer::new(HttpsRedirectService, listener, https_config); // url needs to have port let mut url = Url::parse(url).unwrap(); diff --git a/plane/plane-tests/tests/common/test_env.rs b/plane/plane-tests/tests/common/test_env.rs index 15c76ef2a..e3c28df13 100644 --- a/plane/plane-tests/tests/common/test_env.rs +++ b/plane/plane-tests/tests/common/test_env.rs @@ -4,15 +4,13 @@ use super::{ }; use chrono::Duration; use plane::{ - client::PlaneClient, controller::ControllerServer, database::PlaneDatabase, dns::run_dns_with_listener, drone::runtime::unix_socket::{MessageToClient, MessageToServer, UnixSocketRuntimeConfig}, drone::{runtime::docker::DockerRuntimeConfig, Drone, DroneConfig, ExecutorConfig}, - names::{AcmeDnsServerName, ControllerName, DroneName, Name, ProxyName}, + names::{AcmeDnsServerName, ControllerName, DroneName, Name}, proxy::AcmeEabConfiguration, - proxy::{cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection}, typed_unix_socket::{server::TypedUnixSocketServer, WrappedMessage}, types::{ClusterName, DronePoolName}, util::random_string, diff --git a/plane/src/proxy/mod.rs b/plane/src/proxy/mod.rs index 2f03d4031..7f2159ef1 100644 --- a/plane/src/proxy/mod.rs +++ b/plane/src/proxy/mod.rs @@ -3,10 +3,14 @@ use crate::names::ProxyName; use crate::proxy::cert_manager::watcher_manager_pair; use crate::{client::PlaneClient, signals::wait_for_shutdown_signal, types::ClusterName}; use anyhow::Result; -use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; +use dynamic_proxy::server::{ + HttpsConfig, ServerWithHttpRedirect, ServerWithHttpRedirectConfig, + ServerWithHttpRedirectHttpsConfig, SimpleHttpServer, +}; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; +use std::sync::Arc; use std::time::Duration; use tokio::net::TcpListener; use url::Url; @@ -112,47 +116,36 @@ pub async fn run_proxy(config: ProxyConfig) -> Result<()> { let proxy_connection = ProxyConnection::new(config.name, client, config.cluster, cert_manager); - let https_redirect = config.port_config.https_port.is_some(); - - if config.port_config.https_port.is_some() { + let server = if let Some(https_port) = config.port_config.https_port { + tracing::info!("Waiting for initial certificate."); cert_watcher.wait_for_initial_cert().await?; - } - let tcp_addr = SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - config.port_config.http_port, - ); - let tcp_listener = TcpListener::bind(tcp_addr).await?; - let http_server = - SimpleHttpServer::new(proxy_connection.state(), tcp_listener, HttpsConfig::http())?; + let https_config = ServerWithHttpRedirectHttpsConfig { + https_port, + resolver: Arc::new(cert_watcher), + }; - let https_server = if let Some(https_port) = config.port_config.https_port { - tracing::info!("Waiting for initial certificate."); + let server_config = ServerWithHttpRedirectConfig { + http_port: config.port_config.http_port, + https_config: Some(https_config), + }; - let tcp_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), https_port); - let tcp_listener = TcpListener::bind(tcp_addr).await?; - let https_server = SimpleHttpServer::new( - proxy_connection.state(), - tcp_listener, - HttpsConfig::from_resolver(cert_watcher), - )?; - - Some(https_server) + ServerWithHttpRedirect::new(proxy_connection.state(), server_config).await? } else { - None + let server_config = ServerWithHttpRedirectConfig { + http_port: config.port_config.http_port, + https_config: None, + }; + + ServerWithHttpRedirect::new(proxy_connection.state(), server_config).await? }; wait_for_shutdown_signal().await; tracing::info!("Shutting down proxy server."); - http_server + server .graceful_shutdown_with_timeout(Duration::from_secs(10)) .await; - if let Some(https_server) = https_server { - https_server - .graceful_shutdown_with_timeout(Duration::from_secs(10)) - .await; - } Ok(()) } From a3129a2d50266d379084d8c70afdc75579544caa Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 17:27:42 -0400 Subject: [PATCH 10/66] progress --- Cargo.lock | 1 + plane/plane-tests/Cargo.toml | 1 + plane/plane-tests/tests/common/test_env.rs | 51 +++++++++++++++++----- plane/src/drone/runtime/unix_socket/mod.rs | 3 +- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d44c0082..439a63f25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2081,6 +2081,7 @@ dependencies = [ "axum 0.7.6", "bollard", "chrono", + "dynamic-proxy", "futures-util", "hyper 0.14.28", "plane-dynamic", diff --git a/plane/plane-tests/Cargo.toml b/plane/plane-tests/Cargo.toml index d9dfa1dac..68af9e5c8 100644 --- a/plane/plane-tests/Cargo.toml +++ b/plane/plane-tests/Cargo.toml @@ -9,6 +9,7 @@ async-trait = "0.1.74" axum = "0.7.5" bollard = "0.17.0" chrono = { version = "0.4.31", features = ["serde"] } +dynamic-proxy = { path = "../../dynamic-proxy" } futures-util = "0.3.29" hyper = { version = "0.14.27", features = ["server"] } plane = { path = "../plane-dynamic", package = "plane-dynamic" } diff --git a/plane/plane-tests/tests/common/test_env.rs b/plane/plane-tests/tests/common/test_env.rs index e3c28df13..e2d06645e 100644 --- a/plane/plane-tests/tests/common/test_env.rs +++ b/plane/plane-tests/tests/common/test_env.rs @@ -3,23 +3,16 @@ use super::{ resources::{database::DevDatabase, pebble::Pebble}, }; use chrono::Duration; +use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; use plane::{ - controller::ControllerServer, - database::PlaneDatabase, - dns::run_dns_with_listener, - drone::runtime::unix_socket::{MessageToClient, MessageToServer, UnixSocketRuntimeConfig}, - drone::{runtime::docker::DockerRuntimeConfig, Drone, DroneConfig, ExecutorConfig}, - names::{AcmeDnsServerName, ControllerName, DroneName, Name}, - proxy::AcmeEabConfiguration, - typed_unix_socket::{server::TypedUnixSocketServer, WrappedMessage}, - types::{ClusterName, DronePoolName}, - util::random_string, + client::PlaneClient, controller::ControllerServer, database::PlaneDatabase, dns::run_dns_with_listener, drone::{runtime::{docker::DockerRuntimeConfig, unix_socket::{MessageToClient, MessageToServer, UnixSocketRuntimeConfig}}, Drone, DroneConfig, ExecutorConfig}, names::{AcmeDnsServerName, ControllerName, DroneName, Name, ProxyName}, proxy::{cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection, AcmeEabConfiguration}, typed_unix_socket::{server::TypedUnixSocketServer, WrappedMessage}, types::{ClusterName, DronePoolName}, util::random_string }; use std::{ - net::{IpAddr, Ipv4Addr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; +use tokio::net::TcpListener; use tokio::sync::broadcast::Receiver; use tracing::subscriber::DefaultGuard; use tracing_appender::non_blocking::WorkerGuard; @@ -113,6 +106,36 @@ impl TestEnvironment { .expect("Unable to construct controller.") } + pub async fn proxy( + &mut self, + controller: &ControllerServer, + ) -> Result> { + let cluster: ClusterName = "localhost:9090".parse().unwrap(); + + let client = PlaneClient::new(controller.url().clone()); + + let (_, cert_manager) = watcher_manager_pair(cluster.clone(), None, None) + .await + .unwrap(); + + let proxy_connection = + ProxyConnection::new(ProxyName::new_random(), client, cluster, cert_manager); + + let addr: SocketAddr = ([0, 0, 0, 0], 0).into(); + tracing::info!(%addr, "Listening for HTTP connections."); + let tcp_listener = TcpListener::bind(addr).await.unwrap(); + let port = tcp_listener.local_addr().unwrap().port(); + + // Spawn the server on a separate task + let server = + SimpleHttpServer::new(proxy_connection.state(), tcp_listener, HttpsConfig::Http)?; + + Ok(Proxy { + port, + _server: server, + }) + } + pub async fn controller_with_forward_auth(&mut self, forward_auth: &Url) -> ControllerServer { let db = self.db().await; let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); @@ -253,6 +276,12 @@ impl TestEnvironment { } } +pub struct Proxy { + #[allow(dead_code)] // Used in tests. + pub port: u16, + _server: SimpleHttpServer, +} + #[allow(dead_code)] // Used in tests. pub struct DroneWithSocket { pub socket_server: TypedUnixSocketServer, diff --git a/plane/src/drone/runtime/unix_socket/mod.rs b/plane/src/drone/runtime/unix_socket/mod.rs index 5b75773f1..f1a1b9b02 100644 --- a/plane/src/drone/runtime/unix_socket/mod.rs +++ b/plane/src/drone/runtime/unix_socket/mod.rs @@ -142,7 +142,8 @@ impl Runtime for UnixSocketRuntime { impl UnixSocketRuntime { pub async fn new(config: UnixSocketRuntimeConfig) -> Result { - let client = TypedUnixSocketClient::new(&config.socket_path).await?; + let client: TypedUnixSocketClient = + TypedUnixSocketClient::new(&config.socket_path).await?; Ok(Self { client }) } } From 11cb6e2ff14d268df55e132ec2ae8071343205c0 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 18:03:39 -0400 Subject: [PATCH 11/66] lifecycle test with dummy drone --- plane/plane-tests/tests/common/test_env.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plane/plane-tests/tests/common/test_env.rs b/plane/plane-tests/tests/common/test_env.rs index e2d06645e..38c1f6435 100644 --- a/plane/plane-tests/tests/common/test_env.rs +++ b/plane/plane-tests/tests/common/test_env.rs @@ -5,7 +5,24 @@ use super::{ use chrono::Duration; use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; use plane::{ - client::PlaneClient, controller::ControllerServer, database::PlaneDatabase, dns::run_dns_with_listener, drone::{runtime::{docker::DockerRuntimeConfig, unix_socket::{MessageToClient, MessageToServer, UnixSocketRuntimeConfig}}, Drone, DroneConfig, ExecutorConfig}, names::{AcmeDnsServerName, ControllerName, DroneName, Name, ProxyName}, proxy::{cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection, AcmeEabConfiguration}, typed_unix_socket::{server::TypedUnixSocketServer, WrappedMessage}, types::{ClusterName, DronePoolName}, util::random_string + client::PlaneClient, + controller::ControllerServer, + database::PlaneDatabase, + dns::run_dns_with_listener, + drone::{ + runtime::{ + docker::DockerRuntimeConfig, + unix_socket::{MessageToClient, MessageToServer, UnixSocketRuntimeConfig}, + }, + Drone, DroneConfig, ExecutorConfig, + }, + names::{AcmeDnsServerName, ControllerName, DroneName, Name, ProxyName}, + proxy::{ + cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection, AcmeEabConfiguration, + }, + typed_unix_socket::{server::TypedUnixSocketServer, WrappedMessage}, + types::{ClusterName, DronePoolName}, + util::random_string, }; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, From 3b40d799751bdec9b6db0c2fe9a60d9cd0db771d Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 23 Sep 2024 20:54:27 -0400 Subject: [PATCH 12/66] pass test with no connection token --- Cargo.lock | 3 + plane/plane-tests/Cargo.toml | 5 ++ plane/plane-tests/tests/common/mod.rs | 4 +- plane/plane-tests/tests/common/proxy_mock.rs | 66 ++++++++++++++++ .../tests/common/simple_axum_server.rs | 76 +++++++++++++++++++ plane/plane-tests/tests/proxy.rs | 18 +++++ plane/src/proxy/mod.rs | 7 +- plane/src/proxy/proxy_connection.rs | 3 - plane/src/proxy/proxy_server.rs | 29 +++---- plane/src/proxy/request.rs | 4 + 10 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 plane/plane-tests/tests/common/proxy_mock.rs create mode 100644 plane/plane-tests/tests/common/simple_axum_server.rs create mode 100644 plane/plane-tests/tests/proxy.rs diff --git a/Cargo.lock b/Cargo.lock index 439a63f25..f35e65e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2083,10 +2083,13 @@ dependencies = [ "chrono", "dynamic-proxy", "futures-util", + "http 1.1.0", + "http-body-util", "hyper 0.14.28", "plane-dynamic", "plane-test-macro", "reqwest 0.11.27", + "serde", "serde_json", "thiserror", "tokio", diff --git a/plane/plane-tests/Cargo.toml b/plane/plane-tests/Cargo.toml index 68af9e5c8..535b185d3 100644 --- a/plane/plane-tests/Cargo.toml +++ b/plane/plane-tests/Cargo.toml @@ -22,3 +22,8 @@ tracing = "0.1.40" tracing-appender = "0.2.2" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } url = "2.4.1" + +[dev-dependencies] +http = "1.1.0" +http-body-util = "0.1.2" +serde = "1.0.210" diff --git a/plane/plane-tests/tests/common/mod.rs b/plane/plane-tests/tests/common/mod.rs index f452b0b47..c77f0766f 100644 --- a/plane/plane-tests/tests/common/mod.rs +++ b/plane/plane-tests/tests/common/mod.rs @@ -7,9 +7,11 @@ use tokio::time::timeout; pub mod async_drop; pub mod auth_mock; pub mod docker; +pub mod proxy_mock; pub mod resources; +pub mod simple_axum_server; pub mod test_env; -pub mod timeout; +pub mod timeout; // NOTE: copied from dynamic-proxy pub fn run_test(name: &str, time_limit: Duration, test_function: F) where diff --git a/plane/plane-tests/tests/common/proxy_mock.rs b/plane/plane-tests/tests/common/proxy_mock.rs new file mode 100644 index 000000000..18be0c843 --- /dev/null +++ b/plane/plane-tests/tests/common/proxy_mock.rs @@ -0,0 +1,66 @@ +use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; +use plane::{ + protocol::{RouteInfoRequest, RouteInfoResponse}, + proxy::proxy_server::ProxyState, +}; +use std::net::SocketAddr; +use tokio::{net::TcpListener, sync::mpsc}; + +pub struct MockProxy { + proxy_state: ProxyState, + route_info_request_receiver: mpsc::Receiver, + addr: SocketAddr, + server: SimpleHttpServer, +} + +impl MockProxy { + pub async fn new() -> Self { + let proxy_state = ProxyState::new(); + let (route_info_request_sender, route_info_request_receiver) = mpsc::channel(1); + + proxy_state.inner.route_map.set_sender(move |m| { + route_info_request_sender + .try_send(m) + .expect("Failed to send route info request"); + }); + + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind listener"); + let addr = listener.local_addr().expect("Failed to get local address"); + + let server = SimpleHttpServer::new(proxy_state.clone(), listener, HttpsConfig::http()) + .expect("Failed to create server"); + + Self { + proxy_state, + route_info_request_receiver, + addr, + server, + } + } + + pub fn addr(&self) -> SocketAddr { + self.addr + } + + pub async fn recv_route_info_request(&mut self) -> RouteInfoRequest { + self.route_info_request_receiver + .recv() + .await + .expect("Failed to receive route info request") + } + + pub async fn expect_no_route_info_request(&mut self) { + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + assert!( + self.route_info_request_receiver.is_empty(), + "Expected no route info request, but got: {}", + self.route_info_request_receiver.len() + ); + } + + pub async fn send_route_info_response(&mut self, response: RouteInfoResponse) { + self.proxy_state.inner.route_map.receive(response); + } +} diff --git a/plane/plane-tests/tests/common/simple_axum_server.rs b/plane/plane-tests/tests/common/simple_axum_server.rs new file mode 100644 index 000000000..999e0ca74 --- /dev/null +++ b/plane/plane-tests/tests/common/simple_axum_server.rs @@ -0,0 +1,76 @@ +use axum::{body::Body, extract::Request, routing::any, Json, Router}; +use http::Method; +use http_body_util::BodyExt; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, net::SocketAddr}; +use tokio::net::TcpListener; + +pub struct SimpleAxumServer { + handle: tokio::task::JoinHandle<()>, + addr: SocketAddr, +} + +#[allow(unused)] +impl SimpleAxumServer { + pub async fn new() -> Self { + let app = Router::new() + .route("/*path", any(return_request_info)) + .route("/", any(return_request_info)); + + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let tcp_listener = TcpListener::bind(addr).await.unwrap(); + let addr = tcp_listener.local_addr().unwrap(); + + let handle = tokio::spawn(async { + axum::serve(tcp_listener, app.into_make_service()) + .await + .unwrap(); + }); + + Self { handle, addr } + } + + pub fn addr(&self) -> SocketAddr { + self.addr + } +} + +impl Drop for SimpleAxumServer { + fn drop(&mut self) { + self.handle.abort(); + } +} + +// Handler function for the root route +async fn return_request_info(method: Method, request: Request) -> Json { + let method = method.to_string(); + + let path = request.uri().path().to_string(); + let query = request.uri().query().unwrap_or("").to_string(); + + let headers: HashMap = request + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string())) + .collect(); + + let body = request.into_body().collect().await.unwrap().to_bytes(); + let body = String::from_utf8(body.to_vec()).unwrap(); + + Json(RequestInfo { + path, + query, + method, + headers, + body, + }) +} + +#[derive(Serialize, Deserialize)] +pub struct RequestInfo { + pub path: String, + pub query: String, + pub method: String, + pub headers: HashMap, + pub body: String, +} diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs new file mode 100644 index 000000000..6eec7631d --- /dev/null +++ b/plane/plane-tests/tests/proxy.rs @@ -0,0 +1,18 @@ +use common::{proxy_mock::MockProxy, test_env::TestEnvironment}; +use plane_test_macro::plane_test; +use reqwest::StatusCode; + +mod common; + +#[plane_test] +async fn proxy_no_bearer_token(env: TestEnvironment) { + let mut proxy = MockProxy::new().await; + let url = format!("http://{}", proxy.addr()); + let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + + proxy.expect_no_route_info_request().await; + + let response = handle.await.unwrap(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); +} diff --git a/plane/src/proxy/mod.rs b/plane/src/proxy/mod.rs index 7f2159ef1..b0476e67e 100644 --- a/plane/src/proxy/mod.rs +++ b/plane/src/proxy/mod.rs @@ -4,15 +4,12 @@ use crate::proxy::cert_manager::watcher_manager_pair; use crate::{client::PlaneClient, signals::wait_for_shutdown_signal, types::ClusterName}; use anyhow::Result; use dynamic_proxy::server::{ - HttpsConfig, ServerWithHttpRedirect, ServerWithHttpRedirectConfig, - ServerWithHttpRedirectHttpsConfig, SimpleHttpServer, + ServerWithHttpRedirect, ServerWithHttpRedirectConfig, ServerWithHttpRedirectHttpsConfig, }; use serde::{Deserialize, Serialize}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use tokio::net::TcpListener; use url::Url; pub mod cert_manager; @@ -21,7 +18,7 @@ pub mod command; mod connection_monitor; pub mod proxy_connection; // mod proxy_service; -mod proxy_server; +pub mod proxy_server; // mod rewriter; mod request; mod route_map; diff --git a/plane/src/proxy/proxy_connection.rs b/plane/src/proxy/proxy_connection.rs index 827f279f1..22b9e32fb 100644 --- a/plane/src/proxy/proxy_connection.rs +++ b/plane/src/proxy/proxy_connection.rs @@ -31,7 +31,6 @@ impl ProxyConnection { loop { let mut conn = proxy_connection.connect_with_retry(&name).await; - state.set_connected(true); let sender = conn.sender(MessageFromProxy::CertManagerRequest); cert_manager.set_request_sender(move |m| { @@ -73,8 +72,6 @@ impl ProxyConnection { } } } - - state.set_connected(false); } }) }; diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 2fb297461..4a5c06102 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -3,21 +3,21 @@ use super::{ route_map::RouteMap, }; use dynamic_proxy::{ - body::SimpleBody, - hyper::{body::Incoming, service::Service, Request, Response, Uri}, + body::{simple_empty_body, SimpleBody}, + hyper::{body::Incoming, service::Service, Request, Response, StatusCode, Uri}, proxy::ProxyClient, request::MutableRequest, }; -use std::{future::Future, sync::atomic::AtomicBool}; +use std::future::{ready, Future}; use std::{pin::Pin, sync::Arc}; pub struct ProxyStateInner { pub route_map: RouteMap, pub proxy_client: ProxyClient, pub monitor: ConnectionMonitorHandle, - pub connected: AtomicBool, } +#[derive(Clone)] pub struct ProxyState { pub inner: Arc, } @@ -34,25 +34,12 @@ impl ProxyState { route_map: RouteMap::new(), proxy_client: ProxyClient::new(), monitor: ConnectionMonitorHandle::new(), - connected: AtomicBool::new(false), }; Self { inner: Arc::new(inner), } } - - pub fn set_connected(&self, connected: bool) { - self.inner - .connected - .store(connected, std::sync::atomic::Ordering::Relaxed); - } - - pub fn connected(&self) -> bool { - self.inner - .connected - .load(std::sync::atomic::Ordering::Relaxed) - } } impl Service> for ProxyState { @@ -73,8 +60,12 @@ impl Service> for ProxyState { let mut uri_parts = request.parts.uri.clone().into_parts(); let bearer_token = get_and_maybe_remove_bearer_token(&mut uri_parts); - // TODO - let bearer_token = bearer_token.unwrap(); + let Some(bearer_token) = bearer_token else { + return Box::pin(ready(Ok(Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(simple_empty_body()) + .unwrap()))); + }; request.parts.uri = Uri::from_parts(uri_parts).unwrap(); diff --git a/plane/src/proxy/request.rs b/plane/src/proxy/request.rs index e7bf2afcb..e3c051fdf 100644 --- a/plane/src/proxy/request.rs +++ b/plane/src/proxy/request.rs @@ -50,6 +50,10 @@ pub fn get_and_maybe_remove_bearer_token(parts: &mut uri::Parts) -> Option (full_path, "/"), }; + if token.is_empty() { + return None; + } + let token = BearerToken::from(token.to_string()); if token.is_static() { From 779d1eef704af6ae647e473c9e086fe2a23692f7 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 06:33:56 -0400 Subject: [PATCH 13/66] get rid of warnings --- plane/plane-tests/tests/common/proxy_mock.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plane/plane-tests/tests/common/proxy_mock.rs b/plane/plane-tests/tests/common/proxy_mock.rs index 18be0c843..a6abcad35 100644 --- a/plane/plane-tests/tests/common/proxy_mock.rs +++ b/plane/plane-tests/tests/common/proxy_mock.rs @@ -10,9 +10,10 @@ pub struct MockProxy { proxy_state: ProxyState, route_info_request_receiver: mpsc::Receiver, addr: SocketAddr, - server: SimpleHttpServer, + _server: SimpleHttpServer, } +#[allow(unused)] impl MockProxy { pub async fn new() -> Self { let proxy_state = ProxyState::new(); @@ -36,7 +37,7 @@ impl MockProxy { proxy_state, route_info_request_receiver, addr, - server, + _server: server, } } From f6b52dafce904f24b206d025ff751e7b2f3e87e1 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 06:39:47 -0400 Subject: [PATCH 14/66] add test for bad connection string --- plane/plane-tests/tests/proxy.rs | 25 +++++++++++++++++++++++++ plane/src/proxy/proxy_server.rs | 6 ++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index 6eec7631d..130400ee2 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -1,4 +1,5 @@ use common::{proxy_mock::MockProxy, test_env::TestEnvironment}; +use plane::{protocol::RouteInfoResponse, types::BearerToken}; use plane_test_macro::plane_test; use reqwest::StatusCode; @@ -16,3 +17,27 @@ async fn proxy_no_bearer_token(env: TestEnvironment) { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } + +#[plane_test] +async fn proxy_bad_bearer_token(env: TestEnvironment) { + let mut proxy = MockProxy::new().await; + let url = format!("http://{}/abc123/", proxy.addr()); + let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: None, + }) + .await; + + let response = handle.await.unwrap(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); +} diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 4a5c06102..fcebe8958 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -76,8 +76,10 @@ impl Service> for ProxyState { let route_info = inner.route_map.lookup(&bearer_token).await; let Some(route_info) = route_info else { - // TODO - panic!("Route info not found for bearer token"); + return Ok(Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(simple_empty_body()) + .unwrap()); }; request.set_upstream_address(route_info.address.0); From 77dada1c17cf1852af7df87530c6ffb59d84ee56 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 06:43:38 -0400 Subject: [PATCH 15/66] add bad gateway test --- plane/plane-tests/tests/proxy.rs | 41 +++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index 130400ee2..ddfdd2390 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -1,5 +1,12 @@ +use std::{net::SocketAddr, str::FromStr}; + use common::{proxy_mock::MockProxy, test_env::TestEnvironment}; -use plane::{protocol::RouteInfoResponse, types::BearerToken}; +use plane::{ + log_types::BackendAddr, + names::{BackendName, Name}, + protocol::{RouteInfo, RouteInfoResponse}, + types::{BearerToken, ClusterName, SecretToken}, +}; use plane_test_macro::plane_test; use reqwest::StatusCode; @@ -41,3 +48,35 @@ async fn proxy_bad_bearer_token(env: TestEnvironment) { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } + +#[plane_test] +async fn proxy_backend_unreachable(env: TestEnvironment) { + let mut proxy = MockProxy::new().await; + let url = format!("http://{}/abc123/", proxy.addr()); + let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(SocketAddr::from(([123, 234, 123, 234], 12345))), + secret_token: SecretToken::from("secret".to_string()), + cluster: ClusterName::from_str("test").unwrap(), + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_GATEWAY); +} From b9a770579a8c2308a594ca84553b9812b2d590bd Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 06:45:59 -0400 Subject: [PATCH 16/66] add backend timeout test --- plane/plane-tests/tests/proxy.rs | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index ddfdd2390..d8f1a5c29 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -9,6 +9,7 @@ use plane::{ }; use plane_test_macro::plane_test; use reqwest::StatusCode; +use tokio::net::TcpListener; mod common; @@ -80,3 +81,41 @@ async fn proxy_backend_unreachable(env: TestEnvironment) { assert_eq!(response.status(), StatusCode::BAD_GATEWAY); } + +#[plane_test] +async fn proxy_backend_timeout(env: TestEnvironment) { + // We will start a listener, but never respond on it, to simulate a timeout. + let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))) + .await + .unwrap(); + let addr = listener.local_addr().unwrap(); + + let mut proxy = MockProxy::new().await; + let url = format!("http://{}/abc123/", proxy.addr()); + let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(addr), + secret_token: SecretToken::from("secret".to_string()), + cluster: ClusterName::from_str("test").unwrap(), + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap(); + + assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); +} From 1e124596f9cc4aeced052ba91a1320472f47928d Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 06:50:10 -0400 Subject: [PATCH 17/66] add backend accepts test --- plane/plane-tests/tests/proxy.rs | 42 +++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index d8f1a5c29..89310f3d6 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -1,6 +1,10 @@ use std::{net::SocketAddr, str::FromStr}; -use common::{proxy_mock::MockProxy, test_env::TestEnvironment}; +use common::{ + proxy_mock::MockProxy, + simple_axum_server::{RequestInfo, SimpleAxumServer}, + test_env::TestEnvironment, +}; use plane::{ log_types::BackendAddr, names::{BackendName, Name}, @@ -119,3 +123,39 @@ async fn proxy_backend_timeout(env: TestEnvironment) { assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); } + +#[plane_test] +async fn proxy_backend_accepts(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let url = format!("http://{}/abc123/", proxy.addr()); + let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster: ClusterName::from_str("test").unwrap(), + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let request_info: RequestInfo = response.json().await.unwrap(); + assert_eq!(request_info.path, "/"); + assert_eq!(request_info.method, "GET"); +} From a36721776464a3eb61b6b525c4a8e29544cb07b4 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 07:41:29 -0400 Subject: [PATCH 18/66] subdomain verification --- .../tests/common/localhost_resolver.rs | 22 ++++ plane/plane-tests/tests/common/mod.rs | 1 + plane/plane-tests/tests/common/proxy_mock.rs | 4 + plane/plane-tests/tests/proxy.rs | 111 +++++++++++++++--- plane/src/proxy/proxy_server.rs | 45 +++++-- 5 files changed, 158 insertions(+), 25 deletions(-) create mode 100644 plane/plane-tests/tests/common/localhost_resolver.rs diff --git a/plane/plane-tests/tests/common/localhost_resolver.rs b/plane/plane-tests/tests/common/localhost_resolver.rs new file mode 100644 index 000000000..4348c1b3b --- /dev/null +++ b/plane/plane-tests/tests/common/localhost_resolver.rs @@ -0,0 +1,22 @@ +use hyper::client::connect::dns::Name; +use reqwest::dns::{Resolve, Resolving}; +use std::{future::ready, net::SocketAddr, sync::Arc}; + +/// A reqwest-compatible DNS resolver that resolves all requests to localhost. +struct LocalhostResolver; + +impl Resolve for LocalhostResolver { + fn resolve(&self, _name: Name) -> Resolving { + let addrs = vec![SocketAddr::from(([127, 0, 0, 1], 0))]; + let addrs: Box + Send> = Box::new(addrs.into_iter()); + Box::pin(ready(Ok(addrs))) + } +} + +#[allow(unused)] +pub fn localhost_client() -> reqwest::Client { + reqwest::Client::builder() + .dns_resolver(Arc::new(LocalhostResolver)) + .build() + .unwrap() +} diff --git a/plane/plane-tests/tests/common/mod.rs b/plane/plane-tests/tests/common/mod.rs index c77f0766f..1011493fc 100644 --- a/plane/plane-tests/tests/common/mod.rs +++ b/plane/plane-tests/tests/common/mod.rs @@ -7,6 +7,7 @@ use tokio::time::timeout; pub mod async_drop; pub mod auth_mock; pub mod docker; +pub mod localhost_resolver; pub mod proxy_mock; pub mod resources; pub mod simple_axum_server; diff --git a/plane/plane-tests/tests/common/proxy_mock.rs b/plane/plane-tests/tests/common/proxy_mock.rs index a6abcad35..2db37c8e6 100644 --- a/plane/plane-tests/tests/common/proxy_mock.rs +++ b/plane/plane-tests/tests/common/proxy_mock.rs @@ -45,6 +45,10 @@ impl MockProxy { self.addr } + pub fn port(&self) -> u16 { + self.addr.port() + } + pub async fn recv_route_info_request(&mut self) -> RouteInfoRequest { self.route_info_request_receiver .recv() diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index 89310f3d6..b9ecd395a 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -1,6 +1,5 @@ -use std::{net::SocketAddr, str::FromStr}; - use common::{ + localhost_resolver::localhost_client, proxy_mock::MockProxy, simple_axum_server::{RequestInfo, SimpleAxumServer}, test_env::TestEnvironment, @@ -9,10 +8,11 @@ use plane::{ log_types::BackendAddr, names::{BackendName, Name}, protocol::{RouteInfo, RouteInfoResponse}, - types::{BearerToken, ClusterName, SecretToken}, + types::{BearerToken, ClusterName, SecretToken, Subdomain}, }; use plane_test_macro::plane_test; use reqwest::StatusCode; +use std::{net::SocketAddr, str::FromStr}; use tokio::net::TcpListener; mod common; @@ -57,8 +57,11 @@ async fn proxy_bad_bearer_token(env: TestEnvironment) { #[plane_test] async fn proxy_backend_unreachable(env: TestEnvironment) { let mut proxy = MockProxy::new().await; - let url = format!("http://{}/abc123/", proxy.addr()); - let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); let route_info_request = proxy.recv_route_info_request().await; assert_eq!( @@ -73,7 +76,7 @@ async fn proxy_backend_unreachable(env: TestEnvironment) { backend_id: BackendName::new_random(), address: BackendAddr(SocketAddr::from(([123, 234, 123, 234], 12345))), secret_token: SecretToken::from("secret".to_string()), - cluster: ClusterName::from_str("test").unwrap(), + cluster, user: None, user_data: None, subdomain: None, @@ -81,7 +84,7 @@ async fn proxy_backend_unreachable(env: TestEnvironment) { }) .await; - let response = handle.await.unwrap(); + let response = handle.await.unwrap().unwrap(); assert_eq!(response.status(), StatusCode::BAD_GATEWAY); } @@ -95,8 +98,11 @@ async fn proxy_backend_timeout(env: TestEnvironment) { let addr = listener.local_addr().unwrap(); let mut proxy = MockProxy::new().await; - let url = format!("http://{}/abc123/", proxy.addr()); - let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); let route_info_request = proxy.recv_route_info_request().await; assert_eq!( @@ -111,7 +117,7 @@ async fn proxy_backend_timeout(env: TestEnvironment) { backend_id: BackendName::new_random(), address: BackendAddr(addr), secret_token: SecretToken::from("secret".to_string()), - cluster: ClusterName::from_str("test").unwrap(), + cluster, user: None, user_data: None, subdomain: None, @@ -119,7 +125,7 @@ async fn proxy_backend_timeout(env: TestEnvironment) { }) .await; - let response = handle.await.unwrap(); + let response = handle.await.unwrap().unwrap(); assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); } @@ -129,8 +135,11 @@ async fn proxy_backend_accepts(env: TestEnvironment) { let server = SimpleAxumServer::new().await; let mut proxy = MockProxy::new().await; - let url = format!("http://{}/abc123/", proxy.addr()); - let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); let route_info_request = proxy.recv_route_info_request().await; assert_eq!( @@ -145,7 +154,7 @@ async fn proxy_backend_accepts(env: TestEnvironment) { backend_id: BackendName::new_random(), address: BackendAddr(server.addr()), secret_token: SecretToken::from("secret".to_string()), - cluster: ClusterName::from_str("test").unwrap(), + cluster, user: None, user_data: None, subdomain: None, @@ -153,9 +162,81 @@ async fn proxy_backend_accepts(env: TestEnvironment) { }) .await; - let response = handle.await.unwrap(); + let response = handle.await.unwrap().unwrap(); assert_eq!(response.status(), StatusCode::OK); let request_info: RequestInfo = response.json().await.unwrap(); assert_eq!(request_info.path, "/"); assert_eq!(request_info.method, "GET"); } + +#[plane_test] +async fn proxy_expected_subdomain_not_present(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: Some(Subdomain::from_str("missing-subdomain").unwrap()), + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); +} + +#[plane_test] +async fn proxy_expected_subdomain_is_present(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://mysubdomain.plane.test:{port}/abc123/"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: Some(Subdomain::from_str("mysubdomain").unwrap()), + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); +} diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index fcebe8958..9a64ac5fa 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -1,10 +1,11 @@ use super::{ - connection_monitor::ConnectionMonitorHandle, request::get_and_maybe_remove_bearer_token, + connection_monitor::ConnectionMonitorHandle, + request::{get_and_maybe_remove_bearer_token, subdomain_from_host}, route_map::RouteMap, }; use dynamic_proxy::{ body::{simple_empty_body, SimpleBody}, - hyper::{body::Incoming, service::Service, Request, Response, StatusCode, Uri}, + hyper::{body::Incoming, header, service::Service, Request, Response, StatusCode, Uri}, proxy::ProxyClient, request::MutableRequest, }; @@ -61,10 +62,7 @@ impl Service> for ProxyState { let bearer_token = get_and_maybe_remove_bearer_token(&mut uri_parts); let Some(bearer_token) = bearer_token else { - return Box::pin(ready(Ok(Response::builder() - .status(StatusCode::UNAUTHORIZED) - .body(simple_empty_body()) - .unwrap()))); + return Box::pin(ready(status_code_to_response(StatusCode::UNAUTHORIZED))); }; request.parts.uri = Uri::from_parts(uri_parts).unwrap(); @@ -76,12 +74,30 @@ impl Service> for ProxyState { let route_info = inner.route_map.lookup(&bearer_token).await; let Some(route_info) = route_info else { - return Ok(Response::builder() - .status(StatusCode::UNAUTHORIZED) - .body(simple_empty_body()) - .unwrap()); + return status_code_to_response(StatusCode::UNAUTHORIZED); }; + // Check cluster and subdomain. + let Some(host) = request + .parts + .headers + .get(header::HOST) + .and_then(|h| h.to_str().ok()) + else { + return status_code_to_response(StatusCode::BAD_REQUEST); + }; + + let Ok(request_subdomain) = subdomain_from_host(host, &route_info.cluster) else { + // The host header does not match the expected cluster. + return status_code_to_response(StatusCode::FORBIDDEN); + }; + + if let Some(subdomain) = route_info.subdomain { + if request_subdomain != Some(&subdomain) { + return status_code_to_response(StatusCode::FORBIDDEN); + } + } + request.set_upstream_address(route_info.address.0); let request = request.into_request_with_simple_body(); @@ -109,3 +125,12 @@ impl Service> for ProxyState { }) } } + +fn status_code_to_response( + status_code: StatusCode, +) -> Result, Box> { + Ok(Response::builder() + .status(status_code) + .body(simple_empty_body()) + .unwrap()) +} From 00a65ffad2455e5da945f51dfbd2eef8dfabbfdd Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 07:42:38 -0400 Subject: [PATCH 19/66] remove dead code --- plane/src/proxy/proxy_redirects_http.rs | 0 plane/src/proxy/proxy_service.rs | 500 ------------------------ plane/src/proxy/rewriter.rs | 286 -------------- plane/src/proxy/shutdown_signal.rs | 24 -- 4 files changed, 810 deletions(-) delete mode 100644 plane/src/proxy/proxy_redirects_http.rs delete mode 100644 plane/src/proxy/proxy_service.rs delete mode 100644 plane/src/proxy/rewriter.rs delete mode 100644 plane/src/proxy/shutdown_signal.rs diff --git a/plane/src/proxy/proxy_redirects_http.rs b/plane/src/proxy/proxy_redirects_http.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/plane/src/proxy/proxy_service.rs b/plane/src/proxy/proxy_service.rs deleted file mode 100644 index 1dc6daca2..000000000 --- a/plane/src/proxy/proxy_service.rs +++ /dev/null @@ -1,500 +0,0 @@ -use super::connection_monitor::ConnectionMonitorHandle; -use super::rewriter::RequestRewriterError; -use super::route_map::RouteMap; -use super::tls::TlsStream; -use super::{ForwardableRequestInfo, Protocol}; -use crate::names::BackendName; -use crate::proxy::cert_manager::CertWatcher; -use crate::proxy::rewriter::RequestRewriter; -use crate::proxy::tls::TlsAcceptor; -use crate::SERVER_NAME; -use axum::http::uri::PathAndQuery; -use futures_util::{Future, FutureExt}; -use hyper::server::conn::AddrIncoming; -use hyper::{ - client::HttpConnector, server::conn::AddrStream, service::Service, Body, Request, Response, -}; -use std::convert::Infallible; -use std::net::SocketAddr; -use std::pin::Pin; -use std::sync::{atomic::AtomicBool, Arc}; -use std::{ - future::ready, - io::ErrorKind, - task::{self, Poll}, -}; -use tokio::io::copy_bidirectional; -use tokio::task::JoinHandle; -use tokio_rustls::rustls::ServerConfig; -use url::Url; - -const PLANE_BACKEND_ID_HEADER: &str = "x-plane-backend-id"; - -const DEFAULT_CORS_HEADERS: &[(&str, &str)] = &[ - ("Access-Control-Allow-Origin", "*"), - ( - "Access-Control-Allow-Methods", - "GET, POST, PUT, DELETE, OPTIONS", - ), - ( - "Access-Control-Allow-Headers", - "Content-Type, Authorization", - ), - ("Access-Control-Allow-Credentials", "true"), -]; - -fn response_builder() -> hyper::http::response::Builder { - let mut request = hyper::Response::builder(); - request = request.header("Access-Control-Allow-Origin", "*"); - request = request.header( - "Access-Control-Allow-Methods", - "GET, POST, PUT, DELETE, OPTIONS", - ); - request = request.header( - "Access-Control-Allow-Headers", - "Content-Type, Authorization", - ); - request = request.header("Access-Control-Allow-Credentials", "true"); - request -} - -#[derive(Debug, thiserror::Error)] -pub enum ProxyError { - #[error("Invalid or expired connection token")] - InvalidConnectionToken, - - #[error("Missing `host` header")] - MissingHostHeader, - - #[error("Bad request")] - BadRequest, - - #[error("Invalid subdomain")] - InvalidSubdomain, - - #[error("HTTP error: {0}")] - HttpError(#[from] hyper::http::Error), - - #[error("Error binding server: {0}")] - BindError(hyper::Error), - - #[error("Error upgrading request: {0}")] - UpgradeError(hyper::Error), - - #[error("Error making request: {0} (backend: {1})")] - RequestError(hyper::Error, BackendName), - - #[error("Error making upgradable request: {0}")] - UpgradableRequestError(hyper::Error), -} - -impl From for ProxyError { - fn from(err: RequestRewriterError) -> Self { - match err { - RequestRewriterError::InvalidHostHeader => ProxyError::BadRequest, - } - } -} - -pub struct ProxyState { - pub route_map: RouteMap, - http_client: hyper::Client, - pub monitor: ConnectionMonitorHandle, - connected: AtomicBool, -} - -impl Default for ProxyState { - fn default() -> Self { - Self::new() - } -} - -impl ProxyState { - pub fn new() -> Self { - Self { - route_map: RouteMap::new(), - http_client: hyper::Client::builder().build_http::(), - monitor: ConnectionMonitorHandle::new(), - connected: AtomicBool::new(false), - } - } - - pub fn set_connected(&self, connected: bool) { - self.connected - .store(connected, std::sync::atomic::Ordering::Relaxed); - } - - pub fn connected(&self) -> bool { - self.connected.load(std::sync::atomic::Ordering::Relaxed) - } -} - -struct RequestHandler { - state: Arc, - https_redirect: bool, - remote_meta: ForwardableRequestInfo, - root_redirect_url: Option, -} - -impl RequestHandler { - async fn handle_request( - self: Arc, - req: hyper::Request, - ) -> Result, Infallible> { - let result = self.handle_request_inner(req).await; - match result { - Ok(response) => Ok(response), - Err(err) => { - let (status_code, body) = match err { - ProxyError::InvalidConnectionToken => ( - hyper::StatusCode::GONE, - "The backend is no longer available or the connection token is invalid.", - ), - ProxyError::MissingHostHeader => { - (hyper::StatusCode::BAD_REQUEST, "Bad request") - } - ProxyError::InvalidSubdomain => { - (hyper::StatusCode::UNAUTHORIZED, "Invalid subdomain") - } - ProxyError::BadRequest => (hyper::StatusCode::BAD_REQUEST, "Bad request"), - ProxyError::RequestError(err, backend) => { - tracing::warn!(?err, %backend, "Error proxying request to backend."); - (hyper::StatusCode::BAD_GATEWAY, "Connect error") - } - err => { - tracing::error!(?err, "Unhandled error handling request."); - (hyper::StatusCode::INTERNAL_SERVER_ERROR, "Internal error") - } - }; - Ok(response_builder() - .status(status_code) - .header(hyper::header::SERVER, SERVER_NAME) - .body(hyper::Body::from(body.to_string())) - .expect("Static response is always valid")) - } - } - } - - async fn handle_request_inner( - self: Arc, - req: hyper::Request, - ) -> Result, ProxyError> { - // Handle "/ready" - if req.uri().path() == "/ready" { - if self.state.connected() { - return Ok(response_builder() - .status(hyper::StatusCode::OK) - .header(hyper::header::SERVER, SERVER_NAME) - .body("Plane Proxy server (ready)".into())?); - } else { - return Ok(response_builder() - .status(hyper::StatusCode::SERVICE_UNAVAILABLE) - .header(hyper::header::SERVER, SERVER_NAME) - .body("Plane Proxy server (not ready)".into())?); - } - } - - if self.https_redirect { - let Some(host) = req - .headers() - .get(hyper::header::HOST) - .and_then(|value| value.to_str().ok()) - else { - return Err(ProxyError::MissingHostHeader); - }; - - let host = match host.parse() { - Ok(host) => host, - Err(err) => { - tracing::warn!(?err, ?host, "Invalid host header."); - return Err(ProxyError::BadRequest); - } - }; - - let mut uri_parts = req.uri().clone().into_parts(); - uri_parts.scheme = Some("https".parse().expect("https is a valid scheme.")); - uri_parts.authority = Some(host); - uri_parts.path_and_query = uri_parts - .path_and_query - .or_else(|| Some(PathAndQuery::from_static(""))); - let uri = hyper::Uri::from_parts(uri_parts).expect("URI parts are valid."); - return Ok(response_builder() - .status(hyper::StatusCode::MOVED_PERMANENTLY) - .header(hyper::header::LOCATION, uri.to_string()) - .header(hyper::header::SERVER, SERVER_NAME) - .body(hyper::Body::empty())?); - } - - if req.uri().path() == "/" { - if let Some(root_redirect_url) = &self.root_redirect_url { - return Ok(response_builder() - .status(hyper::StatusCode::MOVED_PERMANENTLY) - .header(hyper::header::LOCATION, root_redirect_url.to_string()) - .header(hyper::header::SERVER, SERVER_NAME) - .body(hyper::Body::empty())?); - } - } - - self.handle_proxy_request(req).await - } - - async fn handle_proxy_request( - self: Arc, - req: hyper::Request, - ) -> Result, ProxyError> { - let Some(mut request_rewriter) = RequestRewriter::new(req, self.remote_meta) else { - tracing::warn!("Request rewriter failed to create."); - return Err(ProxyError::BadRequest); - }; - - let route_info = self - .state - .route_map - .lookup(request_rewriter.bearer_token()) - .await; - - let Some(route_info) = route_info else { - return Err(ProxyError::InvalidConnectionToken); - }; - - let subdomain = match request_rewriter.get_subdomain(&route_info.cluster) { - Ok(subdomain) => subdomain, - Err(err) => { - tracing::warn!(?err, "Subdomain not found in request rewriter."); - return Err(ProxyError::InvalidSubdomain); - } - }; - if subdomain != route_info.subdomain.as_deref() { - tracing::warn!( - "Subdomain mismatch! subdomain in header: {:?}, subdomain in backend: {:?}", - subdomain, - route_info.subdomain - ); - return Err(ProxyError::InvalidSubdomain); - } - - let backend_id = route_info.backend_id.clone(); - request_rewriter.set_authority(route_info.address.0); - - let mut response = if request_rewriter.should_upgrade() { - let (req, req_clone) = request_rewriter.into_request_pair(&route_info); - let response = self - .state - .http_client - .request(req_clone) - .await - .map_err(ProxyError::UpgradableRequestError)?; - let response_clone = clone_response_empty_body(&response); - - let mut response_upgrade = hyper::upgrade::on(response) - .await - .map_err(ProxyError::UpgradeError)?; - let monitor = self.state.monitor.monitor(); - let backend_id = backend_id.clone(); - - tokio::spawn(async move { - let mut req_upgrade = match hyper::upgrade::on(req).await { - Ok(req) => req, - Err(error) => { - tracing::error!(?error, "Error upgrading connection."); - return; - } - }; - - monitor - .lock() - .expect("Monitor lock was poisoned.") - .inc_connection(&backend_id); - - match copy_bidirectional(&mut req_upgrade, &mut response_upgrade).await { - Ok(_) => (), - Err(error) if error.kind() == ErrorKind::UnexpectedEof => { - tracing::info!("Upgraded connection closed with UnexpectedEof."); - } - Err(error) if error.kind() == ErrorKind::TimedOut => { - tracing::info!("Upgraded connection timed out."); - } - Err(error) if error.kind() == ErrorKind::ConnectionReset => { - tracing::info!("Connection reset by peer."); - } - Err(error) if error.kind() == ErrorKind::BrokenPipe => { - tracing::info!("Broken pipe."); - } - Err(error) => { - tracing::error!(?error, "Error with upgraded connection."); - } - } - - monitor - .lock() - .expect("Monitor lock was poisoned.") - .dec_connection(&backend_id); - }); - - response_clone - } else { - let req = request_rewriter.into_request(&route_info); - self.state.monitor.touch_backend(&backend_id); - self.state - .http_client - .request(req) - .await - .map_err(|e| ProxyError::RequestError(e, backend_id.clone()))? - }; - - let headers = response.headers_mut(); - headers.insert( - PLANE_BACKEND_ID_HEADER, - backend_id - .to_string() - .parse() - .expect("Backend ID is a valid header value."), - ); - - for (key, value) in DEFAULT_CORS_HEADERS { - if !headers.contains_key(*key) { - headers.insert( - *key, - value.parse().expect("CORS header is a valid header value."), - ); - } - } - - Ok(response) - } -} - -fn clone_response_empty_body(response: &Response) -> Response { - let mut builder = Response::builder(); - - builder - .headers_mut() - .expect("Builder::headers_mut should always work on a new builder.") - .extend(response.headers().clone()); - - builder = builder.status(response.status()); - - builder - .body(Body::empty()) - .expect("Response is always valid.") -} - -pub struct ProxyService { - handler: Arc, -} - -impl Service> for ProxyService { - type Response = Response; - type Error = Infallible; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Request) -> Self::Future { - Box::pin(self.handler.clone().handle_request(req)) - } -} - -pub struct ProxyMakeService { - pub state: Arc, - pub https_redirect: bool, - pub root_redirect_url: Option, -} - -impl ProxyMakeService { - pub fn serve_http(self, port: u16, shutdown_future: F) -> Result, ProxyError> - where - F: Future + Send + 'static, - { - let addr: SocketAddr = ([0, 0, 0, 0], port).into(); - tracing::info!(%addr, "Listening for HTTP connections."); - let server = hyper::Server::bind(&addr) - .serve(self) - .with_graceful_shutdown(shutdown_future); - let handle = tokio::spawn(async { - let _ = server.await; - }); - - Ok(handle) - } - - pub fn serve_https( - self, - port: u16, - cert_watcher: CertWatcher, - shutdown_future: F, - ) -> Result, ProxyError> - where - F: Future + Send + 'static, - { - let server_config = ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_cert_resolver(Arc::new(cert_watcher)); - - let addr: SocketAddr = ([0, 0, 0, 0], port).into(); - let incoming = AddrIncoming::bind(&addr).map_err(ProxyError::BindError)?; - tracing::info!(%addr, "Listening for HTTPS connections."); - - let tls_acceptor = TlsAcceptor::new(Arc::new(server_config), incoming); - - let server = hyper::Server::builder(tls_acceptor) - .serve(self) - .with_graceful_shutdown(shutdown_future); - let handle = tokio::spawn(async { - let _ = server.await; - }); - - Ok(handle) - } -} - -impl<'a> Service<&'a AddrStream> for ProxyMakeService { - type Response = ProxyService; - type Error = ProxyError; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: &'a AddrStream) -> Self::Future { - let remote_ip = req.remote_addr().ip(); - let handler = Arc::new(RequestHandler { - state: self.state.clone(), - https_redirect: self.https_redirect, - remote_meta: ForwardableRequestInfo { - ip: remote_ip, - protocol: Protocol::Http, - }, - root_redirect_url: self.root_redirect_url.clone(), - }); - ready(Ok(ProxyService { handler })).boxed() - } -} - -impl<'a> Service<&'a TlsStream> for ProxyMakeService { - type Response = ProxyService; - type Error = ProxyError; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: &'a TlsStream) -> Self::Future { - let remote_ip = req.remote_ip; - let handler = Arc::new(RequestHandler { - state: self.state.clone(), - https_redirect: false, - remote_meta: ForwardableRequestInfo { - ip: remote_ip, - protocol: Protocol::Https, - }, - root_redirect_url: self.root_redirect_url.clone(), - }); - ready(Ok(ProxyService { handler })).boxed() - } -} diff --git a/plane/src/proxy/rewriter.rs b/plane/src/proxy/rewriter.rs deleted file mode 100644 index 74810e920..000000000 --- a/plane/src/proxy/rewriter.rs +++ /dev/null @@ -1,286 +0,0 @@ -use super::{subdomain::subdomain_from_host, ForwardableRequestInfo}; -use crate::{ - protocol::RouteInfo, - types::{BearerToken, ClusterName}, -}; -use hyper::{ - header::HOST, - http::{request, uri}, - Body, HeaderMap, Request, Uri, -}; -use reqwest::header::HeaderValue; -use std::{borrow::BorrowMut, net::SocketAddr, str::FromStr}; -use tungstenite::http::uri::PathAndQuery; - -const VERIFIED_HEADER_PREFIX: &str = "x-verified-"; -const USERNAME_HEADER: &str = "x-verified-username"; -const AUTH_SECRET_HEADER: &str = "x-verified-secret"; -const AUTH_USER_DATA_HEADER: &str = "x-verified-user-data"; -const PATH_PREFIX_HEADER: &str = "x-verified-path"; -const BACKEND_ID_HEADER: &str = "x-verified-backend"; -const X_FORWARDED_FOR_HEADER: &str = "x-forwarded-for"; -const X_FORWARDED_PROTO_HEADER: &str = "x-forwarded-proto"; - -#[derive(Debug, thiserror::Error, PartialEq, Eq)] -pub enum RequestRewriterError { - #[error("Invalid `host` header")] - InvalidHostHeader, -} - -impl From for RequestRewriterError { - fn from(_: hyper::header::ToStrError) -> Self { - RequestRewriterError::InvalidHostHeader - } -} - -pub struct RequestRewriter { - parts: request::Parts, - uri_parts: uri::Parts, - body: Body, - bearer_token: BearerToken, - prefix_uri: Uri, - remote_meta: ForwardableRequestInfo, -} - -impl RequestRewriter { - pub fn new(request: Request, remote_meta: ForwardableRequestInfo) -> Option { - let (parts, body) = request.into_parts(); - - let mut uri_parts = parts.uri.clone().into_parts(); - uri_parts.scheme = Some("http".parse().expect("Scheme is valid.")); - - let bearer_token = match extract_bearer_token(&mut uri_parts) { - Some(bearer_token) => bearer_token, - None => { - tracing::warn!(uri=?parts.uri, "Bearer token not found in URI."); - return None; - } - }; - - let mut prefix_uri_parts = parts.uri.clone().into_parts(); - prefix_uri_parts.path_and_query = - Some(PathAndQuery::from_str(&format!("/{}/", bearer_token)).expect("Path is valid.")); - let prefix_uri = Uri::from_parts(prefix_uri_parts).expect("URI parts are valid."); - - Some(Self { - parts, - uri_parts, - body, - bearer_token, - prefix_uri, - remote_meta, - }) - } - - pub fn set_authority(&mut self, addr: SocketAddr) { - self.uri_parts.authority = Some( - addr.to_string() - .parse() - .expect("SocketAddr is a valid authority."), - ); - } - - pub fn bearer_token(&self) -> &BearerToken { - &self.bearer_token - } - - /// Returns the subdomain of the request's host header, after stripping the cluster name. - /// Returns Ok(Some(subdomain)) if a subdomain is found. - /// Returns Ok(None) if no subdomain is found, but the host header matches the cluster name. - /// Returns Err(RequestRewriterError::InvalidHostHeader) if the host header does not - /// match the cluster name, or no host header is found. - pub fn get_subdomain( - &self, - cluster: &ClusterName, - ) -> Result, RequestRewriterError> { - let Some(hostname) = self.parts.headers.get(HOST) else { - return Err(RequestRewriterError::InvalidHostHeader); - }; - - let hostname = match hostname.to_str() { - Ok(hostname) => hostname, - Err(err) => { - tracing::warn!(?hostname, ?err, "Host header is not valid UTF-8."); - return Err(RequestRewriterError::InvalidHostHeader); - } - }; - - subdomain_from_host(hostname, cluster) - } - - fn into_parts(self) -> (request::Parts, Body, Uri, ForwardableRequestInfo) { - let Self { - mut parts, - uri_parts, - body, - prefix_uri, - remote_meta, - .. - } = self; - - let uri = Uri::from_parts(uri_parts).expect("URI parts are valid."); - parts.uri = uri; - - (parts, body, prefix_uri, remote_meta) - } - - pub fn into_request(self, route_info: &RouteInfo) -> Request { - let (mut parts, body, prefix_uri, remote_meta) = self.into_parts(); - - let headers = parts.headers.borrow_mut(); - set_headers_from_route_info(headers, route_info, &prefix_uri, remote_meta); - - Request::from_parts(parts, body) - } - - pub fn into_request_pair(self, route_info: &RouteInfo) -> (Request, Request) { - let (parts, body, prefix_uri, remote_meta) = self.into_parts(); - let req2 = clone_request_with_empty_body(&parts, route_info, &prefix_uri, remote_meta); - let req1 = Request::from_parts(parts, body); - - (req1, req2) - } - - pub fn should_upgrade(&self) -> bool { - let Some(conn_header) = self.parts.headers.get("connection") else { - return false; - }; - - let Ok(conn_header) = conn_header.to_str() else { - return false; - }; - - conn_header - .to_lowercase() - .split(',') - .any(|s| s.trim() == "upgrade") - } -} - -fn clone_request_with_empty_body( - parts: &request::Parts, - route_info: &RouteInfo, - prefix_uri: &Uri, - remote_meta: ForwardableRequestInfo, -) -> request::Request { - let mut builder = request::Builder::new() - .method(parts.method.clone()) - .uri(parts.uri.clone()); - - let headers = builder - .headers_mut() - .expect("Can always call headers_mut() on a new builder."); - - headers.extend(parts.headers.clone()); - set_headers_from_route_info(headers, route_info, prefix_uri, remote_meta); - - builder - .body(Body::empty()) - .expect("Request is always valid.") -} - -fn extract_bearer_token(parts: &mut uri::Parts) -> Option { - let Some(path_and_query) = parts.path_and_query.clone() else { - panic!("No path and query"); - }; - - let full_path = path_and_query.path().strip_prefix('/')?; - - // Split the incoming path into the token and the path to proxy to. If there is no slash, the token is - // the full incoming path, and the path to proxy to is just `/`. - let (token, path) = match full_path.split_once('/') { - Some((token, path)) => (token, path), - None => (full_path, "/"), - }; - - let token = BearerToken::from(token.to_string()); - - if token.is_static() { - // We don't rewrite the URL if using a static token. - return Some(token); - } - - let query = path_and_query - .query() - .map(|query| format!("?{}", query)) - .unwrap_or_default(); - - parts.path_and_query = Some( - PathAndQuery::from_str(format!("/{}{}", path, query).as_str()) - .expect("Path and query is valid."), - ); - - Some(token) -} - -fn set_headers_from_route_info( - headers: &mut HeaderMap, - route_info: &RouteInfo, - prefix_uri: &Uri, - remote_meta: ForwardableRequestInfo, -) { - let mut headers_to_remove = Vec::new(); - for header_name in headers.keys() { - if header_name.as_str().starts_with(VERIFIED_HEADER_PREFIX) { - headers_to_remove.push(header_name.clone()); - } - } - - for header_name in headers_to_remove { - headers.remove(header_name); - } - - if let Some(user) = &route_info.user { - headers.insert( - USERNAME_HEADER, - HeaderValue::from_str(user.as_str()).expect("User is valid."), - ); - } - - let forwards = if let Some(forwards) = headers.get(X_FORWARDED_FOR_HEADER) { - let forwards = forwards.to_str().unwrap_or("").to_string(); - format!("{}, {}", forwards, remote_meta.ip) - } else { - remote_meta.ip.to_string() - }; - - headers.insert( - X_FORWARDED_FOR_HEADER, - HeaderValue::from_str(forwards.as_str()).expect("Forwards are valid."), - ); - - if headers.get(X_FORWARDED_PROTO_HEADER).is_none() { - headers.insert( - "x-forwarded-proto", - HeaderValue::from_static(remote_meta.protocol.as_str()), - ); - } - - headers.insert( - AUTH_SECRET_HEADER, - HeaderValue::from_str(&route_info.secret_token.to_string()).expect("Secret is valid."), - ); - - headers.insert( - AUTH_USER_DATA_HEADER, - HeaderValue::from_str( - &serde_json::to_string(&route_info.user_data) - .expect("JSON value should always serialize."), - ) - .expect("User data is valid"), - ); - - headers.insert( - PATH_PREFIX_HEADER, - HeaderValue::from_str(&prefix_uri.to_string()).expect("Path is valid."), - ); - - headers.insert( - BACKEND_ID_HEADER, - route_info - .backend_id - .to_string() - .parse() - .expect("Backend ID is a valid header value."), - ); -} diff --git a/plane/src/proxy/shutdown_signal.rs b/plane/src/proxy/shutdown_signal.rs deleted file mode 100644 index 3a8142920..000000000 --- a/plane/src/proxy/shutdown_signal.rs +++ /dev/null @@ -1,24 +0,0 @@ -use futures_util::Future; -use tokio::sync::broadcast; - -pub struct ShutdownSignal { - send_shutdown: broadcast::Sender<()>, -} - -impl ShutdownSignal { - pub fn new() -> Self { - let (send_shutdown, _) = broadcast::channel::<()>(1); - Self { send_shutdown } - } - - pub fn shutdown(&self) { - let _ = self.send_shutdown.send(()); - } - - pub fn subscribe(&self) -> impl Future + Send + 'static { - let mut receiver = self.send_shutdown.subscribe(); - async move { - let _ = receiver.recv().await; - } - } -} From a2f9781aba8be4e2ec91ffc464f6b3b400b32ae0 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 08:22:31 -0400 Subject: [PATCH 20/66] add x-verified- headers --- Cargo.lock | 1 + dynamic-proxy/src/request.rs | 8 ++- plane/Cargo.toml | 1 + plane/src/proxy/proxy_server.rs | 96 +++++++++++++++++++++++++-------- plane/src/types/mod.rs | 6 +++ 5 files changed, 89 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f35e65e72..7e34cb5d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2019,6 +2019,7 @@ dependencies = [ "async-trait", "axum 0.6.20", "bollard", + "bytes", "chrono", "clap", "colored", diff --git a/dynamic-proxy/src/request.rs b/dynamic-proxy/src/request.rs index 937fdbc8b..413cefaab 100644 --- a/dynamic-proxy/src/request.rs +++ b/dynamic-proxy/src/request.rs @@ -2,7 +2,7 @@ use bytes::Bytes; use http::{ request::Parts, uri::{Authority, Scheme}, - HeaderName, HeaderValue, Request, Uri, + HeaderMap, HeaderName, HeaderValue, Request, Uri, }; use http_body::Body; use std::{net::SocketAddr, str::FromStr}; @@ -11,7 +11,7 @@ use crate::body::{to_simple_body, SimpleBody}; pub struct MutableRequest where - T: Body, + T: Body + Send + Sync + 'static, T::Error: Into>, { pub parts: Parts, @@ -52,6 +52,10 @@ where let value = HeaderValue::from_str(value).unwrap(); self.parts.headers.append(key, value); } + + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.parts.headers + } } pub fn should_upgrade(request: &Request) -> bool { diff --git a/plane/Cargo.toml b/plane/Cargo.toml index 3e3835249..9bbdad6df 100644 --- a/plane/Cargo.toml +++ b/plane/Cargo.toml @@ -16,6 +16,7 @@ async-stream = "0.3.5" async-trait = "0.1.74" axum = { version = "0.6.20", features = ["ws"] } bollard = "0.17.0" +bytes = "1.7.2" chrono = { version = "0.4.31", features = ["serde"] } clap = { version = "4.4.10", features = ["derive"] } colored = "2.0.4" diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 9a64ac5fa..5fab6fd7f 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -3,9 +3,16 @@ use super::{ request::{get_and_maybe_remove_bearer_token, subdomain_from_host}, route_map::RouteMap, }; +use crate::{names::Name, protocol::RouteInfo}; +use bytes::Bytes; use dynamic_proxy::{ body::{simple_empty_body, SimpleBody}, - hyper::{body::Incoming, header, service::Service, Request, Response, StatusCode, Uri}, + hyper::{ + body::{Body, Incoming}, + header, + service::Service, + Request, Response, StatusCode, Uri, + }, proxy::ProxyClient, request::MutableRequest, }; @@ -59,6 +66,7 @@ impl Service> for ProxyState { // extract the bearer token from the request let mut uri_parts = request.parts.uri.clone().into_parts(); + let original_path = request.parts.uri.path().to_string(); let bearer_token = get_and_maybe_remove_bearer_token(&mut uri_parts); let Some(bearer_token) = bearer_token else { @@ -77,28 +85,10 @@ impl Service> for ProxyState { return status_code_to_response(StatusCode::UNAUTHORIZED); }; - // Check cluster and subdomain. - let Some(host) = request - .parts - .headers - .get(header::HOST) - .and_then(|h| h.to_str().ok()) - else { - return status_code_to_response(StatusCode::BAD_REQUEST); - }; - - let Ok(request_subdomain) = subdomain_from_host(host, &route_info.cluster) else { - // The host header does not match the expected cluster. - return status_code_to_response(StatusCode::FORBIDDEN); - }; - - if let Some(subdomain) = route_info.subdomain { - if request_subdomain != Some(&subdomain) { - return status_code_to_response(StatusCode::FORBIDDEN); - } + if let Err(status_code) = prepare_request(&mut request, &route_info, &original_path) { + return status_code_to_response(status_code); } - request.set_upstream_address(route_info.address.0); let request = request.into_request_with_simple_body(); let (res, upgrade_handler) = inner.proxy_client.request(request).await.unwrap(); @@ -126,6 +116,70 @@ impl Service> for ProxyState { } } +fn prepare_request( + request: &mut MutableRequest, + route_info: &RouteInfo, + original_path: &str, +) -> Result<(), StatusCode> +where + T: Body + Send + Sync, + T::Error: Into>, +{ + // Check cluster and subdomain. + let Some(host) = request + .parts + .headers + .get(header::HOST) + .and_then(|h| h.to_str().ok()) + else { + return Err(StatusCode::BAD_REQUEST); + }; + + let Ok(request_subdomain) = subdomain_from_host(host, &route_info.cluster) else { + // The host header does not match the expected cluster. + return Err(StatusCode::FORBIDDEN); + }; + + if let Some(subdomain) = &route_info.subdomain { + if request_subdomain != Some(subdomain) { + return Err(StatusCode::FORBIDDEN); + } + } + + request.set_upstream_address(route_info.address.0); + + // Remove x-verified-* headers from inbound request. + { + let headers = request.headers_mut(); + let mut headers_to_remove = Vec::new(); + headers.iter_mut().for_each(|(name, _)| { + if name.as_str().starts_with("x-verified-") { + headers_to_remove.push(name.clone()); + } + }); + + for header in headers_to_remove { + headers.remove(&header); + } + } + + // Set special Plane headers. + if let Some(username) = &route_info.user { + request.add_header("x-verified-username", username); + } + + if let Some(user_data) = &route_info.user_data { + let user_data_str = serde_json::to_string(user_data).unwrap_or_default(); + request.add_header("x-verified-user-data", &user_data_str); + } + + request.add_header("x-verified-path", original_path); + request.add_header("x-verified-backend", route_info.backend_id.as_str()); + request.add_header("x-verified-secret", route_info.secret_token.as_str()); + + Ok(()) +} + fn status_code_to_response( status_code: StatusCode, ) -> Result, Box> { diff --git a/plane/src/types/mod.rs b/plane/src/types/mod.rs index a48875bb1..66f072f4b 100644 --- a/plane/src/types/mod.rs +++ b/plane/src/types/mod.rs @@ -372,6 +372,12 @@ impl Display for SecretToken { } } +impl SecretToken { + pub fn as_str(&self) -> &str { + &self.0 + } +} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct ConnectResponse { pub backend_id: BackendName, From dd2dc078e4a2bb3717d192b691f0ddd0912b9090 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 08:44:01 -0400 Subject: [PATCH 21/66] add proxy_headers tests --- plane/plane-tests/tests/proxy_headers.rs | 134 +++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 plane/plane-tests/tests/proxy_headers.rs diff --git a/plane/plane-tests/tests/proxy_headers.rs b/plane/plane-tests/tests/proxy_headers.rs new file mode 100644 index 000000000..e2e32c226 --- /dev/null +++ b/plane/plane-tests/tests/proxy_headers.rs @@ -0,0 +1,134 @@ +use common::{ + localhost_resolver::localhost_client, + proxy_mock::MockProxy, + simple_axum_server::{RequestInfo, SimpleAxumServer}, + test_env::TestEnvironment, +}; +use plane::{ + log_types::BackendAddr, + names::{BackendName, Name}, + protocol::{RouteInfo, RouteInfoResponse}, + types::{BearerToken, ClusterName, SecretToken}, +}; +use plane_test_macro::plane_test; +use reqwest::StatusCode; +use serde_json::json; +use std::str::FromStr; + +mod common; + +#[plane_test] +async fn proxy_fake_verified_headers_are_stripped(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + + let handle = tokio::spawn( + client + .get(url) + .header("x-verified-blah", "foobar") // this header should be removed + .header("this-header-is-ok", "blah") // this header should be preserved + .send(), + ); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let request_info: RequestInfo = response.json().await.unwrap(); + assert_eq!(request_info.path, "/"); + assert_eq!(request_info.method, "GET"); + + assert_eq!(request_info.headers.get("x-verified-blah"), None); + assert_eq!( + request_info.headers.get("this-header-is-ok"), + Some(&"blah".to_string()) + ); +} + +#[plane_test] +async fn proxy_plane_headers_are_set(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/a/b/c/"); + let client = localhost_client(); + + let handle = tokio::spawn(client.get(url).send()); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::try_from("backend123".to_string()).unwrap(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret987".to_string()), + cluster, + user: Some("auser123".to_string()), + user_data: Some(json!({ + "access": "readonly", + "email": "a@example.com", + })), + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let request_info: RequestInfo = response.json().await.unwrap(); + assert_eq!(request_info.path, "/a/b/c/"); + assert_eq!(request_info.method, "GET"); + + assert_eq!( + request_info.headers.get("x-verified-username"), + Some(&"auser123".to_string()) + ); + assert_eq!( + request_info.headers.get("x-verified-user-data"), + Some(&r#"{"access":"readonly","email":"a@example.com"}"#.to_string()) + ); + assert_eq!( + request_info.headers.get("x-verified-secret"), + Some(&"secret987".to_string()) + ); + assert_eq!( + request_info.headers.get("x-verified-path"), + Some(&"/abc123/a/b/c/".to_string()) + ); + assert_eq!( + request_info.headers.get("x-verified-backend"), + Some(&"backend123".to_string()) + ); +} From 61a770a6f48848b1026da0f66463ab2b047eafa4 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 08:54:57 -0400 Subject: [PATCH 22/66] add test of CORS headers in error response --- plane/plane-tests/tests/proxy.rs | 2 +- plane/plane-tests/tests/proxy_cors.rs | 73 +++++++++++++++++++++++++++ plane/src/proxy/proxy_server.rs | 32 ++++++++++-- 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 plane/plane-tests/tests/proxy_cors.rs diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index b9ecd395a..c29c97c62 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -51,7 +51,7 @@ async fn proxy_bad_bearer_token(env: TestEnvironment) { let response = handle.await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.status(), StatusCode::FORBIDDEN); } #[plane_test] diff --git a/plane/plane-tests/tests/proxy_cors.rs b/plane/plane-tests/tests/proxy_cors.rs new file mode 100644 index 000000000..523365ce3 --- /dev/null +++ b/plane/plane-tests/tests/proxy_cors.rs @@ -0,0 +1,73 @@ +use common::{ + localhost_resolver::localhost_client, proxy_mock::MockProxy, + simple_axum_server::SimpleAxumServer, test_env::TestEnvironment, +}; +use plane::{protocol::RouteInfoResponse, types::BearerToken}; +use plane_test_macro::plane_test; +use reqwest::StatusCode; + +mod common; + +#[plane_test] +async fn proxy_forbidden_request_has_cors_headers(env: TestEnvironment) { + let _server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + + let handle = tokio::spawn(client.get(url).send()); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: None, + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response + .headers() + .get("access-control-allow-origin") + .unwrap() + .to_str() + .unwrap(), + "*" + ); + assert_eq!( + response + .headers() + .get("access-control-allow-methods") + .unwrap() + .to_str() + .unwrap(), + "*" + ); + assert_eq!( + response + .headers() + .get("access-control-allow-headers") + .unwrap() + .to_str() + .unwrap(), + "*" + ); + assert_eq!( + response + .headers() + .get("access-control-allow-credentials") + .unwrap() + .to_str() + .unwrap(), + "true" + ); +} diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 5fab6fd7f..569804969 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -9,7 +9,7 @@ use dynamic_proxy::{ body::{simple_empty_body, SimpleBody}, hyper::{ body::{Body, Incoming}, - header, + header::{self, HeaderValue}, service::Service, Request, Response, StatusCode, Uri, }, @@ -82,7 +82,7 @@ impl Service> for ProxyState { let route_info = inner.route_map.lookup(&bearer_token).await; let Some(route_info) = route_info else { - return status_code_to_response(StatusCode::UNAUTHORIZED); + return status_code_to_response(StatusCode::FORBIDDEN); }; if let Err(status_code) = prepare_request(&mut request, &route_info, &original_path) { @@ -183,8 +183,32 @@ where fn status_code_to_response( status_code: StatusCode, ) -> Result, Box> { - Ok(Response::builder() + let mut response = Response::builder() .status(status_code) .body(simple_empty_body()) - .unwrap()) + .unwrap(); + + apply_cors_headers(&mut response); + + Ok(response) +} + +fn apply_cors_headers(response: &mut Response) { + let headers = response.headers_mut(); + headers.insert( + header::ACCESS_CONTROL_ALLOW_ORIGIN, + HeaderValue::from_static("*"), + ); + headers.insert( + header::ACCESS_CONTROL_ALLOW_METHODS, + HeaderValue::from_static("*"), + ); + headers.insert( + header::ACCESS_CONTROL_ALLOW_HEADERS, + HeaderValue::from_static("*"), + ); + headers.insert( + header::ACCESS_CONTROL_ALLOW_CREDENTIALS, + HeaderValue::from_static("true"), + ); } From f7b50623a89a6a58a307b229bd3221ac5139e729 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 08:59:45 -0400 Subject: [PATCH 23/66] valid response has CORS headers --- plane/plane-tests/tests/proxy_cors.rs | 82 ++++++++++++++++++++++++++- plane/src/proxy/proxy_server.rs | 4 +- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/plane/plane-tests/tests/proxy_cors.rs b/plane/plane-tests/tests/proxy_cors.rs index 523365ce3..d12dc35e7 100644 --- a/plane/plane-tests/tests/proxy_cors.rs +++ b/plane/plane-tests/tests/proxy_cors.rs @@ -2,16 +2,20 @@ use common::{ localhost_resolver::localhost_client, proxy_mock::MockProxy, simple_axum_server::SimpleAxumServer, test_env::TestEnvironment, }; -use plane::{protocol::RouteInfoResponse, types::BearerToken}; +use plane::{ + log_types::BackendAddr, + names::{BackendName, Name}, + protocol::{RouteInfo, RouteInfoResponse}, + types::{BearerToken, ClusterName, SecretToken}, +}; use plane_test_macro::plane_test; use reqwest::StatusCode; +use std::str::FromStr; mod common; #[plane_test] async fn proxy_forbidden_request_has_cors_headers(env: TestEnvironment) { - let _server = SimpleAxumServer::new().await; - let mut proxy = MockProxy::new().await; let port = proxy.port(); let url = format!("http://plane.test:{port}/abc123/"); @@ -71,3 +75,75 @@ async fn proxy_forbidden_request_has_cors_headers(env: TestEnvironment) { "true" ); } + +#[plane_test] +async fn proxy_valid_request_has_cors_headers(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + let mut proxy = MockProxy::new().await; + let cluster = ClusterName::from_str(&format!("plane.test:{}", proxy.port())).unwrap(); + let port = proxy.port(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + + let handle = tokio::spawn(client.get(url).send()); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get("access-control-allow-origin") + .unwrap() + .to_str() + .unwrap(), + "*" + ); + assert_eq!( + response + .headers() + .get("access-control-allow-methods") + .unwrap() + .to_str() + .unwrap(), + "*" + ); + assert_eq!( + response + .headers() + .get("access-control-allow-headers") + .unwrap() + .to_str() + .unwrap(), + "*" + ); + assert_eq!( + response + .headers() + .get("access-control-allow-credentials") + .unwrap() + .to_str() + .unwrap(), + "true" + ); +} diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 569804969..49376e049 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -91,7 +91,7 @@ impl Service> for ProxyState { let request = request.into_request_with_simple_body(); - let (res, upgrade_handler) = inner.proxy_client.request(request).await.unwrap(); + let (mut res, upgrade_handler) = inner.proxy_client.request(request).await.unwrap(); if let Some(upgrade_handler) = upgrade_handler { let monitor = inner.monitor.monitor(); @@ -111,6 +111,8 @@ impl Service> for ProxyState { inner.monitor.touch_backend(&route_info.backend_id); } + apply_cors_headers(&mut res); + Ok(res) }) } From 27d33e518d322beb00d56b02b1a330160bfdfa14 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 10:21:51 -0400 Subject: [PATCH 24/66] x-forwarded-* headers --- dynamic-proxy/src/request.rs | 3 +- dynamic-proxy/src/server.rs | 62 ++++++++++++++++--- .../tests/common/hello_world_service.rs | 20 +++++- dynamic-proxy/tests/hello_world_http.rs | 5 +- dynamic-proxy/tests/https_test.rs | 5 +- dynamic-proxy/tests/test_http_versions.rs | 10 ++- plane/plane-tests/tests/proxy.rs | 40 ++++++++++++ 7 files changed, 131 insertions(+), 14 deletions(-) diff --git a/dynamic-proxy/src/request.rs b/dynamic-proxy/src/request.rs index 413cefaab..a192c3d62 100644 --- a/dynamic-proxy/src/request.rs +++ b/dynamic-proxy/src/request.rs @@ -1,3 +1,4 @@ +use crate::body::{to_simple_body, SimpleBody}; use bytes::Bytes; use http::{ request::Parts, @@ -7,8 +8,6 @@ use http::{ use http_body::Body; use std::{net::SocketAddr, str::FromStr}; -use crate::body::{to_simple_body, SimpleBody}; - pub struct MutableRequest where T: Body + Send + Sync + 'static, diff --git a/dynamic-proxy/src/server.rs b/dynamic-proxy/src/server.rs index dd1503e09..69f33e5bb 100644 --- a/dynamic-proxy/src/server.rs +++ b/dynamic-proxy/src/server.rs @@ -2,13 +2,18 @@ use crate::{ body::SimpleBody, graceful_shutdown::GracefulShutdown, https_redirect::HttpsRedirectService, }; use anyhow::Result; +use http::HeaderValue; use hyper::{body::Incoming, service::Service, Request, Response}; use hyper_util::{ rt::{TokioExecutor, TokioIo}, server::conn::auto::Builder as ServerBuilder, }; use rustls::{server::ResolvesServerCert, ServerConfig}; -use std::{net::SocketAddr, sync::Arc, time::Duration}; +use std::{ + net::{IpAddr, SocketAddr}, + sync::Arc, + time::Duration, +}; use tokio::{net::TcpListener, select, task::JoinSet}; use tokio_rustls::TlsAcceptor; @@ -36,14 +41,15 @@ where _ = recv.changed() => break, }; - let stream = match stream { - Ok((stream, _)) => stream, + let (stream, remote_addr) = match stream { + Ok((stream, remote_addr)) => (stream, remote_addr), Err(e) => { tracing::warn!(?e, "Failed to accept connection."); continue; } }; - let service = service.clone(); + let remote_ip = remote_addr.ip(); + let service = WrappedService::new(service.clone(), remote_ip, "http"); let server = ServerBuilder::new(TokioExecutor::new()); let io = TokioIo::new(stream); @@ -87,14 +93,15 @@ where _ = recv.changed() => break, }; - let stream = match stream { - Ok((stream, _)) => stream, + let (stream, remote_addr) = match stream { + Ok((stream, remote_addr)) => (stream, remote_addr), Err(e) => { tracing::warn!(?e, "Failed to accept connection."); continue; } }; - let service = service.clone(); + let remote_ip = remote_addr.ip(); + let service = WrappedService::new(service.clone(), remote_ip, "https"); let tls_acceptor = tls_acceptor.clone(); let graceful_shutdown = graceful_shutdown.clone(); @@ -181,6 +188,7 @@ impl SimpleHttpServer { } pub async fn graceful_shutdown(mut self) { + println!("Shutting down"); let graceful_shutdown = self .graceful_shutdown .take() @@ -281,3 +289,43 @@ impl ServerWithHttpRedirect { } } } + +/// A service that wraps another service and sets +/// X-Forwarded-For and X-Forwarded-Proto headers. +struct WrappedService { + inner: S, + forwarded_for: IpAddr, + forwarded_proto: &'static str, +} + +impl WrappedService { + pub fn new(inner: S, forwarded_for: IpAddr, forwarded_proto: &'static str) -> Self { + Self { + inner, + forwarded_for, + forwarded_proto, + } + } +} + +impl Service> for WrappedService +where + S: Service, Response = Response>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn call(&self, request: Request) -> Self::Future { + let mut request = request; + request.headers_mut().insert( + "X-Forwarded-For", + HeaderValue::from_str(&format!("{}", self.forwarded_for)).unwrap(), + ); + request.headers_mut().insert( + "X-Forwarded-Proto", + HeaderValue::from_str(self.forwarded_proto).unwrap(), + ); + self.inner.call(request) + } +} diff --git a/dynamic-proxy/tests/common/hello_world_service.rs b/dynamic-proxy/tests/common/hello_world_service.rs index 1e0b3f858..ca645df2a 100644 --- a/dynamic-proxy/tests/common/hello_world_service.rs +++ b/dynamic-proxy/tests/common/hello_world_service.rs @@ -14,11 +14,29 @@ impl Service> for HelloWorldService { fn call(&self, request: Request) -> Self::Future { Box::pin(async { + let x_forwarded_for = request + .headers() + .get("x-forwarded-for") + .map(|h| h.to_str().unwrap().to_string()) + .unwrap_or_default(); + let x_forwarded_proto = request + .headers() + .get("x-forwarded-proto") + .map(|h| h.to_str().unwrap().to_string()) + .unwrap_or_default(); + let _ = request.collect().await.unwrap().to_bytes(); + let body = format!( + "Hello, world! X-Forwarded-For: {}, X-Forwarded-Proto: {}", + x_forwarded_for, x_forwarded_proto + ); + let response = Response::builder() .status(200) - .body(to_simple_body(Full::new(Bytes::from("Hello, world!")))) + .body(to_simple_body(Full::new(Bytes::from( + body.as_bytes().to_vec(), + )))) .unwrap(); Ok(response) diff --git a/dynamic-proxy/tests/hello_world_http.rs b/dynamic-proxy/tests/hello_world_http.rs index 2c79bac6b..d9cb4ab2e 100644 --- a/dynamic-proxy/tests/hello_world_http.rs +++ b/dynamic-proxy/tests/hello_world_http.rs @@ -19,5 +19,8 @@ async fn test_hello_world_http() { let client = reqwest::Client::new(); let res = client.get(url).send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); - assert_eq!(res.text().await.unwrap(), "Hello, world!"); + assert_eq!( + res.text().await.unwrap(), + "Hello, world! X-Forwarded-For: 127.0.0.1, X-Forwarded-Proto: http" + ); } diff --git a/dynamic-proxy/tests/https_test.rs b/dynamic-proxy/tests/https_test.rs index e9fa19467..95f06902a 100644 --- a/dynamic-proxy/tests/https_test.rs +++ b/dynamic-proxy/tests/https_test.rs @@ -32,5 +32,8 @@ async fn test_https() { let res = client.get(&url).send().await.unwrap(); assert!(res.status().is_success()); - assert_eq!(res.text().await.unwrap(), "Hello, world!"); + assert_eq!( + res.text().await.unwrap(), + "Hello, world! X-Forwarded-For: 127.0.0.1, X-Forwarded-Proto: https" + ); } diff --git a/dynamic-proxy/tests/test_http_versions.rs b/dynamic-proxy/tests/test_http_versions.rs index 7c5defe8e..3734d6f4e 100644 --- a/dynamic-proxy/tests/test_http_versions.rs +++ b/dynamic-proxy/tests/test_http_versions.rs @@ -20,7 +20,10 @@ async fn test_http1() { let res = client.get(url).send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.version(), reqwest::Version::HTTP_11); - assert_eq!(res.text().await.unwrap(), "Hello, world!"); + assert_eq!( + res.text().await.unwrap(), + "Hello, world! X-Forwarded-For: 127.0.0.1, X-Forwarded-Proto: http" + ); } #[tokio::test] @@ -40,5 +43,8 @@ async fn test_http2() { let res = client.get(url).send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.version(), reqwest::Version::HTTP_2); - assert_eq!(res.text().await.unwrap(), "Hello, world!"); + assert_eq!( + res.text().await.unwrap(), + "Hello, world! X-Forwarded-For: 127.0.0.1, X-Forwarded-Proto: http" + ); } diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index c29c97c62..fd163e35e 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -240,3 +240,43 @@ async fn proxy_expected_subdomain_is_present(env: TestEnvironment) { let response = handle.await.unwrap().unwrap(); assert_eq!(response.status(), StatusCode::OK); } + +#[plane_test] +async fn proxy_backend_passes_forwarded_headers(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let request_info: RequestInfo = response.json().await.unwrap(); + let headers = request_info.headers; + assert_eq!(headers.get("x-forwarded-for").unwrap(), "127.0.0.1"); + assert_eq!(headers.get("x-forwarded-proto").unwrap(), "http"); +} From ff50340e738e417fc6ccee5673dac9cd82532ecf Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 10:33:02 -0400 Subject: [PATCH 25/66] x-plane-backend-id header --- plane/plane-tests/tests/proxy.rs | 42 ++++++++++++++++++++++++++++++++ plane/src/proxy/proxy_server.rs | 7 +++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index fd163e35e..71a699728 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -280,3 +280,45 @@ async fn proxy_backend_passes_forwarded_headers(env: TestEnvironment) { assert_eq!(headers.get("x-forwarded-for").unwrap(), "127.0.0.1"); assert_eq!(headers.get("x-forwarded-proto").unwrap(), "http"); } + +#[plane_test] +async fn proxy_returns_backend_id_in_header(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); + + let backend_id = BackendName::new_random(); + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: backend_id.clone(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let headers = response.headers(); + assert_eq!( + headers.get("x-plane-backend-id").unwrap().to_str().unwrap(), + &backend_id.to_string() + ); +} diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 49376e049..6f0670cc8 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -99,19 +99,24 @@ impl Service> for ProxyState { .lock() .expect("Monitor lock poisoned") .inc_connection(&route_info.backend_id); + let backend_id = route_info.backend_id.clone(); tokio::spawn(async move { upgrade_handler.run().await.unwrap(); monitor .lock() .expect("Monitor lock poisoned") - .dec_connection(&route_info.backend_id); + .dec_connection(&backend_id); }); } else { inner.monitor.touch_backend(&route_info.backend_id); } apply_cors_headers(&mut res); + res.headers_mut().insert( + "x-plane-backend-id", + HeaderValue::from_str(&route_info.backend_id.to_string()).unwrap(), + ); Ok(res) }) From 6970b6e2da61b3fd5e22ab57294276e0f7356f7e Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 10:47:50 -0400 Subject: [PATCH 26/66] error handling in https redirect service --- dynamic-proxy/src/body.rs | 7 ++-- dynamic-proxy/src/https_redirect.rs | 56 +++++++++++++++++++---------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/dynamic-proxy/src/body.rs b/dynamic-proxy/src/body.rs index def167013..c899ddfed 100644 --- a/dynamic-proxy/src/body.rs +++ b/dynamic-proxy/src/body.rs @@ -1,3 +1,5 @@ +//! Provides a concrete, boxed body and error type. + use bytes::Bytes; use http_body::Body; use http_body_util::combinators::BoxBody; @@ -10,10 +12,9 @@ pub type SimpleBody = BoxBody; pub fn to_simple_body(body: B) -> SimpleBody where B: Body + Send + Sync + 'static, - B::Error: Into>, + B::Error: Into, { - body.map_err(|e| e.into() as Box) - .boxed() + body.map_err(|e| e.into() as BoxedError).boxed() } pub fn simple_empty_body() -> SimpleBody { diff --git a/dynamic-proxy/src/https_redirect.rs b/dynamic-proxy/src/https_redirect.rs index 98faa2912..4fdf6d6bf 100644 --- a/dynamic-proxy/src/https_redirect.rs +++ b/dynamic-proxy/src/https_redirect.rs @@ -1,4 +1,4 @@ -use crate::body::{simple_empty_body, SimpleBody}; +use crate::body::{simple_empty_body, BoxedError, SimpleBody}; use http::{ header, uri::{Authority, Scheme}, @@ -7,23 +7,24 @@ use http::{ use hyper::{body::Incoming, service::Service}; use std::{future::ready, pin::Pin, str::FromStr}; +/// A hyper service that redirects HTTP requests to HTTPS. #[derive(Debug, Clone)] pub struct HttpsRedirectService; -impl Service> for HttpsRedirectService { - type Response = Response; - type Error = http::Error; - type Future = Pin, http::Error>>>>; - - fn call(&self, request: Request) -> Self::Future { +impl HttpsRedirectService { + fn call_inner(request: Request) -> Result, StatusCode> { // Get the host header. - let hostname = request.headers().get(header::HOST).unwrap(); + let hostname = request + .headers() + .get(header::HOST) + .ok_or(StatusCode::BAD_REQUEST)?; // Parse the host header into an authority. - let authority = Authority::from_str(hostname.to_str().unwrap()) - .expect("Valid host is valid authority."); - // Strip the path. let authority = - Authority::from_str(authority.host()).expect("Valid host is valid authority."); + Authority::from_str(hostname.to_str().map_err(|_| StatusCode::BAD_REQUEST)?) + .map_err(|_| StatusCode::BAD_REQUEST)?; + // Strip the port. + let authority = + Authority::from_str(authority.host()).expect("Valid host is always valid authority."); let request_uri = request.uri().clone(); @@ -31,20 +32,39 @@ impl Service> for HttpsRedirectService { let mut parts = request_uri.into_parts(); parts.scheme = Some(Scheme::HTTPS); - // Remove the port from the authority if it exists - // let authority = parts.authority.map(|d| d.host().to_string()).unwrap(); - // parts.authority = Some(Authority::from_str(authority).unwrap()); - parts.authority = Some(authority); // Build the new URI - let new_uri = Uri::from_parts(parts).unwrap(); + let new_uri = Uri::from_parts(parts).expect("URI is always valid"); let response = Response::builder() .status(StatusCode::FOUND) .header(header::LOCATION, new_uri.to_string()) .body(simple_empty_body()); - Box::pin(ready(response)) + response.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) + } +} + +impl Service> for HttpsRedirectService { + type Response = Response; + type Error = BoxedError; + type Future = Pin, BoxedError>>>>; + + fn call(&self, request: Request) -> Self::Future { + let result = Self::call_inner(request); + + let result = match result { + Ok(response) => response, + Err(status) => { + tracing::error!("Error redirecting to HTTPS: {}", status); + Response::builder() + .status(status) + .body(simple_empty_body()) + .unwrap() + } + }; + + Box::pin(ready(Ok(result))) } } From 0a34df94380a97d2b2cc451c6ebe3692abe9eae1 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 10:48:08 -0400 Subject: [PATCH 27/66] nit --- dynamic-proxy/src/https_redirect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynamic-proxy/src/https_redirect.rs b/dynamic-proxy/src/https_redirect.rs index 4fdf6d6bf..356c9c71c 100644 --- a/dynamic-proxy/src/https_redirect.rs +++ b/dynamic-proxy/src/https_redirect.rs @@ -61,7 +61,7 @@ impl Service> for HttpsRedirectService { Response::builder() .status(status) .body(simple_empty_body()) - .unwrap() + .expect("Response is always valid") } }; From 29b9ff2ae7967343a909d622421a0a01366f3f1a Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 10:53:12 -0400 Subject: [PATCH 28/66] proxy comments --- dynamic-proxy/src/proxy.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dynamic-proxy/src/proxy.rs b/dynamic-proxy/src/proxy.rs index 33625dde5..dcc5916eb 100644 --- a/dynamic-proxy/src/proxy.rs +++ b/dynamic-proxy/src/proxy.rs @@ -11,6 +11,9 @@ use hyper_util::{ }; use std::time::Duration; +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); + +/// A client for proxying HTTP requests to an upstream server. pub struct ProxyClient { client: Client, timeout: Duration, @@ -36,10 +39,14 @@ impl ProxyClient { let client = Client::builder(TokioExecutor::new()).build(HttpConnector::new()); Self { client, - timeout: Duration::from_secs(10), + timeout: DEFAULT_TIMEOUT, } } + /// Sends an HTTP request to the upstream server and returns the response. + /// If the request establishes a websocket connection, an upgrade handler is returned. + /// In this case, you must call and await `.run()` on the upgrade handler (i.e. in a tokio task) + /// to ensure that messages are properly sent and received. pub async fn request( &self, request: Request, From a9f8b8ed0004b0a33c920ae5c5cb0172c0848725 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 10:56:31 -0400 Subject: [PATCH 29/66] request docs --- dynamic-proxy/src/request.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/dynamic-proxy/src/request.rs b/dynamic-proxy/src/request.rs index a192c3d62..e897efe13 100644 --- a/dynamic-proxy/src/request.rs +++ b/dynamic-proxy/src/request.rs @@ -8,6 +8,7 @@ use http::{ use http_body::Body; use std::{net::SocketAddr, str::FromStr}; +/// Represents an HTTP request (from hyper) with helpers for mutating it. pub struct MutableRequest where T: Body + Send + Sync + 'static, @@ -35,6 +36,7 @@ where Request::from_parts(self.parts, to_simple_body(self.body)) } + /// Rewrite the request so that it points to the given upstream address. pub fn set_upstream_address(&mut self, address: SocketAddr) { let uri = std::mem::take(&mut self.parts.uri); let mut uri_parts = uri.into_parts(); @@ -46,9 +48,19 @@ where self.parts.uri = Uri::from_parts(uri_parts).expect("URI should always be valid."); } + /// Add a header to the request. + /// + /// If the header is invalid, it will be ignored and logged. pub fn add_header(&mut self, key: &str, value: &str) { - let key = HeaderName::from_str(key).unwrap(); - let value = HeaderValue::from_str(value).unwrap(); + let Ok(key) = HeaderName::from_str(key) else { + tracing::error!("Attempted to set invalid header name: {}", key); + return; + }; + let Ok(value) = HeaderValue::from_str(value) else { + // Not logging the value, which could be sensitive. + tracing::error!("Attempted to set invalid header value with key: {}", key); + return; + }; self.parts.headers.append(key, value); } From 374075643176bb5e11164ce5958c8e931c5a108c Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 11:00:43 -0400 Subject: [PATCH 30/66] server --- dynamic-proxy/src/server.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/dynamic-proxy/src/server.rs b/dynamic-proxy/src/server.rs index 69f33e5bb..4a65bddca 100644 --- a/dynamic-proxy/src/server.rs +++ b/dynamic-proxy/src/server.rs @@ -17,11 +17,15 @@ use std::{ use tokio::{net::TcpListener, select, task::JoinSet}; use tokio_rustls::TlsAcceptor; +/// A simple server that wraps a hyper service and handles requests. +/// The server can be configured to listen for either HTTP and HTTPS, +/// and supports graceful shutdown and x-forwarded-* headers. pub struct SimpleHttpServer { handle: tokio::task::JoinHandle>, graceful_shutdown: Option, } +#[must_use] // Otherwise, the tasks we started would be stopped as soon as the graceful shutdown is initiated. async fn listen_loop( listener: TcpListener, service: S, @@ -69,6 +73,7 @@ where join_set } +#[must_use] // Otherwise, the tasks we started would be stopped as soon as the graceful shutdown is initiated. async fn listen_loop_tls( listener: TcpListener, service: S, @@ -201,9 +206,11 @@ impl SimpleHttpServer { .graceful_shutdown .take() .expect("self.graceful_shutdown is always set"); - tokio::time::timeout(timeout, graceful_shutdown.shutdown()) - .await - .unwrap(); + let result = tokio::time::timeout(timeout, graceful_shutdown.shutdown()).await; + + if let Err(e) = result { + tracing::warn!(?e, "Timed out waiting for graceful shutdown, aborting."); + } } } @@ -320,11 +327,12 @@ where let mut request = request; request.headers_mut().insert( "X-Forwarded-For", - HeaderValue::from_str(&format!("{}", self.forwarded_for)).unwrap(), + HeaderValue::from_str(&format!("{}", self.forwarded_for)) + .expect("X-Forwarded-For is always valid"), ); request.headers_mut().insert( "X-Forwarded-Proto", - HeaderValue::from_str(self.forwarded_proto).unwrap(), + HeaderValue::from_str(self.forwarded_proto).expect("X-Forwarded-Proto is always valid"), ); self.inner.call(request) } From b36046ace6bf0cd71541f100753986bed7599adc Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 11:05:24 -0400 Subject: [PATCH 31/66] upgrade --- dynamic-proxy/src/upgrade.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dynamic-proxy/src/upgrade.rs b/dynamic-proxy/src/upgrade.rs index 9082d53d8..c90195c36 100644 --- a/dynamic-proxy/src/upgrade.rs +++ b/dynamic-proxy/src/upgrade.rs @@ -30,6 +30,10 @@ pub fn split_response(response: Response) -> (Response, Respon (response1, response2) } +/// Wraps connection state that is needed to upgrade a connection so it can be passed back +/// from the connection handler. +/// +/// The receiver should call `.run` to turn this into a future, and then await it. pub struct UpgradeHandler { pub request: Request, pub response: Response, From aff6fcd15fdae967cef5a6ca57eb8ea7b688431d Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 11:08:29 -0400 Subject: [PATCH 32/66] plane-test deps --- dynamic-proxy/tests/common/cert.rs | 2 ++ dynamic-proxy/tests/common/hello_world_service.rs | 1 + dynamic-proxy/tests/common/simple_axum_server.rs | 1 + dynamic-proxy/tests/common/simple_upgrade_service.rs | 3 +++ dynamic-proxy/tests/common/websocket_echo_server.rs | 1 + plane/plane-tests/Cargo.toml | 8 +++----- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/dynamic-proxy/tests/common/cert.rs b/dynamic-proxy/tests/common/cert.rs index fc9784e44..208834628 100644 --- a/dynamic-proxy/tests/common/cert.rs +++ b/dynamic-proxy/tests/common/cert.rs @@ -6,6 +6,8 @@ use std::sync::Arc; const CERTIFICATE_SUBJECT_ALT_NAME: &str = "plane.test"; +/// A certificate resolver that generates its own certificate on creation, +/// and uses that for all requests. #[derive(Debug)] pub struct StaticCertificateResolver { certified_key: Arc, diff --git a/dynamic-proxy/tests/common/hello_world_service.rs b/dynamic-proxy/tests/common/hello_world_service.rs index ca645df2a..666136e7f 100644 --- a/dynamic-proxy/tests/common/hello_world_service.rs +++ b/dynamic-proxy/tests/common/hello_world_service.rs @@ -4,6 +4,7 @@ use http_body_util::{BodyExt, Full}; use hyper::{body::Incoming, service::Service, Request, Response}; use std::{convert::Infallible, future::Future, pin::Pin}; +/// A service that returns a greeting with the X-Forwarded-For and X-Forwarded-Proto headers. #[derive(Clone)] pub struct HelloWorldService; diff --git a/dynamic-proxy/tests/common/simple_axum_server.rs b/dynamic-proxy/tests/common/simple_axum_server.rs index 999e0ca74..b6a89cb1e 100644 --- a/dynamic-proxy/tests/common/simple_axum_server.rs +++ b/dynamic-proxy/tests/common/simple_axum_server.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, net::SocketAddr}; use tokio::net::TcpListener; +/// A simple server that returns the request info as json. pub struct SimpleAxumServer { handle: tokio::task::JoinHandle<()>, addr: SocketAddr, diff --git a/dynamic-proxy/tests/common/simple_upgrade_service.rs b/dynamic-proxy/tests/common/simple_upgrade_service.rs index ba1e5e5f6..81bfdcf4b 100644 --- a/dynamic-proxy/tests/common/simple_upgrade_service.rs +++ b/dynamic-proxy/tests/common/simple_upgrade_service.rs @@ -12,6 +12,9 @@ use hyper_util::rt::TokioIo; use std::{convert::Infallible, future::Future, pin::Pin}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +/// A service that upgrades the connection and echos messages back to the client. +/// (Note: this does not use the actual websocket protocol on the wire, but is sufficient to +/// test the upgrade path.) #[derive(Clone)] pub struct SimpleUpgradeService; diff --git a/dynamic-proxy/tests/common/websocket_echo_server.rs b/dynamic-proxy/tests/common/websocket_echo_server.rs index c84586c1a..9afd21dc5 100644 --- a/dynamic-proxy/tests/common/websocket_echo_server.rs +++ b/dynamic-proxy/tests/common/websocket_echo_server.rs @@ -7,6 +7,7 @@ use axum::{ use std::net::SocketAddr; use tokio::net::TcpListener; +/// A websocket echo server that echos messages back to the client. pub struct WebSocketEchoServer { handle: tokio::task::JoinHandle<()>, addr: SocketAddr, diff --git a/plane/plane-tests/Cargo.toml b/plane/plane-tests/Cargo.toml index 535b185d3..362d37880 100644 --- a/plane/plane-tests/Cargo.toml +++ b/plane/plane-tests/Cargo.toml @@ -11,10 +11,13 @@ bollard = "0.17.0" chrono = { version = "0.4.31", features = ["serde"] } dynamic-proxy = { path = "../../dynamic-proxy" } futures-util = "0.3.29" +http = "1.1.0" +http-body-util = "0.1.2" hyper = { version = "0.14.27", features = ["server"] } plane = { path = "../plane-dynamic", package = "plane-dynamic" } plane-test-macro = { path = "plane-test-macro" } reqwest = { version = "0.11.22", features = ["json", "rustls-tls"], default-features = false } +serde = "1.0.210" serde_json = "1.0.107" thiserror = "1.0.50" tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread", "signal"] } @@ -22,8 +25,3 @@ tracing = "0.1.40" tracing-appender = "0.2.2" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } url = "2.4.1" - -[dev-dependencies] -http = "1.1.0" -http-body-util = "0.1.2" -serde = "1.0.210" From f5e36fb200bfe9799197844403e3882a6859e541 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 11:17:57 -0400 Subject: [PATCH 33/66] more cleanup --- plane/plane-tests/tests/common/mod.rs | 4 ++-- plane/src/proxy/cert_pair.rs | 3 +-- plane/src/proxy/mod.rs | 2 -- plane/src/proxy/proxy_server.rs | 5 ++++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plane/plane-tests/tests/common/mod.rs b/plane/plane-tests/tests/common/mod.rs index 1011493fc..d8f8efde7 100644 --- a/plane/plane-tests/tests/common/mod.rs +++ b/plane/plane-tests/tests/common/mod.rs @@ -10,9 +10,9 @@ pub mod docker; pub mod localhost_resolver; pub mod proxy_mock; pub mod resources; -pub mod simple_axum_server; +pub mod simple_axum_server; // NOTE: copied from dynamic-proxy pub mod test_env; -pub mod timeout; // NOTE: copied from dynamic-proxy +pub mod timeout; pub fn run_test(name: &str, time_limit: Duration, test_function: F) where diff --git a/plane/src/proxy/cert_pair.rs b/plane/src/proxy/cert_pair.rs index 1a6713459..bee43ea4b 100644 --- a/plane/src/proxy/cert_pair.rs +++ b/plane/src/proxy/cert_pair.rs @@ -75,8 +75,7 @@ impl CertificatePair { .map(|cert| CertificateDer::from(cert.to_vec())) .collect(); - // let private_key = PrivateKey(key.secret_der().to_vec()); // NB. rustls 0.22 gets rid of this; the PrivateKeyDer is passed to any_supported_type directly. - let key = any_supported_type(&key)?; + let key = any_supported_type(key)?; let certified_key = CertifiedKey::new(certs, key); diff --git a/plane/src/proxy/mod.rs b/plane/src/proxy/mod.rs index b0476e67e..8c573812f 100644 --- a/plane/src/proxy/mod.rs +++ b/plane/src/proxy/mod.rs @@ -17,9 +17,7 @@ mod cert_pair; pub mod command; mod connection_monitor; pub mod proxy_connection; -// mod proxy_service; pub mod proxy_server; -// mod rewriter; mod request; mod route_map; diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 6f0670cc8..1b3235847 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -73,7 +73,10 @@ impl Service> for ProxyState { return Box::pin(ready(status_code_to_response(StatusCode::UNAUTHORIZED))); }; - request.parts.uri = Uri::from_parts(uri_parts).unwrap(); + let Ok(uri) = Uri::from_parts(uri_parts) else { + return Box::pin(ready(status_code_to_response(StatusCode::BAD_REQUEST))); + }; + request.parts.uri = uri; let inner = self.inner.clone(); From 5f5e4004c9de522f251ba5770592c3d7e8129629 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 11:38:13 -0400 Subject: [PATCH 34/66] clean up unwraps --- dynamic-proxy/src/proxy.rs | 4 ++-- plane/src/proxy/proxy_server.rs | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/dynamic-proxy/src/proxy.rs b/dynamic-proxy/src/proxy.rs index dcc5916eb..4beeabaa0 100644 --- a/dynamic-proxy/src/proxy.rs +++ b/dynamic-proxy/src/proxy.rs @@ -9,7 +9,7 @@ use hyper_util::{ client::legacy::{connect::HttpConnector, Client}, rt::TokioExecutor, }; -use std::time::Duration; +use std::{convert::Infallible, time::Duration}; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); @@ -52,7 +52,7 @@ impl ProxyClient { request: Request, ) -> Result< (Response, Option), - Box, + Infallible, > { let url = request.uri().to_string(); diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 1b3235847..3aed6acc1 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -94,7 +94,15 @@ impl Service> for ProxyState { let request = request.into_request_with_simple_body(); - let (mut res, upgrade_handler) = inner.proxy_client.request(request).await.unwrap(); + let result = inner.proxy_client.request(request).await; + + let (mut res, upgrade_handler) = match result { + Ok((res, upgrade_handler)) => (res, upgrade_handler), + Err(e) => { + tracing::error!("Error proxying request: {}", e); + return status_code_to_response(StatusCode::INTERNAL_SERVER_ERROR); + } + }; if let Some(upgrade_handler) = upgrade_handler { let monitor = inner.monitor.monitor(); @@ -104,7 +112,9 @@ impl Service> for ProxyState { .inc_connection(&route_info.backend_id); let backend_id = route_info.backend_id.clone(); tokio::spawn(async move { - upgrade_handler.run().await.unwrap(); + if let Err(err) = upgrade_handler.run().await { + tracing::error!("Error running upgrade handler: {}", err); + }; monitor .lock() @@ -118,7 +128,7 @@ impl Service> for ProxyState { apply_cors_headers(&mut res); res.headers_mut().insert( "x-plane-backend-id", - HeaderValue::from_str(&route_info.backend_id.to_string()).unwrap(), + HeaderValue::from_str(&route_info.backend_id.to_string()).expect("Backend ID is always a valid header value"), ); Ok(res) @@ -196,7 +206,7 @@ fn status_code_to_response( let mut response = Response::builder() .status(status_code) .body(simple_empty_body()) - .unwrap(); + .expect("Failed to build response"); apply_cors_headers(&mut response); From e58038164ab2fc90cb406e341a8ee5b850ea25bf Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 11:38:22 -0400 Subject: [PATCH 35/66] format --- dynamic-proxy/src/proxy.rs | 5 +---- plane/src/proxy/proxy_server.rs | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/dynamic-proxy/src/proxy.rs b/dynamic-proxy/src/proxy.rs index 4beeabaa0..f46c335a1 100644 --- a/dynamic-proxy/src/proxy.rs +++ b/dynamic-proxy/src/proxy.rs @@ -50,10 +50,7 @@ impl ProxyClient { pub async fn request( &self, request: Request, - ) -> Result< - (Response, Option), - Infallible, - > { + ) -> Result<(Response, Option), Infallible> { let url = request.uri().to_string(); let res = self.handle_request(request).await; diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 3aed6acc1..9e9d3ec6c 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -128,7 +128,8 @@ impl Service> for ProxyState { apply_cors_headers(&mut res); res.headers_mut().insert( "x-plane-backend-id", - HeaderValue::from_str(&route_info.backend_id.to_string()).expect("Backend ID is always a valid header value"), + HeaderValue::from_str(&route_info.backend_id.to_string()) + .expect("Backend ID is always a valid header value"), ); Ok(res) From 5c445c10c57f29030daa2d40c7299c42cd9e1370 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 11:39:23 -0400 Subject: [PATCH 36/66] replace unwraps --- dynamic-proxy/src/proxy.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dynamic-proxy/src/proxy.rs b/dynamic-proxy/src/proxy.rs index f46c335a1..cef894c09 100644 --- a/dynamic-proxy/src/proxy.rs +++ b/dynamic-proxy/src/proxy.rs @@ -63,7 +63,7 @@ impl ProxyClient { Response::builder() .status(StatusCode::GATEWAY_TIMEOUT) .body(simple_empty_body()) - .unwrap(), + .expect("Failed to build response"), None, )); } @@ -73,7 +73,7 @@ impl ProxyClient { Response::builder() .status(StatusCode::BAD_GATEWAY) .body(simple_empty_body()) - .unwrap(), + .expect("Failed to build response"), None, )); } From c16a88ac4074917741dd3fa91487bfefdb7370f1 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 15:36:29 -0400 Subject: [PATCH 37/66] some pr nits --- dynamic-proxy/src/server.rs | 4 ++++ dynamic-proxy/tests/graceful.rs | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/dynamic-proxy/src/server.rs b/dynamic-proxy/src/server.rs index 4a65bddca..54c8b2e2d 100644 --- a/dynamic-proxy/src/server.rs +++ b/dynamic-proxy/src/server.rs @@ -216,6 +216,10 @@ impl SimpleHttpServer { impl Drop for SimpleHttpServer { fn drop(&mut self) { + if self.graceful_shutdown.is_some() { + tracing::warn!("Shutting down SimpleHttpServer without a call to graceful_shutdown. Connections will be dropped abruptly!"); + } + self.handle.abort(); } } diff --git a/dynamic-proxy/tests/graceful.rs b/dynamic-proxy/tests/graceful.rs index 5597fb28d..0b15541f6 100644 --- a/dynamic-proxy/tests/graceful.rs +++ b/dynamic-proxy/tests/graceful.rs @@ -7,10 +7,17 @@ use std::net::SocketAddr; use tokio::net::TcpListener; use tokio::time::Duration; -mod common; - // Ref: https://github.com/hyperium/hyper-util/blob/master/examples/server_graceful.rs +async fn slow_hello_world( + _: hyper::Request, +) -> Result, Infallible> { + tokio::time::sleep(Duration::from_secs(1)).await; // emulate slow request + let body = http_body_util::Full::::from("Hello, world!".to_owned()); + let body = to_simple_body(body); + Ok(hyper::Response::new(body)) +} + #[tokio::test] async fn test_graceful_shutdown() { // Start the server @@ -18,12 +25,7 @@ async fn test_graceful_shutdown() { let listener = TcpListener::bind(addr).await.unwrap(); let addr = listener.local_addr().unwrap(); let server = SimpleHttpServer::new( - hyper::service::service_fn(|_| async move { - tokio::time::sleep(Duration::from_secs(1)).await; // emulate slow request - let body = http_body_util::Full::::from("Hello, world!".to_owned()); - let body = to_simple_body(body); - Ok::<_, Infallible>(hyper::Response::new(body)) - }), + hyper::service::service_fn(slow_hello_world), listener, HttpsConfig::Http, ) From b3c7ab33d035bb46131bbdf2b7a9e864a489adfa Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 15:39:26 -0400 Subject: [PATCH 38/66] use consts for headers --- dynamic-proxy/src/server.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dynamic-proxy/src/server.rs b/dynamic-proxy/src/server.rs index 54c8b2e2d..1aa323373 100644 --- a/dynamic-proxy/src/server.rs +++ b/dynamic-proxy/src/server.rs @@ -17,6 +17,12 @@ use std::{ use tokio::{net::TcpListener, select, task::JoinSet}; use tokio_rustls::TlsAcceptor; +/// Header which passes the client's IP address to the backend. +const X_FORWARDED_FOR: &str = "x-forwarded-for"; + +/// Header which passes the client's protocol (http or https) to the backend. +const X_FORWARDED_PROTO: &str = "x-forwarded-proto"; + /// A simple server that wraps a hyper service and handles requests. /// The server can be configured to listen for either HTTP and HTTPS, /// and supports graceful shutdown and x-forwarded-* headers. @@ -330,12 +336,12 @@ where fn call(&self, request: Request) -> Self::Future { let mut request = request; request.headers_mut().insert( - "X-Forwarded-For", + X_FORWARDED_FOR, HeaderValue::from_str(&format!("{}", self.forwarded_for)) .expect("X-Forwarded-For is always valid"), ); request.headers_mut().insert( - "X-Forwarded-Proto", + X_FORWARDED_PROTO, HeaderValue::from_str(self.forwarded_proto).expect("X-Forwarded-Proto is always valid"), ); self.inner.call(request) From ef07e70501a66c9568d69fce2438ad1d8e13cf6e Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 15:43:09 -0400 Subject: [PATCH 39/66] undo macos bug fix --- plane/plane-tests/tests/common/resources/pebble.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plane/plane-tests/tests/common/resources/pebble.rs b/plane/plane-tests/tests/common/resources/pebble.rs index 0bed017c2..908d07673 100644 --- a/plane/plane-tests/tests/common/resources/pebble.rs +++ b/plane/plane-tests/tests/common/resources/pebble.rs @@ -8,7 +8,6 @@ use plane::proxy::AcmeEabConfiguration; use reqwest::Client; use serde_json::json; use std::os::unix::fs::PermissionsExt; -use std::path::Path; use std::time::{Duration, SystemTime}; use url::Url; @@ -96,12 +95,12 @@ impl Pebble { ) -> Result { let scratch_dir = env.scratch_dir.clone(); - #[cfg(target_os = "macos")] - avoid_weird_mac_bug(&env.run_name, &scratch_dir).await?; - let pebble_dir = scratch_dir.canonicalize()?.join("pebble"); std::fs::create_dir_all(&pebble_dir)?; + #[cfg(target_os = "macos")] + avoid_weird_mac_bug(&env.run_name, &scratch_dir).await?; + let mut pebble_config = json!({ "pebble": { "listenAddress": "0.0.0.0:14000", @@ -191,7 +190,7 @@ impl Pebble { /// the scratch directory itself (i.e. the parent of the pebble directory) seems to prevent this /// from happening. #[cfg(target_os = "macos")] -pub async fn avoid_weird_mac_bug(name: &str, scratch_dir: &Path) -> Result<()> { +pub async fn avoid_weird_mac_bug(name: &str, scratch_dir: &std::path::Path) -> Result<()> { println!( "Creating dummy container for macos {}", scratch_dir.to_str().unwrap() From d1f467c11677674a4d1dd47940a612fb99f88087 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 16:40:38 -0400 Subject: [PATCH 40/66] send server header --- .../plane-tests/tests/proxy_server_header.rs | 83 +++++++++++++++++++ plane/src/proxy/proxy_server.rs | 11 ++- 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 plane/plane-tests/tests/proxy_server_header.rs diff --git a/plane/plane-tests/tests/proxy_server_header.rs b/plane/plane-tests/tests/proxy_server_header.rs new file mode 100644 index 000000000..e50d349ce --- /dev/null +++ b/plane/plane-tests/tests/proxy_server_header.rs @@ -0,0 +1,83 @@ +use common::{ + localhost_resolver::localhost_client, proxy_mock::MockProxy, + simple_axum_server::SimpleAxumServer, test_env::TestEnvironment, +}; +use plane::{ + log_types::BackendAddr, + names::{BackendName, Name}, + protocol::{RouteInfo, RouteInfoResponse}, + types::{BearerToken, ClusterName, SecretToken}, +}; +use plane_test_macro::plane_test; +use reqwest::StatusCode; +use std::str::FromStr; + +mod common; + +#[plane_test] +async fn proxy_bad_request_includes_server_header(env: TestEnvironment) { + let mut proxy = MockProxy::new().await; + let url = format!("http://{}/abc/", proxy.addr()); + let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); + + let _ = proxy.recv_route_info_request().await; + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc".to_string()), + route_info: None, + }) + .await; + + let response = handle.await.unwrap(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert!(response + .headers() + .get("server") + .unwrap() + .to_str() + .unwrap() + .starts_with("Plane/"),); +} + +#[plane_test] +async fn proxy_valid_request_includes_server_header(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert!(response + .headers() + .get("server") + .unwrap() + .to_str() + .unwrap() + .starts_with("Plane/"),); +} diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 9e9d3ec6c..dce096bf0 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -3,7 +3,7 @@ use super::{ request::{get_and_maybe_remove_bearer_token, subdomain_from_host}, route_map::RouteMap, }; -use crate::{names::Name, protocol::RouteInfo}; +use crate::{names::Name, protocol::RouteInfo, SERVER_NAME}; use bytes::Bytes; use dynamic_proxy::{ body::{simple_empty_body, SimpleBody}, @@ -125,7 +125,7 @@ impl Service> for ProxyState { inner.monitor.touch_backend(&route_info.backend_id); } - apply_cors_headers(&mut res); + apply_general_headers(&mut res); res.headers_mut().insert( "x-plane-backend-id", HeaderValue::from_str(&route_info.backend_id.to_string()) @@ -209,12 +209,14 @@ fn status_code_to_response( .body(simple_empty_body()) .expect("Failed to build response"); - apply_cors_headers(&mut response); + apply_general_headers(&mut response); Ok(response) } -fn apply_cors_headers(response: &mut Response) { +/// Mutates a request to add static headers present on all responses +/// (error or valid). +fn apply_general_headers(response: &mut Response) { let headers = response.headers_mut(); headers.insert( header::ACCESS_CONTROL_ALLOW_ORIGIN, @@ -232,4 +234,5 @@ fn apply_cors_headers(response: &mut Response) { header::ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"), ); + headers.insert(header::SERVER, HeaderValue::from_static(SERVER_NAME)); } From 89c0ea030e13f4ceef02dd58e068196ab620debf Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 16:51:25 -0400 Subject: [PATCH 41/66] use GONE header --- plane/plane-tests/tests/proxy.rs | 4 ++-- plane/plane-tests/tests/proxy_cors.rs | 4 ++-- plane/plane-tests/tests/proxy_server_header.rs | 6 +++--- plane/src/proxy/proxy_server.rs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index 71a699728..806405e84 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -27,7 +27,7 @@ async fn proxy_no_bearer_token(env: TestEnvironment) { let response = handle.await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[plane_test] @@ -51,7 +51,7 @@ async fn proxy_bad_bearer_token(env: TestEnvironment) { let response = handle.await.unwrap(); - assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!(response.status(), StatusCode::GONE); } #[plane_test] diff --git a/plane/plane-tests/tests/proxy_cors.rs b/plane/plane-tests/tests/proxy_cors.rs index d12dc35e7..f3705fb2f 100644 --- a/plane/plane-tests/tests/proxy_cors.rs +++ b/plane/plane-tests/tests/proxy_cors.rs @@ -15,7 +15,7 @@ use std::str::FromStr; mod common; #[plane_test] -async fn proxy_forbidden_request_has_cors_headers(env: TestEnvironment) { +async fn proxy_gone_request_has_cors_headers(env: TestEnvironment) { let mut proxy = MockProxy::new().await; let port = proxy.port(); let url = format!("http://plane.test:{port}/abc123/"); @@ -37,7 +37,7 @@ async fn proxy_forbidden_request_has_cors_headers(env: TestEnvironment) { .await; let response = handle.await.unwrap().unwrap(); - assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!(response.status(), StatusCode::GONE); assert_eq!( response .headers() diff --git a/plane/plane-tests/tests/proxy_server_header.rs b/plane/plane-tests/tests/proxy_server_header.rs index e50d349ce..00c22567e 100644 --- a/plane/plane-tests/tests/proxy_server_header.rs +++ b/plane/plane-tests/tests/proxy_server_header.rs @@ -15,7 +15,7 @@ use std::str::FromStr; mod common; #[plane_test] -async fn proxy_bad_request_includes_server_header(env: TestEnvironment) { +async fn proxy_error_response_includes_server_header(env: TestEnvironment) { let mut proxy = MockProxy::new().await; let url = format!("http://{}/abc/", proxy.addr()); let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); @@ -29,7 +29,7 @@ async fn proxy_bad_request_includes_server_header(env: TestEnvironment) { .await; let response = handle.await.unwrap(); - assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!(response.status(), StatusCode::GONE); assert!(response .headers() .get("server") @@ -40,7 +40,7 @@ async fn proxy_bad_request_includes_server_header(env: TestEnvironment) { } #[plane_test] -async fn proxy_valid_request_includes_server_header(env: TestEnvironment) { +async fn proxy_valid_response_includes_server_header(env: TestEnvironment) { let server = SimpleAxumServer::new().await; let mut proxy = MockProxy::new().await; diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index dce096bf0..4959ea816 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -70,7 +70,7 @@ impl Service> for ProxyState { let bearer_token = get_and_maybe_remove_bearer_token(&mut uri_parts); let Some(bearer_token) = bearer_token else { - return Box::pin(ready(status_code_to_response(StatusCode::UNAUTHORIZED))); + return Box::pin(ready(status_code_to_response(StatusCode::BAD_REQUEST))); }; let Ok(uri) = Uri::from_parts(uri_parts) else { @@ -85,7 +85,7 @@ impl Service> for ProxyState { let route_info = inner.route_map.lookup(&bearer_token).await; let Some(route_info) = route_info else { - return status_code_to_response(StatusCode::FORBIDDEN); + return status_code_to_response(StatusCode::GONE); }; if let Err(status_code) = prepare_request(&mut request, &route_info, &original_path) { From 5ddbffb970125040f160f326ad0dc32f6be2bdb0 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 16:54:15 -0400 Subject: [PATCH 42/66] return MOVED_PERMANENTLY instead of FOUND --- dynamic-proxy/src/https_redirect.rs | 2 +- dynamic-proxy/tests/test_http_redirect.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dynamic-proxy/src/https_redirect.rs b/dynamic-proxy/src/https_redirect.rs index 356c9c71c..c15af625f 100644 --- a/dynamic-proxy/src/https_redirect.rs +++ b/dynamic-proxy/src/https_redirect.rs @@ -38,7 +38,7 @@ impl HttpsRedirectService { let new_uri = Uri::from_parts(parts).expect("URI is always valid"); let response = Response::builder() - .status(StatusCode::FOUND) + .status(StatusCode::MOVED_PERMANENTLY) .header(header::LOCATION, new_uri.to_string()) .body(simple_empty_body()); diff --git a/dynamic-proxy/tests/test_http_redirect.rs b/dynamic-proxy/tests/test_http_redirect.rs index d83569019..0cd952576 100644 --- a/dynamic-proxy/tests/test_http_redirect.rs +++ b/dynamic-proxy/tests/test_http_redirect.rs @@ -37,7 +37,7 @@ async fn do_request(url: &str) -> Response { async fn test_https_redirect() { let response = do_request("http://foo.bar.baz").await; - assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY); assert_eq!( response.headers().get(header::LOCATION).unwrap(), "https://foo.bar.baz/" @@ -48,7 +48,7 @@ async fn test_https_redirect() { async fn test_https_redirect_with_slash_path() { let response = do_request("http://foo.bar.baz/").await; - assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY); assert_eq!( response.headers().get(header::LOCATION).unwrap(), "https://foo.bar.baz/" @@ -59,7 +59,7 @@ async fn test_https_redirect_with_slash_path() { async fn test_https_redirect_with_path() { let response = do_request("http://foo.bar.baz/abc/123").await; - assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY); assert_eq!( response.headers().get(header::LOCATION).unwrap(), "https://foo.bar.baz/abc/123" @@ -70,7 +70,7 @@ async fn test_https_redirect_with_path() { async fn test_https_redirect_with_query_params() { let response = do_request("http://foo.bar.baz/?a=1&b=2").await; - assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY); assert_eq!( response.headers().get(header::LOCATION).unwrap(), "https://foo.bar.baz/?a=1&b=2" From c11cb6c2b74df0ac97f4aaaedea1e761cf9a32d3 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 17:53:24 -0400 Subject: [PATCH 43/66] proxy ready endpoint --- plane/plane-tests/tests/common/proxy_mock.rs | 4 ++ plane/plane-tests/tests/common/test_env.rs | 2 + plane/plane-tests/tests/proxy_ready.rs | 39 ++++++++++++++++++++ plane/src/proxy/proxy_connection.rs | 3 ++ plane/src/proxy/proxy_server.rs | 24 +++++++++++- 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 plane/plane-tests/tests/proxy_ready.rs diff --git a/plane/plane-tests/tests/common/proxy_mock.rs b/plane/plane-tests/tests/common/proxy_mock.rs index 2db37c8e6..1d2ef1e8c 100644 --- a/plane/plane-tests/tests/common/proxy_mock.rs +++ b/plane/plane-tests/tests/common/proxy_mock.rs @@ -49,6 +49,10 @@ impl MockProxy { self.addr.port() } + pub fn set_ready(&self, ready: bool) { + self.proxy_state.set_ready(ready); + } + pub async fn recv_route_info_request(&mut self) -> RouteInfoRequest { self.route_info_request_receiver .recv() diff --git a/plane/plane-tests/tests/common/test_env.rs b/plane/plane-tests/tests/common/test_env.rs index 38c1f6435..57c770a10 100644 --- a/plane/plane-tests/tests/common/test_env.rs +++ b/plane/plane-tests/tests/common/test_env.rs @@ -150,6 +150,7 @@ impl TestEnvironment { Ok(Proxy { port, _server: server, + _connection: proxy_connection, }) } @@ -297,6 +298,7 @@ pub struct Proxy { #[allow(dead_code)] // Used in tests. pub port: u16, _server: SimpleHttpServer, + _connection: ProxyConnection, } #[allow(dead_code)] // Used in tests. diff --git a/plane/plane-tests/tests/proxy_ready.rs b/plane/plane-tests/tests/proxy_ready.rs new file mode 100644 index 000000000..d524b3a82 --- /dev/null +++ b/plane/plane-tests/tests/proxy_ready.rs @@ -0,0 +1,39 @@ +use common::{proxy_mock::MockProxy, test_env::TestEnvironment}; +use plane_test_macro::plane_test; +use reqwest::StatusCode; + +mod common; + +#[plane_test] +async fn proxy_not_ready(env: TestEnvironment) { + let proxy = MockProxy::new().await; + let url = format!("http://{}/ready", proxy.addr()); + let response = reqwest::get(url).await.expect("Failed to send request"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); +} + +#[plane_test] +async fn proxy_ready(env: TestEnvironment) { + let proxy = MockProxy::new().await; + proxy.set_ready(true); + let url = format!("http://{}/ready", proxy.addr()); + let response = reqwest::get(url).await.expect("Failed to send request"); + + assert_eq!(response.status(), StatusCode::OK); +} + +/// Tests that the proxy becomes ready when it connects to the controller. +/// It's surprisingly hard to test that the proxy becomes non-ready when the +/// controller shuts down, because we don't actually drop the connection +/// task (we rely on the process exiting to do that). This is kind of a bug, +/// at least for the purpose of testing. +#[plane_test] +async fn proxy_becomes_ready(env: TestEnvironment) { + let controller = env.controller().await; + let proxy = env.proxy(&controller).await.unwrap(); + + let url = format!("http://127.0.0.1:{}/ready", proxy.port); + let response = reqwest::get(&url).await.expect("Failed to send request"); + assert_eq!(response.status(), StatusCode::OK); +} diff --git a/plane/src/proxy/proxy_connection.rs b/plane/src/proxy/proxy_connection.rs index 22b9e32fb..92d952c6a 100644 --- a/plane/src/proxy/proxy_connection.rs +++ b/plane/src/proxy/proxy_connection.rs @@ -21,6 +21,7 @@ impl ProxyConnection { cluster: ClusterName, mut cert_manager: CertManager, ) -> Self { + tracing::info!("Creating proxy connection"); let state = Arc::new(ProxyState::new()); let handle = { @@ -30,7 +31,9 @@ impl ProxyConnection { let mut proxy_connection = client.proxy_connection(&cluster); loop { + state.set_ready(false); let mut conn = proxy_connection.connect_with_retry(&name).await; + state.set_ready(true); let sender = conn.sender(MessageFromProxy::CertManagerRequest); cert_manager.set_request_sender(move |m| { diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 4959ea816..945caacf9 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -16,13 +16,17 @@ use dynamic_proxy::{ proxy::ProxyClient, request::MutableRequest, }; -use std::future::{ready, Future}; +use std::{ + future::{ready, Future}, + sync::atomic::{AtomicBool, Ordering}, +}; use std::{pin::Pin, sync::Arc}; pub struct ProxyStateInner { pub route_map: RouteMap, pub proxy_client: ProxyClient, pub monitor: ConnectionMonitorHandle, + pub connected: AtomicBool, } #[derive(Clone)] @@ -42,12 +46,21 @@ impl ProxyState { route_map: RouteMap::new(), proxy_client: ProxyClient::new(), monitor: ConnectionMonitorHandle::new(), + connected: AtomicBool::new(false), }; Self { inner: Arc::new(inner), } } + + pub fn set_ready(&self, ready: bool) { + self.inner.connected.store(ready, Ordering::Relaxed); + } + + pub fn is_ready(&self) -> bool { + self.inner.connected.load(Ordering::Relaxed) + } } impl Service> for ProxyState { @@ -62,6 +75,15 @@ impl Service> for ProxyState { >; fn call(&self, request: Request) -> Self::Future { + // Handle "/ready" + if request.uri().path() == "/ready" { + if self.is_ready() { + return Box::pin(ready(status_code_to_response(StatusCode::OK))); + } else { + return Box::pin(ready(status_code_to_response(StatusCode::SERVICE_UNAVAILABLE))); + } + } + let mut request = MutableRequest::from_request(request); // extract the bearer token from the request From ff8b0b990cac32de0a5b0f2f442bea9d2ffe187e Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 17:58:48 -0400 Subject: [PATCH 44/66] fix proxy test --- plane/plane-tests/tests/proxy_ready.rs | 8 +++++++- plane/src/proxy/proxy_server.rs | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plane/plane-tests/tests/proxy_ready.rs b/plane/plane-tests/tests/proxy_ready.rs index d524b3a82..f8c1348f2 100644 --- a/plane/plane-tests/tests/proxy_ready.rs +++ b/plane/plane-tests/tests/proxy_ready.rs @@ -32,8 +32,14 @@ async fn proxy_ready(env: TestEnvironment) { async fn proxy_becomes_ready(env: TestEnvironment) { let controller = env.controller().await; let proxy = env.proxy(&controller).await.unwrap(); - + let url = format!("http://127.0.0.1:{}/ready", proxy.port); + let response = reqwest::get(&url).await.expect("Failed to send request"); + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + + // Wait for the proxy to become ready. + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + let response = reqwest::get(&url).await.expect("Failed to send request"); assert_eq!(response.status(), StatusCode::OK); } diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 945caacf9..b197873d6 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -80,7 +80,9 @@ impl Service> for ProxyState { if self.is_ready() { return Box::pin(ready(status_code_to_response(StatusCode::OK))); } else { - return Box::pin(ready(status_code_to_response(StatusCode::SERVICE_UNAVAILABLE))); + return Box::pin(ready(status_code_to_response( + StatusCode::SERVICE_UNAVAILABLE, + ))); } } From 79508d5f0e527ca1f5061f5920637dbe38f2b6c5 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 17:59:32 -0400 Subject: [PATCH 45/66] race comment --- plane/plane-tests/tests/proxy_ready.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/plane/plane-tests/tests/proxy_ready.rs b/plane/plane-tests/tests/proxy_ready.rs index f8c1348f2..145eb5151 100644 --- a/plane/plane-tests/tests/proxy_ready.rs +++ b/plane/plane-tests/tests/proxy_ready.rs @@ -34,6 +34,7 @@ async fn proxy_becomes_ready(env: TestEnvironment) { let proxy = env.proxy(&controller).await.unwrap(); let url = format!("http://127.0.0.1:{}/ready", proxy.port); + // NB. This is a race, we can remove it if this test starts to flake. let response = reqwest::get(&url).await.expect("Failed to send request"); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); From 73d4e42f754266f376f97710177ac72a443aff8d Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 24 Sep 2024 18:22:27 -0400 Subject: [PATCH 46/66] fix root redirect test --- plane/plane-tests/tests/cert_manager.rs | 12 ++++++++-- plane/plane-tests/tests/common/proxy_mock.rs | 10 +++++++- plane/plane-tests/tests/common/test_env.rs | 17 +++++++++---- plane/plane-tests/tests/proxy.rs | 22 ++++++++++++++++- plane/src/proxy/mod.rs | 18 +++++++++++--- plane/src/proxy/proxy_connection.rs | 2 +- plane/src/proxy/proxy_server.rs | 25 ++++++++++++++++++-- 7 files changed, 91 insertions(+), 15 deletions(-) diff --git a/plane/plane-tests/tests/cert_manager.rs b/plane/plane-tests/tests/cert_manager.rs index 36d91e58f..fdc1e60ae 100644 --- a/plane/plane-tests/tests/cert_manager.rs +++ b/plane/plane-tests/tests/cert_manager.rs @@ -1,10 +1,12 @@ +use std::sync::Arc; + use crate::common::timeout::WithTimeout; use common::test_env::TestEnvironment; use plane::{ names::{Name, ProxyName}, proxy::{ - cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection, AcmeConfig, - AcmeEabConfiguration, + cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection, + proxy_server::ProxyState, AcmeConfig, AcmeEabConfiguration, }, }; use plane_test_macro::plane_test; @@ -39,11 +41,14 @@ async fn cert_manager_does_refresh(env: TestEnvironment) { .await .unwrap(); + let state = Arc::new(ProxyState::new(None)); + let _proxy_connection = ProxyConnection::new( ProxyName::new_random(), controller.client(), env.cluster.clone(), cert_manager, + state.clone(), ); cert_watcher .wait_for_initial_cert() @@ -86,11 +91,14 @@ async fn cert_manager_does_refresh_eab(env: TestEnvironment) { .await .unwrap(); + let state = Arc::new(ProxyState::new(None)); + let _proxy_connection = ProxyConnection::new( ProxyName::new_random(), controller.client(), env.cluster.clone(), cert_manager, + state.clone(), ); cert_watcher .wait_for_initial_cert() diff --git a/plane/plane-tests/tests/common/proxy_mock.rs b/plane/plane-tests/tests/common/proxy_mock.rs index 1d2ef1e8c..128b459dd 100644 --- a/plane/plane-tests/tests/common/proxy_mock.rs +++ b/plane/plane-tests/tests/common/proxy_mock.rs @@ -16,7 +16,15 @@ pub struct MockProxy { #[allow(unused)] impl MockProxy { pub async fn new() -> Self { - let proxy_state = ProxyState::new(); + Self::new_inner(None).await + } + + pub async fn new_with_root_redirect(root_redirect_url: String) -> Self { + Self::new_inner(Some(root_redirect_url)).await + } + + async fn new_inner(root_redirect_url: Option) -> Self { + let proxy_state = ProxyState::new(root_redirect_url); let (route_info_request_sender, route_info_request_receiver) = mpsc::channel(1); proxy_state.inner.route_map.set_sender(move |m| { diff --git a/plane/plane-tests/tests/common/test_env.rs b/plane/plane-tests/tests/common/test_env.rs index 57c770a10..6b44d95f6 100644 --- a/plane/plane-tests/tests/common/test_env.rs +++ b/plane/plane-tests/tests/common/test_env.rs @@ -18,7 +18,8 @@ use plane::{ }, names::{AcmeDnsServerName, ControllerName, DroneName, Name, ProxyName}, proxy::{ - cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection, AcmeEabConfiguration, + cert_manager::watcher_manager_pair, proxy_connection::ProxyConnection, + proxy_server::ProxyState, AcmeEabConfiguration, }, typed_unix_socket::{server::TypedUnixSocketServer, WrappedMessage}, types::{ClusterName, DronePoolName}, @@ -131,12 +132,19 @@ impl TestEnvironment { let client = PlaneClient::new(controller.url().clone()); + let state = Arc::new(ProxyState::new(Some("https://plane.test".to_string()))); + let (_, cert_manager) = watcher_manager_pair(cluster.clone(), None, None) .await .unwrap(); - let proxy_connection = - ProxyConnection::new(ProxyName::new_random(), client, cluster, cert_manager); + let proxy_connection = ProxyConnection::new( + ProxyName::new_random(), + client, + cluster, + cert_manager, + state.clone(), + ); let addr: SocketAddr = ([0, 0, 0, 0], 0).into(); tracing::info!(%addr, "Listening for HTTP connections."); @@ -144,8 +152,7 @@ impl TestEnvironment { let port = tcp_listener.local_addr().unwrap().port(); // Spawn the server on a separate task - let server = - SimpleHttpServer::new(proxy_connection.state(), tcp_listener, HttpsConfig::Http)?; + let server = SimpleHttpServer::new(state, tcp_listener, HttpsConfig::Http)?; Ok(Proxy { port, diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index 806405e84..af31f894a 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -18,7 +18,7 @@ use tokio::net::TcpListener; mod common; #[plane_test] -async fn proxy_no_bearer_token(env: TestEnvironment) { +async fn proxy_root_no_redirect(env: TestEnvironment) { let mut proxy = MockProxy::new().await; let url = format!("http://{}", proxy.addr()); let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") }); @@ -28,6 +28,26 @@ async fn proxy_no_bearer_token(env: TestEnvironment) { let response = handle.await.unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert!(response.headers().get("location").is_none()); +} + +#[plane_test] +async fn proxy_root_redirect(env: TestEnvironment) { + let proxy = MockProxy::new_with_root_redirect("https://plane.test/".to_string()).await; + let url = format!("http://{}", proxy.addr()); + + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + let response = client.get(url).send().await.unwrap(); + + assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY); + assert_eq!( + response.headers().get("location").unwrap(), + "https://plane.test/" + ); } #[plane_test] diff --git a/plane/src/proxy/mod.rs b/plane/src/proxy/mod.rs index 8c573812f..0eb173e1d 100644 --- a/plane/src/proxy/mod.rs +++ b/plane/src/proxy/mod.rs @@ -6,6 +6,7 @@ use anyhow::Result; use dynamic_proxy::server::{ ServerWithHttpRedirect, ServerWithHttpRedirectConfig, ServerWithHttpRedirectHttpsConfig, }; +use proxy_server::ProxyState; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; @@ -109,7 +110,18 @@ pub async fn run_proxy(config: ProxyConfig) -> Result<()> { ) .await?; - let proxy_connection = ProxyConnection::new(config.name, client, config.cluster, cert_manager); + let state = Arc::new(ProxyState::new( + config.root_redirect_url.map(|u| u.to_string()), + )); + + // This returns a guard, we need to keep it in scope so that the connection is not terminated. + let _proxy_connection = ProxyConnection::new( + config.name, + client, + config.cluster, + cert_manager, + state.clone(), + ); let server = if let Some(https_port) = config.port_config.https_port { tracing::info!("Waiting for initial certificate."); @@ -125,14 +137,14 @@ pub async fn run_proxy(config: ProxyConfig) -> Result<()> { https_config: Some(https_config), }; - ServerWithHttpRedirect::new(proxy_connection.state(), server_config).await? + ServerWithHttpRedirect::new(state, server_config).await? } else { let server_config = ServerWithHttpRedirectConfig { http_port: config.port_config.http_port, https_config: None, }; - ServerWithHttpRedirect::new(proxy_connection.state(), server_config).await? + ServerWithHttpRedirect::new(state, server_config).await? }; wait_for_shutdown_signal().await; diff --git a/plane/src/proxy/proxy_connection.rs b/plane/src/proxy/proxy_connection.rs index 92d952c6a..4ad653469 100644 --- a/plane/src/proxy/proxy_connection.rs +++ b/plane/src/proxy/proxy_connection.rs @@ -20,9 +20,9 @@ impl ProxyConnection { client: PlaneClient, cluster: ClusterName, mut cert_manager: CertManager, + state: Arc, ) -> Self { tracing::info!("Creating proxy connection"); - let state = Arc::new(ProxyState::new()); let handle = { let state = state.clone(); diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index b197873d6..2d60a4561 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -27,6 +27,9 @@ pub struct ProxyStateInner { pub proxy_client: ProxyClient, pub monitor: ConnectionMonitorHandle, pub connected: AtomicBool, + + /// If set, the "root" path (/) will redirect to this URL. + pub root_redirect_url: Option, } #[derive(Clone)] @@ -36,17 +39,18 @@ pub struct ProxyState { impl Default for ProxyState { fn default() -> Self { - Self::new() + Self::new(None) } } impl ProxyState { - pub fn new() -> Self { + pub fn new(root_redirect_url: Option) -> Self { let inner = ProxyStateInner { route_map: RouteMap::new(), proxy_client: ProxyClient::new(), monitor: ConnectionMonitorHandle::new(), connected: AtomicBool::new(false), + root_redirect_url, }; Self { @@ -86,6 +90,22 @@ impl Service> for ProxyState { } } + if request.uri().path() == "/" { + if let Some(root_redirect_url) = &self.inner.root_redirect_url { + let mut response = Response::builder() + .status(StatusCode::MOVED_PERMANENTLY) + .header(header::LOCATION, root_redirect_url) + .body(simple_empty_body()) + .expect("Failed to build response"); + + apply_general_headers(&mut response); + + return Box::pin(ready(Ok(response))); + } else { + return Box::pin(ready(status_code_to_response(StatusCode::BAD_REQUEST))); + } + } + let mut request = MutableRequest::from_request(request); // extract the bearer token from the request @@ -94,6 +114,7 @@ impl Service> for ProxyState { let bearer_token = get_and_maybe_remove_bearer_token(&mut uri_parts); let Some(bearer_token) = bearer_token else { + // This should have already been handled by the root redirect above. return Box::pin(ready(status_code_to_response(StatusCode::BAD_REQUEST))); }; From 74d1661368bb3e531a0493633e2474d3570fcc07 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Wed, 25 Sep 2024 09:10:32 -0400 Subject: [PATCH 47/66] add static token test --- plane/plane-tests/tests/proxy.rs | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/plane/plane-tests/tests/proxy.rs b/plane/plane-tests/tests/proxy.rs index af31f894a..58b3d71c0 100644 --- a/plane/plane-tests/tests/proxy.rs +++ b/plane/plane-tests/tests/proxy.rs @@ -189,6 +189,45 @@ async fn proxy_backend_accepts(env: TestEnvironment) { assert_eq!(request_info.method, "GET"); } +#[plane_test] +async fn proxy_static_token(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/s.abc123/foobar"); + let client = localhost_client(); + let handle = tokio::spawn(client.get(url).send()); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("s.abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("s.abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: BackendName::new_random(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let request_info: RequestInfo = response.json().await.unwrap(); + assert_eq!(request_info.path, "/s.abc123/foobar"); // With static tokens, we pass along the original path. + assert_eq!(request_info.method, "GET"); +} + #[plane_test] async fn proxy_expected_subdomain_not_present(env: TestEnvironment) { let server = SimpleAxumServer::new().await; From d17025a55e45208cd54b4f3baa54f6efc712781c Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Wed, 25 Sep 2024 10:15:00 -0400 Subject: [PATCH 48/66] connection monitor touch test --- plane/plane-tests/tests/common/proxy_mock.rs | 7 +- .../tests/proxy_connection_monitor.rs | 66 +++++++++++++++++++ plane/src/proxy/connection_monitor.rs | 15 ++++- plane/src/proxy/mod.rs | 2 +- 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 plane/plane-tests/tests/proxy_connection_monitor.rs diff --git a/plane/plane-tests/tests/common/proxy_mock.rs b/plane/plane-tests/tests/common/proxy_mock.rs index 128b459dd..72a7c8ddc 100644 --- a/plane/plane-tests/tests/common/proxy_mock.rs +++ b/plane/plane-tests/tests/common/proxy_mock.rs @@ -1,7 +1,8 @@ use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; use plane::{ + names::BackendName, protocol::{RouteInfoRequest, RouteInfoResponse}, - proxy::proxy_server::ProxyState, + proxy::{connection_monitor::BackendEntry, proxy_server::ProxyState}, }; use std::net::SocketAddr; use tokio::{net::TcpListener, sync::mpsc}; @@ -23,6 +24,10 @@ impl MockProxy { Self::new_inner(Some(root_redirect_url)).await } + pub fn backend_entry(&self, backend_id: &BackendName) -> Option { + self.proxy_state.inner.monitor.get_backend_entry(backend_id) + } + async fn new_inner(root_redirect_url: Option) -> Self { let proxy_state = ProxyState::new(root_redirect_url); let (route_info_request_sender, route_info_request_receiver) = mpsc::channel(1); diff --git a/plane/plane-tests/tests/proxy_connection_monitor.rs b/plane/plane-tests/tests/proxy_connection_monitor.rs new file mode 100644 index 000000000..65d6bfd60 --- /dev/null +++ b/plane/plane-tests/tests/proxy_connection_monitor.rs @@ -0,0 +1,66 @@ +use common::{ + localhost_resolver::localhost_client, proxy_mock::MockProxy, + simple_axum_server::SimpleAxumServer, test_env::TestEnvironment, +}; +use plane::{ + log_types::BackendAddr, + names::{BackendName, Name}, + protocol::{RouteInfo, RouteInfoResponse}, + types::{BearerToken, ClusterName, SecretToken}, +}; +use plane_test_macro::plane_test; +use reqwest::StatusCode; +use std::str::FromStr; + +mod common; + +#[plane_test] +async fn proxy_marks_backend_as_recently_active(env: TestEnvironment) { + let server = SimpleAxumServer::new().await; + let backend_name = BackendName::new_random(); + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); + let url = format!("http://plane.test:{port}/abc123/"); + let client = localhost_client(); + println!("sending request"); + let handle = tokio::spawn(client.get(url).send()); + + let backend_entry = proxy.backend_entry(&backend_name); + assert!(backend_entry.is_none()); + + println!("waiting for route info request"); + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + println!("received route info request"); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: backend_name.clone(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + println!("waiting for response"); + let response = handle.await.unwrap().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + println!("received response"); + + let Some(backend_entry) = proxy.backend_entry(&backend_name) else { + panic!("Backend entry not found"); + }; + assert_eq!(backend_entry.active_connections, 0); + assert_eq!(backend_entry.had_recent_connection, true); +} diff --git a/plane/src/proxy/connection_monitor.rs b/plane/src/proxy/connection_monitor.rs index 64357270f..83ca54769 100644 --- a/plane/src/proxy/connection_monitor.rs +++ b/plane/src/proxy/connection_monitor.rs @@ -8,13 +8,13 @@ use tokio::task::JoinHandle; type BackendNameListener = Box; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct BackendEntry { /// The current number of active connections to the backend. - active_connections: u32, + pub active_connections: u32, /// Whether the backend has had a recent connection (since this value was last checked). - had_recent_connection: bool, + pub had_recent_connection: bool, } #[derive(Default)] @@ -149,6 +149,15 @@ impl ConnectionMonitorHandle { Self { monitor, handle } } + pub fn get_backend_entry(&self, backend_id: &BackendName) -> Option { + self.monitor + .lock() + .expect("Monitor lock was poisoned.") + .backends + .get(backend_id) + .cloned() + } + pub fn monitor(&self) -> Arc> { self.monitor.clone() } diff --git a/plane/src/proxy/mod.rs b/plane/src/proxy/mod.rs index 0eb173e1d..40e908c1d 100644 --- a/plane/src/proxy/mod.rs +++ b/plane/src/proxy/mod.rs @@ -16,7 +16,7 @@ use url::Url; pub mod cert_manager; mod cert_pair; pub mod command; -mod connection_monitor; +pub mod connection_monitor; pub mod proxy_connection; pub mod proxy_server; mod request; From 404c031b0d86f39f1ddda495025a7d95f5741b34 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Wed, 25 Sep 2024 10:29:18 -0400 Subject: [PATCH 49/66] add test for websocket connection monitor --- Cargo.lock | 1 + plane/plane-tests/Cargo.toml | 3 +- plane/plane-tests/tests/common/mod.rs | 3 +- .../tests/common/websocket_echo_server.rs | 61 +++++++++++++++++++ .../tests/proxy_connection_monitor.rs | 58 ++++++++++++++++-- 5 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 plane/plane-tests/tests/common/websocket_echo_server.rs diff --git a/Cargo.lock b/Cargo.lock index 7e34cb5d0..2727f1116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2094,6 +2094,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "tokio-tungstenite 0.24.0", "tracing", "tracing-appender", "tracing-subscriber", diff --git a/plane/plane-tests/Cargo.toml b/plane/plane-tests/Cargo.toml index 362d37880..a6dd0b68e 100644 --- a/plane/plane-tests/Cargo.toml +++ b/plane/plane-tests/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0.75" async-trait = "0.1.74" -axum = "0.7.5" +axum = { version = "0.7.5", features = ["ws"] } bollard = "0.17.0" chrono = { version = "0.4.31", features = ["serde"] } dynamic-proxy = { path = "../../dynamic-proxy" } @@ -21,6 +21,7 @@ serde = "1.0.210" serde_json = "1.0.107" thiserror = "1.0.50" tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread", "signal"] } +tokio-tungstenite = "0.24.0" tracing = "0.1.40" tracing-appender = "0.2.2" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/plane/plane-tests/tests/common/mod.rs b/plane/plane-tests/tests/common/mod.rs index d8f8efde7..279b1e69d 100644 --- a/plane/plane-tests/tests/common/mod.rs +++ b/plane/plane-tests/tests/common/mod.rs @@ -10,9 +10,10 @@ pub mod docker; pub mod localhost_resolver; pub mod proxy_mock; pub mod resources; -pub mod simple_axum_server; // NOTE: copied from dynamic-proxy +pub mod simple_axum_server; // NOTE: copied from dynamic-proxy (until we merge them back) pub mod test_env; pub mod timeout; +pub mod websocket_echo_server; // NOTE: copied from dynamic-proxy (until we merge them back) pub fn run_test(name: &str, time_limit: Duration, test_function: F) where diff --git a/plane/plane-tests/tests/common/websocket_echo_server.rs b/plane/plane-tests/tests/common/websocket_echo_server.rs new file mode 100644 index 000000000..9afd21dc5 --- /dev/null +++ b/plane/plane-tests/tests/common/websocket_echo_server.rs @@ -0,0 +1,61 @@ +use axum::{ + extract::ws::{Message, WebSocket, WebSocketUpgrade}, + response::IntoResponse, + routing::get, + Router, +}; +use std::net::SocketAddr; +use tokio::net::TcpListener; + +/// A websocket echo server that echos messages back to the client. +pub struct WebSocketEchoServer { + handle: tokio::task::JoinHandle<()>, + addr: SocketAddr, +} + +#[allow(unused)] +impl WebSocketEchoServer { + pub async fn new() -> Self { + let app = Router::new().route("/ws", get(ws_handler)); + + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let tcp_listener = TcpListener::bind(addr).await.unwrap(); + let addr = tcp_listener.local_addr().unwrap(); + + let handle = tokio::spawn(async move { + axum::serve(tcp_listener, app.into_make_service()) + .await + .unwrap(); + }); + + Self { handle, addr } + } + + pub fn addr(&self) -> SocketAddr { + self.addr + } +} + +impl Drop for WebSocketEchoServer { + fn drop(&mut self) { + self.handle.abort(); + } +} + +async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse { + ws.on_upgrade(handle_socket) +} + +async fn handle_socket(mut socket: WebSocket) { + while let Some(msg) = socket.recv().await { + if let Ok(msg) = msg { + if let Message::Text(text) = msg { + if socket.send(Message::Text(text)).await.is_err() { + break; + } + } + } else { + break; + } + } +} diff --git a/plane/plane-tests/tests/proxy_connection_monitor.rs b/plane/plane-tests/tests/proxy_connection_monitor.rs index 65d6bfd60..ccec1871c 100644 --- a/plane/plane-tests/tests/proxy_connection_monitor.rs +++ b/plane/plane-tests/tests/proxy_connection_monitor.rs @@ -1,6 +1,7 @@ use common::{ localhost_resolver::localhost_client, proxy_mock::MockProxy, simple_axum_server::SimpleAxumServer, test_env::TestEnvironment, + websocket_echo_server::WebSocketEchoServer, }; use plane::{ log_types::BackendAddr, @@ -11,6 +12,7 @@ use plane::{ use plane_test_macro::plane_test; use reqwest::StatusCode; use std::str::FromStr; +use tokio_tungstenite::connect_async; mod common; @@ -24,13 +26,11 @@ async fn proxy_marks_backend_as_recently_active(env: TestEnvironment) { let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap(); let url = format!("http://plane.test:{port}/abc123/"); let client = localhost_client(); - println!("sending request"); - let handle = tokio::spawn(client.get(url).send()); let backend_entry = proxy.backend_entry(&backend_name); assert!(backend_entry.is_none()); - println!("waiting for route info request"); + let handle = tokio::spawn(client.get(url).send()); let route_info_request = proxy.recv_route_info_request().await; assert_eq!( route_info_request.token, @@ -53,10 +53,8 @@ async fn proxy_marks_backend_as_recently_active(env: TestEnvironment) { }) .await; - println!("waiting for response"); let response = handle.await.unwrap().unwrap(); assert_eq!(response.status(), StatusCode::OK); - println!("received response"); let Some(backend_entry) = proxy.backend_entry(&backend_name) else { panic!("Backend entry not found"); @@ -64,3 +62,53 @@ async fn proxy_marks_backend_as_recently_active(env: TestEnvironment) { assert_eq!(backend_entry.active_connections, 0); assert_eq!(backend_entry.had_recent_connection, true); } + +#[plane_test] +async fn proxy_marks_websocket_backend_as_active(env: TestEnvironment) { + let server = WebSocketEchoServer::new().await; + let backend_name = BackendName::new_random(); + + let mut proxy = MockProxy::new().await; + let port = proxy.port(); + let cluster = ClusterName::from_str(&format!("localhost:{}", port)).unwrap(); + let url = format!("ws://localhost:{port}/abc123/ws"); + + let handle = tokio::spawn(connect_async(url)); + + let route_info_request = proxy.recv_route_info_request().await; + assert_eq!( + route_info_request.token, + BearerToken::from("abc123".to_string()) + ); + + proxy + .send_route_info_response(RouteInfoResponse { + token: BearerToken::from("abc123".to_string()), + route_info: Some(RouteInfo { + backend_id: backend_name.clone(), + address: BackendAddr(server.addr()), + secret_token: SecretToken::from("secret".to_string()), + cluster, + user: None, + user_data: None, + subdomain: None, + }), + }) + .await; + + let (mut ws_stream, _) = handle.await.unwrap().unwrap(); + + let Some(backend_entry) = proxy.backend_entry(&backend_name) else { + panic!("Backend entry not found"); + }; + assert_eq!(backend_entry.active_connections, 1); + assert_eq!(backend_entry.had_recent_connection, true); + + ws_stream.close(None).await.unwrap(); + drop(ws_stream); + + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + let backend_entry = proxy.backend_entry(&backend_name).unwrap(); + assert_eq!(backend_entry.active_connections, 0); + assert_eq!(backend_entry.had_recent_connection, true); +} From 8929e2d15b3ba9e9310b14c34b2612cf153fa82a Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 29 Sep 2024 15:59:15 -0400 Subject: [PATCH 50/66] remove access-control-allow-credentials header --- plane/src/proxy/proxy_server.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 2d60a4561..7cbd9edb2 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -275,9 +275,5 @@ fn apply_general_headers(response: &mut Response) { header::ACCESS_CONTROL_ALLOW_HEADERS, HeaderValue::from_static("*"), ); - headers.insert( - header::ACCESS_CONTROL_ALLOW_CREDENTIALS, - HeaderValue::from_static("true"), - ); headers.insert(header::SERVER, HeaderValue::from_static(SERVER_NAME)); } From 8331c0b8b79044a78f2812ec6c9fc120a6214e5c Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 29 Sep 2024 16:01:24 -0400 Subject: [PATCH 51/66] cleaner assert --- plane/plane-tests/tests/proxy_connection_monitor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plane/plane-tests/tests/proxy_connection_monitor.rs b/plane/plane-tests/tests/proxy_connection_monitor.rs index ccec1871c..b5ef768cf 100644 --- a/plane/plane-tests/tests/proxy_connection_monitor.rs +++ b/plane/plane-tests/tests/proxy_connection_monitor.rs @@ -102,7 +102,7 @@ async fn proxy_marks_websocket_backend_as_active(env: TestEnvironment) { panic!("Backend entry not found"); }; assert_eq!(backend_entry.active_connections, 1); - assert_eq!(backend_entry.had_recent_connection, true); + assert!(backend_entry.had_recent_connection); ws_stream.close(None).await.unwrap(); drop(ws_stream); @@ -110,5 +110,5 @@ async fn proxy_marks_websocket_backend_as_active(env: TestEnvironment) { tokio::time::sleep(std::time::Duration::from_millis(100)).await; let backend_entry = proxy.backend_entry(&backend_name).unwrap(); assert_eq!(backend_entry.active_connections, 0); - assert_eq!(backend_entry.had_recent_connection, true); + assert!(backend_entry.had_recent_connection); } From dac234947a1d25f2eccccae1647f6f7144b255b4 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 29 Sep 2024 17:12:56 -0400 Subject: [PATCH 52/66] remove access-control-allow-credentials check --- plane/plane-tests/tests/proxy_cors.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/plane/plane-tests/tests/proxy_cors.rs b/plane/plane-tests/tests/proxy_cors.rs index f3705fb2f..f4a5d5ff9 100644 --- a/plane/plane-tests/tests/proxy_cors.rs +++ b/plane/plane-tests/tests/proxy_cors.rs @@ -65,15 +65,6 @@ async fn proxy_gone_request_has_cors_headers(env: TestEnvironment) { .unwrap(), "*" ); - assert_eq!( - response - .headers() - .get("access-control-allow-credentials") - .unwrap() - .to_str() - .unwrap(), - "true" - ); } #[plane_test] @@ -137,13 +128,4 @@ async fn proxy_valid_request_has_cors_headers(env: TestEnvironment) { .unwrap(), "*" ); - assert_eq!( - response - .headers() - .get("access-control-allow-credentials") - .unwrap() - .to_str() - .unwrap(), - "true" - ); } From d558c9af8799745743c434cc1db6757d95c66aa8 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 30 Sep 2024 19:35:56 -0400 Subject: [PATCH 53/66] clippy dynamic-proxy --- dynamic-proxy/tests/test_http_redirect.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dynamic-proxy/tests/test_http_redirect.rs b/dynamic-proxy/tests/test_http_redirect.rs index 0cd952576..7639bd2e7 100644 --- a/dynamic-proxy/tests/test_http_redirect.rs +++ b/dynamic-proxy/tests/test_http_redirect.rs @@ -28,9 +28,8 @@ async fn do_request(url: &str) -> Response { url.set_port(Some(port)).unwrap(); // Request to http://foo.bar.baz should redirect to https://foo.bar.baz - let response = get_client().get(url).send().await.unwrap(); - response + get_client().get(url).send().await.unwrap() } #[tokio::test] From b2efbeb6f9fa2b62c1a6f90d21b232debef1d44c Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 30 Sep 2024 20:17:20 -0400 Subject: [PATCH 54/66] plane-tests clippy --- plane/plane-tests/tests/proxy_connection_monitor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plane/plane-tests/tests/proxy_connection_monitor.rs b/plane/plane-tests/tests/proxy_connection_monitor.rs index b5ef768cf..fb659da69 100644 --- a/plane/plane-tests/tests/proxy_connection_monitor.rs +++ b/plane/plane-tests/tests/proxy_connection_monitor.rs @@ -60,7 +60,7 @@ async fn proxy_marks_backend_as_recently_active(env: TestEnvironment) { panic!("Backend entry not found"); }; assert_eq!(backend_entry.active_connections, 0); - assert_eq!(backend_entry.had_recent_connection, true); + assert!(backend_entry.had_recent_connection); } #[plane_test] From f4c819980e2d03dfae6e6218de310e52d3cf3a2d Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 30 Sep 2024 20:37:54 -0400 Subject: [PATCH 55/66] add tests for static token and other PR fixes --- .../tests/common/websocket_echo_server.rs | 15 ++-- plane/plane-tests/tests/common/mod.rs | 4 +- .../tests/common/websocket_echo_server.rs | 15 ++-- plane/src/proxy/request.rs | 71 ++++++++++++++++++- 4 files changed, 90 insertions(+), 15 deletions(-) diff --git a/dynamic-proxy/tests/common/websocket_echo_server.rs b/dynamic-proxy/tests/common/websocket_echo_server.rs index 9afd21dc5..96de9ad1c 100644 --- a/dynamic-proxy/tests/common/websocket_echo_server.rs +++ b/dynamic-proxy/tests/common/websocket_echo_server.rs @@ -48,14 +48,17 @@ async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse { async fn handle_socket(mut socket: WebSocket) { while let Some(msg) = socket.recv().await { - if let Ok(msg) = msg { - if let Message::Text(text) = msg { - if socket.send(Message::Text(text)).await.is_err() { - break; + match msg { + Ok(msg) => { + if let Message::Text(text) = msg { + if socket.send(Message::Text(text)).await.is_err() { + break; + } } } - } else { - break; + Err(e) => { + panic!("Error receiving message: {}", e); + } } } } diff --git a/plane/plane-tests/tests/common/mod.rs b/plane/plane-tests/tests/common/mod.rs index 279b1e69d..8ea45bdce 100644 --- a/plane/plane-tests/tests/common/mod.rs +++ b/plane/plane-tests/tests/common/mod.rs @@ -10,10 +10,10 @@ pub mod docker; pub mod localhost_resolver; pub mod proxy_mock; pub mod resources; -pub mod simple_axum_server; // NOTE: copied from dynamic-proxy (until we merge them back) +pub mod simple_axum_server; // TODO: copied from dynamic-proxy (until we merge them back) pub mod test_env; pub mod timeout; -pub mod websocket_echo_server; // NOTE: copied from dynamic-proxy (until we merge them back) +pub mod websocket_echo_server; // TODO: copied from dynamic-proxy (until we merge them back) pub fn run_test(name: &str, time_limit: Duration, test_function: F) where diff --git a/plane/plane-tests/tests/common/websocket_echo_server.rs b/plane/plane-tests/tests/common/websocket_echo_server.rs index 9afd21dc5..96de9ad1c 100644 --- a/plane/plane-tests/tests/common/websocket_echo_server.rs +++ b/plane/plane-tests/tests/common/websocket_echo_server.rs @@ -48,14 +48,17 @@ async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse { async fn handle_socket(mut socket: WebSocket) { while let Some(msg) = socket.recv().await { - if let Ok(msg) = msg { - if let Message::Text(text) = msg { - if socket.send(Message::Text(text)).await.is_err() { - break; + match msg { + Ok(msg) => { + if let Message::Text(text) = msg { + if socket.send(Message::Text(text)).await.is_err() { + break; + } } } - } else { - break; + Err(e) => { + panic!("Error receiving message: {}", e); + } } } } diff --git a/plane/src/proxy/request.rs b/plane/src/proxy/request.rs index e3c051fdf..9928d2179 100644 --- a/plane/src/proxy/request.rs +++ b/plane/src/proxy/request.rs @@ -47,7 +47,7 @@ pub fn get_and_maybe_remove_bearer_token(parts: &mut uri::Parts) -> Option (token, path), - None => (full_path, "/"), + None => (full_path, ""), }; if token.is_empty() { @@ -76,6 +76,8 @@ pub fn get_and_maybe_remove_bearer_token(parts: &mut uri::Parts) -> Option Date: Mon, 30 Sep 2024 20:38:40 -0400 Subject: [PATCH 56/66] mark parts added in plane --- dynamic-proxy/src/graceful_shutdown.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dynamic-proxy/src/graceful_shutdown.rs b/dynamic-proxy/src/graceful_shutdown.rs index 6083d6ab2..0bd6fa5dc 100644 --- a/dynamic-proxy/src/graceful_shutdown.rs +++ b/dynamic-proxy/src/graceful_shutdown.rs @@ -12,7 +12,7 @@ use std::{ }; use tokio::sync::watch; -#[derive(Clone)] +#[derive(Clone)] // Added in Plane pub struct GracefulShutdown { tx: watch::Sender<()>, } @@ -34,6 +34,7 @@ impl GracefulShutdown { }) } + // Added in Plane pub fn subscribe(&self) -> watch::Receiver<()> { self.tx.subscribe() } From cb9d5b61b458feeb5fc2191f7853e76afd345057 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 30 Sep 2024 20:44:12 -0400 Subject: [PATCH 57/66] pr --- LICENSE | 24 ++++++++++++++++++++++++ dynamic-proxy/tests/graceful_https.rs | 8 +++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index ca5f511a5..7e1646724 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +Contains code from hyperium/hyper-util, licensed under the MIT license: + +Copyright (c) 2023 Sean McArthur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/dynamic-proxy/tests/graceful_https.rs b/dynamic-proxy/tests/graceful_https.rs index 0320214f7..2b9433cc8 100644 --- a/dynamic-proxy/tests/graceful_https.rs +++ b/dynamic-proxy/tests/graceful_https.rs @@ -46,9 +46,11 @@ async fn test_graceful_shutdown_https() { .build() .unwrap(); - let _client = client.clone(); - let _url = url.clone(); - let response_handle = tokio::spawn(async move { _client.get(&_url).send().await.unwrap() }); + let response_handle = { + let client = client.clone(); + let url = url.clone(); + tokio::spawn(async move { client.get(&url).send().await.unwrap() }) + }; tokio::time::sleep(Duration::from_millis(100)).await; From 8a586fab42b9e32605cd8e41120d81b34e24f051 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Mon, 30 Sep 2024 20:52:00 -0400 Subject: [PATCH 58/66] more nits from pr --- dynamic-proxy/tests/graceful.rs | 8 +++++--- plane/src/proxy/proxy_server.rs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dynamic-proxy/tests/graceful.rs b/dynamic-proxy/tests/graceful.rs index 0b15541f6..5ca779c85 100644 --- a/dynamic-proxy/tests/graceful.rs +++ b/dynamic-proxy/tests/graceful.rs @@ -36,9 +36,11 @@ async fn test_graceful_shutdown() { // Create a client and start a POST request without finishing the body let client = reqwest::Client::new(); - let _client = client.clone(); - let _url = url.clone(); - let response_handle = tokio::spawn(async move { _client.get(&_url).send().await.unwrap() }); + let response_handle = { + let client = client.clone(); + let url = url.clone(); + tokio::spawn(async move { client.get(&url).send().await.unwrap() }) + }; tokio::time::sleep(Duration::from_millis(100)).await; diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 7cbd9edb2..1af239a77 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -144,7 +144,7 @@ impl Service> for ProxyState { let (mut res, upgrade_handler) = match result { Ok((res, upgrade_handler)) => (res, upgrade_handler), Err(e) => { - tracing::error!("Error proxying request: {}", e); + tracing::error!(?e, "Error proxying request"); return status_code_to_response(StatusCode::INTERNAL_SERVER_ERROR); } }; From 8933a3de9e1ec8a00c7c42b66597705a949d46e2 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Tue, 1 Oct 2024 15:02:45 -0700 Subject: [PATCH 59/66] complete thought --- plane/src/proxy/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plane/src/proxy/mod.rs b/plane/src/proxy/mod.rs index 40e908c1d..bcd2f5b99 100644 --- a/plane/src/proxy/mod.rs +++ b/plane/src/proxy/mod.rs @@ -40,7 +40,7 @@ impl Protocol { #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct ServerPortConfig { /// The port to listen on for HTTP requests. - /// If https_port is provided, this + /// If https_port is provided, this port will only serve a redirect to HTTPS. pub http_port: u16, /// The port to listen on for HTTPS requests. From b7449be942d9428d0dfd2c8cac6899b5ad72cfed Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 6 Oct 2024 18:38:17 -0700 Subject: [PATCH 60/66] use expect --- plane/src/proxy/proxy_server.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plane/src/proxy/proxy_server.rs b/plane/src/proxy/proxy_server.rs index 1af239a77..789e263aa 100644 --- a/plane/src/proxy/proxy_server.rs +++ b/plane/src/proxy/proxy_server.rs @@ -235,7 +235,8 @@ where } if let Some(user_data) = &route_info.user_data { - let user_data_str = serde_json::to_string(user_data).unwrap_or_default(); + let user_data_str = + serde_json::to_string(user_data).expect("User data is always serializable"); request.add_header("x-verified-user-data", &user_data_str); } From 08448756760590f89f013702e9fddfe1e8ffcfd3 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 6 Oct 2024 18:42:48 -0700 Subject: [PATCH 61/66] use panic in websocket echo server --- dynamic-proxy/tests/common/websocket_echo_server.rs | 2 +- plane/plane-tests/tests/common/websocket_echo_server.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dynamic-proxy/tests/common/websocket_echo_server.rs b/dynamic-proxy/tests/common/websocket_echo_server.rs index 96de9ad1c..a406eb766 100644 --- a/dynamic-proxy/tests/common/websocket_echo_server.rs +++ b/dynamic-proxy/tests/common/websocket_echo_server.rs @@ -52,7 +52,7 @@ async fn handle_socket(mut socket: WebSocket) { Ok(msg) => { if let Message::Text(text) = msg { if socket.send(Message::Text(text)).await.is_err() { - break; + panic!("WebSocket connection closed."); } } } diff --git a/plane/plane-tests/tests/common/websocket_echo_server.rs b/plane/plane-tests/tests/common/websocket_echo_server.rs index 96de9ad1c..a406eb766 100644 --- a/plane/plane-tests/tests/common/websocket_echo_server.rs +++ b/plane/plane-tests/tests/common/websocket_echo_server.rs @@ -52,7 +52,7 @@ async fn handle_socket(mut socket: WebSocket) { Ok(msg) => { if let Message::Text(text) = msg { if socket.send(Message::Text(text)).await.is_err() { - break; + panic!("WebSocket connection closed."); } } } From 0e0ec6025d339bfa94596271038f49435814d2ec Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 6 Oct 2024 19:30:07 -0700 Subject: [PATCH 62/66] nits from pr --- dynamic-proxy/tests/common/simple_upgrade_service.rs | 4 +++- dynamic-proxy/tests/graceful_https.rs | 4 ++-- dynamic-proxy/tests/test_upgrade.rs | 6 +++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dynamic-proxy/tests/common/simple_upgrade_service.rs b/dynamic-proxy/tests/common/simple_upgrade_service.rs index 81bfdcf4b..dee216099 100644 --- a/dynamic-proxy/tests/common/simple_upgrade_service.rs +++ b/dynamic-proxy/tests/common/simple_upgrade_service.rs @@ -1,5 +1,6 @@ use bytes::Bytes; use dynamic_proxy::body::{to_simple_body, SimpleBody}; +use http::header::CONNECTION; use http_body_util::{Empty, Full}; use hyper::{ body::Incoming, @@ -29,9 +30,10 @@ impl Service> for SimpleUpgradeService { // Handle upgrade let mut res = Response::new(to_simple_body(Empty::new())); *res.status_mut() = StatusCode::SWITCHING_PROTOCOLS; + res.headers_mut() + .insert(CONNECTION, HeaderValue::from_static("upgrade")); res.headers_mut() .insert(UPGRADE, HeaderValue::from_static("websocket")); - tokio::task::spawn(async move { if let Ok(upgraded) = hyper::upgrade::on(&mut req).await { if let Err(e) = handle_upgraded_connection(upgraded).await { diff --git a/dynamic-proxy/tests/graceful_https.rs b/dynamic-proxy/tests/graceful_https.rs index 2b9433cc8..c36cb680f 100644 --- a/dynamic-proxy/tests/graceful_https.rs +++ b/dynamic-proxy/tests/graceful_https.rs @@ -52,12 +52,12 @@ async fn test_graceful_shutdown_https() { tokio::spawn(async move { client.get(&url).send().await.unwrap() }) }; - tokio::time::sleep(Duration::from_millis(100)).await; + tokio::time::sleep(Duration::from_millis(600)).await; // Call server.graceful_shutdown() let shutdown_task = tokio::spawn(async move { server.graceful_shutdown().await }); - tokio::time::sleep(Duration::from_millis(100)).await; + tokio::time::sleep(Duration::from_millis(200)).await; let response = response_handle.await.unwrap(); diff --git a/dynamic-proxy/tests/test_upgrade.rs b/dynamic-proxy/tests/test_upgrade.rs index 7efbcdc6c..a84cbcc18 100644 --- a/dynamic-proxy/tests/test_upgrade.rs +++ b/dynamic-proxy/tests/test_upgrade.rs @@ -3,7 +3,7 @@ use common::simple_upgrade_service::SimpleUpgradeService; use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; use http_body_util::Empty; use hyper::{ - header::{HeaderValue, UPGRADE}, + header::{HeaderValue, UPGRADE, CONNECTION}, Request, StatusCode, }; use hyper_util::rt::TokioIo; @@ -54,6 +54,10 @@ async fn test_upgrade() { res.headers().get(UPGRADE).unwrap(), &HeaderValue::from_static("websocket") ); + assert_eq!( + res.headers().get(CONNECTION).unwrap(), + &HeaderValue::from_static("upgrade") + ); let upgraded = hyper::upgrade::on(res).await.unwrap(); let mut upgraded = TokioIo::new(upgraded); From 97b007864115a6d1a6d96fa059239803753c9f37 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 6 Oct 2024 19:32:08 -0700 Subject: [PATCH 63/66] nit --- dynamic-proxy/src/proxy.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/dynamic-proxy/src/proxy.rs b/dynamic-proxy/src/proxy.rs index cef894c09..93c4ee960 100644 --- a/dynamic-proxy/src/proxy.rs +++ b/dynamic-proxy/src/proxy.rs @@ -14,20 +14,12 @@ use std::{convert::Infallible, time::Duration}; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); /// A client for proxying HTTP requests to an upstream server. +#[derive(Clone)] pub struct ProxyClient { client: Client, timeout: Duration, } -impl Clone for ProxyClient { - fn clone(&self) -> Self { - Self { - client: self.client.clone(), - timeout: self.timeout, - } - } -} - impl Default for ProxyClient { fn default() -> Self { Self::new() From dc9d644f1f1008f7edb6dd021edc6baffb30b68d Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 6 Oct 2024 19:38:24 -0700 Subject: [PATCH 64/66] unify logging --- dynamic-proxy/src/proxy.rs | 8 ++------ dynamic-proxy/tests/test_upgrade.rs | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/dynamic-proxy/src/proxy.rs b/dynamic-proxy/src/proxy.rs index 93c4ee960..5d6561445 100644 --- a/dynamic-proxy/src/proxy.rs +++ b/dynamic-proxy/src/proxy.rs @@ -50,7 +50,7 @@ impl ProxyClient { let res = match res { Ok(res) => res, Err(ProxyError::Timeout) => { - tracing::error!(url, "Upstream request failed"); + tracing::warn!(url, "Upstream request failed"); return Ok(( Response::builder() .status(StatusCode::GATEWAY_TIMEOUT) @@ -60,7 +60,7 @@ impl ProxyClient { )); } Err(e) => { - tracing::error!(url, ?e, "Upstream request failed"); + tracing::warn!(url, ?e, "Upstream request failed"); return Ok(( Response::builder() .status(StatusCode::BAD_GATEWAY) @@ -108,16 +108,12 @@ impl ProxyClient { &self, request: Request, ) -> Result, ProxyError> { - let url = request.uri().to_string(); - let res = match tokio::time::timeout(self.timeout, self.client.request(request)).await { Ok(Ok(res)) => res, Err(_) => { - tracing::warn!(url, "Upstream request timed out."); return Err(ProxyError::Timeout); } Ok(Err(e)) => { - tracing::error!(url, "Upstream request failed: {}", e); return Err(ProxyError::RequestFailed(e.into())); } }; diff --git a/dynamic-proxy/tests/test_upgrade.rs b/dynamic-proxy/tests/test_upgrade.rs index a84cbcc18..fd874af30 100644 --- a/dynamic-proxy/tests/test_upgrade.rs +++ b/dynamic-proxy/tests/test_upgrade.rs @@ -3,7 +3,7 @@ use common::simple_upgrade_service::SimpleUpgradeService; use dynamic_proxy::server::{HttpsConfig, SimpleHttpServer}; use http_body_util::Empty; use hyper::{ - header::{HeaderValue, UPGRADE, CONNECTION}, + header::{HeaderValue, CONNECTION, UPGRADE}, Request, StatusCode, }; use hyper_util::rt::TokioIo; From 11c3d5ea04755a7889187af7ff27d0ebe3a22f23 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 6 Oct 2024 19:41:54 -0700 Subject: [PATCH 65/66] add dynamic-proxy to workspace --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 9114f4fc5..43463fe53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "dynamic-proxy", "plane/plane-tests", "plane/plane-dynamic", "plane", From bf178728ae27f51c35693061149b5d2f5ca28dee Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Sun, 6 Oct 2024 19:46:42 -0700 Subject: [PATCH 66/66] add dynamic-proxy to default-members --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 43463fe53..0e88a01ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,6 @@ members = [ # https://github.com/rust-lang/cargo/pull/9252/files default-members = [ "plane", - "plane/plane-tests" + "plane/plane-tests", + "dynamic-proxy", ]