1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
//! A very simple crate to use `letsencrypt.org` to serve an encrypted
//! website using warp.
use futures::channel::oneshot;
use warp::Filter;
/// Run forever on the current thread, serving using TLS to serve on the given domain.
///
/// This function accepts a single [`warp::Filter`](warp::Filter)
/// which is the site to host. `lets_encrypt` requires the capability
/// to serve port 80 and port 443. It obtains TLS credentials from
/// `letsencrypt.org` and then serves up the site on port 443. It
/// also serves redirects on port 80. Errors are reported on stderr.
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);
// Use DirectoryUrl::LetsEncrypStaging for dev/testing.
let url = acme_lib::DirectoryUrl::LetsEncrypt;
// Save/load keys and certificates to current dir.
let persist = acme_lib::persist::FilePersist::new(".");
// Create a directory entrypoint.
let dir = acme_lib::Directory::from_url(persist, url)?;
// Reads the private account key from persistence, or
// creates a new one before accessing the API to establish
// that it's there.
let acc = dir.account(email)?;
// Order a new TLS certificate for a domain.
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()
{
// If the ownership of the domain(s) have already been authorized
// in a previous order, you might be able to skip validation. The
// ACME API provider decides.
let ord_csr = loop {
// are we done?
if let Some(ord_csr) = ord_new.confirm_validations() {
break ord_csr;
}
// Get the possible authorizations (for a single domain
// this will only be one element).
let auths = ord_new.authorizations()?;
// For HTTP, the challenge is a text file that needs to
// be placed in your web server's root:
//
// /var/www/.well-known/acme-challenge/<token>
//
// The important thing is that it's accessible over the
// web for the domain(s) you are trying to get a
// certificate for:
//
// http://mydomain.io/.well-known/acme-challenge/<token>
let chall = auths[0].http_challenge();
// The token is the filename.
// let token: &'static str =
// Box::leak(chall.http_token().to_string().into_boxed_str());
// The proof is the contents of the file
let proof = chall.http_proof();
// Here you must do "something" to place
// the file/contents in the correct place.
// update_my_web_server(&path, &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
});
// After the file is accessible from the web, the calls
// this to tell the ACME API to start checking the
// existence of the proof.
//
// The order at ACME will change status to either
// confirm ownership of the domain, or fail due to the
// not finding the proof. To see the change, we poll
// the API with 5000 milliseconds wait between.
chall.validate(5000)?;
tx80.send(()).unwrap(); // Now stop the server on port 80
// Update the state against the ACME API.
ord_new.refresh()?;
};
// Ownership is proven. Create a private/public key pair for the
// certificate. These are provided for convenience, you can
// provide your own keypair instead if you want.
let pkey_pri = acme_lib::create_p384_key();
// Submit the CSR. This causes the ACME provider to enter a state
// of "processing" that must be polled until the certificate is
// either issued or rejected. Again we poll for the status change.
let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?;
// Now download the certificate. Also stores the cert in the
// persistence.
let cert = ord_cert.download_and_save_cert()?;
std::fs::write(&pem_name, cert.certificate())?;
std::fs::write(&key_name, cert.private_key())?;
}
// Now we have working keys, let us use them!
let (tx80, rx80) = oneshot::channel();
{
// First start the redirecting from port 80 to port 443.
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();
{
// Now start our actual site.
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,
);
}
// Now wait until it is time to grab a new certificate.
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; // FIXME very hokey!
} else if let Some(time_to_renew) = time_to_expiration(&pem_name) {
// Presumably we already failed to renew, so let's
// just keep using our current certificate as long
// as we can!
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)); // FIXME very hokey!
} 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()
}