volo_http/client/
target.rs

1//! HTTP target address related types
2//!
3//! See [`Target`], [`RemoteTarget`] for more details.
4
5use std::{
6    borrow::Cow,
7    fmt,
8    net::{IpAddr, SocketAddr},
9};
10
11use faststr::FastStr;
12use http::uri::{Scheme, Uri};
13use volo::{client::Apply, context::Context, net::Address};
14
15use super::utils::{get_default_port, is_default_port};
16use crate::{
17    client::dns::Port,
18    context::ClientContext,
19    error::{
20        ClientError,
21        client::{Result, bad_scheme, no_address, port_unavailable, scheme_unavailable},
22    },
23    utils::consts,
24};
25
26/// HTTP target server descriptor
27#[derive(Clone, Debug, Default)]
28pub enum Target {
29    #[default]
30    /// No target specified
31    None,
32    /// Remote target, supports service name (domain name by default) or ip address
33    Remote(RemoteTarget),
34    /// Local target, usually using a unix domain socket.
35    #[cfg(target_family = "unix")]
36    Local(std::os::unix::net::SocketAddr),
37}
38
39impl fmt::Display for Target {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Target::None => f.write_str("none"),
43            Target::Remote(rt) => write!(f, "{rt}"),
44            #[cfg(target_family = "unix")]
45            Target::Local(sa) => {
46                if let Some(path) = sa.as_pathname().and_then(std::path::Path::to_str) {
47                    f.write_str(path)
48                } else {
49                    f.write_str("[unnamed]")
50                }
51            }
52        }
53    }
54}
55
56/// Remote part of [`Target`]
57#[derive(Clone, Debug)]
58pub struct RemoteTarget {
59    /// Target scheme
60    pub scheme: Scheme,
61    /// Target host descriptor
62    pub host: RemoteHost,
63    /// Target port
64    pub port: u16,
65}
66
67impl fmt::Display for RemoteTarget {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.write_str(self.scheme.as_str())?;
70        f.write_str("://")?;
71        write!(f, "{}", self.host)?;
72        if !is_default_port(&self.scheme, self.port) {
73            write!(f, ":{}", self.port)?;
74        }
75        Ok(())
76    }
77}
78
79/// Remote address of [`RemoteTarget`]
80#[derive(Clone, Debug)]
81pub enum RemoteHost {
82    /// Ip address
83    Ip(IpAddr),
84    /// Service name, usually a domain name
85    Name(FastStr),
86}
87
88impl fmt::Display for RemoteHost {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::Ip(ip) => {
92                if ip.is_ipv4() {
93                    write!(f, "{ip}")
94                } else {
95                    write!(f, "[{ip}]")
96                }
97            }
98            Self::Name(name) => f.write_str(name),
99        }
100    }
101}
102
103fn check_scheme(scheme: &Scheme) -> Result<()> {
104    if scheme == &Scheme::HTTPS {
105        #[cfg(not(feature = "__tls"))]
106        {
107            tracing::error!("[Volo-HTTP] https is not allowed when feature `tls` is not enabled");
108            return Err(bad_scheme(scheme.clone()));
109        }
110        #[cfg(feature = "__tls")]
111        return Ok(());
112    }
113    if scheme == &Scheme::HTTP {
114        return Ok(());
115    }
116    tracing::error!("[Volo-HTTP] scheme '{scheme}' is unsupported");
117    Err(bad_scheme(scheme.clone()))
118}
119
120impl Target {
121    /// Create a [`Target`] by a scheme, host and port without checking scheme
122    ///
123    /// # Safety
124    ///
125    /// Users must ensure that the scheme is valid.
126    ///
127    /// - HTTP is always valid
128    /// - HTTPS is valid if any feature of tls is enabled
129    /// - Other schemes are always invalid
130    pub const unsafe fn new_host_unchecked(scheme: Scheme, host: FastStr, port: u16) -> Self {
131        Self::Remote(RemoteTarget {
132            scheme,
133            host: RemoteHost::Name(host),
134            port,
135        })
136    }
137
138    /// Create a [`Target`] by a scheme, ip address and port without checking scheme
139    ///
140    /// # Safety
141    ///
142    /// Users must ensure that the scheme is valid.
143    ///
144    /// - HTTP is always valid
145    /// - HTTPS is valid if any feature of tls is enabled
146    /// - Other schemes are always invalid
147    pub const unsafe fn new_addr_unchecked(scheme: Scheme, ip: IpAddr, port: u16) -> Self {
148        Self::Remote(RemoteTarget {
149            scheme,
150            host: RemoteHost::Ip(ip),
151            port,
152        })
153    }
154
155    /// Create a [`Target`] through a scheme, host name and a port
156    pub fn new_host<S>(scheme: Option<Scheme>, host: S, port: Option<u16>) -> Result<Self>
157    where
158        S: Into<Cow<'static, str>>,
159    {
160        let scheme = scheme.unwrap_or(Scheme::HTTP);
161        check_scheme(&scheme)?;
162        let host = FastStr::from(host.into());
163        let port = match port {
164            Some(p) => p,
165            None => get_default_port(&scheme),
166        };
167        // SAFETY: we've checked scheme
168        Ok(unsafe { Self::new_host_unchecked(scheme, host, port) })
169    }
170
171    /// Create a [`Target`] through a scheme, ip address and a port
172    pub fn new_addr(scheme: Option<Scheme>, ip: IpAddr, port: Option<u16>) -> Result<Self> {
173        let scheme = scheme.unwrap_or(Scheme::HTTP);
174        check_scheme(&scheme)?;
175        let port = match port {
176            Some(p) => p,
177            None => get_default_port(&scheme),
178        };
179        // SAFETY: we've checked scheme
180        Ok(unsafe { Self::new_addr_unchecked(scheme, ip, port) })
181    }
182
183    /// Create a [`Target`] through a host name
184    pub fn from_host<S>(host: S) -> Self
185    where
186        S: Into<Cow<'static, str>>,
187    {
188        let host = FastStr::from(host.into());
189        // SAFETY: HTTP is always valid
190        unsafe { Self::new_host_unchecked(Scheme::HTTP, host, consts::HTTP_DEFAULT_PORT) }
191    }
192
193    /// Create a [`Target`] from [`Uri`]
194    pub fn from_uri(uri: &Uri) -> Result<Self> {
195        let scheme = uri.scheme().cloned().unwrap_or(Scheme::HTTP);
196        check_scheme(&scheme)?;
197        let host = uri.host().ok_or_else(no_address)?;
198        let port = match uri.port_u16() {
199            Some(p) => p,
200            None => get_default_port(&scheme),
201        };
202
203        // SAFETY: we've checked scheme
204        Ok(unsafe {
205            match host.parse::<IpAddr>() {
206                Ok(ip) => Self::new_addr_unchecked(scheme, ip, port),
207                Err(_) => {
208                    Self::new_host_unchecked(scheme, FastStr::from_string(host.to_owned()), port)
209                }
210            }
211        })
212    }
213
214    /// Set a new scheme to the [`Target`]
215    ///
216    /// Note that if the previous is default port of the previous scheme, the port will be also
217    /// updated to default port of the new scheme.
218    pub fn set_scheme(&mut self, scheme: Scheme) -> Result<()> {
219        let rt = match self.remote_mut() {
220            Some(rt) => rt,
221            None => {
222                tracing::warn!("[Volo-HTTP] set scheme to an empty target or uds is invalid");
223                return Err(scheme_unavailable());
224            }
225        };
226        check_scheme(&scheme)?;
227        if is_default_port(&rt.scheme, rt.port) {
228            rt.port = get_default_port(&scheme);
229        }
230        rt.scheme = scheme;
231        Ok(())
232    }
233
234    /// Set a new port to the [`Target`]
235    pub fn set_port(&mut self, port: u16) -> Result<()> {
236        let rt = match self.remote_mut() {
237            Some(rt) => rt,
238            None => {
239                tracing::warn!("[Volo-HTTP] set port to an empty target or uds is invalid");
240                return Err(port_unavailable());
241            }
242        };
243        rt.port = port;
244        Ok(())
245    }
246
247    /// Return if the [`Target`] is [`Target::None`]
248    pub fn is_none(&self) -> bool {
249        matches!(self, Target::None)
250    }
251
252    /// Get a reference of the [`RemoteTarget`].
253    pub fn remote_ref(&self) -> Option<&RemoteTarget> {
254        match self {
255            Self::Remote(remote) => Some(remote),
256            _ => None,
257        }
258    }
259
260    /// Get a mutable reference of the [`RemoteTarget`].
261    pub fn remote_mut(&mut self) -> Option<&mut RemoteTarget> {
262        match self {
263            Self::Remote(remote) => Some(remote),
264            _ => None,
265        }
266    }
267
268    /// Return the remote [`IpAddr`] if the [`Target`] is an IP address.
269    pub fn remote_ip(&self) -> Option<&IpAddr> {
270        if let Self::Remote(rt) = &self {
271            if let RemoteHost::Ip(ip) = &rt.host {
272                return Some(ip);
273            }
274        }
275        None
276    }
277
278    /// Return the remote host name if the [`Target`] is a host name.
279    pub fn remote_host(&self) -> Option<&FastStr> {
280        if let Self::Remote(rt) = &self {
281            if let RemoteHost::Name(name) = &rt.host {
282                return Some(name);
283            }
284        }
285        None
286    }
287
288    /// Return the unix socket address if the [`Target`] is it.
289    #[cfg(target_family = "unix")]
290    pub fn unix_socket_addr(&self) -> Option<&std::os::unix::net::SocketAddr> {
291        if let Self::Local(sa) = &self {
292            Some(sa)
293        } else {
294            None
295        }
296    }
297
298    /// Return target scheme if the [`Target`] is a remote address
299    pub fn scheme(&self) -> Option<&Scheme> {
300        if let Self::Remote(rt) = self {
301            Some(&rt.scheme)
302        } else {
303            None
304        }
305    }
306
307    /// Return target port if the [`Target`] is a remote address
308    pub fn port(&self) -> Option<u16> {
309        if let Self::Remote(rt) = self {
310            Some(rt.port)
311        } else {
312            None
313        }
314    }
315}
316
317impl From<Address> for Target {
318    fn from(value: Address) -> Self {
319        match value {
320            Address::Ip(sa) => {
321                // SAFETY: HTTP is always valid
322                unsafe { Target::new_addr_unchecked(Scheme::HTTP, sa.ip(), sa.port()) }
323            }
324            #[cfg(target_family = "unix")]
325            Address::Unix(uds) => Target::Local(uds),
326        }
327    }
328}
329
330fn ipv6_strip_brackets(src: FastStr) -> FastStr {
331    let bytes = src.as_bytes();
332    match (bytes.first(), bytes.last()) {
333        // SAFETY: the range must be valid because its first and last characters must be ascii char
334        (Some(b'['), Some(b']')) => unsafe { src.index(1, src.len() - 1) },
335        _ => src,
336    }
337}
338
339impl Apply<ClientContext> for Target {
340    type Error = ClientError;
341
342    fn apply(self, cx: &mut ClientContext) -> Result<(), Self::Error> {
343        cx.set_target(self.clone());
344
345        match self {
346            Self::Remote(rt) => {
347                match rt.host {
348                    RemoteHost::Ip(ip) => {
349                        let sa = SocketAddr::new(ip, rt.port);
350                        let callee = cx.rpc_info_mut().callee_mut();
351                        callee.set_service_name(FastStr::from_string(format!("{}", sa.ip())));
352                        callee.set_address(Address::Ip(sa));
353                    }
354                    RemoteHost::Name(host) => {
355                        let port = rt.port;
356                        let callee = cx.rpc_info_mut().callee_mut();
357                        callee.set_service_name(ipv6_strip_brackets(host));
358                        // Since Service Discover (DNS) can only access the `callee`, we must
359                        // insert port into `callee` so that Service Discover can return the full
360                        // address (IP with port) for transporting.
361                        callee.insert(Port(port));
362                    }
363                }
364            }
365            #[cfg(target_family = "unix")]
366            Self::Local(uds) => {
367                let callee = cx.rpc_info_mut().callee_mut();
368                callee.set_address(Address::Unix(uds));
369                callee.set_service_name(FastStr::from_static_str("unix-domain-socket"));
370            }
371            Self::None => {}
372        }
373
374        Ok(())
375    }
376}
377
378#[cfg(test)]
379mod target_tests {
380    use faststr::FastStr;
381
382    use super::ipv6_strip_brackets;
383
384    #[test]
385    fn ipv6_strip_test() {
386        {
387            let s = FastStr::from_static_str("foo");
388            assert_eq!(ipv6_strip_brackets(s.clone()), s);
389        }
390        {
391            let s = FastStr::from_static_str("127.0.0.1");
392            assert_eq!(ipv6_strip_brackets(s.clone()), s);
393        }
394        {
395            let s = FastStr::from_static_str("[[[");
396            assert_eq!(ipv6_strip_brackets(s.clone()), s);
397        }
398        {
399            let s = FastStr::from_static_str("]]]");
400            assert_eq!(ipv6_strip_brackets(s.clone()), s);
401        }
402        {
403            let s = FastStr::from_static_str("(::1)");
404            assert_eq!(ipv6_strip_brackets(s.clone()), s);
405        }
406        {
407            let s = FastStr::from_static_str("[::1]");
408            assert_eq!(ipv6_strip_brackets(s), "::1");
409        }
410    }
411}