taba_client/transport/
mod.rs1use std::io::{self, BufReader, Read, Write};
5use std::net::{TcpStream, ToSocketAddrs};
6use std::sync::{Arc, Mutex};
7use std::time::{Duration, Instant};
8
9use rustls::pki_types::pem::PemObject;
10use rustls::pki_types::{CertificateDer, ServerName};
11use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
12
13use crate::http::{self, ReadError, Request, Response, Url};
14
15#[derive(Debug, Clone, Copy)]
16pub struct Timeouts {
17 pub connect: Duration,
19 pub total: Duration,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum TransportError {
26 Connect(String),
28 Timeout { sent: bool },
30 Io(String),
32 Protocol(String),
34}
35
36pub trait Transport: Send + Sync {
37 fn send(
38 &self,
39 url: &Url,
40 request: &Request,
41 timeouts: &Timeouts,
42 ) -> Result<Response, TransportError>;
43}
44
45pub struct TcpTransport {
47 ca_pem: Option<Vec<u8>>,
48 tls: Mutex<Option<Arc<ClientConfig>>>,
49}
50
51impl TcpTransport {
52 pub fn new(ca_pem: Option<Vec<u8>>) -> Self {
54 TcpTransport {
55 ca_pem,
56 tls: Mutex::new(None),
57 }
58 }
59
60 fn tls_config(&self) -> Result<Arc<ClientConfig>, TransportError> {
61 let mut cached = self.tls.lock().unwrap_or_else(|e| e.into_inner());
62 if let Some(config) = cached.as_ref() {
63 return Ok(config.clone());
64 }
65 let config =
66 Arc::new(build_tls_config(self.ca_pem.as_deref()).map_err(TransportError::Connect)?);
67 *cached = Some(config.clone());
68 Ok(config)
69 }
70}
71
72pub fn build_tls_config(ca_pem: Option<&[u8]>) -> Result<ClientConfig, String> {
75 let mut roots = RootCertStore::empty();
76 for cert in rustls_native_certs::load_native_certs().certs {
77 let _ = roots.add(cert);
78 }
79 if let Some(pem) = ca_pem {
80 let mut found = 0;
81 for cert in CertificateDer::pem_slice_iter(pem) {
82 let cert = cert.map_err(|e| format!("invalid CA certificate: {e}"))?;
83 roots
84 .add(cert)
85 .map_err(|e| format!("CA certificate refused: {e}"))?;
86 found += 1;
87 }
88 if found == 0 {
89 return Err("the CA file has no PEM certificate".into());
90 }
91 }
92 let provider = Arc::new(rustls::crypto::ring::default_provider());
93 let builder = ClientConfig::builder_with_provider(provider)
94 .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
95 .map_err(|e| e.to_string())?;
96 Ok(builder.with_root_certificates(roots).with_no_client_auth())
97}
98
99struct Deadlined {
101 sock: TcpStream,
102 deadline: Instant,
103}
104
105impl Deadlined {
106 fn remaining(&self) -> io::Result<Duration> {
107 let left = self.deadline.saturating_duration_since(Instant::now());
108 if left.is_zero() {
109 Err(io::ErrorKind::TimedOut.into())
110 } else {
111 Ok(left)
112 }
113 }
114}
115
116impl Read for Deadlined {
117 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
118 self.sock.set_read_timeout(Some(self.remaining()?))?;
119 self.sock.read(buf)
120 }
121}
122
123impl Write for Deadlined {
124 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
125 self.sock.set_write_timeout(Some(self.remaining()?))?;
126 self.sock.write(buf)
127 }
128
129 fn flush(&mut self) -> io::Result<()> {
130 self.sock.flush()
131 }
132}
133
134fn is_timeout(e: &io::Error) -> bool {
135 matches!(
136 e.kind(),
137 io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock
138 )
139}
140
141fn connect(url: &Url, timeouts: &Timeouts) -> Result<TcpStream, TransportError> {
142 let addrs = (url.host.as_str(), url.port)
143 .to_socket_addrs()
144 .map_err(|e| TransportError::Connect(format!("could not find {}: {e}", url.host)))?;
145 let mut last = None;
146 for addr in addrs {
147 match TcpStream::connect_timeout(&addr, timeouts.connect) {
148 Ok(sock) => {
149 let _ = sock.set_nodelay(true);
150 return Ok(sock);
151 }
152 Err(e) if is_timeout(&e) => return Err(TransportError::Timeout { sent: false }),
153 Err(e) => last = Some(e),
154 }
155 }
156 Err(TransportError::Connect(match last {
157 Some(e) => format!("could not connect to {}:{}: {e}", url.host, url.port),
158 None => format!("{} has no address", url.host),
159 }))
160}
161
162impl Transport for TcpTransport {
163 fn send(
164 &self,
165 url: &Url,
166 request: &Request,
167 timeouts: &Timeouts,
168 ) -> Result<Response, TransportError> {
169 let sock = connect(url, timeouts)?;
170 let mut stream = Deadlined {
171 sock,
172 deadline: Instant::now() + timeouts.total,
173 };
174 let bytes = http::encode_request(&url.host_header(), request);
175 if url.https {
176 let name = ServerName::try_from(url.host.clone()).map_err(|_| {
177 TransportError::Connect(format!("`{}` is not a valid server name", url.host))
178 })?;
179 let mut conn = ClientConnection::new(self.tls_config()?, name)
180 .map_err(|e| TransportError::Connect(e.to_string()))?;
181 while conn.is_handshaking() {
183 conn.complete_io(&mut stream).map_err(|e| {
184 if is_timeout(&e) {
185 TransportError::Timeout { sent: false }
186 } else {
187 TransportError::Connect(format!("TLS failure: {e}"))
188 }
189 })?;
190 }
191 exchange(StreamOwned::new(conn, stream), &bytes)
192 } else {
193 exchange(stream, &bytes)
194 }
195 }
196}
197
198fn exchange<S: Read + Write>(mut stream: S, bytes: &[u8]) -> Result<Response, TransportError> {
199 if let Err(e) = stream.write_all(bytes).and_then(|_| stream.flush()) {
200 return Err(if is_timeout(&e) {
201 TransportError::Timeout { sent: true }
202 } else {
203 TransportError::Io(format!("failed to send: {e}"))
204 });
205 }
206 let mut reader = BufReader::new(stream);
207 match http::read_response(&mut reader) {
208 Ok(response) => Ok(response),
209 Err(ReadError::Io { error, .. }) if is_timeout(&error) => {
210 Err(TransportError::Timeout { sent: true })
211 }
212 Err(ReadError::Io { error, .. }) => Err(TransportError::Io(format!(
213 "failed to read the response: {error}"
214 ))),
215 Err(ReadError::Malformed(why)) => Err(TransportError::Protocol(why.to_owned())),
216 }
217}
218
219#[cfg(test)]
220mod tests;