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