itsi-server 0.2.26-aarch64-linux → 0.2.27-aarch64-linux

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +5 -3
  3. data/Rakefile +18 -5
  4. data/ext/itsi_acme/Cargo.toml +2 -1
  5. data/ext/itsi_acme/src/acceptor.rs +1 -1
  6. data/ext/itsi_acme/src/acme.rs +31 -3
  7. data/ext/itsi_acme/src/http_challenge.rs +81 -0
  8. data/ext/itsi_acme/src/https_helper.rs +3 -1
  9. data/ext/itsi_acme/src/jose.rs +6 -2
  10. data/ext/itsi_acme/src/lib.rs +2 -0
  11. data/ext/itsi_acme/src/resolver.rs +27 -4
  12. data/ext/itsi_acme/src/state.rs +183 -22
  13. data/ext/itsi_scheduler/Cargo.toml +1 -1
  14. data/ext/itsi_scheduler/src/itsi_scheduler.rs +115 -64
  15. data/ext/itsi_scheduler/src/lib.rs +2 -1
  16. data/ext/itsi_server/Cargo.lock +2 -2
  17. data/ext/itsi_server/Cargo.toml +2 -1
  18. data/ext/itsi_server/src/lib.rs +15 -0
  19. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +0 -1
  20. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +9 -0
  21. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +114 -4
  22. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +22 -1
  23. data/ext/itsi_server/src/ruby_types/itsi_server.rs +100 -0
  24. data/ext/itsi_server/src/server/binds/listener.rs +9 -24
  25. data/ext/itsi_server/src/server/binds/tls.rs +372 -67
  26. data/ext/itsi_server/src/server/middleware_stack/mod.rs +3 -9
  27. data/ext/itsi_server/src/server/signal.rs +14 -9
  28. data/ext/itsi_server/src/services/itsi_http_service.rs +51 -10
  29. data/lib/itsi/http_request.rb +10 -0
  30. data/lib/itsi/server/3.1/itsi_server.so +0 -0
  31. data/lib/itsi/server/3.2/itsi_server.so +0 -0
  32. data/lib/itsi/server/3.3/itsi_server.so +0 -0
  33. data/lib/itsi/server/3.4/itsi_server.so +0 -0
  34. data/lib/itsi/server/4.0/itsi_server.so +0 -0
  35. data/lib/itsi/server/config/options/certificates.md +37 -9
  36. data/lib/itsi/server/config/options/fiber_scheduler.md +2 -0
  37. data/lib/itsi/server/default_config/Itsi.rb +4 -0
  38. data/lib/itsi/server/rack_interface.rb +47 -3
  39. data/lib/itsi/server/version.rb +1 -1
  40. data/lib/itsi/server.rb +24 -0
  41. metadata +3 -2
@@ -1,16 +1,17 @@
1
1
  use crate::default_responses::{NOT_FOUND_RESPONSE, TIMEOUT_RESPONSE};
2
2
  use crate::ruby_types::itsi_server::itsi_server_config::ItsiServerTokenPreference;
3
3
  use crate::server::http_message_types::{
4
- ConversionExt, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
4
+ ConversionExt, HttpBody, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
5
5
  };
6
6
  use crate::server::lifecycle_event::LifecycleEvent;
7
7
  use crate::server::middleware_stack::MiddlewareLayer;
8
8
  use crate::server::serve_strategy::acceptor::AcceptorArgs;
9
9
  use crate::server::signal::{send_lifecycle_event, SHUTDOWN_REQUESTED};
10
+ use bytes::Bytes;
10
11
  use chrono::{self, DateTime, Local};
11
12
  use either::Either;
12
13
  use http::header::ACCEPT_ENCODING;
13
- use http::{HeaderValue, Request};
14
+ use http::{HeaderValue, Request, StatusCode};
14
15
  use hyper::body::Incoming;
15
16
  use regex::Regex;
16
17
  use smallvec::SmallVec;
