use cyberbrain_core::{Error, Result};
use rustls::ServerConfig;
use rustls_pki_types::pem::PemObject;
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
use std::path::Path;
use std::sync::{Arc, Once};
const DRAIN: std::time::Duration = std::time::Duration::from_secs(20);
static TLS_PROVIDER: Once = Once::new();
fn install_tls_provider() {
TLS_PROVIDER.call_once(|| {
let _ = rustls::crypto::ring::default_provider().install_default();
});
}
#[derive(Clone)]
pub struct Certificate {
config: Arc<ServerConfig>,
pub fingerprint: String,
pub pinnable: bool,
}
pub fn load(cert: &Path, key: &Path) -> Result<Certificate> {
install_tls_provider();
let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(cert)
.map_err(|e| {
Error::Config(format!(
"cannot read the certificate {}: {e}",
cert.display()
))
})?
.collect::<std::result::Result<_, _>>()
.map_err(|e| {
Error::Config(format!(
"cannot read the certificate {}: {e}",
cert.display()
))
})?;
let leaf = chain.first().cloned().ok_or_else(|| {
Error::Config(format!(
"{} holds no certificate; a PEM file with a private key in it is not the \
certificate, it is the --tls-key",
cert.display()
))
})?;
let key_der = PrivateKeyDer::from_pem_file(key)
.map_err(|e| Error::Config(format!("cannot read the key {}: {e}", key.display())))?;
let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(chain, key_der)
.map_err(|e| {
Error::Config(format!(
"the certificate and the key do not go together ({e}). {} and {} have to be \
the pair that was issued together.",
cert.display(),
key.display()
))
})?;
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Ok(Certificate {
config: Arc::new(config),
fingerprint: fingerprint(&leaf),
pinnable: false,
})
}
pub fn own(dir: &Path, names: &[String]) -> Result<Certificate> {
let (cert, key) = ensure_self_signed(dir, names)?;
Ok(Certificate {
pinnable: true,
..load(&cert, &key)?
})
}
pub fn named(dir: &Path, cert: &Path, key: &Path) -> Result<Certificate> {
Ok(Certificate {
pinnable: is_own_pair(dir, cert, key),
..load(cert, key)?
})
}
pub fn is_own_pair(dir: &Path, cert: &Path, key: &Path) -> bool {
let same = |a: &Path, b: &Path| match (a.canonicalize(), b.canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => a == b,
};
same(cert, &dir.join(OWN_CERT)) && same(key, &dir.join(OWN_KEY))
}
pub fn ensure_self_signed(
dir: &Path,
names: &[String],
) -> Result<(std::path::PathBuf, std::path::PathBuf)> {
let cert_path = dir.join(OWN_CERT);
let key_path = dir.join(OWN_KEY);
if cert_path.exists() && key_path.exists() {
return Ok((cert_path, key_path));
}
if cert_path.exists() != key_path.exists() {
return Err(Error::Config(format!(
"one half of the hub's certificate is missing: {} and {} come as a pair. \
Delete the one that is left to have a new pair made, or put the other one back.",
cert_path.display(),
key_path.display()
)));
}
let mut params = rcgen::CertificateParams::new(names.to_vec())
.map_err(|e| Error::Config(format!("cannot build a certificate for {names:?}: {e}")))?;
params.distinguished_name = rcgen::DistinguishedName::new();
params
.distinguished_name
.push(rcgen::DnType::CommonName, "Cyberbrain Hub");
let key = rcgen::KeyPair::generate()
.map_err(|e| Error::Config(format!("cannot generate a key: {e}")))?;
let cert = params
.self_signed(&key)
.map_err(|e| Error::Config(format!("cannot sign the certificate: {e}")))?;
std::fs::write(&cert_path, cert.pem()).map_err(|e| Error::Io {
path: cert_path.clone(),
source: e,
})?;
write_private(&key_path, &key.serialize_pem()).map_err(|e| Error::Io {
path: key_path.clone(),
source: e,
})?;
Ok((cert_path, key_path))
}
pub fn names_for(addr: &std::net::SocketAddr) -> Vec<String> {
let mut names = vec![
super::page::hostname(),
"localhost".to_string(),
"127.0.0.1".to_string(),
];
if !addr.ip().is_unspecified() && !addr.ip().is_loopback() {
names.push(addr.ip().to_string());
}
names.dedup();
names
}
fn write_private(path: &Path, contents: &str) -> std::io::Result<()> {
use std::io::Write;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
#[cfg(windows)]
{
use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW;
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
let sddl: Vec<u16> = "D:P(A;;FA;;;SY)(A;;FA;;;BA)\0".encode_utf16().collect();
let mut descriptor = std::ptr::null_mut();
let built = unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
1, &mut descriptor,
std::ptr::null_mut(),
)
};
if built != 0 {
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::FromRawHandle;
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Storage::FileSystem::{
CREATE_NEW, CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_GENERIC_WRITE,
};
let attributes = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor,
bInheritHandle: 0,
};
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
FILE_GENERIC_WRITE,
0, &attributes,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error());
}
let mut f = unsafe { std::fs::File::from_raw_handle(handle as _) };
return f.write_all(contents.as_bytes());
}
}
let mut f = opts.open(path)?;
f.write_all(contents.as_bytes())
}
pub const OWN_CERT: &str = "hub-cert.pem";
pub const OWN_KEY: &str = "hub-key.pem";
pub fn fingerprint_of(path: &Path) -> Result<String> {
let leaf = CertificateDer::pem_file_iter(path)
.map_err(|e| Error::Config(format!("cannot read {}: {e}", path.display())))?
.next()
.transpose()
.map_err(|e| Error::Config(format!("cannot read {}: {e}", path.display())))?
.ok_or_else(|| Error::Config(format!("{} holds no certificate", path.display())))?;
Ok(fingerprint(&leaf))
}
pub fn fingerprint(cert: &CertificateDer<'_>) -> String {
use sha2::Digest;
let digest = sha2::Sha256::digest(cert.as_ref());
digest
.iter()
.map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(":")
}
pub async fn serve(
listener: tokio::net::TcpListener,
mut make: axum::extract::connect_info::IntoMakeServiceWithConnectInfo<
axum::Router,
std::net::SocketAddr,
>,
cert: Certificate,
stop: impl std::future::Future<Output = ()> + Send + 'static,
) -> Result<()> {
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::service::TowerToHyperService;
use tower::Service;
let acceptor = tokio_rustls::TlsAcceptor::from(cert.config);
let (alive, mut all_gone) = tokio::sync::mpsc::channel::<()>(1);
let mut stop = std::pin::pin!(stop);
loop {
let (stream, peer) = tokio::select! {
accepted = listener.accept() => match accepted {
Ok(pair) => pair,
Err(e) => {
super::service::log(&format!("accept failed: {e}"));
continue;
}
},
() = &mut stop => break,
};
let svc = match make.call(peer).await {
Ok(svc) => svc,
Err(never) => match never {},
};
let acceptor = acceptor.clone();
let alive = alive.clone();
tokio::spawn(async move {
let _alive = alive;
let tls = match acceptor.accept(stream).await {
Ok(tls) => tls,
Err(e) => {
super::service::log(&format!("TLS handshake with {peer} failed: {e}"));
return;
}
};
let served = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(TokioIo::new(tls), TowerToHyperService::new(svc))
.await;
if let Err(e) = served {
super::service::log(&format!("connection from {peer} ended: {e}"));
}
});
}
super::service::log("stop requested; closing the listener");
drop(listener);
drop(alive);
if tokio::time::timeout(DRAIN, all_gone.recv()).await.is_err() {
super::service::log(&format!(
"still serving after {}s; going anyway",
DRAIN.as_secs()
));
}
Ok(())
}