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