itsi-server 0.1.1 → 0.1.6

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/exe/itsi +88 -28
  3. data/ext/itsi_error/Cargo.toml +2 -0
  4. data/ext/itsi_error/src/from.rs +71 -0
  5. data/ext/itsi_error/src/lib.rs +12 -37
  6. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  7. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  8. data/ext/itsi_rb_helpers/Cargo.toml +2 -0
  9. data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  10. data/ext/itsi_rb_helpers/src/lib.rs +90 -10
  11. data/ext/itsi_scheduler/Cargo.toml +24 -0
  12. data/ext/itsi_scheduler/extconf.rb +6 -0
  13. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  14. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  15. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  16. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  17. data/ext/itsi_scheduler/src/lib.rs +38 -0
  18. data/ext/itsi_server/Cargo.toml +20 -3
  19. data/ext/itsi_server/extconf.rb +1 -1
  20. data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  21. data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  22. data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  23. data/ext/itsi_server/src/lib.rs +61 -7
  24. data/ext/itsi_server/src/request/itsi_request.rs +238 -104
  25. data/ext/itsi_server/src/response/itsi_response.rs +347 -0
  26. data/ext/itsi_server/src/response/mod.rs +1 -0
  27. data/ext/itsi_server/src/server/bind.rs +54 -23
  28. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  29. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  30. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
  31. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
  32. data/ext/itsi_server/src/server/itsi_server.rs +196 -134
  33. data/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
  34. data/ext/itsi_server/src/server/listener.rs +241 -132
  35. data/ext/itsi_server/src/server/mod.rs +7 -1
  36. data/ext/itsi_server/src/server/process_worker.rs +196 -0
  37. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +254 -0
  38. data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  39. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +247 -0
  40. data/ext/itsi_server/src/server/signal.rs +70 -0
  41. data/ext/itsi_server/src/server/thread_worker.rs +368 -0
  42. data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +132 -0
  43. data/ext/itsi_server/src/server/tls.rs +137 -52
  44. data/ext/itsi_tracing/Cargo.toml +4 -0
  45. data/ext/itsi_tracing/src/lib.rs +36 -6
  46. data/lib/itsi/request.rb +30 -14
  47. data/lib/itsi/server/rack/handler/itsi.rb +25 -0
  48. data/lib/itsi/server/scheduler_mode.rb +6 -0
  49. data/lib/itsi/server/version.rb +1 -1
  50. data/lib/itsi/server.rb +82 -2
  51. data/lib/itsi/signals.rb +23 -0
  52. data/lib/itsi/stream_io.rb +38 -0
  53. metadata +39 -25
  54. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  55. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
@@ -1,20 +1,104 @@
1
1
  use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
- use itsi_tracing::{info, warn};
3
+ use itsi_tracing::info;
4
+ use locked_dir_cache::LockedDirCache;
4
5
  use rcgen::{CertificateParams, DnType, KeyPair, SanType};
6
+ use rustls::{
7
+ pki_types::{CertificateDer, PrivateKeyDer},
8
+ ClientConfig, RootCertStore,
9
+ };
5
10
  use rustls_pemfile::{certs, pkcs8_private_keys};
6
- use std::{collections::HashMap, fs, io::BufReader};
7
- use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig};
8
-
11
+ use std::{
12
+ collections::HashMap,
13
+ env, fs,
14
+ io::{BufReader, Error},
15
+ sync::Arc,
16
+ };
17
+ use tokio::sync::Mutex;
18
+ use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
19
+ use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
20
+ mod locked_dir_cache;
9
21
  const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
10
22
  const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
11
23
 
24
+ #[derive(Clone)]
25
+ pub enum ItsiTlsAcceptor {
26
+ Manual(TlsAcceptor),
27
+ Automatic(
28
+ AcmeAcceptor,
29
+ Arc<Mutex<AcmeState<Error>>>,
30
+ Arc<ServerConfig>,
31
+ ),
32
+ }
33
+
12
34
  // Generates a TLS configuration based on either :
13
35
  // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
14
36
  // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
