itsi-server 0.2.25 → 0.2.27.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +939 -987
- data/Cargo.toml +0 -1
- data/Rakefile +18 -5
- data/ext/itsi_acme/Cargo.toml +2 -1
- data/ext/itsi_acme/src/acceptor.rs +1 -1
- data/ext/itsi_acme/src/acme.rs +31 -3
- data/ext/itsi_acme/src/http_challenge.rs +81 -0
- data/ext/itsi_acme/src/https_helper.rs +3 -1
- data/ext/itsi_acme/src/jose.rs +6 -2
- data/ext/itsi_acme/src/lib.rs +2 -0
- data/ext/itsi_acme/src/resolver.rs +27 -4
- data/ext/itsi_acme/src/state.rs +183 -22
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +115 -64
- data/ext/itsi_scheduler/src/lib.rs +2 -1
- data/ext/itsi_server/Cargo.lock +2 -2
- data/ext/itsi_server/Cargo.toml +2 -1
- data/ext/itsi_server/src/lib.rs +15 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +9 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +95 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +22 -1
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +100 -0
- data/ext/itsi_server/src/server/binds/listener.rs +9 -24
- data/ext/itsi_server/src/server/binds/tls.rs +372 -67
- data/ext/itsi_server/src/services/itsi_http_service.rs +46 -2
- data/lib/itsi/http_request.rb +10 -0
- data/lib/itsi/server/rack_interface.rb +45 -2
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +24 -0
- metadata +3 -20
- data/vendor/rb-sys-build/.cargo-ok +0 -1
- data/vendor/rb-sys-build/.cargo_vcs_info.json +0 -6
- data/vendor/rb-sys-build/Cargo.lock +0 -294
- data/vendor/rb-sys-build/Cargo.toml +0 -71
- data/vendor/rb-sys-build/Cargo.toml.orig +0 -32
- data/vendor/rb-sys-build/LICENSE-APACHE +0 -190
- data/vendor/rb-sys-build/LICENSE-MIT +0 -21
- data/vendor/rb-sys-build/src/bindings/sanitizer.rs +0 -185
- data/vendor/rb-sys-build/src/bindings/stable_api.rs +0 -247
- data/vendor/rb-sys-build/src/bindings/wrapper.h +0 -71
- data/vendor/rb-sys-build/src/bindings.rs +0 -280
- data/vendor/rb-sys-build/src/cc.rs +0 -421
- data/vendor/rb-sys-build/src/lib.rs +0 -12
- data/vendor/rb-sys-build/src/rb_config/flags.rs +0 -101
- data/vendor/rb-sys-build/src/rb_config/library.rs +0 -132
- data/vendor/rb-sys-build/src/rb_config/search_path.rs +0 -57
- data/vendor/rb-sys-build/src/rb_config.rs +0 -906
- data/vendor/rb-sys-build/src/utils.rs +0 -53
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
use base64::{engine::general_purpose, Engine as _};
|
|
2
|
-
use itsi_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
|
|
2
|
+
use itsi_acme::{AcmeAcceptor, AcmeConfig, AcmeState, Http01Handler, ResolvesServerCertAcme};
|
|
3
3
|
use itsi_error::Result;
|
|
4
|
-
use itsi_tracing::info;
|
|
4
|
+
use itsi_tracing::{error, info};
|
|
5
5
|
use locked_dir_cache::LockedDirCache;
|
|
6
|
+
use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock};
|
|
6
7
|
use rcgen::ExtendedKeyUsagePurpose;
|
|
7
8
|
use rcgen::{
|
|
8
9
|
BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, KeyUsagePurpose,
|
|
@@ -17,9 +18,14 @@ use std::{
|
|
|
17
18
|
collections::HashMap,
|
|
18
19
|
fs,
|
|
19
20
|
io::{BufReader, Error},
|
|
20
|
-
sync::
|
|
21
|
+
sync::{
|
|
22
|
+
atomic::{AtomicBool, Ordering},
|
|
23
|
+
Arc,
|
|
24
|
+
},
|
|
25
|
+
thread::JoinHandle,
|
|
21
26
|
};
|
|
22
|
-
use tokio::
|
|
27
|
+
use tokio::runtime::Builder as RuntimeBuilder;
|
|
28
|
+
use tokio::sync::{mpsc, watch};
|
|
23
29
|
use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
|
|
24
30
|
|
|
25
31
|
use crate::env::{
|
|
@@ -29,14 +35,300 @@ use crate::env::{
|
|
|
29
35
|
|
|
30
36
|
mod locked_dir_cache;
|
|
31
37
|
|
|
38
|
+
#[derive(Debug, Clone)]
|
|
39
|
+
pub struct ManagedTlsDomainStatus {
|
|
40
|
+
pub domain: String,
|
|
41
|
+
pub status: String,
|
|
42
|
+
pub last_error: Option<String>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Clone)]
|
|
46
|
+
struct DynamicAcmeConfigTemplate {
|
|
47
|
+
client_config: Arc<ClientConfig>,
|
|
48
|
+
directory_url: String,
|
|
49
|
+
contact: Vec<String>,
|
|
50
|
+
cache_dir: String,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl DynamicAcmeConfigTemplate {
|
|
54
|
+
fn state_for_domain(
|
|
55
|
+
&self,
|
|
56
|
+
domain: &str,
|
|
57
|
+
resolver: Arc<ResolvesServerCertAcme>,
|
|
58
|
+
http01_handler: Arc<Http01Handler>,
|
|
59
|
+
http01_enabled: bool,
|
|
60
|
+
) -> AcmeState<Error> {
|
|
61
|
+
let state = AcmeConfig::new([domain])
|
|
62
|
+
.contact(self.contact.clone())
|
|
63
|
+
.cache(LockedDirCache::new(self.cache_dir.clone()))
|
|
64
|
+
.directory(&self.directory_url)
|
|
65
|
+
.client_tls_config(self.client_config.clone());
|
|
66
|
+
let mut state = AcmeState::new_with_resolver(
|
|
67
|
+
state,
|
|
68
|
+
resolver,
|
|
69
|
+
http01_handler,
|
|
70
|
+
Some(domain.to_string()),
|
|
71
|
+
);
|
|
72
|
+
state.set_http01_enabled(http01_enabled);
|
|
73
|
+
state
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
enum DynamicAcmeCommand {
|
|
78
|
+
Register(String),
|
|
79
|
+
Unregister(String),
|
|
80
|
+
Shutdown,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[derive(Clone)]
|
|
84
|
+
pub struct DynamicAcmeManager {
|
|
85
|
+
inner: Arc<DynamicAcmeManagerInner>,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
struct DynamicAcmeManagerInner {
|
|
89
|
+
resolver: Arc<ResolvesServerCertAcme>,
|
|
90
|
+
http01_registry: Arc<ParkingRwLock<HashMap<String, Arc<Http01Handler>>>>,
|
|
91
|
+
statuses: Arc<ParkingRwLock<HashMap<String, ManagedTlsDomainStatus>>>,
|
|
92
|
+
http01_enabled: Arc<AtomicBool>,
|
|
93
|
+
initialized: AtomicBool,
|
|
94
|
+
initial_domains: Vec<String>,
|
|
95
|
+
command_tx: mpsc::UnboundedSender<DynamicAcmeCommand>,
|
|
96
|
+
thread_handle: ParkingMutex<Option<JoinHandle<()>>>,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
impl DynamicAcmeManager {
|
|
100
|
+
fn new(template: DynamicAcmeConfigTemplate, initial_domains: Vec<String>) -> Self {
|
|
101
|
+
let resolver = ResolvesServerCertAcme::new();
|
|
102
|
+
let http01_handler = Arc::new(Http01Handler::new());
|
|
103
|
+
let http01_registry = Arc::new(ParkingRwLock::new(HashMap::new()));
|
|
104
|
+
let statuses = Arc::new(ParkingRwLock::new(HashMap::new()));
|
|
105
|
+
let http01_enabled = Arc::new(AtomicBool::new(false));
|
|
106
|
+
let (command_tx, mut command_rx) = mpsc::unbounded_channel();
|
|
107
|
+
|
|
108
|
+
let resolver_clone = resolver.clone();
|
|
109
|
+
let http01_handler_clone = http01_handler.clone();
|
|
110
|
+
let http01_registry_clone = http01_registry.clone();
|
|
111
|
+
let statuses_clone = statuses.clone();
|
|
112
|
+
let http01_enabled_clone = http01_enabled.clone();
|
|
113
|
+
|
|
114
|
+
let thread_handle = std::thread::spawn(move || {
|
|
115
|
+
let runtime = RuntimeBuilder::new_current_thread()
|
|
116
|
+
.enable_all()
|
|
117
|
+
.build()
|
|
118
|
+
.expect("Failed to build dynamic ACME runtime");
|
|
119
|
+
runtime.block_on(async move {
|
|
120
|
+
let mut cancellations: HashMap<String, watch::Sender<bool>> = HashMap::new();
|
|
121
|
+
|
|
122
|
+
while let Some(command) = command_rx.recv().await {
|
|
123
|
+
match command {
|
|
124
|
+
DynamicAcmeCommand::Register(domain) => {
|
|
125
|
+
let domain = domain.to_ascii_lowercase();
|
|
126
|
+
if cancellations.contains_key(&domain) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
statuses_clone.write().insert(
|
|
131
|
+
domain.clone(),
|
|
132
|
+
ManagedTlsDomainStatus {
|
|
133
|
+
domain: domain.clone(),
|
|
134
|
+
status: "pending".to_string(),
|
|
135
|
+
last_error: None,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
http01_registry_clone
|
|
139
|
+
.write()
|
|
140
|
+
.insert(domain.clone(), http01_handler_clone.clone());
|
|
141
|
+
|
|
142
|
+
let (cancel_tx, mut cancel_rx) = watch::channel(false);
|
|
143
|
+
cancellations.insert(domain.clone(), cancel_tx);
|
|
144
|
+
|
|
145
|
+
let resolver = resolver_clone.clone();
|
|
146
|
+
let http01_handler = http01_handler_clone.clone();
|
|
147
|
+
let registry = http01_registry_clone.clone();
|
|
148
|
+
let statuses = statuses_clone.clone();
|
|
149
|
+
let template = template.clone();
|
|
150
|
+
let enabled = http01_enabled_clone.clone();
|
|
151
|
+
let task_domain = domain.clone();
|
|
152
|
+
|
|
153
|
+
tokio::spawn(async move {
|
|
154
|
+
statuses.write().insert(
|
|
155
|
+
task_domain.clone(),
|
|
156
|
+
ManagedTlsDomainStatus {
|
|
157
|
+
domain: task_domain.clone(),
|
|
158
|
+
status: "issuing".to_string(),
|
|
159
|
+
last_error: None,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
let mut state = template.state_for_domain(
|
|
163
|
+
&task_domain,
|
|
164
|
+
resolver.clone(),
|
|
165
|
+
http01_handler,
|
|
166
|
+
enabled.load(Ordering::SeqCst),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
loop {
|
|
170
|
+
tokio::select! {
|
|
171
|
+
changed = cancel_rx.changed() => {
|
|
172
|
+
if changed.is_ok() && *cancel_rx.borrow() {
|
|
173
|
+
resolver.remove_auth_key(&task_domain);
|
|
174
|
+
resolver.remove_cert_for_domain(&task_domain);
|
|
175
|
+
registry.write().remove(&task_domain);
|
|
176
|
+
statuses.write().remove(&task_domain);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
event = futures::StreamExt::next(&mut state) => {
|
|
181
|
+
match event {
|
|
182
|
+
Some(Ok(_)) => {
|
|
183
|
+
let mut statuses = statuses.write();
|
|
184
|
+
if let Some(status) = statuses.get_mut(&task_domain) {
|
|
185
|
+
status.status = "active".to_string();
|
|
186
|
+
status.last_error = None;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
Some(Err(err)) => {
|
|
190
|
+
let mut statuses = statuses.write();
|
|
191
|
+
if let Some(status) = statuses.get_mut(&task_domain) {
|
|
192
|
+
status.status = "failed".to_string();
|
|
193
|
+
status.last_error = Some(err.to_string());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
None => break,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
DynamicAcmeCommand::Unregister(domain) => {
|
|
204
|
+
let domain = domain.to_ascii_lowercase();
|
|
205
|
+
if let Some(cancel_tx) = cancellations.remove(&domain) {
|
|
206
|
+
let _ = cancel_tx.send(true);
|
|
207
|
+
} else {
|
|
208
|
+
http01_registry_clone.write().remove(&domain);
|
|
209
|
+
resolver_clone.remove_auth_key(&domain);
|
|
210
|
+
resolver_clone.remove_cert_for_domain(&domain);
|
|
211
|
+
statuses_clone.write().remove(&domain);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
DynamicAcmeCommand::Shutdown => {
|
|
215
|
+
for (_, cancel_tx) in cancellations.drain() {
|
|
216
|
+
let _ = cancel_tx.send(true);
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
Self {
|
|
226
|
+
inner: Arc::new(DynamicAcmeManagerInner {
|
|
227
|
+
resolver,
|
|
228
|
+
http01_registry,
|
|
229
|
+
statuses,
|
|
230
|
+
http01_enabled,
|
|
231
|
+
initialized: AtomicBool::new(false),
|
|
232
|
+
initial_domains,
|
|
233
|
+
command_tx,
|
|
234
|
+
thread_handle: ParkingMutex::new(Some(thread_handle)),
|
|
235
|
+
}),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
pub fn resolver(&self) -> Arc<ResolvesServerCertAcme> {
|
|
240
|
+
self.inner.resolver.clone()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
pub fn set_http01_enabled(&self, enabled: bool) {
|
|
244
|
+
self.inner.http01_enabled.store(enabled, Ordering::SeqCst);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
pub fn initialize_domains(&self) {
|
|
248
|
+
if self.inner.initialized.swap(true, Ordering::SeqCst) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for domain in &self.inner.initial_domains {
|
|
253
|
+
self.register_domain(domain.clone());
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
pub fn register_domain(&self, domain: String) {
|
|
258
|
+
let _ = self
|
|
259
|
+
.inner
|
|
260
|
+
.command_tx
|
|
261
|
+
.send(DynamicAcmeCommand::Register(domain));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
pub fn unregister_domain(&self, domain: &str) {
|
|
265
|
+
let _ = self
|
|
266
|
+
.inner
|
|
267
|
+
.command_tx
|
|
268
|
+
.send(DynamicAcmeCommand::Unregister(domain.to_string()));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
pub fn http01_response(&self, host: &str, path: &str) -> Option<String> {
|
|
272
|
+
self.inner
|
|
273
|
+
.http01_registry
|
|
274
|
+
.read()
|
|
275
|
+
.get(host)
|
|
276
|
+
.and_then(|handler| handler.handle_challenge_request(path))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
pub fn statuses(&self) -> Vec<ManagedTlsDomainStatus> {
|
|
280
|
+
let mut statuses = self
|
|
281
|
+
.inner
|
|
282
|
+
.statuses
|
|
283
|
+
.read()
|
|
284
|
+
.values()
|
|
285
|
+
.cloned()
|
|
286
|
+
.collect::<Vec<_>>();
|
|
287
|
+
statuses.sort_by(|a, b| a.domain.cmp(&b.domain));
|
|
288
|
+
statuses
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
impl Drop for DynamicAcmeManagerInner {
|
|
293
|
+
fn drop(&mut self) {
|
|
294
|
+
let _ = self.command_tx.send(DynamicAcmeCommand::Shutdown);
|
|
295
|
+
if let Some(handle) = self.thread_handle.lock().take() {
|
|
296
|
+
if let Err(err) = handle.join() {
|
|
297
|
+
error!("Dynamic ACME manager thread join failed: {:?}", err);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
32
303
|
#[derive(Clone)]
|
|
33
304
|
pub enum ItsiTlsAcceptor {
|
|
34
305
|
Manual(TlsAcceptor),
|
|
35
|
-
Automatic
|
|
36
|
-
AcmeAcceptor,
|
|
37
|
-
|
|
38
|
-
Arc<ServerConfig>,
|
|
39
|
-
|
|
306
|
+
Automatic {
|
|
307
|
+
acme_acceptor: AcmeAcceptor,
|
|
308
|
+
manager: DynamicAcmeManager,
|
|
309
|
+
server_config: Arc<ServerConfig>,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
impl ItsiTlsAcceptor {
|
|
314
|
+
pub fn manager(&self) -> Option<DynamicAcmeManager> {
|
|
315
|
+
match self {
|
|
316
|
+
ItsiTlsAcceptor::Automatic { manager, .. } => Some(manager.clone()),
|
|
317
|
+
ItsiTlsAcceptor::Manual(_) => None,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
pub fn set_http01_enabled(&self, enabled: bool) {
|
|
322
|
+
if let ItsiTlsAcceptor::Automatic { manager, .. } = self {
|
|
323
|
+
manager.set_http01_enabled(enabled);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
pub fn initialize_domains(&self) {
|
|
328
|
+
if let ItsiTlsAcceptor::Automatic { manager, .. } = self {
|
|
329
|
+
manager.initialize_domains();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
40
332
|
}
|
|
41
333
|
|
|
42
334
|
/// Generates a TLS configuration based on either :
|
|
@@ -52,66 +344,79 @@ pub fn configure_tls(
|
|
|
52
344
|
let domains = query_params
|
|
53
345
|
.get("domains")
|
|
54
346
|
.map(|v| v.split(',').map(String::from).collect::<Vec<_>>())
|
|
55
|
-
.or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]))
|
|
347
|
+
.or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]))
|
|
348
|
+
.unwrap_or_default();
|
|
56
349
|
|
|
57
350
|
if query_params.get("cert").is_some_and(|c| c == "acme") {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
itsi_error::ItsiError::ArgumentError(format!(
|
|
86
|
-
"Invalid ACME CA Pem path {:?}",
|
|
87
|
-
e
|
|
88
|
-
))
|
|
89
|
-
})?;
|
|
90
|
-
root_cert_store.add_parsable_certificates(der_certs);
|
|
91
|
-
|
|
92
|
-
let client_config = ClientConfig::builder()
|
|
351
|
+
let directory_url = &*ITSI_ACME_DIRECTORY_URL;
|
|
352
|
+
info!(
|
|
353
|
+
domains = format!("{:?}", domains),
|
|
354
|
+
directory_url, "Requesting acme cert"
|
|
355
|
+
);
|
|
356
|
+
let acme_contact_email = query_params
|
|
357
|
+
.get("acme_email")
|
|
358
|
+
.map(|s| s.to_string())
|
|
359
|
+
.or_else(|| (*ITSI_ACME_CONTACT_EMAIL).as_ref().ok().map(|s| s.to_string()))
|
|
360
|
+
.ok_or_else(|| itsi_error::ItsiError::ArgumentError(
|
|
361
|
+
"acme_email query param or ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate let's encrypt certificates".to_string(),
|
|
362
|
+
))?;
|
|
363
|
+
|
|
364
|
+
let client_config = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
|
|
365
|
+
let mut root_cert_store = RootCertStore::empty();
|
|
366
|
+
|
|
367
|
+
let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
|
|
368
|
+
let mut ca_reader = BufReader::new(&ca_pem[..]);
|
|
369
|
+
let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
|
|
370
|
+
.collect::<std::result::Result<Vec<CertificateDer>, _>>()
|
|
371
|
+
.map_err(|e| {
|
|
372
|
+
itsi_error::ItsiError::ArgumentError(format!("Invalid ACME CA Pem path {:?}", e))
|
|
373
|
+
})?;
|
|
374
|
+
root_cert_store.add_parsable_certificates(der_certs);
|
|
375
|
+
|
|
376
|
+
Arc::new(
|
|
377
|
+
ClientConfig::builder()
|
|
93
378
|
.with_root_certificates(root_cert_store)
|
|
94
|
-
.with_no_client_auth()
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
379
|
+
.with_no_client_auth(),
|
|
380
|
+
)
|
|
381
|
+
} else {
|
|
382
|
+
let mut root_store = RootCertStore::empty();
|
|
383
|
+
root_store.extend(
|
|
384
|
+
webpki_roots::TLS_SERVER_ROOTS
|
|
385
|
+
.iter()
|
|
386
|
+
.map(|ta| rustls::pki_types::TrustAnchor {
|
|
387
|
+
subject: ta.subject.clone(),
|
|
388
|
+
subject_public_key_info: ta.subject_public_key_info.clone(),
|
|
389
|
+
name_constraints: ta.name_constraints.clone(),
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
Arc::new(
|
|
393
|
+
ClientConfig::builder()
|
|
394
|
+
.with_root_certificates(root_store)
|
|
395
|
+
.with_no_client_auth(),
|
|
396
|
+
)
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
let manager = DynamicAcmeManager::new(
|
|
400
|
+
DynamicAcmeConfigTemplate {
|
|
401
|
+
client_config,
|
|
402
|
+
directory_url: directory_url.to_string(),
|
|
403
|
+
contact: vec![format!("mailto:{}", acme_contact_email)],
|
|
404
|
+
cache_dir: ITSI_ACME_CACHE_DIR.to_string(),
|
|
405
|
+
},
|
|
406
|
+
domains.clone(),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
let mut rustls_config = ServerConfig::builder()
|
|
410
|
+
.with_no_client_auth()
|
|
411
|
+
.with_cert_resolver(manager.resolver());
|
|
412
|
+
|
|
413
|
+
rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
|
414
|
+
|
|
415
|
+
return Ok(ItsiTlsAcceptor::Automatic {
|
|
416
|
+
acme_acceptor: AcmeAcceptor::new(manager.resolver()),
|
|
417
|
+
manager,
|
|
418
|
+
server_config: Arc::new(rustls_config),
|
|
419
|
+
});
|
|
115
420
|
}
|
|
116
421
|
let (certs, key) = if let (Some(cert_path), Some(key_path)) =
|
|
117
422
|
(query_params.get("cert"), query_params.get("key"))
|
|
@@ -121,7 +426,7 @@ pub fn configure_tls(
|
|
|
121
426
|
let key = load_private_key(key_path);
|
|
122
427
|
(certs, key)
|
|
123
428
|
} else {
|
|
124
|
-
generate_ca_signed_cert(domains.
|
|
429
|
+
generate_ca_signed_cert(if domains.is_empty() { vec![host.to_owned()] } else { domains })?
|
|
125
430
|
};
|
|
126
431
|
|
|
127
432
|
let mut config = ServerConfig::builder()
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
use crate::default_responses::{NOT_FOUND_RESPONSE, TIMEOUT_RESPONSE};
|
|
2
2
|
use crate::ruby_types::itsi_server::itsi_server_config::ItsiServerTokenPreference;
|
|
3
3
|
use crate::server::http_message_types::{
|
|
4
|
-
ConversionExt, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
|
|
4
|
+
ConversionExt, HttpBody, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
|
|
5
5
|
};
|
|
6
6
|
use crate::server::lifecycle_event::LifecycleEvent;
|
|
7
7
|
use crate::server::middleware_stack::MiddlewareLayer;
|
|
8
8
|
use crate::server::serve_strategy::acceptor::AcceptorArgs;
|
|
9
9
|
use crate::server::signal::{send_lifecycle_event, SHUTDOWN_REQUESTED};
|
|
10
|
+
use bytes::Bytes;
|
|
10
11
|
use chrono::{self, DateTime, Local};
|
|
11
12
|
use either::Either;
|
|
12
13
|
use http::header::ACCEPT_ENCODING;
|
|
13
|
-
use http::{HeaderValue, Request};
|
|
14
|
+
use http::{HeaderValue, Request, StatusCode};
|
|
14
15
|
use hyper::body::Incoming;
|
|
15
16
|
use regex::Regex;
|
|
16
17
|
use smallvec::SmallVec;
|
|
@@ -174,6 +175,7 @@ impl HttpRequestContext {
|
|
|
174
175
|
const SERVER_TOKEN_VERSION: HeaderValue =
|
|
175
176
|
HeaderValue::from_static(concat!("Itsi/", env!("CARGO_PKG_VERSION")));
|
|
176
177
|
const SERVER_TOKEN_NAME: HeaderValue = HeaderValue::from_static("Itsi");
|
|
178
|
+
const TEXT_PLAIN_UTF8: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8");
|
|
177
179
|
|
|
178
180
|
impl ItsiHttpService {
|
|
179
181
|
pub async fn handle_request(&self, req: Request<Incoming>) -> itsi_error::Result<HttpResponse> {
|
|
@@ -188,6 +190,10 @@ impl ItsiHttpService {
|
|
|
188
190
|
let token_preference = self.server_params.itsi_server_token_preference;
|
|
189
191
|
|
|
190
192
|
let service_future = async move {
|
|
193
|
+
if let Some(acme_response) = self.acme_http01_response(&req) {
|
|
194
|
+
return Ok(acme_response);
|
|
195
|
+
}
|
|
196
|
+
|
|
191
197
|
let Some((stack, matching_pattern)) =
|
|
192
198
|
self.server_params.middleware.get().unwrap().stack_for(&req)
|
|
193
199
|
else {
|
|
@@ -264,4 +270,42 @@ impl ItsiHttpService {
|
|
|
264
270
|
service_future.await
|
|
265
271
|
}
|
|
266
272
|
}
|
|
273
|
+
|
|
274
|
+
fn acme_http01_response(&self, req: &HttpRequest) -> Option<HttpResponse> {
|
|
275
|
+
let host = normalize_host_header(req.header("host")?)?;
|
|
276
|
+
let managers = self.server_params.acme_managers.read();
|
|
277
|
+
let key_authorization = managers
|
|
278
|
+
.iter()
|
|
279
|
+
.find_map(|(_, manager)| manager.http01_response(host, req.uri().path()))?;
|
|
280
|
+
|
|
281
|
+
let mut builder = http::Response::builder()
|
|
282
|
+
.status(StatusCode::OK)
|
|
283
|
+
.header(http::header::CONTENT_TYPE, TEXT_PLAIN_UTF8);
|
|
284
|
+
|
|
285
|
+
if req.method() == http::Method::HEAD {
|
|
286
|
+
builder = builder.header(http::header::CONTENT_LENGTH, "0");
|
|
287
|
+
return builder.body(HttpBody::empty()).ok();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
builder
|
|
291
|
+
.header(
|
|
292
|
+
http::header::CONTENT_LENGTH,
|
|
293
|
+
key_authorization.len().to_string(),
|
|
294
|
+
)
|
|
295
|
+
.body(HttpBody::full(Bytes::from(key_authorization)))
|
|
296
|
+
.ok()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn normalize_host_header(host: &str) -> Option<&str> {
|
|
301
|
+
let host = host.trim();
|
|
302
|
+
if host.is_empty() {
|
|
303
|
+
return None;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if let Some(stripped) = host.strip_prefix('[') {
|
|
307
|
+
return stripped.split(']').next();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
Some(host.split(':').next().unwrap_or(host))
|
|
267
311
|
}
|
data/lib/itsi/http_request.rb
CHANGED
|
@@ -145,6 +145,16 @@ module Itsi
|
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
def partial_hijack
|
|
149
|
+
UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
|
|
150
|
+
server_sock.autoclose = false
|
|
151
|
+
response.partial_hijack(server_sock.fileno)
|
|
152
|
+
server_sock.sync = true
|
|
153
|
+
app_sock.sync = true
|
|
154
|
+
app_sock
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
148
158
|
# Rack expects env["rack.hijack"] to respond to #call.
|
|
149
159
|
def call
|
|
150
160
|
hijack
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
module Itsi
|
|
2
2
|
class Server
|
|
3
3
|
module RackInterface
|
|
4
|
+
class PartialHijackStream
|
|
5
|
+
def initialize(response)
|
|
6
|
+
@response = response
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def write(chunk)
|
|
10
|
+
@response.write(chunk.to_s)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(*)
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def <<(chunk)
|
|
18
|
+
write(chunk)
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def flush
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close_write
|
|
27
|
+
@response.close_write
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def close_read
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def close
|
|
35
|
+
@response.close_write
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def closed?
|
|
39
|
+
@response.closed?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
4
43
|
# Builds a handler proc that is compatible with Rack applications.
|
|
5
44
|
def self.for(app)
|
|
6
45
|
require "rack"
|
|
@@ -38,7 +77,8 @@ module Itsi
|
|
|
38
77
|
response.status = status
|
|
39
78
|
|
|
40
79
|
# 2. Set Headers
|
|
41
|
-
|
|
80
|
+
hijack_callback = headers.delete("rack.hijack")
|
|
81
|
+
body_streamer = streaming_body?(body) ? body : hijack_callback
|
|
42
82
|
|
|
43
83
|
response.reserve_headers(headers.size)
|
|
44
84
|
|
|
@@ -57,7 +97,10 @@ module Itsi
|
|
|
57
97
|
# the server will begin to stream it to the client.
|
|
58
98
|
|
|
59
99
|
|
|
60
|
-
if
|
|
100
|
+
if hijack_callback
|
|
101
|
+
stream = status == 101 ? request.partial_hijack : PartialHijackStream.new(response)
|
|
102
|
+
body_streamer.call(stream)
|
|
103
|
+
elsif body_streamer
|
|
61
104
|
# If we're partially hijacked or returned a streaming body,
|
|
62
105
|
# stream this response.
|
|
63
106
|
body_streamer.call(response)
|
data/lib/itsi/server/version.rb
CHANGED
data/lib/itsi/server.rb
CHANGED
|
@@ -32,6 +32,30 @@ module Itsi
|
|
|
32
32
|
@running && !@running.empty?
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def current_server
|
|
36
|
+
@running&.last || raise("No running Itsi::Server instance")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tls_bindings
|
|
40
|
+
current_server.tls_bindings
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tls_domains(listener_id = nil)
|
|
44
|
+
current_server.tls_domains(listener_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tls_domain_statuses(listener_id = nil)
|
|
48
|
+
current_server.tls_domain_statuses(listener_id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def register_tls_domain(domain, listener_id = nil)
|
|
52
|
+
current_server.register_tls_domain(domain, listener_id)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def unregister_tls_domain(domain, listener_id = nil)
|
|
56
|
+
current_server.unregister_tls_domain(domain, listener_id)
|
|
57
|
+
end
|
|
58
|
+
|
|
35
59
|
def start_in_background_thread(cli_params = {}, &blk)
|
|
36
60
|
@background_threads ||= []
|
|
37
61
|
server, background_thread = start(cli_params, background: true, &blk)
|