-
Notifications
You must be signed in to change notification settings - Fork 50
/
https.rs
262 lines (219 loc) · 9.41 KB
/
https.rs
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//! Everything required for setting up HTTPS.
use acme_lib::create_p384_key;
use acme_lib::persist::FilePersist;
use acme_lib::{Directory, DirectoryUrl, Error};
use actix_web::{App, HttpServer};
use std::sync::mpsc;
use std::{
fs::{self, File},
io::BufReader,
path::PathBuf,
};
use crate::errors::AtomicServerResult;
/// Starts an HTTP Actix server for HTTPS certificate initialization
pub async fn cert_init_server(config: &crate::config::Config) -> AtomicServerResult<()> {
let address = format!("{}:{}", config.opts.ip, config.opts.port);
tracing::warn!("Server temporarily running in HTTP mode at {}, running Let's Encrypt Certificate initialization...", address);
let mut well_known_folder = config.static_path.clone();
well_known_folder.push("well-known");
fs::create_dir_all(&well_known_folder)?;
let (tx, rx) = mpsc::channel();
let address_clone = address.clone();
std::thread::spawn(move || {
actix_web::rt::System::new().block_on(async move {
let init_server = HttpServer::new(move || {
App::new().service(
actix_files::Files::new("/.well-known", well_known_folder.clone())
.show_files_listing(),
)
});
let running_server = init_server.bind(&address_clone)?.run();
tx.send(running_server.handle())
.expect("Error sending handle during HTTPS init");
running_server.await
})
});
let handle = rx.recv().expect("Error receiving handle during HTTPS init");
let agent = ureq::builder()
.timeout(std::time::Duration::from_secs(2))
.build();
let well_known_url = format!("http://{}/.well-known/", &config.opts.domain);
tracing::info!("Testing availability of {}", &well_known_url);
let resp = agent
.get(&well_known_url)
.call()
.expect("Unable to send request for Let's Encrypt initialization");
if resp.status() != 200 {
return Err(
"Server for HTTP initialization not available, returning a non-200 status code".into(),
);
} else {
tracing::info!("Server for HTTP initialization running correctly");
}
crate::https::request_cert(config).map_err(|e| format!("Certification init failed: {}", e))?;
tracing::warn!("HTTPS TLS Cert init sucesful! Stopping HTTP server, starting HTTPS...");
handle.stop(true).await;
Ok(())
}
/// Writes keys to disk using LetsEncrypt
pub fn request_cert(config: &crate::config::Config) -> Result<(), Error> {
// Use DirectoryUrl::LetsEncrypStaging for dev/testing.
let url = if config.opts.development {
DirectoryUrl::LetsEncryptStaging
} else {
DirectoryUrl::LetsEncrypt
};
fs::create_dir_all(PathBuf::from(&config.https_path))?;
// Save/load keys and certificates to current dir.
let persist = FilePersist::new(&config.https_path);
// Create a directory entrypoint.
let dir = 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 email = config
.opts
.email
.clone()
.expect("ATOMIC_EMAIL must be set for HTTPS init");
tracing::info!("Requesting Let's Encrypt account with {}", email);
let acc = dir.account(&email)?;
// Order a new TLS certificate for a domain.
let mut ord_new = acc.new_order(&config.opts.domain, &[])?;
// 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 = chall.http_token();
let formatted_path = format!("well-known/acme-challenge/{}", token);
let mut challenge_path = config.static_path.clone();
challenge_path.push(formatted_path);
// The proof is the contents of the file
let proof = chall.http_proof();
tracing::info!("Writing ACME challange to {:?}", challenge_path);
fs::create_dir_all(
PathBuf::from(&challenge_path)
.parent()
.expect("Could not find parent folder"),
)
.expect("Unable to create dirs");
fs::write(challenge_path, proof).expect("Unable to write file");
// Here you must do "something" to place
// the file/contents in the correct place.
// update_my_web_server(&path, &proof);
// 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)?;
// Update the state against the ACME API.
ord_new.refresh()?;
};
// Ownership is proven. Create a private key for
// the certificate. These are provided for convenience, you
// can provide your own keypair instead if you want.
let pkey_pri = 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.
tracing::info!("Downloading certificate...");
let cert = ord_cert.download_and_save_cert()?;
fs::write(&config.cert_path, cert.certificate()).expect("Unable to write file");
fs::write(&config.key_path, cert.private_key()).expect("Unable to write file");
set_certs_created_at_file(config);
tracing::info!("HTTPS init Success!");
Ok(())
}
// RUSTLS
pub fn get_https_config(config: &crate::config::Config) -> Result<rustls::ServerConfig, Error> {
use rustls_pemfile::{certs, pkcs8_private_keys};
let https_config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth();
// rustls::NoClientAuth::new()
let cert_file =
&mut BufReader::new(File::open(config.cert_path.clone()).expect("No HTTPS TLS key found."));
let key_file =
&mut BufReader::new(File::open(&config.key_path).expect("Could not open config key path"));
let mut cert_chain = Vec::new();
for bytes in certs(cert_file)? {
let certificate = rustls::Certificate(bytes);
cert_chain.push(certificate);
}
let mut keys = pkcs8_private_keys(key_file)?;
if keys.is_empty() {
panic!("No key found. Consider deleting the `.https` directory and restart to create new keys.")
}
Ok(https_config
.with_single_cert(cert_chain, rustls::PrivateKey(keys.remove(0)))
.expect("Unable to create HTTPS config from certificates"))
}
fn certs_created_at_path(config: &crate::config::Config) -> PathBuf {
// ~/.config/atomic/https
let mut path = config
.cert_path
.parent()
.unwrap_or_else(|| {
panic!(
"Cannot open parent dit of HTTPS certs {:?}",
config.cert_path
)
})
.to_path_buf();
path.push("certs_created_at");
path
}
/// Adds a file to the .https folder to indicate age of certificates
fn set_certs_created_at_file(config: &crate::config::Config) {
let now_string = chrono::Utc::now();
let path = certs_created_at_path(config);
fs::write(&path, now_string.to_string())
.unwrap_or_else(|_| panic!("Unable to write {:?}", &path));
}
/// Checks if the certificates need to be renewed.
/// Will be true if there are no certs yet.
pub fn should_renew_certs_check(config: &crate::config::Config) -> bool {
if std::fs::File::open(&config.cert_path).is_err() {
return true;
}
let path = certs_created_at_path(config);
let created_at = std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Unable to read {:?}", &path))
.parse::<chrono::DateTime<chrono::Utc>>()
.unwrap_or_else(|_| panic!("failed to parse {:?}", &path));
let certs_age: chrono::Duration = chrono::Utc::now() - created_at;
// Let's Encrypt certificates are valid for three months, but I think renewing earlier provides a better UX.
let expired = certs_age > chrono::Duration::weeks(4);
if expired {
tracing::warn!("HTTPS Certificates expired, requesting new ones...")
// This is where I might need to remove the `.https/` folder, but it seems like it's not necessary
};
expired
}