itsi-server 0.1.1 → 0.1.11

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.

Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +2926 -0
  3. data/Cargo.toml +7 -0
  4. data/Rakefile +8 -1
  5. data/exe/itsi +119 -29
  6. data/ext/itsi_error/Cargo.toml +2 -0
  7. data/ext/itsi_error/src/from.rs +68 -0
  8. data/ext/itsi_error/src/lib.rs +13 -38
  9. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  10. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  11. data/ext/itsi_rb_helpers/Cargo.toml +2 -0
  12. data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  13. data/ext/itsi_rb_helpers/src/lib.rs +112 -9
  14. data/ext/itsi_scheduler/Cargo.toml +24 -0
  15. data/ext/itsi_scheduler/extconf.rb +6 -0
  16. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  17. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  18. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  19. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  20. data/ext/itsi_scheduler/src/lib.rs +38 -0
  21. data/ext/itsi_server/Cargo.lock +2956 -0
  22. data/ext/itsi_server/Cargo.toml +25 -4
  23. data/ext/itsi_server/extconf.rb +1 -1
  24. data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  25. data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  26. data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  27. data/ext/itsi_server/src/env.rs +43 -0
  28. data/ext/itsi_server/src/lib.rs +136 -8
  29. data/ext/itsi_server/src/request/itsi_request.rs +258 -103
  30. data/ext/itsi_server/src/response/itsi_response.rs +357 -0
  31. data/ext/itsi_server/src/response/mod.rs +1 -0
  32. data/ext/itsi_server/src/server/bind.rs +65 -29
  33. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  34. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  35. data/ext/itsi_server/src/server/itsi_server.rs +245 -139
  36. data/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
  37. data/ext/itsi_server/src/server/listener.rs +237 -137
  38. data/ext/itsi_server/src/server/mod.rs +7 -1
  39. data/ext/itsi_server/src/server/process_worker.rs +203 -0
  40. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +260 -0
  41. data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  42. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +276 -0
  43. data/ext/itsi_server/src/server/signal.rs +74 -0
  44. data/ext/itsi_server/src/server/thread_worker.rs +399 -0
  45. data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +132 -0
  46. data/ext/itsi_server/src/server/tls.rs +187 -60
  47. data/ext/itsi_tracing/Cargo.toml +4 -0
  48. data/ext/itsi_tracing/src/lib.rs +53 -6
  49. data/lib/itsi/index.html +91 -0
  50. data/lib/itsi/request.rb +38 -14
  51. data/lib/itsi/server/Itsi.rb +127 -0
  52. data/lib/itsi/server/config.rb +36 -0
  53. data/lib/itsi/server/options_dsl.rb +401 -0
  54. data/lib/itsi/server/rack/handler/itsi.rb +36 -0
  55. data/lib/itsi/server/rack_interface.rb +75 -0
  56. data/lib/itsi/server/scheduler_interface.rb +21 -0
  57. data/lib/itsi/server/scheduler_mode.rb +6 -0
  58. data/lib/itsi/server/signal_trap.rb +23 -0
  59. data/lib/itsi/server/version.rb +1 -1
  60. data/lib/itsi/server.rb +79 -9
  61. data/lib/itsi/stream_io.rb +38 -0
  62. metadata +49 -27
  63. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
  64. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
  65. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  66. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
@@ -1,20 +1,115 @@
1
1
  use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