15
37
  // If a non-local host or optional domain parameter is provided,
16
38
  // 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> {
39
+ pub fn configure_tls(
40
+ host: &str,
41
+ query_params: &HashMap<String, String>,
42
+ ) -> Result<ItsiTlsAcceptor> {
43
+ let domains = query_params
44
+ .get("domains")
45
+ .map(|v| v.split(',').map(String::from).collect::<Vec<_>>());
46
+
47
+ if query_params.get("cert").is_none() || query_params.get("key").is_none() {
48
+ if let Some(domains) = domains {
49
+ let directory_url = env::var("ACME_DIRECTORY_URL")
50
+ .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string());
51
+ info!(
52
+ domains = format!("{:?}", domains),
53
+ directory_url, "Requesting acme cert"
54
+ );
55
+
56
+ let mut root_cert_store = RootCertStore::empty();
57
+ if let Ok(ca_pem_path) = env::var("ITSI_ACME_CA_PEM_PATH") {
58
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
59
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
60
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
61
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
62
+ .map_err(|e| {
63
+ itsi_error::ItsiError::ArgumentError(format!(
64
+ "Invalid ACME CA Pem path {:?}",
65
+ e
66
+ ))
67
+ })?;
68
+ root_cert_store.add_parsable_certificates(der_certs);
69
+ }
70
+
71
+ let client_config = ClientConfig::builder()
72
+ .with_root_certificates(root_cert_store)
73
+ .with_no_client_auth();
74
+
75
+ let contact_email = env::var("ITSI_ACME_CONTACT_EMAIL").map_err(|_| {
76
+ itsi_error::ItsiError::ArgumentError(
77
+ "ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
78
+ .to_string(),
79
+ )
80
+ })?;
81
+
82
+ let cache_dir = env::var("ITSI_ACME_CACHE_DIR")
83
+ .unwrap_or_else(|_| "./.rustls_acme_cache".to_string());
84
+
85
+ let acme_state = AcmeConfig::new(domains)
86
+ .contact([format!("mailto:{}", contact_email)])
87
+ .cache(LockedDirCache::new(cache_dir))
88
+ .directory(directory_url)
89
+ .client_tls_config(Arc::new(client_config))
90
+ .state();
91
+ let rustls_config = ServerConfig::builder()
92
+ .with_no_client_auth()
93
+ .with_cert_resolver(acme_state.resolver());
94
+ let acceptor = acme_state.acceptor();
95
+ return Ok(ItsiTlsAcceptor::Automatic(
96
+ acceptor,
97
+ Arc::new(Mutex::new(acme_state)),
98
+ Arc::new(rustls_config),
99
+ ));
100
+ }
101
+ }
18
102
  let (certs, key) = if let (Some(cert_path), Some(key_path)) =
19
103
  (query_params.get("cert"), query_params.get("key"))
20
104
  {
@@ -22,41 +106,20 @@ pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Resu
22
106
  let certs = load_certs(cert_path);
23
107
  let key = load_private_key(key_path);
24
108
  (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
109
  } else {
46
- generate_ca_signed_cert(host)?
110
+ generate_ca_signed_cert(vec![host.to_owned()])?
47
111
  };
48
112
 
49
113
  let mut config = ServerConfig::builder()
50
- .with_safe_defaults()
51
114
  .with_no_client_auth()
52
115
  .with_single_cert(certs, key)
53
116
  .expect("Failed to build TLS config");
54
117
 
55
118
  config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
56
- Ok(config)
119
+ Ok(ItsiTlsAcceptor::Manual(TlsAcceptor::from(Arc::new(config))))
57
120
  }
58
121
 
