Skip to main content

sofie_http/
tls.rs

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
16/// A renewed certificate is served within this long of its files changing.
17const RECHECK_EVERY: Duration = Duration::from_secs(60);
18
19#[derive(Clone)]
20pub struct Tls {
21    pub(crate) acceptor: TlsAcceptor,
22}
23
24impl Tls {
25    /// `certificate` holds the chain, leaf first; `key` a PKCS#8, PKCS#1 or SEC1 key.
26    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    /// Serves HTTPS on the app's address instead of plain HTTP.
43    #[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/// Checks the files' modification times on a handshake at most every `RECHECK_EVERY`,
107/// so no task or runtime is needed and an idle server reads nothing.
108#[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        // Read before the contents, so a write in between is seen by the next check.
123        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    /// A handshake arriving while another checks is served the certificate held.
141    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            // Recorded even if the pair fails, so a broken pair is logged once, not every check.
157            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            // A second panic while a failed test unwinds would abort the run.
230            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}