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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/Cargo.lock +92 -1
- data/README.md +101 -44
- data/doc/architecture.md +54 -3
- data/doc/benchmarks.md +120 -26
- data/ext/kino/Cargo.toml +8 -3
- data/ext/kino/src/env_strings.rs +162 -10
- data/ext/kino/src/lib.rs +11 -7
- data/ext/kino/src/queue.rs +59 -43
- data/ext/kino/src/registry.rs +4 -0
- data/ext/kino/src/request.rs +150 -69
- data/ext/kino/src/response.rs +39 -5
- data/ext/kino/src/server.rs +647 -31
- data/ext/kino/src/tls.rs +119 -9
- data/lib/kino/configuration.rb +6 -0
- data/lib/kino/server.rb +2 -0
- data/lib/kino/templates/kino.rb.tt +5 -0
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +14 -11
- data/sig/kino.rbs +1 -0
- metadata +15 -1
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
}
|
data/lib/kino/configuration.rb
CHANGED
|
@@ -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
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 =
|
|
25
|
-
env = handle_one(env,
|
|
28
|
+
env = worker.take_one
|
|
29
|
+
env = handle_one(env, worker, app, hooks) while env
|
|
26
30
|
else
|
|
27
|
-
batch =
|
|
28
|
-
batch = process(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,
|
|
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(
|
|
44
|
+
request.respond_and_take_one(worker, status, headers, chunks)
|
|
41
45
|
end
|
|
42
|
-
result.equal?(NOT_FUSED) ?
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|