1use std::{io, net::TcpStream, path::Path, sync::Arc, time::Duration};
7
8use rustls_pki_types::ServerName;
9use weir_core::Durability;
10
11use crate::{ClientError, WeirClient};
12
13#[derive(Debug, Clone)]
18pub struct ClientTlsConfig<'a> {
19 pub client_cert: &'a Path,
21 pub client_key: &'a Path,
23 pub ca_cert: &'a Path,
25 pub server_name: &'a str,
27 pub default_durability: Option<Durability>,
29}
30
31pub type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
33
34impl WeirClient<TlsStream> {
35 pub fn connect_tls(
42 addr: impl std::net::ToSocketAddrs,
43 cfg: ClientTlsConfig<'_>,
44 ) -> Result<Self, ClientError> {
45 let mut roots = rustls::RootCertStore::empty();
47 for c in load_certs(cfg.ca_cert)? {
48 roots
49 .add(c)
50 .map_err(|e| ClientError::Protocol(format!("ca cert: {e}")))?;
51 }
52
53 let certs = load_certs(cfg.client_cert)?;
55 let key = load_key(cfg.client_key)?;
56
57 let tls_config = rustls::ClientConfig::builder_with_provider(Arc::new(
59 rustls::crypto::ring::default_provider(),
60 ))
61 .with_safe_default_protocol_versions()
62 .map_err(|e| ClientError::Protocol(format!("tls versions: {e}")))?
63 .with_root_certificates(roots)
64 .with_client_auth_cert(certs, key)
65 .map_err(|e| ClientError::Protocol(format!("client auth cert: {e}")))?;
66
67 let server_name = ServerName::try_from(cfg.server_name.to_string())
69 .map_err(|e| ClientError::Protocol(format!("server name: {e}")))?;
70
71 let conn = rustls::ClientConnection::new(Arc::new(tls_config), server_name)
73 .map_err(|e| ClientError::Protocol(format!("tls connect: {e}")))?;
74 let sock = TcpStream::connect(addr)?;
75 let stream = rustls::StreamOwned::new(conn, sock);
76
77 Ok(Self::from_parts(stream, cfg.default_durability))
78 }
79
80 pub fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
85 self.stream.sock.set_read_timeout(timeout)
86 }
87
88 pub fn set_write_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
91 self.stream.sock.set_write_timeout(timeout)
92 }
93}
94
95fn load_certs(p: &Path) -> Result<Vec<rustls_pki_types::CertificateDer<'static>>, ClientError> {
98 use rustls_pki_types::pem::PemObject;
99 let pem = std::fs::read(p)?;
103 rustls_pki_types::CertificateDer::pem_slice_iter(&pem)
104 .collect::<Result<_, _>>()
105 .map_err(|e| {
106 ClientError::Protocol(format!("invalid certificate PEM in {}: {e}", p.display()))
107 })
108}
109
110fn load_key(p: &Path) -> Result<rustls_pki_types::PrivateKeyDer<'static>, ClientError> {
111 use rustls_pki_types::pem::PemObject;
112 let pem = std::fs::read(p)?;
113 rustls_pki_types::PrivateKeyDer::from_pem_slice(&pem)
114 .map_err(|e| ClientError::Protocol(format!("no private key in file {}: {e}", p.display())))
115}
116
117#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn tmp_file(label: &str, contents: &[u8]) -> std::path::PathBuf {
131 let p = std::env::temp_dir().join(format!("weir_tls_{label}_{}.pem", std::process::id()));
132 std::fs::write(&p, contents).unwrap();
133 p
134 }
135
136 #[test]
137 fn load_key_missing_file_is_io_error() {
138 let err = load_key(Path::new("/weir_no_such_key_xyzzy.pem")).unwrap_err();
139 assert!(matches!(err, ClientError::Io(_)), "{err:?}");
140 }
141
142 #[test]
143 fn load_certs_missing_file_is_io_error() {
144 let err = load_certs(Path::new("/weir_no_such_cert_xyzzy.pem")).unwrap_err();
145 assert!(matches!(err, ClientError::Io(_)), "{err:?}");
146 }
147
148 #[test]
149 fn load_key_file_without_a_key_is_protocol_error() {
150 let p = tmp_file("nokey", b"not a pem private key\n");
153 let err = load_key(&p).unwrap_err();
154 match err {
155 ClientError::Protocol(msg) => assert!(msg.contains("no private key"), "{msg}"),
156 other => panic!("expected Protocol, got {other:?}"),
157 }
158 std::fs::remove_file(&p).ok();
159 }
160}