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.

Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +2917 -0
  3. data/Cargo.toml +7 -0
  4. data/exe/itsi +88 -28
  5. data/ext/itsi_error/Cargo.toml +2 -0
  6. data/ext/itsi_error/src/from.rs +68 -0
  7. data/ext/itsi_error/src/lib.rs +13 -38
  8. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  9. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  10. data/ext/itsi_rb_helpers/Cargo.toml +2 -0
  11. data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  12. data/ext/itsi_rb_helpers/src/lib.rs +90 -10
  13. data/ext/itsi_scheduler/Cargo.toml +24 -0
  14. data/ext/itsi_scheduler/extconf.rb +6 -0
  15. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  16. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  17. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  18. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  19. data/ext/itsi_scheduler/src/lib.rs +38 -0
  20. data/ext/itsi_server/Cargo.lock +2956 -0
  21. data/ext/itsi_server/Cargo.toml +21 -3
  22. data/ext/itsi_server/extconf.rb +1 -1
  23. data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  24. data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  25. data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  26. data/ext/itsi_server/src/env.rs +43 -0
  27. data/ext/itsi_server/src/lib.rs +62 -7
  28. data/ext/itsi_server/src/request/itsi_request.rs +238 -104
  29. data/ext/itsi_server/src/response/itsi_response.rs +347 -0
  30. data/ext/itsi_server/src/response/mod.rs +1 -0
  31. data/ext/itsi_server/src/server/bind.rs +57 -25
  32. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  33. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  34. data/ext/itsi_server/src/server/itsi_server.rs +189 -134
  35. data/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
  36. data/ext/itsi_server/src/server/listener.rs +237 -137
  37. data/ext/itsi_server/src/server/mod.rs +7 -1
  38. data/ext/itsi_server/src/server/process_worker.rs +196 -0
  39. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +254 -0
  40. data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  41. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +257 -0
  42. data/ext/itsi_server/src/server/signal.rs +70 -0
  43. data/ext/itsi_server/src/server/thread_worker.rs +368 -0
  44. data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +132 -0
  45. data/ext/itsi_server/src/server/tls.rs +184 -60
  46. data/ext/itsi_tracing/Cargo.toml +4 -0
  47. data/ext/itsi_tracing/src/lib.rs +36 -6
  48. data/lib/itsi/index.html.erb +91 -0
  49. data/lib/itsi/request.rb +30 -14
  50. data/lib/itsi/server/rack/handler/itsi.rb +25 -0
  51. data/lib/itsi/server/scheduler_mode.rb +6 -0
  52. data/lib/itsi/server/version.rb +1 -1
  53. data/lib/itsi/server.rb +102 -2
  54. data/lib/itsi/signals.rb +23 -0
  55. data/lib/itsi/stream_io.rb +38 -0
  56. metadata +44 -27
  57. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
  58. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
  59. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  60. 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::{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
+
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<Certificate> {
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.into_iter().map(Certificate).collect()
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![Certificate(data)]
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) -> PrivateKey {
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 PrivateKey(keys[0].clone());
180
+ return PrivateKeyDer::try_from(keys[0].clone()).unwrap();
104
181
  }
105
182
  }
106
- PrivateKey(key_data)
183
+ PrivateKeyDer::try_from(key_data).unwrap()
107
184
  }
108
185
 
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.");
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(ITS_CA_KEY).unwrap();
113
- let params = CertificateParams::from_ca_cert_pem(ITS_CA_CERT).unwrap();
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 ca_cert = params.self_signed(&ca_kp).unwrap();
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
- // 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:
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, domain);
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, &ee_key).unwrap();
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 = Certificate(ee_cert_der);
131
- Ok((vec![ee_cert], PrivateKey(ee_key.serialize_der())))
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
- /// 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)
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
  }
@@ -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,41 @@
1
+ use std::env;
2
+
3
+ use atty::{Stream, is};
1
4
  pub use tracing::{debug, error, info, trace, warn};
2
- use tracing_subscriber::{EnvFilter, fmt};
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::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)
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
- .init();
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
- "HTTP_HOST" => host,
21
- "rack.input" => StringIO.new(body),
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.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("-", "_")}"
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.8"
6
6
  end
7
7
  end
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
- # Call our Rack app with our request ENV.
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