- use itsi_tracing::{info, warn};
4
- use rcgen::{CertificateParams, DnType, KeyPair, SanType};
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::{collections::HashMap, fs, io::BufReader};
7
- use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig};
8
-
9
- const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
10
- const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
11
-
12
- // Generates a TLS configuration based on either :
13
- // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
14
- // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
15
- // If a non-local host or optional domain parameter is provided,
16
- // an automated certificate will attempt to be fetched using let's encrypt.
17
- pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Result<ServerConfig> {
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
+ .or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]));
53
+
54
+ if query_params.get("cert").is_some_and(|c| c == "acme") {
55
+ if let Some(domains) = domains {
56
+ let directory_url = &*ITSI_ACME_DIRECTORY_URL;
57
+ info!(
58
+ domains = format!("{:?}", domains),
59
+ directory_url, "Requesting acme cert"
60
+ );
61
+ let acme_contact_email = query_params
62
+ .get("acme_email")
63
+ .map(|s| s.to_string())
64
+ .or_else(|| (*ITSI_ACME_CONTACT_EMAIL).as_ref().ok().map(|s| s.to_string()))
65
+ .ok_or_else(|| itsi_error::ItsiError::ArgumentError(
66
+ "acme_email query param or ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate let's encrypt certificates".to_string(),
67
+ ))?;
68
+
69
+ let acme_config = AcmeConfig::new(domains)
70
+ .contact([format!("mailto:{}", acme_contact_email)])
71
+ .cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
72
+ .directory(directory_url);
73
+
74
+ let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
75
+ let mut root_cert_store = RootCertStore::empty();
76
+
77
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
78
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
79
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
80
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
81
+ .map_err(|e| {
82
+ itsi_error::ItsiError::ArgumentError(format!(
83
+ "Invalid ACME CA Pem path {:?}",
84
+ e
85
+ ))
86
+ })?;
87
+ root_cert_store.add_parsable_certificates(der_certs);
88
+
89
+ let client_config = ClientConfig::builder()
90
+ .with_root_certificates(root_cert_store)
91
+ .with_no_client_auth();
92
+ acme_config
93
+ .client_tls_config(Arc::new(client_config))
94
+ .state()
95
+ } else {
96
+ acme_config.state()
97
+ };
98
+
99
+ let mut rustls_config = ServerConfig::builder()
100
+ .with_no_client_auth()
101
+ .with_cert_resolver(acme_state.resolver());
102
+
103
+ rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
104
+
105
+ let acceptor = acme_state.acceptor();
106
+ return Ok(ItsiTlsAcceptor::Automatic(
107
+ acceptor,
108
+ Arc::new(Mutex::new(acme_state)),
109
+ Arc::new(rustls_config),
110
+ ));
111
+ }
112
+ }
18
113
  let (certs, key) = if let (Some(cert_path), Some(key_path)) =
19
114
  (query_params.get("cert"), query_params.get("key"))
20
115
  {
@@ -22,41 +117,20 @@ pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Resu
22
117
  let certs = load_certs(cert_path);
23
118
  let key = load_private_key(key_path);
24
119
  (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
120
  } else {
46
- generate_ca_signed_cert(host)?
121
+ generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
47
122
  };
48
123
 
49
124
  let mut config = ServerConfig::builder()
50
- .with_safe_defaults()
51
125
  .with_no_client_auth()
52
126
  .with_single_cert(certs, key)
53
127
  .expect("Failed to build TLS config");
54
128
 
55
129
  config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
56
- Ok(config)
130
+ Ok(ItsiTlsAcceptor::Manual(TlsAcceptor::from(Arc::new(config))))
57
131
  }
58
132
 
59
- pub fn load_certs(path: &str) -> Vec<Certificate> {
133
+ pub fn load_certs(path: &str) -> Vec<CertificateDer<'static>> {
60
134
  let data = if let Some(stripped) = path.strip_prefix("base64:") {
61
135
  general_purpose::STANDARD
62
136
  .decode(stripped)
@@ -74,14 +148,20 @@ pub fn load_certs(path: &str) -> Vec<Certificate> {
74
148
  })
75
149
  .collect::<Result<_>>()
76
150
  .expect("Failed to parse certificate file");
77
- certs_der.into_iter().map(Certificate).collect()
151
+ certs_der
152
+ .into_iter()
153
+ .map(|vec| {
154
+ // Convert the owned Vec<u8> into a CertificateDer and force 'static.
155
+ unsafe { std::mem::transmute(CertificateDer::from(vec)) }
156
+ })
157
+ .collect()
78
158
  } else {
79
- vec![Certificate(data)]
159
+ vec![CertificateDer::from(data)]
80
160
  }
81
161
  }
82
162
 
83
163
  /// Loads a private key from a file or Base64.
