1use std::path::{Path, PathBuf};
2use std::sync::{Arc, Mutex, RwLock, TryLockError};
3use std::time::{Duration, Instant, SystemTime};
4
5use rustls::ServerConfig;
6use rustls::crypto::CryptoProvider;
7use rustls::pki_types::pem::PemObject;
8use rustls::pki_types::{CertificateDer, PrivateKeyDer};
9use rustls::server::{ClientHello, ResolvesServerCert};
10use rustls::sign::CertifiedKey;
11use sofie_core::{Error, Result, unpoisoned};
12use tokio_rustls::TlsAcceptor;
13
14use crate::Serving;
15
16const RECHECK_EVERY: Duration = Duration::from_secs(60);
18
19#[derive(Clone)]
20pub struct Tls {
21 pub(crate) acceptor: TlsAcceptor,
22}
23
24impl Tls {
25 pub fn from_pem_files(certificate: &Path, key: &Path) -> Result<Self> {
27 let provider = Arc::new(rustls::crypto::ring::default_provider());
28 let reloading = Reloading::load(certificate, key, Arc::clone(&provider))?;
29 let mut config = ServerConfig::builder_with_provider(provider)
30 .with_safe_default_protocol_versions()
31 .map_err(|error| Error::internal("choosing the TLS protocol versions", error))?
32 .with_no_client_auth()
33 .with_cert_resolver(Arc::new(reloading));
34 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
35 Ok(Self {
36 acceptor: TlsAcceptor::from(Arc::new(config)),
37 })
38 }
39}
40
41impl Serving {
42 #[must_use]
44 pub fn tls(mut self, tls: Tls) -> Self {
45 self.tls = Some(tls);
46 self
47 }
48}
49
50#[derive(Debug)]
51struct PemFiles {
52 certificate: PathBuf,
53 key: PathBuf,
54 provider: Arc<CryptoProvider>,
55}
56
57type Modified = (SystemTime, SystemTime);
58
59impl PemFiles {
60 fn modified(&self) -> Result<Modified> {
61 Ok((modified(&self.certificate)?, modified(&self.key)?))
62 }
63
64 fn certified(&self) -> Result<CertifiedKey> {
65 let chain = CertificateDer::pem_file_iter(&self.certificate)
66 .and_then(Iterator::collect::<std::result::Result<Vec<_>, _>>)
67 .map_err(|error| {
68 Error::internal(
69 format!("reading certificates from {}", self.certificate.display()),
70 error,
71 )
72 })?;
73 if chain.is_empty() {
74 return Err(Error::internal_message(format!(
75 "{} holds no certificate",
76 self.certificate.display()
77 )));
78 }
79 let key = PrivateKeyDer::from_pem_file(&self.key).map_err(|error| {
80 Error::internal(
81 format!("reading the private key from {}", self.key.display()),
82 error,
83 )
84 })?;
85 CertifiedKey::from_der(chain, key, &self.provider).map_err(|error| {
86 Error::internal(
87 format!("pairing {} with its key", self.certificate.display()),
88 error,
89 )
90 })
91 }
92}
93
94fn modified(path: &Path) -> Result<SystemTime> {
95 std::fs::metadata(path)
96 .and_then(|metadata| metadata.modified())
97 .map_err(|error| Error::internal(format!("reading when {} changed", path.display()), error))
98}
99
100#[derive(Debug)]
101struct Watch {
102 modified: Modified,
103 next_check: Instant,
104}
105
106#[derive(Debug)]
109struct Reloading {
110 files: PemFiles,
111 current: RwLock<Arc<CertifiedKey>>,
112 watch: Mutex<Watch>,
113}
114
115impl Reloading {
116 fn load(certificate: &Path, key: &Path, provider: Arc<CryptoProvider>) -> Result<Self> {
117 let files = PemFiles {
118 certificate: certificate.to_owned(),
119 key: key.to_owned(),
120 provider,
121 };
122 let modified = files.modified()?;
124 let certified = files.certified()?;
125 Ok(Self {
126 files,
127 current: RwLock::new(Arc::new(certified)),
128 watch: Mutex::new(Watch {
129 modified,
130 next_check: Instant::now() + RECHECK_EVERY,
131 }),
132 })
133 }
134
135 fn current(&self) -> Arc<CertifiedKey> {
136 self.reload_if_changed();
137 Arc::clone(&unpoisoned::read(&self.current))
138 }
139
140 fn reload_if_changed(&self) {
142 let mut watch = match self.watch.try_lock() {
143 Ok(watch) => watch,
144 Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
145 Err(TryLockError::WouldBlock) => return,
146 };
147 let now = Instant::now();
148 if now < watch.next_check {
149 return;
150 }
151 watch.next_check = now + RECHECK_EVERY;
152 let reloaded = self.files.modified().and_then(|modified| {
153 if modified == watch.modified {
154 return Ok(None);
155 }
156 watch.modified = modified;
158 self.files.certified().map(Some)
159 });
160 match reloaded {
161 Ok(None) => {}
162 Ok(Some(certified)) => {
163 *unpoisoned::write(&self.current) = Arc::new(certified);
164 tracing::info!(tls.certificate = %self.files.certificate.display(), "reloaded the TLS certificate");
165 }
166 Err(error) => {
167 tracing::error!(error = %error, tls.certificate = %self.files.certificate.display(), tls.key = %self.files.key.display(), "reloading the TLS certificate failed; serving the one held");
168 }
169 }
170 }
171}
172
173impl ResolvesServerCert for Reloading {
174 fn resolve(&self, _: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
175 Some(self.current())
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use std::fs::File;
182
183 use super::*;
184
185 const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
186
187 struct Copied {
188 directory: PathBuf,
189 reloading: Reloading,
190 }
191
192 impl Copied {
193 fn of(test: &str) -> Self {
194 let directory =
195 std::env::temp_dir().join(format!("sofie-http-tls-{test}-{}", std::process::id()));
196 std::fs::create_dir_all(&directory).unwrap();
197 for name in ["localhost.crt", "localhost.key"] {
198 std::fs::copy(Path::new(FIXTURES).join(name), directory.join(name)).unwrap();
199 }
200 let reloading = Reloading::load(
201 &directory.join("localhost.crt"),
202 &directory.join("localhost.key"),
203 Arc::new(rustls::crypto::ring::default_provider()),
204 )
205 .unwrap();
206 Self {
207 directory,
208 reloading,
209 }
210 }
211
212 fn replace_key(&self, fixture: &str) {
213 let key = self.directory.join("localhost.key");
214 std::fs::copy(Path::new(FIXTURES).join(fixture), &key).unwrap();
215 let later = self.reloading.watch.lock().unwrap().modified.1 + Duration::from_secs(1);
216 File::options()
217 .write(true)
218 .open(&key)
219 .unwrap()
220 .set_modified(later)
221 .unwrap();
222 self.reloading.watch.lock().unwrap().next_check = Instant::now();
223 }
224 }
225
226 impl Drop for Copied {
227 fn drop(&mut self) {
228 let removed = std::fs::remove_dir_all(&self.directory);
229 if !std::thread::panicking() {
231 removed.unwrap();
232 }
233 }
234 }
235
236 #[test]
237 fn changed_files_are_served_from_the_next_check() {
238 let copied = Copied::of("changed");
239 let before = copied.reloading.current();
240
241 copied.replace_key("localhost.key");
242
243 assert!(!Arc::ptr_eq(&before, &copied.reloading.current()));
244 }
245
246 #[test]
247 fn unchanged_files_are_not_read_again() {
248 let copied = Copied::of("unchanged");
249 let before = copied.reloading.current();
250
251 copied.reloading.watch.lock().unwrap().next_check = Instant::now();
252
253 assert!(Arc::ptr_eq(&before, &copied.reloading.current()));
254 }
255
256 #[test]
257 fn a_failed_reload_keeps_serving_the_certificate_held() {
258 let copied = Copied::of("failed");
259 let before = copied.reloading.current();
260
261 copied.replace_key("other.key");
262
263 assert!(Arc::ptr_eq(&before, &copied.reloading.current()));
264 }
265}