From fb340cce9668c7239d071bd22db95ebd262f6a42 Mon Sep 17 00:00:00 2001 From: Yu Li Date: Fri, 12 Jul 2024 17:39:39 +0800 Subject: [PATCH] chore(volo-http): refactor client target Since the previous `Target` implementation was confusing and complicated, we refactored `Target` to support chain calls, which is more elegant and easy to use. In addition, we removed `TypeMap` and `FastStrMap` in `Target` and added `CallOpt`. Its usage is similar, but it is simpler and more elegant. Signed-off-by: Yu Li --- Cargo.lock | 114 ++-- volo-http/Cargo.toml | 3 +- volo-http/src/client/callopt.rs | 88 +++ volo-http/src/client/discover/mod.rs | 292 --------- volo-http/src/client/{discover => }/dns.rs | 56 +- volo-http/src/client/loadbalance.rs | 2 +- volo-http/src/client/mod.rs | 139 +++-- volo-http/src/client/request_builder.rs | 86 +-- volo-http/src/client/target.rs | 663 +++++++++++++++++++++ 9 files changed, 975 insertions(+), 468 deletions(-) create mode 100644 volo-http/src/client/callopt.rs delete mode 100644 volo-http/src/client/discover/mod.rs rename volo-http/src/client/{discover => }/dns.rs (75%) create mode 100644 volo-http/src/client/target.rs diff --git a/Cargo.lock b/Cargo.lock index f9273496..a408c310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,7 +136,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -158,7 +158,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -169,7 +169,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -231,22 +231,21 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.1.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8" +checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" dependencies = [ "jobserver", "libc", - "once_cell", ] [[package]] @@ -305,7 +304,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -505,7 +504,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -568,7 +567,7 @@ dependencies = [ "faststr", "futures", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", "lazy_static", "metainfo", @@ -721,7 +720,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -967,9 +966,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -984,7 +983,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -1008,9 +1007,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -1032,16 +1031,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", "h2 0.4.5", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1059,7 +1058,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.29", + "hyper 0.14.30", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -1071,7 +1070,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" dependencies = [ - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "pin-project-lite", "tokio", @@ -1088,8 +1087,8 @@ dependencies = [ "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", - "hyper 1.4.0", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -1359,9 +1358,9 @@ dependencies = [ [[package]] name = "matchit" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3c2fcf089c060eb333302d80c5f3ffa8297abecf220f788e4a09ef85f59420" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" @@ -1457,7 +1456,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -1469,7 +1468,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -1604,7 +1603,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -1651,7 +1650,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -1827,7 +1826,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -1890,7 +1889,7 @@ dependencies = [ "scoped-tls", "serde", "serde_yaml", - "syn 2.0.70", + "syn 2.0.71", "toml", "tracing", "tracing-subscriber", @@ -1922,7 +1921,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -2199,7 +2198,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-rustls", "ipnet", "js-sys", @@ -2458,9 +2457,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -2471,9 +2470,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -2502,7 +2501,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -2657,9 +2656,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.70" +version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", @@ -2732,22 +2731,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -2833,7 +2832,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -3002,7 +3001,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] @@ -3227,7 +3226,7 @@ dependencies = [ "quote", "serde", "serde_yaml", - "syn 2.0.70", + "syn 2.0.71", "tempfile", "url_path", "volo", @@ -3286,9 +3285,9 @@ dependencies = [ "h2 0.4.5", "hex", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-timeout", "hyper-util", "matchit", @@ -3311,7 +3310,7 @@ dependencies = [ [[package]] name = "volo-http" -version = "0.2.9" +version = "0.2.10" dependencies = [ "ahash", "async-broadcast", @@ -3324,11 +3323,12 @@ dependencies = [ "futures-util", "hickory-resolver", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "itoa", + "libc", "matchit", "memchr", "metainfo", @@ -3432,7 +3432,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", "wasm-bindgen-shared", ] @@ -3466,7 +3466,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3744,7 +3744,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.70", + "syn 2.0.71", ] [[package]] diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index ca71fddd..50035089 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "volo-http" -version = "0.2.9" +version = "0.2.10" edition.workspace = true homepage.workspace = true repository.workspace = true @@ -76,6 +76,7 @@ sonic-rs = { workspace = true, optional = true } [dev-dependencies] async-stream.workspace = true +libc.workspace = true serde = { workspace = true, features = ["derive"] } tokio-test.workspace = true diff --git a/volo-http/src/client/callopt.rs b/volo-http/src/client/callopt.rs new file mode 100644 index 00000000..a8738382 --- /dev/null +++ b/volo-http/src/client/callopt.rs @@ -0,0 +1,88 @@ +//! Call options for requests +//! +//! See [`CallOpt`] for more details. + +use faststr::FastStr; +use metainfo::{FastStrMap, TypeMap}; + +/// Call options for requests +/// +/// It can be set to a [`Client`][Client] or a [`RequestBuilder`][RequestBuilder]. The +/// [`TargetParser`][TargetParser] will handle [`Target`][Target] and the [`CallOpt`] for +/// applying information to the [`Endpoint`]. +/// +/// [Client]: crate::client::Client +/// [RequestBuilder]: crate::client::RequestBuilder +/// [`TargetParser`]: crate::client::target::TargetParser +#[derive(Debug, Default)] +pub struct CallOpt { + /// `tags` is used to store additional information of the endpoint. + /// + /// Users can use `tags` to store custom data, such as the datacenter name or the region name, + /// which can be used by the service discoverer. + pub tags: TypeMap, + /// `faststr_tags` is a optimized typemap to store additional information of the endpoint. + /// + /// Use [`FastStrMap`] instead of [`TypeMap`] can reduce the Box allocation. + /// + /// This is mainly for performance optimization. + pub faststr_tags: FastStrMap, +} + +impl CallOpt { + /// Create a new [`CallOpt`] + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Check if [`CallOpt`] tags contain entry + #[inline] + pub fn contains(&self) -> bool { + self.tags.contains::() + } + + /// Insert a tag into this [`CallOpt`]. + #[inline] + pub fn insert(&mut self, val: T) { + self.tags.insert(val); + } + + /// Insert a tag into this [`CallOpt`] and return self. + #[inline] + pub fn with(mut self, val: T) -> Self { + self.tags.insert(val); + self + } + + /// Get a reference to a tag previously inserted on this [`CallOpt`]. + #[inline] + pub fn get(&self) -> Option<&T> { + self.tags.get::() + } + + /// Check if [`CallOpt`] tags contain entry + #[inline] + pub fn contains_faststr(&self) -> bool { + self.faststr_tags.contains::() + } + + /// Insert a tag into this [`CallOpt`]. + #[inline] + pub fn insert_faststr(&mut self, val: FastStr) { + self.faststr_tags.insert::(val); + } + + /// Insert a tag into this [`CallOpt`] and return self. + #[inline] + pub fn with_faststr(mut self, val: FastStr) -> Self { + self.faststr_tags.insert::(val); + self + } + + /// Get a reference to a tag previously inserted on this [`CallOpt`]. + #[inline] + pub fn get_faststr(&self) -> Option<&FastStr> { + self.faststr_tags.get::() + } +} diff --git a/volo-http/src/client/discover/mod.rs b/volo-http/src/client/discover/mod.rs deleted file mode 100644 index 4ce11e5b..00000000 --- a/volo-http/src/client/discover/mod.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! Service discover utilities -use std::net::{IpAddr, SocketAddr}; - -use faststr::FastStr; -use http::{uri::Scheme, HeaderValue, Uri}; -use metainfo::{FastStrMap, TypeMap}; -use volo::{context::Endpoint, net::Address}; - -use crate::{ - error::{client::bad_scheme, ClientError}, - utils::consts::{HTTPS_DEFAULT_PORT, HTTP_DEFAULT_PORT}, -}; - -pub(crate) mod dns; -pub use self::dns::Port; - -/// Function for parsing `Target` and updating `Endpoint`. -/// -/// The `TargetParser` usually used for service discover. It can update `Endpoint` from `Target`, -/// and the service discover will resolve the `Endpoint` to `Address`(es) and access them. -pub type TargetParser = fn(&Target, &mut Endpoint); - -#[derive(Default)] -pub struct Target { - inner: TargetInner, - #[cfg(feature = "__tls")] - https: bool, - - faststr_tags: FastStrMap, - tags: TypeMap, -} - -#[derive(Default)] -pub enum TargetInner { - #[default] - None, - Address(Address), - Host { - host: FastStr, - port: u16, - }, -} - -impl Target { - /// Build a `Target` from `Uri`. - /// - /// If there is no host, `None` will be returned. If there is a host, but the uri has something - /// invalid (e.g., unsupported scheme), an error will be returned. - pub fn from_uri(uri: &Uri) -> Option> { - let host = uri.host()?; - let Some(https) = is_https(uri) else { - tracing::error!("[Volo-HTTP] unsupported scheme: {:?}.", uri.scheme()); - return Some(Err(bad_scheme())); - }; - #[cfg(not(feature = "__tls"))] - if https { - tracing::error!("[Volo-HTTP] https is not allowed when feature `tls` is not enabled."); - return Some(Err(bad_scheme())); - } - let port = match uri.port_u16() { - Some(port) => port, - None => { - if https { - HTTPS_DEFAULT_PORT - } else { - HTTP_DEFAULT_PORT - } - } - }; - - let inner = match host - .trim_start_matches('[') - .trim_end_matches(']') - .parse::() - { - Ok(addr) => TargetInner::Address(Address::Ip(SocketAddr::new(addr, port))), - Err(_) => TargetInner::Host { - host: FastStr::from_string(host.to_owned()), - port, - }, - }; - - Some(Ok(Self { - inner, - #[cfg(feature = "__tls")] - https, - ..Default::default() - })) - } - - /// Build a `Target` from an address. - pub fn from_address(address: A, #[cfg(feature = "__tls")] https: bool) -> Self - where - A: Into
, - { - Self { - inner: TargetInner::Address(address.into()), - #[cfg(feature = "__tls")] - https, - ..Default::default() - } - } - - /// Build a `Target` from a host name and port. - /// - /// Note that the `host` must be a host name, it will be used for service discover. - /// - /// It should NOT be an address or something with port. - /// - /// If you have a uri and you are not sure if the host is a host, try `from_uri`. - pub fn from_host(host: S, port: Option, #[cfg(feature = "__tls")] https: bool) -> Self - where - S: AsRef, - { - let port = match port { - Some(p) => p, - None => { - #[cfg(feature = "__tls")] - if https { - HTTPS_DEFAULT_PORT - } else { - HTTP_DEFAULT_PORT - } - #[cfg(not(feature = "__tls"))] - HTTP_DEFAULT_PORT - } - }; - Self { - inner: TargetInner::Host { - host: FastStr::from_string(String::from(host.as_ref())), - port, - }, - #[cfg(feature = "__tls")] - https, - ..Default::default() - } - } - - /// Get the target repr. - pub fn inner(&self) -> &TargetInner { - &self.inner - } - - /// Return if the `Target` is `None`. - pub fn is_none(&self) -> bool { - matches!(self.inner, TargetInner::None) - } - - #[cfg(feature = "__tls")] - pub fn set_https(&mut self, https: bool) { - self.https = https; - } - - #[cfg(feature = "__tls")] - pub fn is_https(&self) -> bool { - if self.is_none() { - false - } else { - self.https - } - } - - /// Return the `Address` if the `Target` is an address. - pub fn address(&self) -> Option<&Address> { - match &self.inner { - TargetInner::Address(addr) => Some(addr), - _ => None, - } - } - - /// Return the host name if the `Target` is a host name. - pub fn host(&self) -> Option<&FastStr> { - match &self.inner { - TargetInner::Host { host, .. } => Some(host), - _ => None, - } - } - - /// Return the port if the `Target` is a host name. - pub fn port(&self) -> Option { - match &self.inner { - &TargetInner::Host { port, .. } => Some(port), - _ => None, - } - } - - /// Insert a tag into this `Target`. - #[inline] - pub fn insert(&mut self, val: T) { - self.tags.insert(val); - } - - /// Check if the tag exists. - #[inline] - pub fn contains(&self) -> bool { - self.tags.contains::() - } - - /// Get a reference to a tag previously inserted on this `Target`. - #[inline] - pub fn get(&self) -> Option<&T> { - self.tags.get::() - } - - /// Remove a tag if it exists and return it. - #[inline] - pub fn remove(&mut self) -> Option { - self.tags.remove::() - } - - /// Insert a tag into this `Target`. - #[inline] - pub fn insert_faststr(&mut self, val: FastStr) { - self.faststr_tags.insert::(val); - } - - /// Check if the tag exists. - #[inline] - pub fn contains_faststr(&self) -> bool { - self.faststr_tags.contains::() - } - - /// Get a reference to a tag previously inserted on this `Target`. - #[inline] - pub fn get_faststr(&self) -> Option<&FastStr> { - self.faststr_tags.get::() - } - - /// Remove a tag if it exists and return it. - #[inline] - pub fn remove_faststr(&mut self) -> Option { - self.faststr_tags.remove::() - } - - /// Generate a `HeaderValue` for `Host` in HTTP headers. - pub fn gen_host(&self) -> Option { - match &self.inner { - TargetInner::None => None, - TargetInner::Address(addr) => { - #[allow(irrefutable_let_patterns)] - if let Address::Ip(socket) = addr { - let port = socket.port(); - // If the port is default port, just ignore it. - #[cfg(feature = "__tls")] - if self.https && port == HTTPS_DEFAULT_PORT { - return HeaderValue::try_from(addr_to_string(socket, false)).ok(); - } - if port == HTTP_DEFAULT_PORT { - return HeaderValue::try_from(addr_to_string(socket, false)).ok(); - } - return HeaderValue::try_from(addr_to_string(socket, true)).ok(); - } - None - } - TargetInner::Host { host, port } => { - #[cfg(feature = "__tls")] - if self.https && *port != HTTPS_DEFAULT_PORT { - return HeaderValue::try_from(format!("{host}:{port}")).ok(); - } - if *port != HTTP_DEFAULT_PORT { - return HeaderValue::try_from(format!("{host}:{port}")).ok(); - } - HeaderValue::from_str(host.as_str()).ok() - } - } - } -} - -fn addr_to_string(addr: &SocketAddr, with_port: bool) -> String { - if with_port { - return addr.to_string(); - } - let ip = addr.ip(); - if ip.is_ipv6() { - format!("[{ip}]") - } else { - ip.to_string() - } -} - -fn is_https(uri: &Uri) -> Option { - let Some(scheme) = uri.scheme() else { - return Some(false); - }; - if scheme == &Scheme::HTTPS { - return Some(true); - } - if scheme == &Scheme::HTTP { - return Some(false); - } - None -} diff --git a/volo-http/src/client/discover/dns.rs b/volo-http/src/client/dns.rs similarity index 75% rename from volo-http/src/client/discover/dns.rs rename to volo-http/src/client/dns.rs index 8711c56b..866479f3 100644 --- a/volo-http/src/client/discover/dns.rs +++ b/volo-http/src/client/dns.rs @@ -1,3 +1,5 @@ +//! Service discover utilities + use std::{net::SocketAddr, ops::Deref, sync::Arc}; use async_broadcast::Receiver; @@ -13,12 +15,13 @@ use volo::{ net::Address, }; -use super::{Target, TargetInner}; +use super::{target::RemoteTargetAddress, Target}; #[cfg(feature = "__tls")] -use crate::{client::transport::TlsTransport, utils::consts::HTTPS_DEFAULT_PORT}; +use crate::client::transport::TlsTransport; use crate::{ + client::callopt::CallOpt, error::client::{bad_host_name, no_address}, - utils::consts::HTTP_DEFAULT_PORT, + utils::consts, }; /// The port for `DnsResolver`, and only used for `DnsResolver`. @@ -97,12 +100,12 @@ impl Discover for DnsResolver { None => { #[cfg(feature = "__tls")] if endpoint.contains::() { - HTTPS_DEFAULT_PORT + consts::HTTPS_DEFAULT_PORT } else { - HTTP_DEFAULT_PORT + consts::HTTP_DEFAULT_PORT } #[cfg(not(feature = "__tls"))] - HTTP_DEFAULT_PORT + consts::HTTP_DEFAULT_PORT } }; @@ -127,24 +130,31 @@ impl Discover for DnsResolver { } } -pub fn parse_target(target: &Target, endpoint: &mut Endpoint) { - match target.inner() { - TargetInner::None => { - // `cargo-clippy` will warn on the `return` when `__tls` is disabled, just ignore it. - #[allow(clippy::needless_return)] - return; - } - TargetInner::Address(addr) => { - endpoint.set_address(addr.clone()); +pub fn parse_target(target: Target, _: &CallOpt, endpoint: &mut Endpoint) { + match target { + Target::None => (), + Target::Remote(rt) => { + let port = rt.port(); + + #[cfg(feature = "__tls")] + if rt.is_https() { + endpoint.insert(TlsTransport); + } + + match rt.addr { + RemoteTargetAddress::Ip(ip) => { + let sa = SocketAddr::new(ip, port); + endpoint.set_address(Address::Ip(sa)); + } + RemoteTargetAddress::Name(host) => { + endpoint.insert(Port(port)); + endpoint.set_service_name(host); + } + } } - TargetInner::Host { host, port } => { - endpoint.insert(Port(*port)); - endpoint.set_service_name(host.clone()); + #[cfg(target_family = "unix")] + Target::Local(unix_socket) => { + endpoint.set_address(Address::Unix(unix_socket.clone())); } } - - #[cfg(feature = "__tls")] - if target.is_https() { - endpoint.insert(TlsTransport); - } } diff --git a/volo-http/src/client/loadbalance.rs b/volo-http/src/client/loadbalance.rs index 07da990a..72b55d0a 100644 --- a/volo-http/src/client/loadbalance.rs +++ b/volo-http/src/client/loadbalance.rs @@ -18,7 +18,7 @@ use volo::{ loadbalance::{random::WeightedRandomBalance, LoadBalance, MkLbLayer}, }; -use super::discover::dns::DnsResolver; +use super::dns::DnsResolver; use crate::{ context::ClientContext, error::{ diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index ba49f8f0..8514b5a3 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -29,9 +29,11 @@ use volo::{ #[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "native-tls"))))] pub use self::transport::TlsTransport; use self::{ - discover::{dns::parse_target, TargetParser}, + callopt::CallOpt, + dns::parse_target, loadbalance::{DefaultLB, DefaultLBService, LbConfig}, meta::{MetaService, MetaServiceConfig}, + target::TargetParser, transport::{ClientConfig, ClientTransport, ClientTransportConfig}, }; use crate::{ @@ -44,13 +46,15 @@ use crate::{ response::ClientResponse, }; -pub mod discover; +pub mod callopt; +pub mod dns; pub mod loadbalance; mod meta; mod request_builder; +pub mod target; mod transport; -pub use self::{discover::Target, request_builder::RequestBuilder}; +pub use self::{request_builder::RequestBuilder, target::Target}; #[doc(hidden)] pub mod prelude { @@ -72,6 +76,7 @@ pub struct ClientBuilder { callee_name: FastStr, caller_name: FastStr, target: Target, + call_opt: CallOpt, target_parser: TargetParser, headers: HeaderMap, inner_layer: IL, @@ -112,6 +117,7 @@ impl ClientBuilder { callee_name: FastStr::empty(), caller_name: FastStr::empty(), target: Default::default(), + call_opt: Default::default(), target_parser: parse_target, headers: Default::default(), inner_layer: Identity::new(), @@ -143,6 +149,7 @@ impl ClientBuilder> { callee_name: self.callee_name, caller_name: self.caller_name, target: self.target, + call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, inner_layer: self.inner_layer, @@ -163,6 +170,7 @@ impl ClientBuilder> { callee_name: self.callee_name, caller_name: self.caller_name, target: self.target, + call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, inner_layer: self.inner_layer, @@ -186,6 +194,7 @@ impl ClientBuilder { callee_name: self.callee_name, caller_name: self.caller_name, target: self.target, + call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, inner_layer: self.inner_layer, @@ -218,6 +227,7 @@ impl ClientBuilder { callee_name: self.callee_name, caller_name: self.caller_name, target: self.target, + call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, inner_layer: Stack::new(layer, self.inner_layer), @@ -253,6 +263,7 @@ impl ClientBuilder { callee_name: self.callee_name, caller_name: self.caller_name, target: self.target, + call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, inner_layer: Stack::new(self.inner_layer, layer), @@ -285,6 +296,7 @@ impl ClientBuilder { callee_name: self.callee_name, caller_name: self.caller_name, target: self.target, + call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, inner_layer: self.inner_layer, @@ -320,6 +332,7 @@ impl ClientBuilder { callee_name: self.callee_name, caller_name: self.caller_name, target: self.target, + call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, inner_layer: self.inner_layer, @@ -340,6 +353,7 @@ impl ClientBuilder { callee_name: self.callee_name, caller_name: self.caller_name, target: self.target, + call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, inner_layer: self.inner_layer, @@ -382,15 +396,11 @@ impl ClientBuilder { /// Set the target address of the client. /// /// If there is no target specified when building a request, client will use this address. - pub fn address(&mut self, address: A, #[cfg(feature = "__tls")] https: bool) -> &mut Self + pub fn address(&mut self, address: A) -> &mut Self where A: Into
, { - self.target = Target::from_address( - address, - #[cfg(feature = "__tls")] - https, - ); + self.target = Target::from_address(address); self } @@ -400,52 +410,39 @@ impl ClientBuilder { /// /// It uses http with port 80 by default. /// - /// To specify scheme or port, use `scheme_host_and_port` instead. + /// For setting scheme and port, use [`Self::with_port`] and [`Self::with_https`] after + /// specifying host. pub fn host(&mut self, host: H) -> &mut Self where H: AsRef, { - self.target = Target::from_host( - host, - None, - #[cfg(feature = "__tls")] - false, - ); + self.target = Target::from_host(host); self } - /// Set the target scheme, host and port of the client. - /// - /// If there is no target specified when building a request, client will use this address. - /// - /// # Panics + /// Set the port of the default target. /// - /// This function will panic when TLS related features are not enable but the `https` is - /// `true`. - pub fn scheme_host_and_port(&mut self, https: bool, host: H, port: Option) -> &mut Self - where - H: AsRef, - { - if cfg!(not(feature = "__tls")) && https { - panic!("tls is not enabled while target uses https"); - } - self.target = Target::from_host( - host, - port, - #[cfg(feature = "__tls")] - https, - ); + /// If there is no target specified, the function will do nothing. + pub fn with_port(&mut self, port: u16) -> &mut Self { + self.target.set_port(port); self } - /// Get a reference of the current target. - pub fn target_ref(&self) -> &Target { - &self.target + /// Set if the default target uses https for transporting. + #[cfg(feature = "__tls")] + pub fn with_https(&mut self, https: bool) -> &mut Self { + self.target.set_https(https); + self } - /// Get a mutable reference of the current target. - pub fn target_mut(&mut self) -> &mut Target { - &mut self.target + /// Set a [`CallOpt`] to the client as default options. + /// + /// The [`CallOpt`] is used for service discover, default is an empty one. + /// + /// See [`CallOpt`] for more details. + pub fn with_callopt(&mut self, call_opt: CallOpt) -> &mut Self { + self.call_opt = call_opt; + self } /// Set a target parser for parsing `Target` and updating `Endpoint`. @@ -472,6 +469,26 @@ impl ClientBuilder { Ok(self) } + /// Get a reference of [`Target`]. + pub fn target_ref(&self) -> &Target { + &self.target + } + + /// Get a mutable reference of [`Target`]. + pub fn target_mut(&mut self) -> &mut Target { + &mut self.target + } + + /// Get a reference of [`CallOpt`]. + pub fn callopt_ref(&self) -> &CallOpt { + &self.call_opt + } + + /// Get a mutable reference of [`CallOpt`]. + pub fn callopt_mut(&mut self) -> &mut CallOpt { + &mut self.call_opt + } + /// Set tls config for the client. #[cfg(feature = "__tls")] #[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "native-tls"))))] @@ -650,6 +667,7 @@ impl ClientBuilder { caller_name, callee_name: self.callee_name, default_target: self.target, + default_call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, }; @@ -665,6 +683,7 @@ struct ClientInner { caller_name: FastStr, callee_name: FastStr, default_target: Target, + default_call_opt: CallOpt, target_parser: TargetParser, headers: HeaderMap, } @@ -764,7 +783,7 @@ impl Client { /// use volo::net::Address; /// use volo_http::{ /// body::{Body, BodyConversion}, - /// client::{discover::Target, Client}, + /// client::{Client, Target}, /// request::ClientRequest, /// }; /// @@ -773,7 +792,8 @@ impl Client { /// let addr: SocketAddr = "[::]:8080".parse().unwrap(); /// let resp = client /// .send_request( - /// Target::from_address(addr, false), + /// Target::from_address(addr), + /// Default::default(), /// ClientRequest::builder() /// .method(Method::GET) /// .uri("/") @@ -792,6 +812,7 @@ impl Client { pub async fn send_request( &self, target: Target, + call_opt: CallOpt, mut request: ClientRequest, timeout: Option, ) -> Result @@ -805,11 +826,14 @@ impl Client { let caller_name = self.inner.caller_name.clone(); let callee_name = self.inner.callee_name.clone(); - let target = match (target.is_none(), self.inner.default_target.is_none()) { + let (target, call_opt) = match (target.is_none(), self.inner.default_target.is_none()) { // The target specified by request exists and we can use it directly. - (false, _) => &target, + (false, _) => (target, &call_opt), // Target is not specified by request, we can use the default target. - (true, false) => &self.inner.default_target, + (true, false) => ( + self.inner.default_target.clone(), + &self.inner.default_call_opt, + ), // Both target are none, return an error. (true, true) => { return Err(no_address()); @@ -828,7 +852,7 @@ impl Client { let mut cx = ClientContext::new(); cx.rpc_info_mut().caller_mut().set_service_name(caller_name); cx.rpc_info_mut().callee_mut().set_service_name(callee_name); - (self.inner.target_parser)(target, cx.rpc_info_mut().callee_mut()); + (self.inner.target_parser)(target, call_opt, cx.rpc_info_mut().callee_mut()); let config = Config { timeout }; cx.rpc_info_mut().set_config(config); @@ -898,7 +922,7 @@ mod client_tests { use http::{header, StatusCode}; use serde::Deserialize; - use super::{discover::dns::DnsResolver, get, Client, DefaultClient}; + use super::{dns::DnsResolver, get, Client, DefaultClient}; use crate::{ body::BodyConversion, error::client::status_error, @@ -981,13 +1005,7 @@ mod client_tests { .await .unwrap(); let mut builder = Client::builder(); - builder - .address( - addr, - #[cfg(feature = "__tls")] - false, - ) - .callee_name("httpbin.org"); + builder.address(addr).callee_name("httpbin.org"); let client = builder.build(); let resp = client @@ -1007,7 +1025,7 @@ mod client_tests { #[tokio::test] async fn client_builder_with_https() { let mut builder = Client::builder(); - builder.scheme_host_and_port(true, "httpbin.org", None); + builder.host("httpbin.org").with_https(true); let client = builder.build(); let resp = client @@ -1031,7 +1049,10 @@ mod client_tests { .await .unwrap(); let mut builder = Client::builder(); - builder.address(addr, true).callee_name("httpbin.org"); + builder + .address(addr) + .with_https(true) + .callee_name("httpbin.org"); let client = builder.build(); let resp = client @@ -1050,7 +1071,7 @@ mod client_tests { #[tokio::test] async fn client_builder_with_port() { let mut builder = Client::builder(); - builder.scheme_host_and_port(false, "httpbin.org", Some(443)); + builder.host("httpbin.org").with_port(443); let client = builder.build(); let resp = client.get("/get").unwrap().send().await.unwrap(); diff --git a/volo-http/src/client/request_builder.rs b/volo-http/src/client/request_builder.rs index 5eb6c39f..f87f1641 100644 --- a/volo-http/src/client/request_builder.rs +++ b/volo-http/src/client/request_builder.rs @@ -10,7 +10,7 @@ use http::{ use motore::service::Service; use volo::net::Address; -use super::{discover::Target, Client}; +use super::{callopt::CallOpt, target::Target, Client}; use crate::{ body::Body, context::ClientContext, @@ -26,6 +26,7 @@ use crate::{ pub struct RequestBuilder<'a, S, B = Body> { client: &'a Client, target: Target, + call_opt: CallOpt, request: ClientRequest, timeout: Option, } @@ -35,7 +36,8 @@ impl<'a, S> RequestBuilder<'a, S, Body> { Self { client, target: Default::default(), - request: Request::new(Body::empty()), + call_opt: Default::default(), + request: Default::default(), timeout: None, } } @@ -158,6 +160,16 @@ impl<'a, S, B> RequestBuilder<'a, S, B> { Ok(self) } + /// Set a [`CallOpt`] to the request. + /// + /// The [`CallOpt`] is used for service discover, default is an empty one. + /// + /// See [`CallOpt`] for more details. + pub fn with_callopt(mut self, call_opt: CallOpt) -> Self { + self.call_opt = call_opt; + self + } + /// Set query for the uri in request from object with `Serialize`. #[cfg(feature = "query")] pub fn set_query(mut self, query: &T) -> Result @@ -216,15 +228,11 @@ impl<'a, S, B> RequestBuilder<'a, S, B> { } /// Set the target address for the request. - pub fn address(mut self, address: A, #[cfg(feature = "__tls")] https: bool) -> Self + pub fn address(mut self, address: A) -> Self where A: Into
, { - self.target = Target::from_address( - address, - #[cfg(feature = "__tls")] - https, - ); + self.target = Target::from_address(address); self } @@ -232,42 +240,49 @@ impl<'a, S, B> RequestBuilder<'a, S, B> { /// /// It uses http with port 80 by default. /// - /// For setting the scheme and port, use `scheme_host_and_port` instead. + /// For setting scheme and port, use [`Self::with_port`] and [`Self::with_https`] after + /// specifying host. pub fn host(mut self, host: H) -> Self where H: AsRef, { - self.target = Target::from_host( - host, - None, - #[cfg(feature = "__tls")] - false, - ); + self.target = Target::from_host(host); self } - /// Set the target scheme, host and port for the request. - /// - /// # Panics - /// - /// This function will panic when TLS related features are not enable but the `https` is - /// `true`. - pub fn scheme_host_and_port(mut self, https: bool, host: H, port: Option) -> Self - where - H: AsRef, - { - if cfg!(not(feature = "__tls")) && https { - panic!("tls is not enabled while target uses https"); - } - self.target = Target::from_host( - host, - port, - #[cfg(feature = "__tls")] - https, - ); + /// Set port for the target address of this request. + pub fn with_port(mut self, port: u16) -> Self { + self.target.set_port(port); + self + } + + /// Set if the request uses https. + #[cfg(feature = "__tls")] + pub fn with_https(mut self, https: bool) -> Self { + self.target.set_https(https); self } + /// Get a reference of [`Target`]. + pub fn target_ref(&self) -> &Target { + &self.target + } + + /// Get a mutable reference of [`Target`]. + pub fn target_mut(&mut self) -> &mut Target { + &mut self.target + } + + /// Get a reference of [`CallOpt`]. + pub fn callopt_ref(&self) -> &CallOpt { + &self.call_opt + } + + /// Get a mutable reference of [`CallOpt`]. + pub fn callopt_mut(&mut self) -> &mut CallOpt { + &mut self.call_opt + } + /// Set the request body. pub fn body(self, body: B2) -> RequestBuilder<'a, S, B2> { let (parts, _) = self.request.into_parts(); @@ -276,6 +291,7 @@ impl<'a, S, B> RequestBuilder<'a, S, B> { RequestBuilder { client: self.client, target: self.target, + call_opt: self.call_opt, request, timeout: self.timeout, } @@ -307,7 +323,7 @@ where /// Send the request and get the response. pub async fn send(self) -> Result { self.client - .send_request(self.target, self.request, self.timeout) + .send_request(self.target, self.call_opt, self.request, self.timeout) .await } } diff --git a/volo-http/src/client/target.rs b/volo-http/src/client/target.rs new file mode 100644 index 00000000..d9ad57df --- /dev/null +++ b/volo-http/src/client/target.rs @@ -0,0 +1,663 @@ +//! HTTP target address related types +//! +//! See [`Target`], [`RemoteTarget`] for more details. + +use std::net::{IpAddr, SocketAddr}; + +use faststr::FastStr; +use http::{uri::Scheme, HeaderValue, Uri}; +use volo::{context::Endpoint, net::Address}; + +use crate::{ + client::callopt::CallOpt, + error::{client::bad_scheme, ClientError}, + utils::consts, +}; + +/// Function for parsing [`Target`] and [`CallOpt`] to update [`Endpoint`]. +/// +/// The `TargetParser` usually used for service discover. It can update [`Endpoint` ]from +/// [`Target`] and [`CallOpt`], and the service discover will resolve the [`Endpoint`] to +/// [`Address`](volo::net::Address)\(es\) and access them. +pub type TargetParser = fn(Target, &CallOpt, &mut Endpoint); + +#[derive(Clone, Debug, Default)] +pub enum Target { + #[default] + None, + Remote(RemoteTarget), + #[cfg(target_family = "unix")] + Local(std::os::unix::net::SocketAddr), +} + +#[derive(Clone, Debug)] +pub struct RemoteTarget { + pub addr: RemoteTargetAddress, + pub port: Option, + #[cfg(feature = "__tls")] + pub https: bool, +} + +#[derive(Clone, Debug)] +pub enum RemoteTargetAddress { + Ip(IpAddr), + Name(FastStr), +} + +impl Target { + /// Build a `Target` from `Uri`. + /// + /// If there is no host, `None` will be returned. If there is a host, but the uri has something + /// invalid (e.g., unsupported scheme), an error will be returned. + pub fn from_uri(uri: &Uri) -> Option> { + let host = uri.host()?; + let Some(https) = is_https(uri) else { + tracing::error!("[Volo-HTTP] unsupported scheme: {:?}.", uri.scheme()); + return Some(Err(bad_scheme())); + }; + #[cfg(not(feature = "__tls"))] + if https { + tracing::error!("[Volo-HTTP] https is not allowed when feature `tls` is not enabled."); + return Some(Err(bad_scheme())); + } + + let addr = match host + .trim_start_matches('[') + .trim_end_matches(']') + .parse::() + { + Ok(ip) => RemoteTargetAddress::Ip(ip), + Err(_) => RemoteTargetAddress::Name(FastStr::from_string(host.to_owned())), + }; + let port = uri.port_u16(); + Some(Ok(Self::Remote(RemoteTarget { + addr, + port, + #[cfg(feature = "__tls")] + https, + }))) + } + + /// Build a `Target` from an address. + pub fn from_address(addr: A) -> Self + where + A: Into
, + { + Self::from(addr.into()) + } + + /// Build a `Target` from a host name. + /// + /// Note that the `host` must be a host name, it will be used for service discover. + /// + /// It should NOT be an address or something with port. + /// + /// If you have a uri and you are not sure if the host is a host, try `from_uri`. + pub fn from_host(host: S) -> Self + where + S: AsRef, + { + Self::Remote(RemoteTarget { + addr: RemoteTargetAddress::Name(FastStr::from_string(host.as_ref().to_owned())), + port: None, + #[cfg(feature = "__tls")] + https: false, + }) + } + + /// Return if the `Target` is `None`. + pub fn is_none(&self) -> bool { + matches!(self, Target::None) + } + + /// Get a reference of the [`RemoteTarget`]. + pub fn remote_ref(&self) -> Option<&RemoteTarget> { + match self { + Self::Remote(remote) => Some(remote), + _ => None, + } + } + + /// Get a mutable reference of the [`RemoteTarget`]. + pub fn remote_mut(&mut self) -> Option<&mut RemoteTarget> { + match self { + Self::Remote(remote) => Some(remote), + _ => None, + } + } + + /// Set remote port and return a new target. + pub fn set_port(&mut self, port: u16) { + if let Some(rt) = self.remote_mut() { + rt.port = Some(port); + } + } + + /// Set if use https for the target. + /// + /// If the [`Target`] cannot use https ([`Target::None`] or [`Target::Local`]), this function + /// will do nothing. + #[cfg(feature = "__tls")] + pub fn set_https(&mut self, https: bool) { + if let Some(rt) = self.remote_mut() { + rt.set_https(https); + } + } + + /// Check if the target uses https. + /// + /// If the [`Target`] cannot use https ([`Target::None`] or [`Target::Local`]), this function + /// will return `false`. + #[cfg(feature = "__tls")] + pub fn is_https(&self) -> bool { + if let Some(rt) = self.remote_ref() { + rt.is_https() + } else { + false + } + } + + /// Return the remote [`IpAddr`] if the [`Target`] is an IP address. + pub fn remote_ip(&self) -> Option<&IpAddr> { + if let Self::Remote(rt) = &self { + if let RemoteTargetAddress::Ip(ip) = &rt.addr { + return Some(ip); + } + } + None + } + + /// Return the remote host name if the [`Target`] is a host name. + pub fn remote_host(&self) -> Option<&FastStr> { + if let Self::Remote(rt) = &self { + if let RemoteTargetAddress::Name(name) = &rt.addr { + return Some(name); + } + } + None + } + + /// Return the unix socket address if the [`Target`] is it. + #[cfg(target_family = "unix")] + pub fn unix_socket_addr(&self) -> Option<&std::os::unix::net::SocketAddr> { + if let Self::Local(sa) = &self { + Some(sa) + } else { + None + } + } + + /// Return the port if the [`Target`] is a remote address and the port is given. + pub fn port(&self) -> Option { + if let Some(remote) = self.remote_ref() { + remote.port + } else { + None + } + } + + /// Generate a `HeaderValue` for `Host` in HTTP headers. + pub fn gen_host(&self) -> Option { + HeaderValue::try_from(self.try_to_string()?).ok() + } + + fn is_default_port(&self) -> bool { + let Some(rt) = self.remote_ref() else { + // Local address does not have port. + return false; + }; + let Some(port) = rt.port else { + // `None` means using default port. + return true; + }; + #[cfg(feature = "__tls")] + if rt.https { + return port == consts::HTTPS_DEFAULT_PORT; + } + port == consts::HTTP_DEFAULT_PORT + } + + fn try_to_string(&self) -> Option { + let rt = self.remote_ref()?; + let without_port = self.is_default_port(); + match rt.addr { + RemoteTargetAddress::Ip(ref ip) => { + if without_port { + return Some(ip.to_string()); + } + // SAFETY: the port must exist if the port is non-default one + let port = rt.port.unwrap(); + if ip.is_ipv6() { + Some(format!("[{ip}]:{port}")) + } else { + Some(format!("{ip}:{port}")) + } + } + RemoteTargetAddress::Name(ref name) => { + if without_port { + return Some(name.to_string()); + } + // SAFETY: the port must exist if the port is non-default one + let port = rt.port.unwrap(); + Some(format!("{name}:{port}")) + } + } + } +} + +impl From
for Target { + fn from(value: Address) -> Self { + match value { + Address::Ip(sa) => Target::Remote(RemoteTarget { + addr: RemoteTargetAddress::Ip(sa.ip()), + port: Some(sa.port()), + #[cfg(feature = "__tls")] + https: false, + }), + #[cfg(target_family = "unix")] + Address::Unix(uds) => Target::Local(uds), + } + } +} + +impl TryFrom for Address { + type Error = Target; + + fn try_from(value: Target) -> Result { + match value { + Target::None => Err(value), + #[cfg(target_family = "unix")] + Target::Local(sa) => Ok(Address::Unix(sa)), + Target::Remote(rt) => { + let port = rt.port(); + if let RemoteTargetAddress::Ip(ip) = rt.addr { + Ok(Address::Ip(SocketAddr::new(ip, port))) + } else { + Err(Target::Remote(rt)) + } + } + } + } +} + +impl RemoteTarget { + /// Get the target port for the [`RemoteTarget`]. + /// + /// If the port has not been set, it will return a default port based on if https is enabled. + pub fn port(&self) -> u16 { + if let Some(port) = self.port { + return port; + } + #[cfg(feature = "__tls")] + if self.https { + return consts::HTTPS_DEFAULT_PORT; + } + consts::HTTP_DEFAULT_PORT + } + + /// Set if use https for the target. + #[cfg(feature = "__tls")] + pub fn set_https(&mut self, https: bool) { + self.https = https; + } + + /// Check if the target uses https. + #[cfg(feature = "__tls")] + pub fn is_https(&self) -> bool { + self.https + } +} + +fn is_https(uri: &Uri) -> Option { + let Some(scheme) = uri.scheme() else { + return Some(false); + }; + if scheme == &Scheme::HTTPS { + return Some(true); + } + if scheme == &Scheme::HTTP { + return Some(false); + } + None +} + +#[cfg(test)] +mod target_tests { + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + + use http::uri::Uri; + use volo::net::Address; + + use super::Target; + + #[cfg(feature = "__tls")] + #[test] + fn test_from_uri() { + // no domain name + let target = Target::from_uri(&Uri::from_static("/api/v1/config")); + assert!(target.is_none()); + + // invalid scheme + let target = Target::from_uri(&Uri::from_static("ftp://github.com")); + assert!(matches!(target, Some(Err(_)))); + + // ipv4 only + let target = Target::from_uri(&Uri::from_static("10.0.0.1")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!( + target.remote_ip().unwrap().to_string(), + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)).to_string(), + ); + assert_eq!(target.port(), None); + assert!(!target.is_https()); + + // ipv4 with port + let target = Target::from_uri(&Uri::from_static("10.0.0.1:8000")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!( + target.remote_ip().unwrap().to_string(), + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)).to_string(), + ); + assert_eq!(target.port(), Some(8000)); + assert!(!target.is_https()); + + // ipv6 with port + let target = Target::from_uri(&Uri::from_static("[ff::1]:8000")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!( + target.remote_ip().unwrap().to_string(), + IpAddr::V6(Ipv6Addr::new(0xff, 0, 0, 0, 0, 0, 0, 1)).to_string(), + ); + assert_eq!(target.port(), Some(8000)); + assert!(!target.is_https()); + + // domain name only + let target = Target::from_uri(&Uri::from_static("github.com")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), None); + assert!(!target.is_https()); + + // domain with scheme (http) + let target = Target::from_uri(&Uri::from_static("http://github.com/")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), None); + assert!(!target.is_https()); + + // domain with scheme (https) + let target = Target::from_uri(&Uri::from_static("https://github.com")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), None); + assert!(target.is_https()); + + // domain with port + let target = Target::from_uri(&Uri::from_static("github.com:8000")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), Some(8000)); + assert!(!target.is_https()); + + // domain with scheme (http) and port + let target = Target::from_uri(&Uri::from_static("http://github.com:8000/")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), Some(8000)); + assert!(!target.is_https()); + + // domain with scheme (https) and port + let target = Target::from_uri(&Uri::from_static("https://github.com:8000/")); + assert!(matches!(target, Some(Ok(_)))); + let target = target.unwrap().unwrap(); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), Some(8000)); + assert!(target.is_https()); + } + + #[cfg(feature = "__tls")] + #[test] + fn test_from_ip_address() { + // IPv4 + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let port = 8000; + let addr = Address::Ip(SocketAddr::new(ip, port)); + let target = Target::from_address(addr); + assert_eq!(target.remote_ip(), Some(&ip)); + assert_eq!(target.port(), Some(port)); + assert!(!target.is_https()); + + // IPv6 + let ip = IpAddr::V6(Ipv6Addr::new(0xff, 0, 0, 0, 0, 0, 0, 0)); + let port = 8000; + let addr = Address::Ip(SocketAddr::new(ip, port)); + let target = Target::from_address(addr); + assert_eq!(target.remote_ip(), Some(&ip)); + assert_eq!(target.port(), Some(port)); + assert!(!target.is_https()); + } + + #[cfg(target_family = "unix")] + #[test] + fn test_from_uds_address() { + #[derive(Debug, PartialEq, Eq)] + struct SocketAddr { + addr: libc::sockaddr_un, + len: libc::socklen_t, + } + + let uds = std::os::unix::net::SocketAddr::from_pathname("/tmp/test.sock").unwrap(); + let addr = Address::Unix(uds.clone()); + let target = Target::from_address(addr); + + // Use a same struct with `PartialEq` and `Eq` and transmute them for comparing. + let uds: SocketAddr = unsafe { std::mem::transmute(uds) }; + let target_uds: SocketAddr = + unsafe { std::mem::transmute(target.unix_socket_addr().unwrap().to_owned()) }; + assert_eq!(target_uds, uds); + assert!(target.port().is_none()); + assert!(!target.is_https()); + } + + #[test] + fn test_from_host() { + let target = Target::from_host("github.com"); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert!(target.port().is_none()); + assert!(!target.is_https()); + } + + #[test] + fn test_uri_with_port() { + // domain name only + let target = Target::from_uri(&Uri::from_static("github.com")); + assert!(matches!(target, Some(Ok(_)))); + let mut target = target.unwrap().unwrap(); + target.set_port(8000); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), Some(8000)); + + // domain name with port and override it + let target = Target::from_uri(&Uri::from_static("github.com:80")); + assert!(matches!(target, Some(Ok(_)))); + let mut target = target.unwrap().unwrap(); + target.set_port(8000); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), Some(8000)); + } + + #[test] + fn test_ip_with_port() { + // IPv4 + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let port = 8000; + let addr = Address::Ip(SocketAddr::new(ip, port)); + let mut target = Target::from_address(addr); + target.set_port(80); + assert_eq!(target.remote_ip(), Some(&ip)); + assert_eq!(target.port(), Some(80)); + + // IPv6 + let ip = IpAddr::V6(Ipv6Addr::new(0xff, 0, 0, 0, 0, 0, 0, 1)); + let port = 8000; + let addr = Address::Ip(SocketAddr::new(ip, port)); + let mut target = Target::from_address(addr); + target.set_port(80); + assert_eq!(target.remote_ip(), Some(&ip)); + assert_eq!(target.port(), Some(80)); + } + + #[cfg(target_family = "unix")] + #[test] + fn test_uds_with_port() { + let uds = std::os::unix::net::SocketAddr::from_pathname("/tmp/test.sock").unwrap(); + let addr = Address::Unix(uds.clone()); + let mut target = Target::from_address(addr); + assert!(target.port().is_none()); + target.set_port(80); + // uds does not have port + assert!(target.port().is_none()); + } + + #[test] + fn test_host_with_port() { + let mut target = Target::from_host("github.com"); + let port = 8000; + target.set_port(port); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert_eq!(target.port(), Some(port)); + } + + #[test] + fn test_uri_with_https() { + // domain name only + let target = Target::from_uri(&Uri::from_static("github.com")); + assert!(matches!(target, Some(Ok(_)))); + let mut target = target.unwrap().unwrap(); + target.set_https(true); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert!(target.is_https()); + + // domain name with http and override it + let target = Target::from_uri(&Uri::from_static("http://github.com")); + assert!(matches!(target, Some(Ok(_)))); + let mut target = target.unwrap().unwrap(); + target.set_https(true); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert!(target.is_https()); + } + + #[test] + fn test_ip_with_https() { + // IPv4 + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let port = 8000; + let addr = Address::Ip(SocketAddr::new(ip, port)); + let mut target = Target::from_address(addr); + target.set_https(true); + assert_eq!(target.remote_ip(), Some(&ip)); + assert!(target.is_https()); + + // IPv6 + let ip = IpAddr::V6(Ipv6Addr::new(0xff, 0, 0, 0, 0, 0, 0, 0)); + let port = 8000; + let addr = Address::Ip(SocketAddr::new(ip, port)); + let mut target = Target::from_address(addr); + target.set_https(true); + assert_eq!(target.remote_ip(), Some(&ip)); + assert!(target.is_https()); + } + + #[cfg(target_family = "unix")] + #[test] + fn test_uds_with_https() { + let uds = std::os::unix::net::SocketAddr::from_pathname("/tmp/test.sock").unwrap(); + let addr = Address::Unix(uds.clone()); + let mut target = Target::from_address(addr); + assert!(target.port().is_none()); + target.set_https(true); + // uds does not have port + assert!(!target.is_https()); + } + + #[test] + fn test_host_with_https() { + let mut target = Target::from_host("github.com"); + target.set_https(true); + assert_eq!(target.remote_host().unwrap(), "github.com"); + assert!(target.is_https()); + } + + fn gen_host_to_string(target: &Target) -> Option { + let host = target.gen_host()?; + Some(host.to_str().map(ToOwned::to_owned).unwrap_or_default()) + } + + #[test] + fn test_gen_host() { + // ipv4 with default http port + let target = Target::from_address(Address::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 80, + ))); + assert_eq!(gen_host_to_string(&target).as_deref(), Some("127.0.0.1")); + // ipv4 with non-default http port + let target = Target::from_address(Address::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 443, + ))); + assert_eq!( + gen_host_to_string(&target).as_deref(), + Some("127.0.0.1:443") + ); + // ipv4 with default https port + let mut target = Target::from_address(Address::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 443, + ))); + target.set_https(true); + assert_eq!(gen_host_to_string(&target).as_deref(), Some("127.0.0.1")); + // ipv4 with non-default https port + let mut target = Target::from_address(Address::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 80, + ))); + target.set_https(true); + assert_eq!(gen_host_to_string(&target).as_deref(), Some("127.0.0.1:80")); + + // ipv6 with default http port + let target = Target::from_address(Address::Ip(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0xff, 0, 0, 0, 0, 0, 0, 1)), + 80, + ))); + assert_eq!(gen_host_to_string(&target).as_deref(), Some("ff::1")); + // ipv6 with non-default http port + let target = Target::from_address(Address::Ip(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0xff, 0, 0, 0, 0, 0, 0, 1)), + 443, + ))); + assert_eq!(gen_host_to_string(&target).as_deref(), Some("[ff::1]:443")); + // ipv6 with default https port + let mut target = Target::from_address(Address::Ip(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0xff, 0, 0, 0, 0, 0, 0, 1)), + 443, + ))); + target.set_https(true); + assert_eq!(gen_host_to_string(&target).as_deref(), Some("ff::1")); + // ipv6 with non-default https port + let mut target = Target::from_address(Address::Ip(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0xff, 0, 0, 0, 0, 0, 0, 1)), + 80, + ))); + target.set_https(true); + assert_eq!(gen_host_to_string(&target).as_deref(), Some("[ff::1]:80")); + } +}