84
- pub fn load_private_key(path: &str) -> PrivateKey {
164
+ pub fn load_private_key(path: &str) -> PrivateKeyDer<'static> {
85
165
  let key_data = if let Some(stripped) = path.strip_prefix("base64:") {
86
166
  general_purpose::STANDARD
87
167
  .decode(stripped)
@@ -100,39 +180,86 @@ pub fn load_private_key(path: &str) -> PrivateKey {
100
180
  .collect::<Result<_>>()
101
181
  .expect("Failed to parse private key");
102
182
  if !keys.is_empty() {
103
- return PrivateKey(keys[0].clone());
183
+ return PrivateKeyDer::try_from(keys[0].clone()).unwrap();
104
184
  }
105
185
  }
106
- PrivateKey(key_data)
186
+ PrivateKeyDer::try_from(key_data).unwrap()
107
187
  }
108
188
 
109
- pub fn generate_ca_signed_cert(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
110
- info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
189
+ pub fn generate_ca_signed_cert(
190
+ domains: Vec<String>,
191
+ ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
192
+ info!(
193
+ domains = format!("{}", domains.join(", ")),
194
+ "Self signed cert",
195
+ );
196
+ info!(
197
+ "Add {} to your system's trusted cert store to resolve certificate errors.",
198
+ format!("{}/itsi_dev_ca.crt", ITSI_LOCAL_CA_DIR.to_str().unwrap())
199
+ );
200
+ info!("Dev CA path can be overridden by setting env var: `ITSI_LOCAL_CA_DIR`.");
201
+ let (ca_key_pem, ca_cert_pem) = get_or_create_local_dev_ca()?;
111
202
 
112
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).unwrap();
113
- let params = CertificateParams::from_ca_cert_pem(ITS_CA_CERT).unwrap();
203
+ let ca_kp = KeyPair::from_pem(&ca_key_pem).expect("Failed to load CA key");
204
+ let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert_pem)
205
+ .expect("Failed to parse embedded CA certificate")
206
+ .self_signed(&ca_kp)
207
+ .expect("Failed to self-sign embedded CA cert");
114
208
 
115
- let ca_cert = params.self_signed(&ca_kp).unwrap();
116
- let ee_key = KeyPair::generate().unwrap();
209
+ let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
117
210
  let mut ee_params = CertificateParams::default();
118
211
 
119
- // Set the domain in the subject alternative names (SAN)
120
- ee_params.subject_alt_names = vec![SanType::DnsName(domain.try_into()?)];
121
- // Optionally, set the Common Name (CN) in the distinguished name:
212
+ use std::net::IpAddr;
213
+
214
+ ee_params.subject_alt_names = domains
215
+ .iter()
216
+ .map(|domain| {
217
+ if let Ok(ip) = domain.parse::<IpAddr>() {
218
+ SanType::IpAddress(ip)
219
+ } else {
220
+ SanType::DnsName(domain.clone().try_into().unwrap())
221
+ }
222
+ })
223
+ .collect();
224
+
122
225
  ee_params
123
226
  .distinguished_name
124
- .push(DnType::CommonName, domain);
227
+ .push(DnType::CommonName, domains[0].clone());
125
228
 
126
229
  ee_params.use_authority_key_identifier_extension = true;
127
230
 
128
- let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ee_key).unwrap();
231
+ let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ca_kp).unwrap();
129
232
  let ee_cert_der = ee_cert.der().to_vec();
130
- let ee_cert = Certificate(ee_cert_der);
131
- Ok((vec![ee_cert], PrivateKey(ee_key.serialize_der())))
233
+ let ee_cert = CertificateDer::from(ee_cert_der);
234
+ let ca_cert = CertificateDer::from(ca_cert.der().to_vec());
235
+ Ok((
236
+ vec![ee_cert, ca_cert],
237
+ PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
238
+ ))
132
239
  }
133
240
 