59
- pub fn load_certs(path: &str) -> Vec<Certificate> {
122
+ pub fn load_certs(path: &str) -> Vec<CertificateDer<'static>> {
60
123
  let data = if let Some(stripped) = path.strip_prefix("base64:") {
61
124
  general_purpose::STANDARD
62
125
  .decode(stripped)
@@ -74,14 +137,20 @@ pub fn load_certs(path: &str) -> Vec<Certificate> {
74
137
  })
75
138
  .collect::<Result<_>>()
76
139
  .expect("Failed to parse certificate file");
77
- certs_der.into_iter().map(Certificate).collect()
140
+ certs_der
141
+ .into_iter()
142
+ .map(|vec| {
143
+ // Convert the owned Vec<u8> into a CertificateDer and force 'static.
144
+ unsafe { std::mem::transmute(CertificateDer::from(vec)) }
145
+ })
146
+ .collect()
78
147
  } else {
79
- vec![Certificate(data)]
148
+ vec![CertificateDer::from(data)]
80
149
  }
81
150
  }
82
151
 
83
152
  /// Loads a private key from a file or Base64.
84
- pub fn load_private_key(path: &str) -> PrivateKey {
153
+ pub fn load_private_key(path: &str) -> PrivateKeyDer<'static> {
85
154
  let key_data = if let Some(stripped) = path.strip_prefix("base64:") {
86
155
  general_purpose::STANDARD
87
156
  .decode(stripped)
@@ -100,39 +169,55 @@ pub fn load_private_key(path: &str) -> PrivateKey {
100
169
  .collect::<Result<_>>()
101
170
  .expect("Failed to parse private key");
102
171
  if !keys.is_empty() {
103
- return PrivateKey(keys[0].clone());
172
+ return PrivateKeyDer::try_from(keys[0].clone()).unwrap();
104
173
  }
105
174
  }
106
- PrivateKey(key_data)
175
+ PrivateKeyDer::try_from(key_data).unwrap()
107
176
  }
108
177
 
109
- pub fn generate_ca_signed_cert(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
178
+ pub fn generate_ca_signed_cert(
179
+ domains: Vec<String>,
180
+ ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
110
181
  info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
111
182
 
112
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).unwrap();
113
- let params = CertificateParams::from_ca_cert_pem(ITS_CA_CERT).unwrap();
183
+ let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
184
+ let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
185
+ .expect("Failed to parse embedded CA certificate")
186
+ .self_signed(&ca_kp)
187
+ .expect("Failed to self-sign embedded CA cert");
114
188
 
115
- let ca_cert = params.self_signed(&ca_kp).unwrap();
116
- let ee_key = KeyPair::generate().unwrap();
189
+ let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
117
190
  let mut ee_params = CertificateParams::default();
118
191
 
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:
192
+ info!(
193
+ "Generated certificate will be valid for domains {:?}",
194
+ domains
195
+ );
196
+ use std::net::IpAddr;
197
+
198
+ ee_params.subject_alt_names = domains
199
+ .iter()
200
+ .map(|domain| {
201
+ if let Ok(ip) = domain.parse::<IpAddr>() {
202
+ SanType::IpAddress(ip)
203
+ } else {
204
+ SanType::DnsName(domain.clone().try_into().unwrap())
205
+ }
206
+ })
207
+ .collect();
208
+
122
209
  ee_params
123
210
  .distinguished_name
124
- .push(DnType::CommonName, domain);
211
+ .push(DnType::CommonName, domains[0].clone());
125
212
 
126
213
  ee_params.use_authority_key_identifier_extension = true;
127
214
 
128
- let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ee_key).unwrap();
215
+ let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ca_kp).unwrap();
129
216
  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())))
132
- }
133
-
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)
217
+ let ee_cert = CertificateDer::from(ee_cert_der);
218
+ let ca_cert = CertificateDer::from(ca_cert.der().to_vec());
219
+ Ok((
220
+ vec![ee_cert, ca_cert],
221
+ PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
222
+ ))
138
223
  }
@@ -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
  }
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
+ "Use ENV['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.6"
6
6
  end
7
7
  end
data/lib/itsi/server.rb CHANGED
@@ -2,12 +2,92 @@
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"
5
9
 
6
10
  module Itsi
7
11
  class Server
