use futures::channel::oneshot;
use warp::Filter;
pub async fn lets_encrypt<F>(service: F, email: &str, domain: &str) -> Result<(), acme_lib::Error>
where
F: warp::Filter<Error = warp::Rejection> + Send + Sync + 'static,
F::Extract: warp::reply::Reply,
F: Clone,
{
let domain = domain.to_string();
let pem_name = format!("{}.pem", domain);
let key_name = format!("{}.key", domain);
let url = acme_lib::DirectoryUrl::LetsEncrypt;
let persist = acme_lib::persist::FilePersist::new(".");
let dir = acme_lib::Directory::from_url(persist, url)?;
let acc = dir.account(email)?;
let mut ord_new = acc.new_order(&domain, &[])?;
loop {
const TMIN: std::time::Duration = std::time::Duration::from_secs(60 * 60 * 24 * 30);
println!(
"The time to expiration of {:?} is {:?}",
pem_name,
time_to_expiration(&pem_name)
);
if time_to_expiration(&pem_name)
.filter(|&t| t > TMIN)
.is_none()
{
let ord_csr = loop {
if let Some(ord_csr) = ord_new.confirm_validations() {
break ord_csr;
}
let auths = ord_new.authorizations()?;
let chall = auths[0].http_challenge();
let proof = chall.http_proof();
let domain = domain.to_string();
use std::str::FromStr;
let token =
warp::path!(".well-known" / "acme-challenge" / String).map(move |_token| {
proof.clone()
});
let redirect = warp::path::tail().map(move |path: warp::path::Tail| {
println!("redirecting to https://{}/{}", domain, path.as_str());
warp::redirect::redirect(
warp::http::Uri::from_str(&format!(
"https://{}/{}",
&domain,
path.as_str()
))
.expect("problem with uri?"),
)
});
let (tx80, rx80) = oneshot::channel();
tokio::task::spawn(async move {
println!("Am serving on port 80!");
warp::serve(token.or(redirect))
.bind_with_graceful_shutdown(([0, 0, 0, 0], 80), async {
rx80.await.ok();
})
.1
.await
});
chall.validate(5000)?;
tx80.send(()).unwrap();
ord_new.refresh()?;
};
let pkey_pri = acme_lib::create_p384_key();
let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?;
let cert = ord_cert.download_and_save_cert()?;
std::fs::write(&pem_name, cert.certificate())?;
std::fs::write(&key_name, cert.private_key())?;
}
let (tx80, rx80) = oneshot::channel();
{
let domain = domain.to_string();
use std::str::FromStr;
let redirect = warp::path::tail().map(move |path: warp::path::Tail| {
println!("redirecting to https://{}/{}", domain, path.as_str());
warp::redirect::redirect(
warp::http::Uri::from_str(&format!("https://{}/{}", &domain, path.as_str()))
.expect("problem with uri?"),
)
});
tokio::task::spawn(
warp::serve(redirect)
.bind_with_graceful_shutdown(([0, 0, 0, 0], 80), async {
rx80.await.ok();
})
.1,
);
}
let (tx, rx) = oneshot::channel();
{
let service = service.clone();
let key_name = key_name.clone();
let pem_name = pem_name.clone();
tokio::spawn(
warp::serve(service)
.tls()
.cert_path(&pem_name)
.key_path(&key_name)
.bind_with_graceful_shutdown(([0, 0, 0, 0], 443), async {
rx.await.ok();
})
.1,
);
}
if let Some(time_to_renew) = time_to_expiration(&pem_name).and_then(|x| x.checked_sub(TMIN))
{
println!("Sleeping for {:?} before renewing", time_to_renew);
tokio::time::sleep(time_to_renew).await;
println!("Now it is time to renew!");
tx.send(()).unwrap();
tx80.send(()).unwrap();
tokio::time::sleep(std::time::Duration::from_secs(1)).await; } else if let Some(time_to_renew) = time_to_expiration(&pem_name) {
println!("Sleeping for {:?} before renewing", time_to_renew);
std::thread::sleep(time_to_renew);
println!("Now it is time to renew!");
tx.send(()).unwrap();
tx80.send(()).unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); } else {
println!("Uh oh... looks like we already are at our limit?");
println!("Waiting an hour before trying again...");
std::thread::sleep(std::time::Duration::from_secs(60 * 60));
}
}
}
fn time_to_expiration<P: AsRef<std::path::Path>>(p: P) -> Option<std::time::Duration> {
let file = std::fs::File::open(p).ok()?;
x509_parser::pem::Pem::read(std::io::BufReader::new(file))
.ok()?
.0
.parse_x509()
.ok()?
.tbs_certificate
.validity
.time_to_expiration()
}