134
- /// Retrieves an ACME certificate for a given domain.
135
- pub fn retrieve_acme_cert(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
136
- warn!("Retrieving ACME cert for {}", domain);
137
- generate_ca_signed_cert(domain)
241
+ fn get_or_create_local_dev_ca() -> Result<(String, String)> {
242
+ let ca_dir = &*ITSI_LOCAL_CA_DIR;
243
+ fs::create_dir_all(ca_dir)?;
244
+
245
+ let key_path = ca_dir.join("itsi_dev_ca.key");
246
+ let cert_path = ca_dir.join("itsi_dev_ca.crt");
247
+
248
+ if key_path.exists() && cert_path.exists() {
249
+ // Already have a local CA
250
+ let key_pem = fs::read_to_string(&key_path)?;
251
+ let cert_pem = fs::read_to_string(&cert_path)?;
252
+
253
+ Ok((key_pem, cert_pem))
254
+ } else {
255
+ let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
256
+
257
+ let CertifiedKey { cert, key_pair } =
258
+ generate_simple_self_signed(subject_alt_names).unwrap();
259
+
260
+ fs::write(&key_path, key_pair.serialize_pem())?;
261
+ fs::write(&cert_path, cert.pem())?;
262
+
263
+ Ok((key_pair.serialize_pem(), cert.pem()))
264
+ }
138
265
  }
@@ -9,4 +9,8 @@ tracing-subscriber = { version = "0.3.19", features = [
9
9
  "env-filter",
10
10
  "std",
11
11
  "fmt",
12
+ "json",
13
+ "ansi",
12
14
  ] }
15
+ tracing-attributes = "0.1"
16
+ atty = "0.2.14"
@@ -1,11 +1,58 @@
1
+ use std::env;
2
+
3
+ use atty::{Stream, is};
4
+ use tracing::level_filters::LevelFilter;
1
5
  pub use tracing::{debug, error, info, trace, warn};
2
- use tracing_subscriber::{EnvFilter, fmt};
6
+ pub use tracing_attributes::instrument; // Explicitly export from tracing-attributes
7
+ use tracing_subscriber::{
8
+ EnvFilter, Layer,
9
+ fmt::{self, format},
10
+ layer::SubscriberExt,
11
+ };
3
12
 
13
+ #[instrument]
4
14
  pub fn init() {
5
- let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
6
- let format = fmt::format().with_level(true).with_target(false).compact();
7
- tracing_subscriber::fmt()
8
- .with_env_filter(env_filter)
15
+ let env_filter = EnvFilter::builder()
16
+ .with_env_var("ITSI_LOG")
17
+ .try_from_env()
18
+ .unwrap_or_else(|_| EnvFilter::new("info"));
19
+
20
+ let format = fmt::format()
21
+ .compact()
22
+ .with_file(false)
23
+ .with_level(true)
24
+ .with_line_number(false)
25
+ .with_source_location(false)
26
+ .with_target(false)
27
+ .with_thread_ids(false);
28
+
29
+ let is_tty = is(Stream::Stdout);
30
+
31
+ let subscriber = tracing_subscriber::fmt()
9
32
  .event_format(format)
10
- .init();
33
+ .with_env_filter(env_filter);
34
+
35
+ if (is_tty && env::var("ITSI_LOG_PLAIN").is_err()) || env::var("ITSI_LOG_ANSI").is_ok() {
36
+ subscriber.with_ansi(true).init();
37
+ } else {
38
+ subscriber
39
+ .fmt_fields(format::JsonFields::default())
40
+ .event_format(fmt::format().json())
41
+ .init();
42
+ }
43
+ }
44
+
45
+ pub fn run_silently<F, R>(f: F) -> R
46
+ where
47
+ F: FnOnce() -> R,
48
+ {
49
+ // Build a minimal subscriber that filters *everything* out
50
+ let no_op_subscriber =
51
+ tracing_subscriber::registry().with(fmt::layer().with_filter(LevelFilter::OFF));
52
+
53
+ // Turn that subscriber into a `Dispatch`
54
+ let no_op_dispatch = tracing::dispatcher::Dispatch::new(no_op_subscriber);
55
+
56
+ // Temporarily set `no_op_dispatch` as the *default* within this closure
57
+ tracing::dispatcher::with_default(&no_op_dispatch, f)
11
58
  }
@@ -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,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "stringio"
4
+ require "socket"
4
5
 
5
6
  module Itsi
6
7
  class Request
7
- def to_env
8
+ attr_accessor :hijacked
9
+
10
+ def to_rack_env
11
+ path = self.path
12
+ host = self.host
13
+ version = self.version
8
14
  {
9
15
  "SERVER_SOFTWARE" => "Itsi",
10
16
  "SCRIPT_NAME" => script_name,
@@ -13,27 +19,45 @@ module Itsi
13
19
  "REQUEST_PATH" => path,
14
20
  "QUERY_STRING" => query_string,
15
21
  "REMOTE_ADDR" => remote_addr,
16
- "SERVER_NAME" => host,
17
22
  "SERVER_PORT" => port.to_s,
23
+ "SERVER_NAME" => host,
24
+ "HTTP_HOST" => host,
18
25
  "SERVER_PROTOCOL" => version,
19
26
  "HTTP_VERSION" => version,
20
- "HTTP_HOST" => host,
21
- "rack.input" => StringIO.new(body),
27
+ "itsi.request" => self,
28
+ "itsi.response" => response,
29
+ "rack.version" => [version],
30
+ "rack.url_scheme" => scheme,
31
+ "rack.input" => build_input_io,
22
32
  "rack.errors" => $stderr,
23
- "rack.version" => version,
24
33
  "rack.multithread" => true,
25
34
  "rack.multiprocess" => true,
26
35
  "rack.run_once" => false,
27
- "rack.multipart.buffer_size" => 16_384
28
- }.merge(
29
- headers.transform_keys do |k|
30
- case k
31
- when "content-length" then "CONTENT_LENGTH"
32
- when "content-type" then "CONTENT_TYPE"
33
- else "HTTP_#{k.upcase.tr("-", "_")}"
34
- end
36
+ "rack.hijack?" => true,
37
+ "rack.multipart.buffer_size" => 16_384,
38
+ "rack.hijack" => build_hijack_proc
39
+ }.tap { |r| headers.each { |(k, v)| r[k] = v } }
40
+ end
41
+
42
+ def build_hijack_proc
43
+ lambda do
44
+ self.hijacked = true
45
+ UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
46
+ response.hijack(server_sock.fileno)
47
+ server_sock.sync = true
48
+ app_sock.sync = true
49
+ app_sock.instance_variable_set("@server_sock", server_sock)
50
+ app_sock
35
51
  end
36
- )
52
+ end
53
+ end
54
+
55
+ def build_input_io
56
+ case body
57
+ when Array then File.open(body.first, "rb")
58
+ when String then StringIO.new(body)
59
+ else body
60
+ end
37
61
  end
