kino 0.5.0 → 0.6.0

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.
data/ext/kino/src/tls.rs CHANGED
@@ -17,7 +17,13 @@ fn pem_bytes(input: &str) -> Result<Vec<u8>, String> {
17
17
  }
18
18
  }
19
19
 
20
- pub fn build_acceptor(cert: &str, key: &str) -> Result<TlsAcceptor, String> {
20
+ pub fn build_acceptor(cert: &str, key: &str, http2: bool) -> Result<TlsAcceptor, String> {
21
+ Ok(TlsAcceptor::from(Arc::new(server_config(
22
+ cert, key, http2,
23
+ )?)))
24
+ }
25
+
26
+ fn server_config(cert: &str, key: &str, http2: bool) -> Result<ServerConfig, String> {
21
27
  let cert_pem = pem_bytes(cert)?;
22
28
  let key_pem = pem_bytes(key)?;
23
29
 
@@ -36,14 +42,19 @@ pub fn build_acceptor(cert: &str, key: &str) -> Result<TlsAcceptor, String> {
36
42
  .with_no_client_auth()
37
43
  .with_single_cert(certs, key)
38
44
  .map_err(|e| format!("TLS config rejected: {e}"))?;
39
- config.alpn_protocols = vec![b"http/1.1".to_vec()];
40
-
41
- Ok(TlsAcceptor::from(Arc::new(config)))
45
+ // Preference order matters: h2 first, so an HTTP/2-capable client
46
+ // negotiates it; a client offering only http/1.1 still matches.
47
+ config.alpn_protocols = if http2 {
48
+ vec![b"h2".to_vec(), b"http/1.1".to_vec()]
49
+ } else {
50
+ vec![b"http/1.1".to_vec()]
51
+ };
52
+ Ok(config)
42
53
  }
43
54
 
44
55
  #[cfg(test)]
45
56
  mod tests {
46
- use super::build_acceptor;
57
+ use super::{build_acceptor, server_config};
47
58
 
48
59
  const CERT: &str = "-----BEGIN CERTIFICATE-----
49
60
  MIIBfjCCASWgAwIBAgIUTs9+cVJSzJjy4TSi9YLEd+i4KiIwCgYIKoZIzj0EAwIw
@@ -65,12 +76,12 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
65
76
 
66
77
  #[test]
67
78
  fn inline_pem_builds_an_acceptor() {
68
- assert!(build_acceptor(CERT, KEY).is_ok());
79
+ assert!(build_acceptor(CERT, KEY, true).is_ok());
69
80
  }
70
81
 
71
82
  #[test]
72
83
  fn missing_file_paths_error_without_panicking() {
73
- let err = build_acceptor("/nonexistent/cert.pem", "/nonexistent/key.pem")
84
+ let err = build_acceptor("/nonexistent/cert.pem", "/nonexistent/key.pem", true)
74
85
  .err()
75
86
  .expect("missing files");
76
87
  assert!(err.contains("cannot read"));
@@ -78,13 +89,15 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
78
89
 
79
90
  #[test]
80
91
  fn pem_without_certificates_is_rejected() {
81
- let err = build_acceptor(KEY, KEY).err().expect("a key is not a cert");
92
+ let err = build_acceptor(KEY, KEY, true)
93
+ .err()
94
+ .expect("a key is not a cert");
82
95
  assert!(err.contains("no certificates found"));
83
96
  }
84
97
 
85
98
  #[test]
86
99
  fn pem_without_a_key_is_rejected() {
87
- let err = build_acceptor(CERT, CERT)
100
+ let err = build_acceptor(CERT, CERT, true)
88
101
  .err()
89
102
  .expect("a cert is not a key");
90
103
  assert!(err.contains("no private key found"));
@@ -95,9 +108,106 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
95
108
  let err = build_acceptor(
96
109
  CERT,
97
110
  "-----BEGIN PRIVATE KEY-----\ngarbage\n-----END PRIVATE KEY-----",
111
+ true,
98
112
  )
99
113
  .err()
100
114
  .expect("garbage key");
101
115
  assert!(!err.is_empty());
102
116
  }
117
+
118
+ #[test]
119
+ fn alpn_advertises_h2_first_when_http2_is_on() {
120
+ let config = server_config(CERT, KEY, true).expect("valid PEM");
121
+ assert_eq!(
122
+ config.alpn_protocols,
123
+ vec![b"h2".to_vec(), b"http/1.1".to_vec()]
124
+ );
125
+ }
126
+
127
+ #[test]
128
+ fn alpn_advertises_only_http11_when_http2_is_off() {
129
+ let config = server_config(CERT, KEY, false).expect("valid PEM");
130
+ assert_eq!(config.alpn_protocols, vec![b"http/1.1".to_vec()]);
131
+ }
132
+
133
+ /// A real rustls handshake over an in-memory pipe: an h2-capable
134
+ /// client must land on "h2", proving the advertisement is not just a
135
+ /// config field but what the wire negotiates. The client skips
136
+ /// certificate verification (the fixture cert is CA-flagged, which
137
+ /// webpki refuses as an end-entity cert): ALPN is under test here,
138
+ /// not the trust chain.
139
+ #[tokio::test]
140
+ async fn handshake_negotiates_h2_with_a_capable_client() {
141
+ use std::sync::Arc;
142
+
143
+ use tokio_rustls::rustls::client::danger::{
144
+ HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
145
+ };
146
+ use tokio_rustls::rustls::crypto::WebPkiSupportedAlgorithms;
147
+ use tokio_rustls::rustls::pki_types::ServerName;
148
+ use tokio_rustls::rustls::ClientConfig;
149
+ use tokio_rustls::{TlsAcceptor, TlsConnector};
150
+
151
+ #[derive(Debug)]
152
+ struct AcceptAny(WebPkiSupportedAlgorithms);
153
+
154
+ impl ServerCertVerifier for AcceptAny {
155
+ fn verify_server_cert(
156
+ &self,
157
+ _end_entity: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
158
+ _intermediates: &[tokio_rustls::rustls::pki_types::CertificateDer<'_>],
159
+ _server_name: &ServerName<'_>,
160
+ _ocsp_response: &[u8],
161
+ _now: tokio_rustls::rustls::pki_types::UnixTime,
162
+ ) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
163
+ Ok(ServerCertVerified::assertion())
164
+ }
165
+
166
+ fn verify_tls12_signature(
167
+ &self,
168
+ _message: &[u8],
169
+ _cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
170
+ _dss: &tokio_rustls::rustls::DigitallySignedStruct,
171
+ ) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
172
+ Ok(HandshakeSignatureValid::assertion())
173
+ }
174
+
175
+ fn verify_tls13_signature(
176
+ &self,
177
+ _message: &[u8],
178
+ _cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
179
+ _dss: &tokio_rustls::rustls::DigitallySignedStruct,
180
+ ) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
181
+ Ok(HandshakeSignatureValid::assertion())
182
+ }
183
+
184
+ fn supported_verify_schemes(&self) -> Vec<tokio_rustls::rustls::SignatureScheme> {
185
+ self.0.supported_schemes()
186
+ }
187
+ }
188
+
189
+ let acceptor =
190
+ TlsAcceptor::from(Arc::new(server_config(CERT, KEY, true).expect("valid PEM")));
191
+
192
+ let provider = tokio_rustls::rustls::crypto::ring::default_provider();
193
+ let verifier = AcceptAny(provider.signature_verification_algorithms);
194
+ let mut client = ClientConfig::builder()
195
+ .dangerous()
196
+ .with_custom_certificate_verifier(Arc::new(verifier))
197
+ .with_no_client_auth();
198
+ client.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
199
+ let connector = TlsConnector::from(Arc::new(client));
200
+
201
+ let (client_io, server_io) = tokio::io::duplex(16 * 1024);
202
+ let server = tokio::spawn(async move { acceptor.accept(server_io).await });
203
+ let domain = ServerName::try_from("localhost").expect("server name");
204
+ let client_stream = connector
205
+ .connect(domain, client_io)
206
+ .await
207
+ .expect("client handshake");
208
+ let server_stream = server.await.expect("join").expect("server handshake");
209
+
210
+ assert_eq!(client_stream.get_ref().1.alpn_protocol(), Some(&b"h2"[..]));
211
+ assert_eq!(server_stream.get_ref().1.alpn_protocol(), Some(&b"h2"[..]));
212
+ }
103
213
  }
@@ -30,6 +30,7 @@ module Kino
30
30
  io_threads: nil,
31
31
  tokio_threads: nil,
32
32
  tls: nil,
33
+ http2: true,
33
34
  environment: nil,
34
35
  pidfile: nil,
35
36
  control_bind: nil,
@@ -245,6 +246,11 @@ module Kino
245
246
  # TLS termination; file paths or inline PEM strings.
246
247
  def tls(cert:, key:) = @config.set(:tls, {cert: cert, key: key})
247
248
 
249
+ # HTTP/2: negotiated via ALPN on TLS binds, served to
250
+ # prior-knowledge clients on plaintext. On by default; set false
251
+ # to serve HTTP/1 only.
252
+ def http2(enabled = true) = @config.set(:http2, !!enabled)
253
+
248
254
  # Sets RACK_ENV (unless already set) before the CLI loads the app.
249
255
  def environment(env) = @config.set(:environment, env.to_s)
250
256
 
data/lib/kino/server.rb CHANGED
@@ -109,6 +109,7 @@ module Kino
109
109
  if @tls && unix?
110
110
  raise ArgumentError, "TLS is not supported on a unix socket bind; terminate TLS at the proxy in front"
111
111
  end
112
+ @http2 = settings.fetch(:http2, true) ? true : false
112
113
  @pidfile = settings[:pidfile]
113
114
  @control_bind = settings[:control_bind]&.to_s
114
115
  @control_token = settings[:control_token]&.to_s
@@ -155,6 +156,7 @@ module Kino
155
156
  io_threads: @io_threads,
156
157
  tokio_threads: @tokio_threads,
157
158
  tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
159
+ http2: @http2,
158
160
  lanes: @lanes, log_requests: @log_requests,
159
161
  mode: @mode.to_s, workers: @workers, threads: @threads, batch: @batch,
160
162
  control_bind: @control_bind, control_token: @control_token
@@ -20,6 +20,11 @@
20
20
  # PEM strings also work).
21
21
  # tls cert: "config/certs/server.pem", key: "config/certs/server.key"
22
22
 
23
+ # HTTP/2 is on by default: negotiated via ALPN on TLS binds, and served
24
+ # to prior-knowledge (h2c) clients on plaintext. Everything else keeps
25
+ # getting HTTP/1.1. Set false to serve HTTP/1 only.
26
+ # http2 false
27
+
23
28
  ## Topology
24
29
 
25
30
  # How many workers to run. Each worker handles requests independently;
data/lib/kino/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Kino
4
4
  # The gem version (single source of truth; ext/kino/Cargo.toml syncs).
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
data/lib/kino/worker.rb CHANGED
@@ -20,12 +20,16 @@ module Kino
20
20
 
21
21
  def run(server_id, worker_id, app, batch_size = 1, hooks = nil)
22
22
  fire_after_worker_boot(hooks, worker_id)
23
+ # One resolution of (server, worker) into a native handle; every take
24
+ # after this skips the registry lookup. nil = server already gone.
25
+ worker = Native.worker(server_id, worker_id)
26
+ return unless worker
23
27
  if batch_size <= 1
24
- env = Native.take_one(server_id, worker_id)
25
- env = handle_one(env, server_id, worker_id, app, hooks) while env
28
+ env = worker.take_one
29
+ env = handle_one(env, worker, app, hooks) while env
26
30
  else
27
- batch = Native.take_batch(server_id, worker_id, batch_size)
28
- batch = process(batch, server_id, worker_id, app, batch_size, hooks) while batch
31
+ batch = worker.take_batch(batch_size)
32
+ batch = process(batch, worker, app, batch_size, hooks) while batch
29
33
  end
30
34
  end
31
35
 
@@ -35,22 +39,21 @@ module Kino
35
39
  NOT_FUSED = Object.new.freeze
36
40
 
37
41
  # Handle one request; returns the next env (fused take) or nil.
38
- def handle_one(env, server_id, worker_id, app, hooks)
42
+ def handle_one(env, worker, app, hooks)
39
43
  result = serve(env, app, hooks) do |request, status, headers, chunks|
40
- request.respond_and_take_one(server_id, worker_id, status, headers, chunks)
44
+ request.respond_and_take_one(worker, status, headers, chunks)
41
45
  end
42
- result.equal?(NOT_FUSED) ? Native.take_one(server_id, worker_id) : result
46
+ result.equal?(NOT_FUSED) ? worker.take_one : result
43
47
  end
44
48
 
45
49
  # Handle every env in the batch; returns the next batch (the last
46
50
  # simple response rides the fused respond_and_take) or nil on shutdown.
47
- def process(batch, server_id, worker_id, app, batch_size, hooks)
51
+ def process(batch, worker, app, batch_size, hooks)
48
52
  last = batch.size - 1
49
53
  batch.each_with_index do |env, index|
50
54
  result = serve(env, app, hooks) do |request, status, headers, chunks|
51
55
  if index == last
52
- request.respond_and_take(server_id, worker_id, batch_size,
53
- status, headers, chunks)
56
+ request.respond_and_take(worker, batch_size, status, headers, chunks)
54
57
  else
55
58
  request.send_simple(status, headers, chunks)
56
59
  NOT_FUSED
@@ -58,7 +61,7 @@ module Kino
58
61
  end
59
62
  return result if index == last && !result.equal?(NOT_FUSED)
60
63
  end
61
- Native.take_batch(server_id, worker_id, batch_size)
64
+ worker.take_batch(batch_size)
62
65
  end
63
66
 
64
67
  # Run one request through the app. Complete bodies are yielded so the
data/sig/kino.rbs CHANGED
@@ -130,6 +130,7 @@ module Kino
130
130
  def io_threads: (int? count) -> untyped
131
131
  def tokio_threads: (int count) -> untyped
132
132
  def tls: (cert: String, key: String) -> untyped
133
+ def http2: (?boolish enabled) -> untyped
133
134
  def environment: (String | Symbol env) -> untyped
134
135
  def pidfile: (String path) -> untyped
135
136
  def control_bind: (String addr) -> String
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kino
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Markin
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: 0.9.128
54
+ - !ruby/object:Gem::Dependency
55
+ name: httpx
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.3'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: rackup
56
70
  requirement: !ruby/object:Gem::Requirement