8
- # Call our Rack app with our request ENV.
12
+
13
+ def self.running?
14
+ @running ||= false
15
+ end
16
+
17
+ def self.start(app:, **opts)
18
+ server = new(app: ->{app}, **opts)
19
+ @running = true
20
+ Signal.trap('INT', 'DEFAULT')
21
+ server.start
22
+ ensure
23
+ @running = false
24
+ end
25
+
9
26
  def self.call(app, request)
10
- app.call(request.to_env)
27
+ respond request, app.call(request.to_env)
28
+ end
29
+
30
+ def self.streaming_body?(body)
31
+ body.respond_to?(:call) && !body.respond_to?(:each)
32
+ end
33
+
34
+ def self.respond(request, (status, headers, body))
35
+ response = request.response
36
+
37
+ # Don't try and respond if we've been hijacked.
38
+ # The hijacker is now responsible for this.
39
+ return if request.hijacked
40
+
41
+ # 1. Set Status
42
+ response.status = status
43
+
44
+ # 2. Set Headers
45
+ headers.each do |key, value|
46
+ next response.add_header(key, value) unless value.is_a?(Array)
47
+
48
+ value.each do |v|
49
+ response.add_header(key, v)
50
+ end
51
+ end
52
+
53
+ # 3. Set Body
54
+ # As soon as we start setting the response
55
+ # the server will begin to stream it to the client.
56
+
57
+ # If we're partially hijacked or returned a streaming body,
58
+ # stream this response.
59
+
60
+ if (body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack"))
61
+ body_streamer.call(StreamIO.new(response))
62
+
63
+ # If we're enumerable with more than one chunk
64
+ # also stream, otherwise write in a single chunk
65
+ elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
66
+ unless body.respond_to?(:each)
67
+ body = body.to_ary
68
+ raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
69
+ end
70
+ # We offset this iteration intentionally,
71
+ # to optimize for the case where there's only one chunk.
72
+ buffer = nil
73
+ body.each do |part|
74
+ response.send_frame(buffer.to_s) if buffer
75
+ buffer = part
76
+ end
77
+
78
+ response.send_and_close(buffer.to_s)
79
+ else
80
+ response.send_and_close(body.to_s)
81
+ end
82
+ ensure
83
+ response.close_write
84
+ body.close if body.respond_to?(:close)
85
+ end
86
+
87
+ def self.start_scheduler_loop(scheduler_class, scheduler_task)
88
+ scheduler = scheduler_class.new
89
+ Fiber.set_scheduler(scheduler)
90
+ [scheduler, Fiber.schedule(&scheduler_task)]
11
91
  end
12
92
 
13
93
  # If scheduler is enabled
@@ -0,0 +1,23 @@
1
+ module Itsi
2
+ module Signals
3
+ DEFAULT_SIGNALS = ["DEFAULT", ""].freeze
4
+ module SignalTrap
5
+ def self.trap(signal, *args, &block)
6
+ if DEFAULT_SIGNALS.include?(command.to_s) && block.nil?
7
+ Itsi::Server.reset_signal_handlers
8
+ nil
9
+ else
10
+ super(signal, *args, &block)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ [Kernel, Signal].each do |receiver|
18
+ receiver.singleton_class.prepend(Itsi::Signals::SignalTrap)
19
+ end
20
+
21
+ [Object].each do |receiver|
22
+ receiver.include(Itsi::Signals::SignalTrap)
23
+ end
@@ -0,0 +1,38 @@
1
+ module Itsi
2
+ class StreamIO
3
+ attr :response
4
+ def initialize(response)
5
+ @response = response
6
+ end
7
+
8
+ def read
9
+ response.recv_frame
10
+ end
11
+
12
+ def write(string)
13
+ response.send_frame(string)
14
+ end
15
+
16
+ def <<(string)
17
+ response.send_frame(string)
18
+ end
19
+
20
+ def flush
21
+ # No-op
22
+ end
23
+
24
+ def close
25
+ close_read
26
+ close_write
27
+ end
28
+
29
+ def close_read
30
+ response.close_read
31
+ end
32
+
33
+ def close_write
34
+ response.close_write
35
+ end
36
+
37
+ end
38
+ end