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}