@@ -174,6 +175,7 @@ impl HttpRequestContext {
174
175
  const SERVER_TOKEN_VERSION: HeaderValue =
175
176
  HeaderValue::from_static(concat!("Itsi/", env!("CARGO_PKG_VERSION")));
176
177
  const SERVER_TOKEN_NAME: HeaderValue = HeaderValue::from_static("Itsi");
178
+ const TEXT_PLAIN_UTF8: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8");
177
179
 
178
180
  impl ItsiHttpService {
179
181
  pub async fn handle_request(&self, req: Request<Incoming>) -> itsi_error::Result<HttpResponse> {
@@ -188,14 +190,15 @@ impl ItsiHttpService {
188
190
  let token_preference = self.server_params.itsi_server_token_preference;
189
191
 
190
192
  let service_future = async move {
191
- let middleware_stack = self
192
- .server_params
193
- .middleware
194
- .get()
195
- .unwrap()
196
- .stack_for(&req)
197
- .unwrap();
198
- let (stack, matching_pattern) = middleware_stack;
193
+ if let Some(acme_response) = self.acme_http01_response(&req) {
194
+ return Ok(acme_response);
195
+ }
196
+
197
+ let Some((stack, matching_pattern)) =
198
+ self.server_params.middleware.get().unwrap().stack_for(&req)
199
+ else {
200
+ return Ok(NOT_FOUND_RESPONSE.to_http_response(accept).await);
201
+ };
199
202
  let mut resp: Option<HttpResponse> = None;
200
203
 
201
204
  let mut context =
@@ -267,4 +270,42 @@ impl ItsiHttpService {
267
270
  service_future.await
268
271
  }
269
272
  }
273
+
274
+ fn acme_http01_response(&self, req: &HttpRequest) -> Option<HttpResponse> {
275
+ let host = normalize_host_header(req.header("host")?)?;
276
+ let managers = self.server_params.acme_managers.read();
277
+ let key_authorization = managers
278
+ .iter()
279
+ .find_map(|(_, manager)| manager.http01_response(host, req.uri().path()))?;
280
+
281
+ let mut builder = http::Response::builder()
282
+ .status(StatusCode::OK)
283
+ .header(http::header::CONTENT_TYPE, TEXT_PLAIN_UTF8);
284
+
285
+ if req.method() == http::Method::HEAD {
286
+ builder = builder.header(http::header::CONTENT_LENGTH, "0");
287
+ return builder.body(HttpBody::empty()).ok();
288
+ }
289
+
290
+ builder
291
+ .header(
292
+ http::header::CONTENT_LENGTH,
293
+ key_authorization.len().to_string(),
294
+ )
295
+ .body(HttpBody::full(Bytes::from(key_authorization)))
296
+ .ok()
297
+ }
298
+ }
299
+
300
+ fn normalize_host_header(host: &str) -> Option<&str> {
301
+ let host = host.trim();
302
+ if host.is_empty() {
303
+ return None;
304
+ }
305
+
306
+ if let Some(stripped) = host.strip_prefix('[') {
307
+ return stripped.split(']').next();
308
+ }
309
+
310
+ Some(host.split(':').next().unwrap_or(host))
270
311
  }
@@ -145,6 +145,16 @@ module Itsi
145
145
  end
146
146
  end
147
147
 
148
+ def partial_hijack
149
+ UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
150
+ server_sock.autoclose = false
151
+ response.partial_hijack(server_sock.fileno)
152
+ server_sock.sync = true
153
+ app_sock.sync = true
154
+ app_sock
155
+ end
156
+ end
157
+
148
158
  # Rack expects env["rack.hijack"] to respond to #call.
149
159
  def call
150
160
  hijack
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -54,12 +54,40 @@ Let's Encrypt enforces strict rate limits on production certificate generation.
54
54
  {{< /callout >}}
55
55
 
56
56
 
57
- {{< callout type="warn" >}}
58
- Currently only the TLS-ALPN-01 challenge type is supported for automated certificates.
59
- The HTTP-01 challenge is not *yet* supported. This means that, for e.g. if your server is sitting behind a CDN or reverse proxy that performs HTTPS termination, you will not be able to rely on the *automated* certificate generation for fully automated, verified e2e encryption.
60
-
61
- Instead you may wish to use:
62
- * [Self-signed](#development--self-signed) certificates
63
- * [Manually](#existing-certificates) install certificates
64
- * Use HTTP between the CDN and the server
65
- {{< /callout >}}
57
+ Itsi supports both ACME challenge types that matter for common deployments:
58
+
59
+ * `TLS-ALPN-01` is used when the certificate authority can reach Itsi directly on the HTTPS listener.
60
+ * `HTTP-01` can be used when you also expose a reachable HTTP listener for the same hostname. In real Let's Encrypt deployments this typically means port `80` must reach Itsi for `/.well-known/acme-challenge/*`.
61
+
62
+ This means setups behind a CDN, WAF, or TLS-terminating proxy can still use automated certificates, provided plain HTTP validation traffic is forwarded to Itsi.
63
+
64
+ E.g. a production configuration that allows HTTP-01 fallback might look like this:
65
+
66
+ ```ruby {filename=Itsi.rb}
67
+ bind "http://0.0.0.0:80"
68
+ bind "https://0.0.0.0:443?cert=acme&domains=example.com&acme_email=you@example.com"
69
+ ```
70
+
71
+ ## Dynamic Domain Registration
72
+ You can add or remove ACME-managed domains while Itsi is already running.
73
+
74
+ This is useful when hostnames are discovered dynamically by your Ruby application, or when you want to defer certificate issuance until a tenant, customer, or site is activated.
75
+
76
+ Runtime APIs:
77
+
78
+ * `Itsi::Server.tls_bindings`
79
+ * `Itsi::Server.tls_domains(listener_id = nil)`
80
+ * `Itsi::Server.tls_domain_statuses(listener_id = nil)`
81
+ * `Itsi::Server.register_tls_domain(domain, listener_id = nil)`
82
+ * `Itsi::Server.unregister_tls_domain(domain, listener_id = nil)`
83
+
84
+ Example:
85
+
86
+ ```ruby
87
+ Itsi::Server.register_tls_domain("customer-a.example.com")
88
+
89
+ status = Itsi::Server.tls_domain_statuses.find { |entry| entry["domain"] == "customer-a.example.com" }
90
+ puts status
91
+ ```
92
+
93
+ When using dynamic issuance with HTTP-01, the same requirement still applies: the domain being issued must be able to reach an Itsi-managed HTTP listener for the ACME challenge path.
@@ -7,6 +7,8 @@ This allows Itsi to process a very large number of IO heavy requests concurrentl
7
7
 
8
8
  Enabling Fiber Scheduler mode can drastically improve application performance if you perform large amounts of blocking IO operations.
9
9
 
10
+ Itsi's bundled scheduler is intended to be practical for real Ruby applications, not just toy socket examples. It integrates with the scheduler hooks used by modern Rubies for socket I/O, DNS lookups, sleeps and timeouts, and process waiting.
11
+
10
12
 
11
13
  ## Configuration File
12
14
  ```ruby {filename="Itsi.rb"}
@@ -37,6 +37,10 @@ fiber_scheduler nil
37
37
  # bind "https://itsi.fyi?cert=acme&acme_email=admin@itsi.fyi"
38
38
  # You can generate certificates for multiple domains at once, by passing a comma-separated list of domains
39
39
  # bind "https://0.0.0.0?domains=foo.itsi.fyi,bar.itsi.fyi&cert=acme&acme_email=admin@itsi.fyi"
40
+ # If HTTPS on 443 is not directly reachable, you can also expose an HTTP listener and
41
+ # Let's Encrypt will be able to validate using HTTP-01 instead.
42
+ # bind "http://0.0.0.0:80"
43
+ # bind "https://0.0.0.0:443?domains=foo.itsi.fyi&cert=acme&acme_email=admin@itsi.fyi"
40
44
  #
41
45
  # If you already have a certificate you can specify it using the cert and key parameters
42
46
  # bind "https://itsi.fyi?cert=/path/to/cert.pem&key=/path/to/key.pem"
@@ -1,6 +1,45 @@
1
1
  module Itsi
2
2
  class Server
3
3
  module RackInterface
4
+ class PartialHijackStream
5
+ def initialize(response)
6
+ @response = response
7
+ end
8
+
9
+ def write(chunk)
10
+ @response.write(chunk.to_s)
11
+ end
12
+
13
+ def read(*)
14
+ nil
15
+ end
16
+
17
+ def <<(chunk)
18
+ write(chunk)
19
+ self
20
+ end
21
+
22
+ def flush
23
+ self
24
+ end
25
+
26
+ def close_write
27
+ @response.close_write
28
+ end
29
+
30
+ def close_read
31
+ true
32
+ end
33
+
34
+ def close
35
+ @response.close_write
36
+ end
37
+
38
+ def closed?
39
+ @response.closed?
40
+ end
41
+ end
42
+
4
43
  # Builds a handler proc that is compatible with Rack applications.
5
44
  def self.for(app)
6
45
  require "rack"
@@ -38,7 +77,8 @@ module Itsi
38
77
  response.status = status
39
78
 
40
79
  # 2. Set Headers
41
- body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
80
+ hijack_callback = headers.delete("rack.hijack")
81
+ body_streamer = streaming_body?(body) ? body : hijack_callback
42
82
 
43
83
  response.reserve_headers(headers.size)
44
84
 
@@ -57,10 +97,14 @@ module Itsi
57
97
  # the server will begin to stream it to the client.
58
98
 
59
99
 
60
- if body_streamer
100
+ if hijack_callback
101
+ stream = status == 101 ? request.partial_hijack : PartialHijackStream.new(response)
102
+ body_streamer.call(stream)
103
+ elsif body_streamer
61
104
  # If we're partially hijacked or returned a streaming body,
62
105
  # stream this response.
63
- body_streamer.call(response)
106
+ stream = status == 101 ? request.partial_hijack : response
107
+ body_streamer.call(stream)
64
108
 
65
109
  elsif body.is_a?(Array)
66
110
  if body.length == 1
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.26"
5
+ VERSION = "0.2.27"
6
6
  end
7
7
  end
data/lib/itsi/server.rb CHANGED
@@ -32,6 +32,30 @@ module Itsi
32
32
  @running && !@running.empty?
33
33
  end
34
34
 
35
+ def current_server
36
+ @running&.last || raise("No running Itsi::Server instance")
37
+ end
38
+
39
+ def tls_bindings
40
+ current_server.tls_bindings
41
+ end
42
+
43
+ def tls_domains(listener_id = nil)
44
+ current_server.tls_domains(listener_id)
45
+ end
46
+
47
+ def tls_domain_statuses(listener_id = nil)
48
+ current_server.tls_domain_statuses(listener_id)
49
+ end
50
+
51
+ def register_tls_domain(domain, listener_id = nil)
52
+ current_server.register_tls_domain(domain, listener_id)
53
+ end
54
+
55
+ def unregister_tls_domain(domain, listener_id = nil)
56
+ current_server.unregister_tls_domain(domain, listener_id)
57
+ end
58
+
35
59
  def start_in_background_thread(cli_params = {}, &blk)
36
60
  @background_threads ||= []
37
61
  server, background_thread = start(cli_params, background: true, &blk)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.26
4
+ version: 0.2.27
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-13 00:00:00.000000000 Z
11
+ date: 2026-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -109,6 +109,7 @@ files:
109
109
  - ext/itsi_acme/src/caches/no.rs
110
110
  - ext/itsi_acme/src/caches/test.rs
111
111
  - ext/itsi_acme/src/config.rs
112
+ - ext/itsi_acme/src/http_challenge.rs
112
113
  - ext/itsi_acme/src/https_helper.rs
113
114
  - ext/itsi_acme/src/incoming.rs
114
115
  - ext/itsi_acme/src/jose.rs