isahc/config/
dial.rs

1//! Configuration for customizing how connections are established and sockets
2//! are opened.
3
4use super::SetOpt;
5use curl::easy::{Easy2, List};
6use http::Uri;
7use std::{convert::TryFrom, fmt, net::SocketAddr, str::FromStr};
8
9/// An error which can be returned when parsing a dial address.
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct DialerParseError(());
12
13impl fmt::Display for DialerParseError {
14    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
15        fmt.write_str("invalid dial address syntax")
16    }
17}
18
19impl std::error::Error for DialerParseError {}
20
21/// A custom address or dialer for connecting to a host.
22///
23/// A dialer can be created from a URI-like string using [`FromStr`]. The
24/// following URI schemes are supported:
25///
26/// - `tcp`: Connect to a TCP address and port pair, like `tcp:127.0.0.1:8080`.
27/// - `unix`: Connect to a Unix socket located on the file system, like
28///   `unix:/path/to/my.sock`. This is only supported on Unix.
29///
30/// The [`Default`] dialer uses the hostname and port specified in each request
31/// as normal.
32///
33/// # Examples
34///
35/// Connect to a Unix socket URI:
36///
37/// ```
38/// use isahc::config::Dialer;
39///
40/// # #[cfg(unix)]
41/// let unix_socket = "unix:/path/to/my.sock".parse::<Dialer>()?;
42/// # Ok::<(), isahc::config::DialerParseError>(())
43/// ```
44#[derive(Clone, Debug)]
45pub struct Dialer(Inner);
46
47#[derive(Clone, Debug, Eq, PartialEq)]
48enum Inner {
49    Default,
50
51    IpSocket(String),
52
53    #[cfg(unix)]
54    UnixSocket(std::path::PathBuf),
55}
56
57impl Dialer {
58    /// Connect to the given IP socket.
59    ///
60    /// Any value that can be converted into a [`SocketAddr`] can be given as an
61    /// argument; check the [`SocketAddr`] documentation for a complete list.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use isahc::config::Dialer;
67    /// use std::net::Ipv4Addr;
68    ///
69    /// let dialer = Dialer::ip_socket((Ipv4Addr::LOCALHOST, 8080));
70    /// ```
71    ///
72    /// ```
73    /// use isahc::config::Dialer;
74    /// use std::net::SocketAddr;
75    ///
76    /// let dialer = Dialer::ip_socket("0.0.0.0:8765".parse::<SocketAddr>()?);
77    /// # Ok::<(), std::net::AddrParseError>(())
78    /// ```
79    pub fn ip_socket(addr: impl Into<SocketAddr>) -> Self {
80        // Create a string in the format CURLOPT_CONNECT_TO expects.
81        Self(Inner::IpSocket(format!("::{}", addr.into())))
82    }
83
84    /// Connect to a Unix socket described by a file.
85    ///
86    /// The path given is not checked ahead of time for correctness or that the
87    /// socket exists. If the socket is invalid an error will be returned when a
88    /// request attempt is made.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use isahc::config::Dialer;
94    ///
95    /// let docker = Dialer::unix_socket("/var/run/docker.sock");
96    /// ```
97    ///
98    /// # Availability
99    ///
100    /// This function is only available on Unix.
101    #[cfg(unix)]
102    pub fn unix_socket(path: impl Into<std::path::PathBuf>) -> Self {
103        Self(Inner::UnixSocket(path.into()))
104    }
105}
106
107impl Default for Dialer {
108    fn default() -> Self {
109        Self(Inner::Default)
110    }
111}
112
113impl From<SocketAddr> for Dialer {
114    fn from(socket_addr: SocketAddr) -> Self {
115        Self::ip_socket(socket_addr)
116    }
117}
118
119impl FromStr for Dialer {
120    type Err = DialerParseError;
121
122    fn from_str(s: &str) -> Result<Self, Self::Err> {
123        if s.starts_with("tcp:") {
124            let addr_str = s[4..].trim_start_matches('/');
125
126            return addr_str
127                .parse::<SocketAddr>()
128                .map(Self::ip_socket)
129                .map_err(|_| DialerParseError(()));
130        }
131
132        #[cfg(unix)]
133        {
134            if s.starts_with("unix:") {
135                // URI paths are always absolute.
136                let mut path = std::path::PathBuf::from("/");
137                path.push(&s[5..].trim_start_matches('/'));
138
139                return Ok(Self(Inner::UnixSocket(path)));
140            }
141        }
142
143        Err(DialerParseError(()))
144    }
145}
146
147impl TryFrom<&'_ str> for Dialer {
148    type Error = DialerParseError;
149
150    fn try_from(str: &str) -> Result<Self, Self::Error> {
151        str.parse()
152    }
153}
154
155impl TryFrom<String> for Dialer {
156    type Error = DialerParseError;
157
158    fn try_from(string: String) -> Result<Self, Self::Error> {
159        string.parse()
160    }
161}
162
163impl TryFrom<Uri> for Dialer {
164    type Error = DialerParseError;
165
166    fn try_from(uri: Uri) -> Result<Self, Self::Error> {
167        // Not the most efficient implementation, but straightforward.
168        uri.to_string().parse()
169    }
170}
171
172impl SetOpt for Dialer {
173    fn set_opt<H>(&self, easy: &mut Easy2<H>) -> Result<(), curl::Error> {
174        let mut connect_to = List::new();
175
176        if let Inner::IpSocket(addr) = &self.0 {
177            connect_to.append(addr)?;
178        }
179
180        easy.connect_to(connect_to)?;
181
182        #[cfg(unix)]
183        easy.unix_socket_path(match &self.0 {
184            Inner::UnixSocket(path) => Some(path),
185            _ => None,
186        })?;
187
188        Ok(())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn parse_tcp_socket_and_port_uri() {
198        let dialer = "tcp:127.0.0.1:1200".parse::<Dialer>().unwrap();
199
200        assert_eq!(dialer.0, Inner::IpSocket("::127.0.0.1:1200".into()));
201    }
202
203    #[test]
204    fn parse_invalid_tcp_uri() {
205        let result = "tcp:127.0.0.1-1200".parse::<Dialer>();
206
207        assert!(result.is_err());
208    }
209
210    #[test]
211    #[cfg(unix)]
212    fn parse_unix_socket_uri() {
213        let dialer = "unix:/path/to/my.sock".parse::<Dialer>().unwrap();
214
215        assert_eq!(dialer.0, Inner::UnixSocket("/path/to/my.sock".into()));
216    }
217
218    #[test]
219    #[cfg(unix)]
220    fn from_unix_socket_uri() {
221        let uri = "unix://path/to/my.sock".parse::<http::Uri>().unwrap();
222        let dialer = Dialer::try_from(uri).unwrap();
223
224        assert_eq!(dialer.0, Inner::UnixSocket("/path/to/my.sock".into()));
225    }
226}