itsi-server 0.1.1 → 0.1.8
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.
Potentially problematic release.
This version of itsi-server might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Cargo.lock +2917 -0
- data/Cargo.toml +7 -0
- data/exe/itsi +88 -28
- data/ext/itsi_error/Cargo.toml +2 -0
- data/ext/itsi_error/src/from.rs +68 -0
- data/ext/itsi_error/src/lib.rs +13 -38
- data/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/ext/itsi_rb_helpers/Cargo.toml +2 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
- data/ext/itsi_rb_helpers/src/lib.rs +90 -10
- data/ext/itsi_scheduler/Cargo.toml +24 -0
- data/ext/itsi_scheduler/extconf.rb +6 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/ext/itsi_scheduler/src/lib.rs +38 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +21 -3
- data/ext/itsi_server/extconf.rb +1 -1
- data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
- data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
- data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +62 -7
- data/ext/itsi_server/src/request/itsi_request.rs +238 -104
- data/ext/itsi_server/src/response/itsi_response.rs +347 -0
- data/ext/itsi_server/src/response/mod.rs +1 -0
- data/ext/itsi_server/src/server/bind.rs +57 -25
- data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
- data/ext/itsi_server/src/server/io_stream.rs +104 -0
- data/ext/itsi_server/src/server/itsi_server.rs +189 -134
- data/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
- data/ext/itsi_server/src/server/listener.rs +237 -137
- data/ext/itsi_server/src/server/mod.rs +7 -1
- data/ext/itsi_server/src/server/process_worker.rs +196 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +254 -0
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +257 -0
- data/ext/itsi_server/src/server/signal.rs +70 -0
- data/ext/itsi_server/src/server/thread_worker.rs +368 -0
- data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +132 -0
- data/ext/itsi_server/src/server/tls.rs +184 -60
- data/ext/itsi_tracing/Cargo.toml +4 -0
- data/ext/itsi_tracing/src/lib.rs +36 -6
- data/lib/itsi/index.html.erb +91 -0
- data/lib/itsi/request.rb +30 -14
- data/lib/itsi/server/rack/handler/itsi.rb +25 -0
- data/lib/itsi/server/scheduler_mode.rb +6 -0
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +102 -2
- data/lib/itsi/signals.rb +23 -0
- data/lib/itsi/stream_io.rb +38 -0
- metadata +44 -27
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
- data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
|
@@ -1,20 +1,112 @@
|
|
|
1
1
|
use base64::{engine::general_purpose, Engine as _};
|
|
2
2
|
use itsi_error::Result;
|
|
3
|
-
use itsi_tracing::
|
|
4
|
-
use
|
|
3
|
+
use itsi_tracing::info;
|
|
4
|
+
use locked_dir_cache::LockedDirCache;
|
|
5
|
+
use rcgen::{
|
|
6
|
+
generate_simple_self_signed, CertificateParams, CertifiedKey, DnType, KeyPair, SanType,
|
|
7
|
+
};
|
|
8
|
+
use rustls::{
|
|
9
|
+
pki_types::{CertificateDer, PrivateKeyDer},
|
|
10
|
+
ClientConfig, RootCertStore,
|
|
11
|
+
};
|
|
5
12
|
use rustls_pemfile::{certs, pkcs8_private_keys};
|
|
6
|
-
use std::{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
use std::{
|
|
14
|
+
collections::HashMap,
|
|
15
|
+
fs,
|
|
16
|
+
io::{BufReader, Error},
|
|
17
|
+
sync::Arc,
|
|
18
|
+
};
|
|
19
|
+
use tokio::sync::Mutex;
|
|
20
|
+
use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
|
|
21
|
+
use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
|
|
22
|
+
|
|
23
|
+
use crate::env::{
|
|
24
|
+
ITSI_ACME_CACHE_DIR, ITSI_ACME_CA_PEM_PATH, ITSI_ACME_CONTACT_EMAIL, ITSI_ACME_DIRECTORY_URL,
|
|
25
|
+
ITSI_LOCAL_CA_DIR,
|
|
26
|
+
};
|
|
27
|
+
mod locked_dir_cache;
|
|
28
|
+
|
|
29
|
+
#[derive(Clone)]
|
|
30
|
+
pub enum ItsiTlsAcceptor {
|
|
31
|
+
Manual(TlsAcceptor),
|
|
32
|
+
Automatic(
|
|
33
|
+
AcmeAcceptor,
|
|
34
|
+
Arc<Mutex<AcmeState<Error>>>,
|
|
35
|
+
Arc<ServerConfig>,
|
|
36
|
+
),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Generates a TLS configuration based on either :
|
|
40
|
+
/// * Input "cert" and "key" options (either paths or Base64-encoded strings) or
|
|
41
|
+
/// * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
|
|
42
|
+
///
|
|
43
|
+
/// If a non-local host or optional domain parameter is provided,
|
|
44
|
+
/// an automated certificate will attempt to be fetched using let's encrypt.
|
|
45
|
+
pub fn configure_tls(
|
|
46
|
+
host: &str,
|
|
47
|
+
query_params: &HashMap<String, String>,
|
|
48
|
+
) -> Result<ItsiTlsAcceptor> {
|
|
49
|
+
let domains = query_params
|
|
50
|
+
.get("domains")
|
|
51
|
+
.map(|v| v.split(',').map(String::from).collect::<Vec<_>>());
|
|
52
|
+
|
|
53
|
+
if query_params.get("cert").is_some_and(|c| c == "auto") {
|
|
54
|
+
if let Some(domains) = domains {
|
|
55
|
+
let directory_url = &*ITSI_ACME_DIRECTORY_URL;
|
|
56
|
+
info!(
|
|
57
|
+
domains = format!("{:?}", domains),
|
|
58
|
+
directory_url, "Requesting acme cert"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let acme_config = AcmeConfig::new(domains)
|
|
62
|
+
.contact([format!("mailto:{}", (*ITSI_ACME_CONTACT_EMAIL).as_ref().map_err(|_| {
|
|
63
|
+
itsi_error::ItsiError::ArgumentError(
|
|
64
|
+
"ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
|
|
65
|
+
.to_string(),
|
|
66
|
+
)
|
|
67
|
+
})?)])
|
|
68
|
+
.cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
|
|
69
|
+
.directory(directory_url);
|
|
70
|
+
|
|
71
|
+
let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
|
|
72
|
+
let mut root_cert_store = RootCertStore::empty();
|
|
73
|
+
|
|
74
|
+
let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
|
|
75
|
+
let mut ca_reader = BufReader::new(&ca_pem[..]);
|
|
76
|
+
let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
|
|
77
|
+
.collect::<std::result::Result<Vec<CertificateDer>, _>>()
|
|
78
|
+
.map_err(|e| {
|
|
79
|
+
itsi_error::ItsiError::ArgumentError(format!(
|
|
80
|
+
"Invalid ACME CA Pem path {:?}",
|
|
81
|
+
e
|
|
82
|
+
))
|
|
83
|
+
})?;
|
|
84
|
+
root_cert_store.add_parsable_certificates(der_certs);
|
|
85
|
+
|
|
86
|
+
let client_config = ClientConfig::builder()
|
|
87
|
+
.with_root_certificates(root_cert_store)
|
|
88
|
+
.with_no_client_auth();
|
|
89
|
+
acme_config
|
|
90
|
+
.client_tls_config(Arc::new(client_config))
|
|
91
|
+
.state()
|
|
92
|
+
} else {
|
|
93
|
+
acme_config.state()
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let mut rustls_config = ServerConfig::builder()
|
|
97
|
+
.with_no_client_auth()
|
|
98
|
+
.with_cert_resolver(acme_state.resolver());
|
|
99
|
+
|
|
100
|
+
rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
|
101
|
+
|
|
102
|
+
let acceptor = acme_state.acceptor();
|
|
103
|
+
return Ok(ItsiTlsAcceptor::Automatic(
|
|
104
|
+
acceptor,
|
|
105
|
+
Arc::new(Mutex::new(acme_state)),
|
|
106
|
+
Arc::new(rustls_config),
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
18
110
|
let (certs, key) = if let (Some(cert_path), Some(key_path)) =
|
|
19
111
|
(query_params.get("cert"), query_params.get("key"))
|
|
20
112
|
{
|
|
@@ -22,41 +114,20 @@ pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Resu
|
|
|
22
114
|
let certs = load_certs(cert_path);
|
|
23
115
|
let key = load_private_key(key_path);
|
|
24
116
|
(certs, key)
|
|
25
|
-
} else if query_params
|
|
26
|
-
.get("cert")
|
|
27
|
-
.map(|v| v == "auto")
|
|
28
|
-
.unwrap_or(false)
|
|
29
|
-
{
|
|
30
|
-
let domain_param = query_params.get("domain");
|
|
31
|
-
let host_string = host.to_string();
|
|
32
|
-
let domain = domain_param.or_else(|| {
|
|
33
|
-
if host_string != "localhost" {
|
|
34
|
-
Some(&host_string)
|
|
35
|
-
} else {
|
|
36
|
-
None
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if let Some(domain) = domain {
|
|
41
|
-
retrieve_acme_cert(domain)?
|
|
42
|
-
} else {
|
|
43
|
-
generate_ca_signed_cert(host)?
|
|
44
|
-
}
|
|
45
117
|
} else {
|
|
46
|
-
generate_ca_signed_cert(host)?
|
|
118
|
+
generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
|
|
47
119
|
};
|
|
48
120
|
|
|
49
121
|
let mut config = ServerConfig::builder()
|
|
50
|
-
.with_safe_defaults()
|
|
51
122
|
.with_no_client_auth()
|
|
52
123
|
.with_single_cert(certs, key)
|
|
53
124
|
.expect("Failed to build TLS config");
|
|
54
125
|
|
|
55
126
|
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
|
56
|
-
Ok(config)
|
|
127
|
+
Ok(ItsiTlsAcceptor::Manual(TlsAcceptor::from(Arc::new(config))))
|
|
57
128
|
}
|
|
58
129
|
|
|
59
|
-
pub fn load_certs(path: &str) -> Vec<
|
|
130
|
+
pub fn load_certs(path: &str) -> Vec<CertificateDer<'static>> {
|
|
60
131
|
let data = if let Some(stripped) = path.strip_prefix("base64:") {
|
|
61
132
|
general_purpose::STANDARD
|
|
62
133
|
.decode(stripped)
|
|
@@ -74,14 +145,20 @@ pub fn load_certs(path: &str) -> Vec<Certificate> {
|
|
|
74
145
|
})
|
|
75
146
|
.collect::<Result<_>>()
|
|
76
147
|
.expect("Failed to parse certificate file");
|
|
77
|
-
certs_der
|
|
148
|
+
certs_der
|
|
149
|
+
.into_iter()
|
|
150
|
+
.map(|vec| {
|
|
151
|
+
// Convert the owned Vec<u8> into a CertificateDer and force 'static.
|
|
152
|
+
unsafe { std::mem::transmute(CertificateDer::from(vec)) }
|
|
153
|
+
})
|
|
154
|
+
.collect()
|
|
78
155
|
} else {
|
|
79
|
-
vec![
|
|
156
|
+
vec![CertificateDer::from(data)]
|
|
80
157
|
}
|
|
81
158
|
}
|
|
82
159
|
|
|
83
160
|
/// Loads a private key from a file or Base64.
|
|
84
|
-
pub fn load_private_key(path: &str) ->
|
|
161
|
+
pub fn load_private_key(path: &str) -> PrivateKeyDer<'static> {
|
|
85
162
|
let key_data = if let Some(stripped) = path.strip_prefix("base64:") {
|
|
86
163
|
general_purpose::STANDARD
|
|
87
164
|
.decode(stripped)
|
|
@@ -100,39 +177,86 @@ pub fn load_private_key(path: &str) -> PrivateKey {
|
|
|
100
177
|
.collect::<Result<_>>()
|
|
101
178
|
.expect("Failed to parse private key");
|
|
102
179
|
if !keys.is_empty() {
|
|
103
|
-
return
|
|
180
|
+
return PrivateKeyDer::try_from(keys[0].clone()).unwrap();
|
|
104
181
|
}
|
|
105
182
|
}
|
|
106
|
-
|
|
183
|
+
PrivateKeyDer::try_from(key_data).unwrap()
|
|
107
184
|
}
|
|
108
185
|
|
|
109
|
-
pub fn generate_ca_signed_cert(
|
|
110
|
-
|
|
186
|
+
pub fn generate_ca_signed_cert(
|
|
187
|
+
domains: Vec<String>,
|
|
188
|
+
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
|
|
189
|
+
info!(
|
|
190
|
+
domains = format!("{}", domains.join(", ")),
|
|
191
|
+
"Self signed cert",
|
|
192
|
+
);
|
|
193
|
+
info!(
|
|
194
|
+
"Add {} to your system's trusted cert store to resolve certificate errors.",
|
|
195
|
+
format!("{}/itsi_dev_ca.crt", ITSI_LOCAL_CA_DIR.to_str().unwrap())
|
|
196
|
+
);
|
|
197
|
+
info!("Dev CA path can be overridden by setting env var: `ITSI_LOCAL_CA_DIR`.");
|
|
198
|
+
let (ca_key_pem, ca_cert_pem) = get_or_create_local_dev_ca()?;
|
|
111
199
|
|
|
112
|
-
let ca_kp = KeyPair::from_pem(
|
|
113
|
-
let
|
|
200
|
+
let ca_kp = KeyPair::from_pem(&ca_key_pem).expect("Failed to load CA key");
|
|
201
|
+
let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert_pem)
|
|
202
|
+
.expect("Failed to parse embedded CA certificate")
|
|
203
|
+
.self_signed(&ca_kp)
|
|
204
|
+
.expect("Failed to self-sign embedded CA cert");
|
|
114
205
|
|
|
115
|
-
let
|
|
116
|
-
let ee_key = KeyPair::generate().unwrap();
|
|
206
|
+
let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
|
|
117
207
|
let mut ee_params = CertificateParams::default();
|
|
118
208
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
209
|
+
use std::net::IpAddr;
|
|
210
|
+
|
|
211
|
+
ee_params.subject_alt_names = domains
|
|
212
|
+
.iter()
|
|
213
|
+
.map(|domain| {
|
|
214
|
+
if let Ok(ip) = domain.parse::<IpAddr>() {
|
|
215
|
+
SanType::IpAddress(ip)
|
|
216
|
+
} else {
|
|
217
|
+
SanType::DnsName(domain.clone().try_into().unwrap())
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
.collect();
|
|
221
|
+
|
|
122
222
|
ee_params
|
|
123
223
|
.distinguished_name
|
|
124
|
-
.push(DnType::CommonName,
|
|
224
|
+
.push(DnType::CommonName, domains[0].clone());
|
|
125
225
|
|
|
126
226
|
ee_params.use_authority_key_identifier_extension = true;
|
|
127
227
|
|
|
128
|
-
let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &
|
|
228
|
+
let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ca_kp).unwrap();
|
|
129
229
|
let ee_cert_der = ee_cert.der().to_vec();
|
|
130
|
-
let ee_cert =
|
|
131
|
-
|
|
230
|
+
let ee_cert = CertificateDer::from(ee_cert_der);
|
|
231
|
+
let ca_cert = CertificateDer::from(ca_cert.der().to_vec());
|
|
232
|
+
Ok((
|
|
233
|
+
vec![ee_cert, ca_cert],
|
|
234
|
+
PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
|
|
235
|
+
))
|
|
132
236
|
}
|
|
133
237
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
238
|
+
fn get_or_create_local_dev_ca() -> Result<(String, String)> {
|
|
239
|
+
let ca_dir = &*ITSI_LOCAL_CA_DIR;
|
|
240
|
+
fs::create_dir_all(ca_dir)?;
|
|
241
|
+
|
|
242
|
+
let key_path = ca_dir.join("itsi_dev_ca.key");
|
|
243
|
+
let cert_path = ca_dir.join("itsi_dev_ca.crt");
|
|
244
|
+
|
|
245
|
+
if key_path.exists() && cert_path.exists() {
|
|
246
|
+
// Already have a local CA
|
|
247
|
+
let key_pem = fs::read_to_string(&key_path)?;
|
|
248
|
+
let cert_pem = fs::read_to_string(&cert_path)?;
|
|
249
|
+
|
|
250
|
+
Ok((key_pem, cert_pem))
|
|
251
|
+
} else {
|
|
252
|
+
let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
|
|
253
|
+
|
|
254
|
+
let CertifiedKey { cert, key_pair } =
|
|
255
|
+
generate_simple_self_signed(subject_alt_names).unwrap();
|
|
256
|
+
|
|
257
|
+
fs::write(&key_path, key_pair.serialize_pem())?;
|
|
258
|
+
fs::write(&cert_path, cert.pem())?;
|
|
259
|
+
|
|
260
|
+
Ok((key_pair.serialize_pem(), cert.pem()))
|
|
261
|
+
}
|
|
138
262
|
}
|
data/ext/itsi_tracing/Cargo.toml
CHANGED
data/ext/itsi_tracing/src/lib.rs
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
|
+
use std::env;
|
|
2
|
+
|
|
3
|
+
use atty::{Stream, is};
|
|
1
4
|
pub use tracing::{debug, error, info, trace, warn};
|
|
2
|
-
use
|
|
5
|
+
pub use tracing_attributes::instrument; // Explicitly export from tracing-attributes
|
|
6
|
+
use tracing_subscriber::{
|
|
7
|
+
EnvFilter,
|
|
8
|
+
fmt::{self, format},
|
|
9
|
+
};
|
|
3
10
|
|
|
11
|
+
#[instrument]
|
|
4
12
|
pub fn init() {
|
|
5
|
-
let env_filter = EnvFilter::
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.
|
|
13
|
+
let env_filter = EnvFilter::builder()
|
|
14
|
+
.with_env_var("ITSI_LOG")
|
|
15
|
+
.try_from_env()
|
|
16
|
+
.unwrap_or_else(|_| EnvFilter::new("info"));
|
|
17
|
+
|
|
18
|
+
let format = fmt::format()
|
|
19
|
+
.compact()
|
|
20
|
+
.with_file(false)
|
|
21
|
+
.with_level(true)
|
|
22
|
+
.with_line_number(false)
|
|
23
|
+
.with_source_location(false)
|
|
24
|
+
.with_target(false)
|
|
25
|
+
.with_thread_ids(false);
|
|
26
|
+
|
|
27
|
+
let is_tty = is(Stream::Stdout);
|
|
28
|
+
|
|
29
|
+
let subscriber = tracing_subscriber::fmt()
|
|
9
30
|
.event_format(format)
|
|
10
|
-
.
|
|
31
|
+
.with_env_filter(env_filter);
|
|
32
|
+
|
|
33
|
+
if (is_tty && env::var("ITSI_LOG_PLAIN").is_err()) || env::var("ITSI_LOG_ANSI").is_ok() {
|
|
34
|
+
subscriber.with_ansi(true).init();
|
|
35
|
+
} else {
|
|
36
|
+
subscriber
|
|
37
|
+
.fmt_fields(format::JsonFields::default())
|
|
38
|
+
.event_format(fmt::format().json())
|
|
39
|
+
.init();
|
|
40
|
+
}
|
|
11
41
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Itsi - Default</title>
|
|
6
|
+
<style>
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
}
|
|
12
|
+
body {
|
|
13
|
+
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
14
|
+
background-color: #f4f4f4;
|
|
15
|
+
color: #333;
|
|
16
|
+
line-height: 1.6;
|
|
17
|
+
}
|
|
18
|
+
.container {
|
|
19
|
+
max-width: 700px;
|
|
20
|
+
margin: 3rem auto;
|
|
21
|
+
background: #fff;
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
24
|
+
padding: 2rem;
|
|
25
|
+
}
|
|
26
|
+
h1 {
|
|
27
|
+
font-size: 1.8rem;
|
|
28
|
+
margin-bottom: 1rem;
|
|
29
|
+
text-align: center;
|
|
30
|
+
color: #444;
|
|
31
|
+
}
|
|
32
|
+
p {
|
|
33
|
+
margin-bottom: 1rem;
|
|
34
|
+
text-align: center;
|
|
35
|
+
color: #666;
|
|
36
|
+
}
|
|
37
|
+
ul.fields {
|
|
38
|
+
list-style: none;
|
|
39
|
+
margin-top: 1.5rem;
|
|
40
|
+
padding: 0;
|
|
41
|
+
}
|
|
42
|
+
ul.fields li {
|
|
43
|
+
background: #fafafa;
|
|
44
|
+
border: 1px solid #eee;
|
|
45
|
+
border-radius: 5px;
|
|
46
|
+
padding: 0.75rem;
|
|
47
|
+
margin-bottom: 0.75rem;
|
|
48
|
+
display: flex;
|
|
49
|
+
justify-content: space-between;
|
|
50
|
+
align-items: center;
|
|
51
|
+
}
|
|
52
|
+
.label {
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
margin-right: 1rem;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
<div class="container">
|
|
60
|
+
<h1>You're running on Itsi!</h1>
|
|
61
|
+
<p>RACK environment:</p>
|
|
62
|
+
|
|
63
|
+
<ul class="fields">
|
|
64
|
+
<li>
|
|
65
|
+
<span class="label">REQUEST_METHOD:</span>
|
|
66
|
+
<span>%{REQUEST_METHOD}</span>
|
|
67
|
+
</li>
|
|
68
|
+
<li>
|
|
69
|
+
<span class="label">PATH_INFO:</span>
|
|
70
|
+
<span>%{PATH_INFO}</span>
|
|
71
|
+
</li>
|
|
72
|
+
<li>
|
|
73
|
+
<span class="label">SERVER_NAME:</span>
|
|
74
|
+
<span>%{SERVER_NAME}</span>
|
|
75
|
+
</li>
|
|
76
|
+
<li>
|
|
77
|
+
<span class="label">SERVER_PORT:</span>
|
|
78
|
+
<span>%{SERVER_PORT}</span>
|
|
79
|
+
</li>
|
|
80
|
+
<li>
|
|
81
|
+
<span class="label">REMOTE_ADDR:</span>
|
|
82
|
+
<span>%{REMOTE_ADDR}</span>
|
|
83
|
+
</li>
|
|
84
|
+
<li>
|
|
85
|
+
<span class="label">HTTP_USER_AGENT:</span>
|
|
86
|
+
<span>%{HTTP_USER_AGENT}</span>
|
|
87
|
+
</li>
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
data/lib/itsi/request.rb
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "stringio"
|
|
4
|
-
|
|
5
3
|
module Itsi
|
|
6
4
|
class Request
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "socket"
|
|
7
|
+
|
|
8
|
+
attr_accessor :hijacked
|
|
9
|
+
|
|
7
10
|
def to_env
|
|
11
|
+
path = self.path
|
|
12
|
+
host = self.host
|
|
13
|
+
version = self.version
|
|
14
|
+
body = self.body
|
|
8
15
|
{
|
|
9
16
|
"SERVER_SOFTWARE" => "Itsi",
|
|
10
17
|
"SCRIPT_NAME" => script_name,
|
|
@@ -13,27 +20,36 @@ module Itsi
|
|
|
13
20
|
"REQUEST_PATH" => path,
|
|
14
21
|
"QUERY_STRING" => query_string,
|
|
15
22
|
"REMOTE_ADDR" => remote_addr,
|
|
16
|
-
"SERVER_NAME" => host,
|
|
17
23
|
"SERVER_PORT" => port.to_s,
|
|
24
|
+
"SERVER_NAME" => host,
|
|
25
|
+
"HTTP_HOST" => host,
|
|
18
26
|
"SERVER_PROTOCOL" => version,
|
|
19
27
|
"HTTP_VERSION" => version,
|
|
20
|
-
"
|
|
21
|
-
"rack.
|
|
28
|
+
"rack.version" => [version],
|
|
29
|
+
"rack.url_scheme" => scheme,
|
|
30
|
+
"rack.input" => \
|
|
31
|
+
case body
|
|
32
|
+
when Array then File.open(body.first, "rb")
|
|
33
|
+
when String then StringIO.new(body)
|
|
34
|
+
else body
|
|
35
|
+
end,
|
|
22
36
|
"rack.errors" => $stderr,
|
|
23
|
-
"rack.version" => version,
|
|
24
37
|
"rack.multithread" => true,
|
|
25
38
|
"rack.multiprocess" => true,
|
|
26
39
|
"rack.run_once" => false,
|
|
27
|
-
"rack.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
"rack.hijack?" => true,
|
|
41
|
+
"rack.multipart.buffer_size" => 16_384,
|
|
42
|
+
"rack.hijack" => lambda do
|
|
43
|
+
self.hijacked = true
|
|
44
|
+
UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
|
|
45
|
+
response.hijack(server_sock.fileno)
|
|
46
|
+
server_sock.sync = true
|
|
47
|
+
app_sock.sync = true
|
|
48
|
+
app_sock.instance_variable_set("@server_sock", server_sock)
|
|
49
|
+
app_sock
|
|
34
50
|
end
|
|
35
51
|
end
|
|
36
|
-
)
|
|
52
|
+
}.tap { |r| headers.each { |(k, v)| r[k] = v } }
|
|
37
53
|
end
|
|
38
54
|
end
|
|
39
55
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
return unless defined?(::Rackup::Handler) || defined?(Rack::Handler)
|
|
2
|
+
|
|
3
|
+
module Rack
|
|
4
|
+
module Handler
|
|
5
|
+
module Itsi
|
|
6
|
+
|
|
7
|
+
def self.run(app, options = {})
|
|
8
|
+
::Itsi::Server.new(
|
|
9
|
+
app: ->{ app },
|
|
10
|
+
binds: ["#{options.fetch(:host, "127.0.0.1")}:#{options.fetch(:Port, 3001)}"],
|
|
11
|
+
workers: options.fetch(:workers, 1),
|
|
12
|
+
threads: options.fetch(:threads, 1),
|
|
13
|
+
).start
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if defined?(Rackup)
|
|
20
|
+
::Rackup::Handler.register("itsi", Rack::Handler::Itsi)
|
|
21
|
+
::Rackup::Handler.register("Itsi", Rack::Handler::Itsi)
|
|
22
|
+
elsif defined?(Rack)
|
|
23
|
+
::Rack::Handler.register("itsi", Rack::Handler::Itsi)
|
|
24
|
+
::Rack::Handler.register("Itsi", Rack::Handler::Itsi)
|
|
25
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
if defined?(ActiveSupport::IsolatedExecutionState) && !ENV["ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL"]
|
|
2
|
+
Itsi.log_info \
|
|
3
|
+
"ActiveSupport Isolated Execution state detected. Automatically switching to :fiber mode. "\
|
|
4
|
+
"Set ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL to disable this behavior"
|
|
5
|
+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
|
6
|
+
end
|
data/lib/itsi/server/version.rb
CHANGED
data/lib/itsi/server.rb
CHANGED
|
@@ -2,12 +2,112 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "server/version"
|
|
4
4
|
require_relative "server/itsi_server"
|
|
5
|
+
require_relative "signals"
|
|
6
|
+
require_relative "request"
|
|
7
|
+
require_relative "stream_io"
|
|
8
|
+
require_relative "server/rack/handler/itsi"
|
|
9
|
+
require 'erb'
|
|
10
|
+
|
|
11
|
+
DEFAULT_INDEX = IO.read(__dir__ + '/index.html.erb')
|
|
5
12
|
|
|
6
13
|
module Itsi
|
|
7
14
|
class Server
|
|
8
|
-
|
|
15
|
+
|
|
16
|
+
def self.running?
|
|
17
|
+
@running ||= false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.start(
|
|
21
|
+
app: ->(env){
|
|
22
|
+
[env['CONTENT_TYPE'], env['HTTP_ACCEPT']].include?('application/json') ?
|
|
23
|
+
[200, {"Content-Type" => "application/json"}, ["{\"message\": \"You're running on Itsi!\"}"]] :
|
|
24
|
+
[200, {"Content-Type" => "text/html"}, [
|
|
25
|
+
DEFAULT_INDEX % {
|
|
26
|
+
REQUEST_METHOD: env['REQUEST_METHOD'],
|
|
27
|
+
PATH_INFO: env['PATH_INFO'],
|
|
28
|
+
SERVER_NAME: env['SERVER_NAME'],
|
|
29
|
+
SERVER_PORT: env['SERVER_PORT'],
|
|
30
|
+
REMOTE_ADDR: env['REMOTE_ADDR'],
|
|
31
|
+
HTTP_USER_AGENT: env['HTTP_USER_AGENT']
|
|
32
|
+
}
|
|
33
|
+
]]
|
|
34
|
+
},
|
|
35
|
+
binds: ['http://0.0.0.0:3000'],
|
|
36
|
+
**opts
|
|
37
|
+
)
|
|
38
|
+
server = new(app: ->{app}, binds: binds, **opts)
|
|
39
|
+
@running = true
|
|
40
|
+
Signal.trap('INT', 'DEFAULT')
|
|
41
|
+
server.start
|
|
42
|
+
ensure
|
|
43
|
+
@running = false
|
|
44
|
+
end
|
|
45
|
+
|
|
9
46
|
def self.call(app, request)
|
|
10
|
-
app.call(request.to_env)
|
|
47
|
+
respond request, app.call(request.to_env)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.streaming_body?(body)
|
|
51
|
+
body.respond_to?(:call) && !body.respond_to?(:each)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.respond(request, (status, headers, body))
|
|
55
|
+
response = request.response
|
|
56
|
+
|
|
57
|
+
# Don't try and respond if we've been hijacked.
|
|
58
|
+
# The hijacker is now responsible for this.
|
|
59
|
+
return if request.hijacked
|
|
60
|
+
|
|
61
|
+
# 1. Set Status
|
|
62
|
+
response.status = status
|
|
63
|
+
|
|
64
|
+
# 2. Set Headers
|
|
65
|
+
headers.each do |key, value|
|
|
66
|
+
next response.add_header(key, value) unless value.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
value.each do |v|
|
|
69
|
+
response.add_header(key, v)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 3. Set Body
|
|
74
|
+
# As soon as we start setting the response
|
|
75
|
+
# the server will begin to stream it to the client.
|
|
76
|
+
|
|
77
|
+
# If we're partially hijacked or returned a streaming body,
|
|
78
|
+
# stream this response.
|
|
79
|
+
|
|
80
|
+
if (body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack"))
|
|
81
|
+
body_streamer.call(StreamIO.new(response))
|
|
82
|
+
|
|
83
|
+
# If we're enumerable with more than one chunk
|
|
84
|
+
# also stream, otherwise write in a single chunk
|
|
85
|
+
elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
|
|
86
|
+
unless body.respond_to?(:each)
|
|
87
|
+
body = body.to_ary
|
|
88
|
+
raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
|
|
89
|
+
end
|
|
90
|
+
# We offset this iteration intentionally,
|
|
91
|
+
# to optimize for the case where there's only one chunk.
|
|
92
|
+
buffer = nil
|
|
93
|
+
body.each do |part|
|
|
94
|
+
response.send_frame(buffer.to_s) if buffer
|
|
95
|
+
buffer = part
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
response.send_and_close(buffer.to_s)
|
|
99
|
+
else
|
|
100
|
+
response.send_and_close(body.to_s)
|
|
101
|
+
end
|
|
102
|
+
ensure
|
|
103
|
+
response.close_write
|
|
104
|
+
body.close if body.respond_to?(:close)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.start_scheduler_loop(scheduler_class, scheduler_task)
|
|
108
|
+
scheduler = scheduler_class.new
|
|
109
|
+
Fiber.set_scheduler(scheduler)
|
|
110
|
+
[scheduler, Fiber.schedule(&scheduler_task)]
|
|
11
111
|
end
|
|
12
112
|
|
|
13
113
|
# If scheduler is enabled
|