38
62
  end
39
63
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+ env = ENV.fetch('APP_ENV') { ENV.fetch('RACK_ENV', 'development') }
3
+
4
+ # This is the default Itsi configuration file, installed when you run `itsi init`
5
+ # It contains a sane starting point for configuring your Itsi server.
6
+ # You can use this file in both development and production environments.
7
+ # Most of the options in this file can be overridden by command line options.
8
+ # Check out itsi -h to learn more about the command line options available to you.
9
+
10
+ # Number of worker processes to spawn
11
+ # If more than 1, Itsi will be booted in Cluster mode
12
+ workers ENV.fetch('ITSI_WORKERS') {
13
+ require 'etc'
14
+ env == 'development' ? 1 : Etc.nprocessors
15
+ }
16
+
17
+ # Number of threads to spawn per worker process
18
+ # For pure CPU bound applicationss, you'll get the best results keeping this number low
19
+ # Setting a value of 1 is great for superficial benchmarks, but in reality
20
+ # it's better to set this a bit higher to allow expensive requests to get overtaken and minimize head-of-line blocking
21
+ threads ENV.fetch('ITSI_THREADS', 3)
22
+
23
+ # If your application is IO bound (e.g. performing a lot of proxied HTTP requests, or heavy queries etc)
24
+ # you can see *substantial* benefits from enabling this option.
25
+ # To set this option, pass a string, not a class (as we will not have loaded the class yet)
26
+ # E.g.
27
+ # `fiber_scheduler "Itsi::Scheduler"` - The default fast and light-weight scheduler that comes with Itsi
28
+ # `fiber_scheduler "Async::Scheduler"` - Bring your own scheduler!
29
+ fiber_scheduler nil
30
+
31
+ # By default Itsi will run the Rack app from config.ru.
32
+ # You can provide an alternative Rack app file name here
33
+ # Or you can inline the app directly inside Itsi.rb.
34
+ # Only one of `run` and `rackup_file` can be used.
35
+ # E.g.
36
+ # require 'rack'
37
+ # run(Rack::Builder.app do
38
+ # use Rack::CommonLogger
39
+ # run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
40
+ # end)
41
+ rackup_file 'config.ru'
42
+
43
+ # If you bind to https, without specifying a certificate, Itsi will use a self-signed certificate.
44
+ # The self-signed certificate will use a CA generated for your host and stored inside `ITSI_LOCAL_CA_DIR` (Defaults to ~/.itsi)
45
+ # bind "https://localhost:3000"
46
+ # bind "https://localhost:3000?domains=dev.itsi.fyi"
47
+ #
48
+ # If you want to use let's encrypt to generate you a real certificate you and pass cert=acme and an acme_email address to generate one.
49
+ # bind "https://itsi.fyi?cert=acme&acme_email=admin@itsi.fyi"
50
+ # You can generate certificates for multiple domains at once, by passing a comma-separated list of domains
51
+ # bind "https://0.0.0.0?domains=foo.itsi.fyi,bar.itsi.fyi&cert=acme&acme_email=admin@itsi.fyi"
52
+ #
53
+ # If you already have a certificate you can specify it using the cert and key parameters
54
+ # bind "https://itsi.fyi?cert=/path/to/cert.pem&key=/path/to/key.pem"
55
+ #
56
+ # You can also bind to a unix socket or a tls unix socket. E.g.
57
+ # bind "unix:///tmp/itsi.sock"
58
+ # bind "tls:///tmp/itsi.secure.sock"
59
+
60
+ if env == 'development'
61
+ bind 'http://localhost:3000'
62
+ else
63
+ bind "https://0.0.0.0?domains=#{ENV['PRODUCTION_DOMAINS']}&cert=acme&acme_email=admin@itsi.fyi"
64
+ end
65
+
66
+ # If you want to preload the application, set preload to true
67
+ # to load the entire rack-app defined in rack_file_name before forking.
68
+ # Alternatively, you can preload just a specific set of gems in a group in your gemfile,
69
+ # by providing the group name here.
70
+ # E.g.
71
+ #
72
+ # preload :preload # Load gems inside the preload group
73
+ # preload false # Don't preload.
74
+ #
75
+ # If you want to be able to perform zero-downtime deploys using a single itsi process,
76
+ # you should disable preloads, so that the application is loaded fresh each time a new worker boots
77
+ preload true
78
+
79
+ # Set the maximum memory limit for each worker process in bytes
80
+ # When this limit is reached, the worker will be gracefully restarted.
81
+ # Only one worker is restarted at a time to ensure we don't take down
82
+ # all of them at once, if they reach the threshold simultaneously.
83
+ worker_memory_limit 48 * 1024 * 1024
84
+
85
+ # You can provide an optional block of code to run, when a worker hits its memory threshold (Use this to send yourself an alert,
86
+ # write metrics to disk etc. etc.)
87
+ after_memory_threshold_reached do |pid|
88
+ puts "Worker #{pid} has reached its memory threshold and will restart"
89
+ end
90
+
91
+ # Do clean up of any non-threadsafe resources before forking a new worker here.
92
+ before_fork {}
93
+
94
+ # Reinitialize any non-threadsafe resources after forking a new worker here.
95
+ after_fork {}
96
+
97
+ # Shutdown timeout
98
+ # Number of seconds to wait for workers to gracefully shutdown before killing them.
99
+ shutdown_timeout 5
100
+
101
+ # Set this to false for application environments that require rack.input to be a rewindable body
102
+ # (like Rails). For rack applications that can stream inputs, you can set this to true for a more memory-efficient approach.
103
+ stream_body false
104
+
105
+ # OOB GC responses threshold
106
+ # Specifies how frequently OOB gc should be triggered during periods where there is a gap in queued requests.
107
+ # Setting this too low can substantially worsen performance
108
+ oob_gc_responses_threshold 512
109
+
110
+ # Set this to false for application environments that require rack.input to be a rewindable body
111
+ # (like Rails). For rack applications that can stream inputs, you can set this to true for a more memory-efficient approach.
112
+ stream_body false
113
+
114
+ # OOB GC responses threshold
115
+ # Specifies how frequently OOB gc should be triggered during periods where there is a gap in queued requests.
116
+ # Setting this too low can substantially worsen performance
117
+ oob_gc_responses_threshold 512
118
+
119
+ # Log level
120
+ # Set this to one of the following values: debug, info, warn, error, fatal
121
+ # Can also be set using the ITSI_LOG environment variable
122
+ log_level :info
123
+
124
+ # Log Format
125
+ # Set this to be either :ansi or :json. If you leave it blank Itsi will try
126
+ # and auto-detect the format based on the TTY environment.
127
+ log_format :auto