quic 0.0.1 → 0.0.2
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 +22 -1
- data/LICENSE-DEPENDENCIES.txt +101 -0
- data/README.md +69 -6
- data/examples/doq_demo.rb +122 -0
- data/examples/echo_demo.rb +23 -12
- data/examples/handshake_demo.rb +18 -20
- data/examples/io_loop_demo.rb +144 -0
- data/examples/verify_demo.rb +105 -0
- data/ext/quic/connection_client.c +403 -68
- data/ext/quic/extconf.rb +138 -28
- data/ext/quic/patches/picotls/0001-openssl-use-raw-public-key-api-for-X25519.patch +74 -0
- data/ext/quic/quic.c +63 -51
- data/ext/quic/quic.h +35 -25
- data/ext/quic/stream.c +59 -23
- data/ext/quic/stream.h +7 -3
- data/lib/quic/connection.rb +69 -9
- data/lib/quic/settings.rb +23 -3
- data/lib/quic/stream.rb +17 -14
- data/lib/quic/transport_params.rb +1 -1
- data/lib/quic/version.rb +2 -2
- data/lib/quic.rb +1 -1
- data/sig/quic.rbs +42 -5
- metadata +31 -9
data/ext/quic/stream.c
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <string.h>
|
|
4
4
|
|
|
5
|
-
VALUE
|
|
5
|
+
VALUE rb_cQUICStream;
|
|
6
6
|
|
|
7
7
|
static void
|
|
8
8
|
quic_stream_free(void *ptr)
|
|
@@ -18,7 +18,7 @@ quic_stream_size(const void *ptr)
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const rb_data_type_t quic_stream_data_type = {
|
|
21
|
-
"
|
|
21
|
+
"QUIC::Stream",
|
|
22
22
|
{NULL, quic_stream_free, quic_stream_size,},
|
|
23
23
|
NULL, NULL,
|
|
24
24
|
RUBY_TYPED_FREE_IMMEDIATELY,
|
|
@@ -35,7 +35,7 @@ quic_stream_alloc(VALUE klass)
|
|
|
35
35
|
VALUE
|
|
36
36
|
quic_stream_new(int64_t stream_id, VALUE client)
|
|
37
37
|
{
|
|
38
|
-
VALUE self = quic_stream_alloc(
|
|
38
|
+
VALUE self = quic_stream_alloc(rb_cQUICStream);
|
|
39
39
|
quic_stream_t *s;
|
|
40
40
|
TypedData_Get_Struct(self, quic_stream_t, &quic_stream_data_type, s);
|
|
41
41
|
s->stream_id = stream_id;
|
|
@@ -53,7 +53,7 @@ quic_stream_new(int64_t stream_id, VALUE client)
|
|
|
53
53
|
quic_stream_write_m and #write_nonblock returns a partial byte count in
|
|
54
54
|
quic_stream_write_nonblock_m). This helper is reached only after the
|
|
55
55
|
caller has confirmed the bytes fit in the current send window, so it
|
|
56
|
-
never raises
|
|
56
|
+
never raises QUIC::Error::WaitWritable on its own. */
|
|
57
57
|
static VALUE
|
|
58
58
|
quic_stream_enqueue(VALUE self, VALUE data, bool fin)
|
|
59
59
|
{
|
|
@@ -61,10 +61,10 @@ quic_stream_enqueue(VALUE self, VALUE data, bool fin)
|
|
|
61
61
|
TypedData_Get_Struct(self, quic_stream_t, &quic_stream_data_type, s);
|
|
62
62
|
|
|
63
63
|
if (s->reset || s->closed) {
|
|
64
|
-
rb_raise(
|
|
64
|
+
rb_raise(rb_eQUICErrorStreamClosed, "stream is closed");
|
|
65
65
|
}
|
|
66
66
|
if (s->fin_sent) {
|
|
67
|
-
rb_raise(
|
|
67
|
+
rb_raise(rb_eQUICErrorStreamClosed, "stream FIN already sent");
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
Check_Type(data, T_STRING);
|
|
@@ -110,7 +110,7 @@ quic_stream_window_left(VALUE client_v, quic_stream_t *s)
|
|
|
110
110
|
/* Blocking write: block by repeatedly invoking Client#pump_once until the
|
|
111
111
|
peer's flow control window has enough room for the full payload, then
|
|
112
112
|
enqueue. IO#write-compatible: always queues all of `data`. Bare Streams
|
|
113
|
-
(built via
|
|
113
|
+
(built via QUIC::Stream.allocate for unit tests, with @client = nil) skip
|
|
114
114
|
the window check entirely. */
|
|
115
115
|
static VALUE
|
|
116
116
|
quic_stream_write_m(int argc, VALUE *argv, VALUE self)
|
|
@@ -132,7 +132,7 @@ quic_stream_write_m(int argc, VALUE *argv, VALUE self)
|
|
|
132
132
|
quic_stream_t *s;
|
|
133
133
|
TypedData_Get_Struct(self, quic_stream_t, &quic_stream_data_type, s);
|
|
134
134
|
/* Loop until the full payload would fit in the current window. pump_once
|
|
135
|
-
raises
|
|
135
|
+
raises QUIC::Error::NotBound if @client has no socket bound; that
|
|
136
136
|
error surfaces verbatim. */
|
|
137
137
|
while ((uint64_t)needed > quic_stream_window_left(client_v, s)) {
|
|
138
138
|
rb_funcall(client_v, rb_intern("pump_once"), 0);
|
|
@@ -143,7 +143,7 @@ quic_stream_write_m(int argc, VALUE *argv, VALUE self)
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/* Non-blocking write: enqueue at most `window_left` bytes from `data` and
|
|
146
|
-
return the count actually queued. Raises
|
|
146
|
+
return the count actually queued. Raises QUIC::Error::WaitWritable if the
|
|
147
147
|
window is zero and we have a non-empty payload to send. If only a partial
|
|
148
148
|
prefix fits, FIN is NOT set on this call (caller re-issues with
|
|
149
149
|
`fin: true` once the remainder is accepted) so we don't half-commit FIN.
|
|
@@ -172,7 +172,7 @@ quic_stream_write_nonblock_m(int argc, VALUE *argv, VALUE self)
|
|
|
172
172
|
uint64_t avail = quic_stream_window_left(client_v, s);
|
|
173
173
|
|
|
174
174
|
if (avail == 0) {
|
|
175
|
-
rb_raise(
|
|
175
|
+
rb_raise(rb_eQUICErrorWaitWritable, "stream send window is full");
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
long take = ((uint64_t)needed <= avail) ? needed : (long)avail;
|
|
@@ -198,7 +198,7 @@ quic_stream_close_write_m(VALUE self)
|
|
|
198
198
|
to drop the returned prefix.
|
|
199
199
|
|
|
200
200
|
- buffer empty + fin_received: raise EOFError (IO#read_nonblock semantics)
|
|
201
|
-
- buffer empty + !fin_received: raise
|
|
201
|
+
- buffer empty + !fin_received: raise QUIC::Error::WaitReadable
|
|
202
202
|
- buffer non-empty: return min(length, buffer.bytesize) bytes
|
|
203
203
|
*/
|
|
204
204
|
static VALUE
|
|
@@ -218,7 +218,7 @@ quic_stream_read_nonblock_m(VALUE self, VALUE length_v)
|
|
|
218
218
|
if (s->fin_received) {
|
|
219
219
|
rb_raise(rb_eEOFError, "end of file reached");
|
|
220
220
|
}
|
|
221
|
-
rb_raise(
|
|
221
|
+
rb_raise(rb_eQUICErrorWaitReadable, "no data available");
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
long take = (length < have) ? length : have;
|
|
@@ -253,7 +253,7 @@ quic_stream_close_read_m(VALUE self)
|
|
|
253
253
|
ngtcp2_conn *conn = quic_client_conn(client_v);
|
|
254
254
|
|
|
255
255
|
/* ngtcp2_conn_shutdown_stream_read sends STOP_SENDING. app_error_code 0
|
|
256
|
-
since no
|
|
256
|
+
since no QUIC-level error API is exposed yet. */
|
|
257
257
|
int rv = ngtcp2_conn_shutdown_stream_read(conn, 0, s->stream_id, 0);
|
|
258
258
|
if (rv != 0) quic_raise_ngtcp2_error(rv);
|
|
259
259
|
return Qnil;
|
|
@@ -268,17 +268,53 @@ quic_stream_close_m(VALUE self)
|
|
|
268
268
|
return Qnil;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/* Abort the send side of the stream with RESET_STREAM
|
|
272
|
+
(ngtcp2_conn_shutdown_stream_write), carrying the given application error
|
|
273
|
+
code (default 0). After #reset, #write / #write_nonblock raise
|
|
274
|
+
QUIC::Error::StreamClosed because quic_stream_enqueue rejects a stream
|
|
275
|
+
with s->reset set. The read side is untouched (use #close_read /
|
|
276
|
+
#close for STOP_SENDING). Idempotent: a second #reset is a no-op.
|
|
277
|
+
|
|
278
|
+
A bare Stream (@client == nil, used by unit-test fixtures) skips the
|
|
279
|
+
ngtcp2 call and only flips s->reset, mirroring the #write / #close_write
|
|
280
|
+
bare-Stream escapes. */
|
|
281
|
+
static VALUE
|
|
282
|
+
quic_stream_reset_m(int argc, VALUE *argv, VALUE self)
|
|
283
|
+
{
|
|
284
|
+
VALUE error_code_v = Qnil;
|
|
285
|
+
rb_scan_args(argc, argv, "01", &error_code_v);
|
|
286
|
+
uint64_t error_code = NIL_P(error_code_v) ? 0 : NUM2ULL(error_code_v);
|
|
287
|
+
|
|
288
|
+
quic_stream_t *s;
|
|
289
|
+
TypedData_Get_Struct(self, quic_stream_t, &quic_stream_data_type, s);
|
|
290
|
+
|
|
291
|
+
if (s->reset) return Qnil; /* idempotent */
|
|
292
|
+
|
|
293
|
+
VALUE client_v = rb_ivar_get(self, rb_intern("@client"));
|
|
294
|
+
if (NIL_P(client_v)) {
|
|
295
|
+
s->reset = true; /* bare Stream escape */
|
|
296
|
+
return Qnil;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
ngtcp2_conn *conn = quic_client_conn(client_v);
|
|
300
|
+
int rv = ngtcp2_conn_shutdown_stream_write(conn, 0, s->stream_id, error_code);
|
|
301
|
+
if (rv != 0) quic_raise_ngtcp2_error(rv);
|
|
302
|
+
s->reset = true;
|
|
303
|
+
return Qnil;
|
|
304
|
+
}
|
|
305
|
+
|
|
271
306
|
void
|
|
272
|
-
Init_quic_stream(VALUE
|
|
307
|
+
Init_quic_stream(VALUE rb_mQUICArg)
|
|
273
308
|
{
|
|
274
|
-
|
|
275
|
-
rb_define_alloc_func(
|
|
276
|
-
rb_define_method(
|
|
277
|
-
rb_define_method(
|
|
278
|
-
rb_define_method(
|
|
279
|
-
rb_define_method(
|
|
280
|
-
rb_define_method(
|
|
281
|
-
rb_define_method(
|
|
282
|
-
rb_define_method(
|
|
309
|
+
rb_cQUICStream = rb_define_class_under(rb_mQUICArg, "Stream", rb_cObject);
|
|
310
|
+
rb_define_alloc_func(rb_cQUICStream, quic_stream_alloc);
|
|
311
|
+
rb_define_method(rb_cQUICStream, "write", quic_stream_write_m, -1);
|
|
312
|
+
rb_define_method(rb_cQUICStream, "write_nonblock", quic_stream_write_nonblock_m, -1);
|
|
313
|
+
rb_define_method(rb_cQUICStream, "close_write", quic_stream_close_write_m, 0);
|
|
314
|
+
rb_define_method(rb_cQUICStream, "read_nonblock", quic_stream_read_nonblock_m, 1);
|
|
315
|
+
rb_define_method(rb_cQUICStream, "eof?", quic_stream_eof_p, 0);
|
|
316
|
+
rb_define_method(rb_cQUICStream, "close_read", quic_stream_close_read_m, 0);
|
|
317
|
+
rb_define_method(rb_cQUICStream, "close", quic_stream_close_m, 0);
|
|
318
|
+
rb_define_method(rb_cQUICStream, "reset", quic_stream_reset_m, -1);
|
|
283
319
|
/* #read (blocking) and #initiator are defined in lib/quic/stream.rb. */
|
|
284
320
|
}
|
data/ext/quic/stream.h
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
#include "quic.h"
|
|
5
5
|
|
|
6
|
-
extern VALUE
|
|
6
|
+
extern VALUE rb_cQUICStream;
|
|
7
7
|
|
|
8
|
-
void Init_quic_stream(VALUE
|
|
8
|
+
void Init_quic_stream(VALUE rb_mQUICArg);
|
|
9
9
|
|
|
10
10
|
/* Internal helpers used by connection_client.c stream callbacks and
|
|
11
11
|
write_pkt's stream-aware path. */
|
|
@@ -29,11 +29,15 @@ typedef struct {
|
|
|
29
29
|
bool closed; /* stream_close callback fired */
|
|
30
30
|
uint64_t close_app_error_code;
|
|
31
31
|
bool close_has_app_error_code;
|
|
32
|
+
/* Set once a peer-initiated (server) stream has been pushed onto the
|
|
33
|
+
owner Client's @accept_queue, so recv_stream_data does not enqueue it
|
|
34
|
+
again on subsequent data arrivals. */
|
|
35
|
+
bool accept_queued;
|
|
32
36
|
} quic_stream_t;
|
|
33
37
|
|
|
34
38
|
extern const rb_data_type_t quic_stream_data_type;
|
|
35
39
|
|
|
36
|
-
/* Allocate a new
|
|
40
|
+
/* Allocate a new QUIC::Stream Ruby object with a zero-initialized
|
|
37
41
|
quic_stream_t. Caller fills stream_id and ivars (@id / @client /
|
|
38
42
|
@pending_chunks / @recv_buffer). */
|
|
39
43
|
VALUE quic_stream_new(int64_t stream_id, VALUE client);
|
data/lib/quic/connection.rb
CHANGED
|
@@ -2,22 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module QUIC
|
|
6
6
|
module Connection
|
|
7
7
|
class Client
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
attr_reader :remote_address
|
|
9
|
+
|
|
10
|
+
# Build a Client and pin a single peer address. address_family takes a
|
|
11
|
+
# Symbol (:inet for IPv4, :inet6 for IPv6) or nil to defer family
|
|
12
|
+
# selection to Addrinfo.udp's implicit resolution. Pin the resolved
|
|
13
|
+
# Addrinfo into @remote_address so callers can match their UDPSocket's
|
|
14
|
+
# connected sockaddr (otherwise DNS round-robin between Client.new and
|
|
15
|
+
# sock.connect can produce a path that ngtcp2 silently drops).
|
|
16
|
+
def self.new(host:, port:, address_family: nil, transport_params: nil, settings: nil)
|
|
17
|
+
family = case address_family
|
|
18
|
+
when nil then nil
|
|
19
|
+
when :inet then Socket::AF_INET
|
|
20
|
+
when :inet6 then Socket::AF_INET6
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "unknown address_family: #{address_family.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
remote_address = if family
|
|
26
|
+
Addrinfo.getaddrinfo(host, port, family, :DGRAM, Socket::IPPROTO_UDP).first
|
|
27
|
+
else
|
|
28
|
+
Addrinfo.udp(host, port)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
remote_sockaddr = remote_address.to_sockaddr
|
|
10
32
|
local_sockaddr = Addrinfo.udp("0.0.0.0", 0).to_sockaddr
|
|
11
33
|
|
|
12
34
|
client = _open(
|
|
13
35
|
local_sockaddr: local_sockaddr,
|
|
14
36
|
remote_sockaddr: remote_sockaddr,
|
|
15
37
|
server_name: host,
|
|
16
|
-
transport_params: transport_params ||
|
|
17
|
-
settings: settings ||
|
|
38
|
+
transport_params: transport_params || QUIC::TransportParams.default,
|
|
39
|
+
settings: settings || QUIC::Settings.default
|
|
18
40
|
)
|
|
19
41
|
client.instance_variable_set(:@host, host)
|
|
20
42
|
client.instance_variable_set(:@port, port)
|
|
43
|
+
client.instance_variable_set(:@remote_address, remote_address)
|
|
21
44
|
client
|
|
22
45
|
end
|
|
23
46
|
|
|
@@ -35,10 +58,10 @@ module Quic
|
|
|
35
58
|
end
|
|
36
59
|
|
|
37
60
|
# Drive the handshake to completion using the bound socket. Raises
|
|
38
|
-
#
|
|
39
|
-
#
|
|
61
|
+
# QUIC::Error::NotBound if #bind has not been called. Propagates any
|
|
62
|
+
# QUIC::Error subclass raised by #read_pkt / #handle_expiry.
|
|
40
63
|
def run
|
|
41
|
-
raise
|
|
64
|
+
raise QUIC::Error::NotBound, "QUIC::Connection::Client#bind(sock) has not been called" if @sock.nil?
|
|
42
65
|
|
|
43
66
|
pump_until { handshake_completed? }
|
|
44
67
|
end
|
|
@@ -49,7 +72,7 @@ module Quic
|
|
|
49
72
|
# fire. Public-ish so Stream blocking ops can share it; documented as
|
|
50
73
|
# "internal" for users.
|
|
51
74
|
def pump_once(timeout: nil)
|
|
52
|
-
raise
|
|
75
|
+
raise QUIC::Error::NotBound, "QUIC::Connection::Client#bind(sock) has not been called" if @sock.nil?
|
|
53
76
|
|
|
54
77
|
while (pkt = write_pkt)
|
|
55
78
|
@sock.send(pkt, 0)
|
|
@@ -79,6 +102,43 @@ module Quic
|
|
|
79
102
|
pump_once until pred.call
|
|
80
103
|
end
|
|
81
104
|
|
|
105
|
+
# Pop the next peer-initiated (server) stream off the accept queue,
|
|
106
|
+
# driving the I/O loop until one arrives. The queue is fed by the
|
|
107
|
+
# recv_stream_data callback the first time a server stream carries data.
|
|
108
|
+
#
|
|
109
|
+
# timeout: nil block until a stream is available (requires #bind)
|
|
110
|
+
# timeout: 0 return immediately; nil if the queue is empty
|
|
111
|
+
# timeout: Numeric block up to that many seconds, then return nil
|
|
112
|
+
#
|
|
113
|
+
# Returns a QUIC::Stream, or nil on timeout.
|
|
114
|
+
def accept_stream(timeout: nil)
|
|
115
|
+
stream = @accept_queue.shift
|
|
116
|
+
return stream unless stream.nil?
|
|
117
|
+
return nil if timeout == 0
|
|
118
|
+
|
|
119
|
+
deadline = timeout && Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
120
|
+
loop do
|
|
121
|
+
if deadline
|
|
122
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
123
|
+
return nil if remaining <= 0.0
|
|
124
|
+
pump_once(timeout: remaining)
|
|
125
|
+
else
|
|
126
|
+
pump_once
|
|
127
|
+
end
|
|
128
|
+
stream = @accept_queue.shift
|
|
129
|
+
return stream unless stream.nil?
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Non-blocking variant of #accept_stream. Returns the next queued
|
|
134
|
+
# server stream, or raises QUIC::Error::WaitReadable (IO::WaitReadable
|
|
135
|
+
# mixin) when the queue is empty. Does not require #bind.
|
|
136
|
+
def accept_stream_nonblock
|
|
137
|
+
stream = @accept_queue.shift
|
|
138
|
+
raise QUIC::Error::WaitReadable, "no server-initiated stream available" if stream.nil?
|
|
139
|
+
stream
|
|
140
|
+
end
|
|
141
|
+
|
|
82
142
|
private
|
|
83
143
|
|
|
84
144
|
def pump_timeout(caller_timeout)
|
data/lib/quic/settings.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module QUIC
|
|
4
4
|
Settings = Data.define(
|
|
5
5
|
:cc_algo,
|
|
6
6
|
:initial_rtt,
|
|
@@ -8,10 +8,27 @@ module Quic
|
|
|
8
8
|
:max_stream_window,
|
|
9
9
|
:handshake_timeout,
|
|
10
10
|
:no_pmtud,
|
|
11
|
-
:alpn
|
|
11
|
+
:alpn,
|
|
12
|
+
:verify_mode,
|
|
13
|
+
:ca_file,
|
|
14
|
+
:ca_path
|
|
12
15
|
)
|
|
13
16
|
|
|
14
17
|
class Settings
|
|
18
|
+
# verify_mode (:peer or :none), ca_file and ca_path control server
|
|
19
|
+
# certificate verification. They default here so that callers building a
|
|
20
|
+
# Settings without them keep working.
|
|
21
|
+
#
|
|
22
|
+
# With verify_mode :peer and both ca_file and ca_path nil, the system
|
|
23
|
+
# default store is used, and SSL_CERT_FILE / SSL_CERT_DIR are honoured.
|
|
24
|
+
# That store is loaded once per process, the first time a connection needs
|
|
25
|
+
# it; later changes to those variables or to the CA bundle are not picked
|
|
26
|
+
# up. Setting ca_file or ca_path trusts only the certificates found there.
|
|
27
|
+
# With verify_mode :none, ca_file and ca_path are ignored.
|
|
28
|
+
def initialize(verify_mode: :peer, ca_file: nil, ca_path: nil, **rest)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
15
32
|
def self.default
|
|
16
33
|
new(
|
|
17
34
|
cc_algo: :cubic,
|
|
@@ -20,7 +37,10 @@ module Quic
|
|
|
20
37
|
max_stream_window: 0,
|
|
21
38
|
handshake_timeout: 10_000_000_000,
|
|
22
39
|
no_pmtud: false,
|
|
23
|
-
alpn: []
|
|
40
|
+
alpn: [],
|
|
41
|
+
verify_mode: :peer,
|
|
42
|
+
ca_file: nil,
|
|
43
|
+
ca_path: nil
|
|
24
44
|
)
|
|
25
45
|
end
|
|
26
46
|
end
|
data/lib/quic/stream.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
#
|
|
3
|
+
module QUIC
|
|
4
|
+
# QUIC::Stream is defined in the C extension (ext/quic/stream.c); this file
|
|
5
5
|
# reopens it to add the bits that are easier to express in Ruby:
|
|
6
6
|
# #initiator (a tiny lookup over @id) and the blocking #read which wraps
|
|
7
|
-
# the C #read_nonblock with an internal Client#
|
|
7
|
+
# the C #read_nonblock with an internal Client#pump_once loop.
|
|
8
8
|
class Stream
|
|
9
9
|
INITIATORS = %i[client_bidi server_bidi client_uni server_uni].freeze
|
|
10
10
|
|
|
@@ -24,28 +24,31 @@ module Quic
|
|
|
24
24
|
# return up to `length` bytes, return nil at EOF.
|
|
25
25
|
# read(nil): block until EOF, return everything that was received
|
|
26
26
|
# (an empty String if nothing arrived before FIN).
|
|
27
|
+
#
|
|
28
|
+
# Both branches drain first and pump only when there is nothing to drain.
|
|
29
|
+
# #read_nonblock already distinguishes the two states we care about --
|
|
30
|
+
# QUIC::Error::WaitReadable for "nothing buffered yet" and EOFError for
|
|
31
|
+
# "nothing buffered and FIN seen" -- so the loop needs no separate
|
|
32
|
+
# predicate over @recv_buffer / #eof?.
|
|
27
33
|
def read(length = nil)
|
|
28
34
|
if length.nil?
|
|
29
|
-
@client.pump_until { eof? }
|
|
30
35
|
out = +""
|
|
31
36
|
out.force_encoding(Encoding::BINARY)
|
|
32
|
-
until eof?
|
|
33
|
-
# Should not happen given pump_until's predicate, but defensive.
|
|
34
|
-
out << read_nonblock(4096)
|
|
35
|
-
end
|
|
36
|
-
# Drain anything that arrived after the predicate was last evaluated.
|
|
37
37
|
loop do
|
|
38
38
|
out << read_nonblock(4096)
|
|
39
|
-
rescue
|
|
39
|
+
rescue QUIC::Error::WaitReadable
|
|
40
|
+
@client.pump_once
|
|
41
|
+
rescue EOFError
|
|
40
42
|
break
|
|
41
43
|
end
|
|
42
44
|
out
|
|
43
45
|
else
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
loop do
|
|
47
|
+
return read_nonblock(length)
|
|
48
|
+
rescue QUIC::Error::WaitReadable
|
|
49
|
+
@client.pump_once
|
|
47
50
|
rescue EOFError
|
|
48
|
-
nil
|
|
51
|
+
return nil
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
end
|
data/lib/quic/version.rb
CHANGED
data/lib/quic.rb
CHANGED
data/sig/quic.rbs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
module
|
|
1
|
+
module QUIC
|
|
2
2
|
VERSION: String
|
|
3
3
|
|
|
4
|
-
def self.library_versions: () -> { ngtcp2: String, openssl: String }
|
|
4
|
+
def self.library_versions: () -> { ngtcp2: String, openssl: String, picotls: String }
|
|
5
5
|
|
|
6
6
|
class Error < StandardError
|
|
7
7
|
attr_reader code: Integer?
|
|
@@ -19,6 +19,15 @@ module Quic
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
class CryptoError < Error
|
|
22
|
+
# The TLS alert this client sent when the handshake failed in
|
|
23
|
+
# #read_pkt, or nil. Alerts received from the peer are not recorded.
|
|
24
|
+
attr_reader tls_alert: Integer?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class CertificateVerifyFailed < CryptoError
|
|
28
|
+
# X509_V_ERR_* (comparable with OpenSSL::X509::V_ERR_*), or nil when
|
|
29
|
+
# the server sent no certificate.
|
|
30
|
+
attr_reader verify_result: Integer?
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
class HandshakeTimeout < Error
|
|
@@ -69,13 +78,18 @@ module Quic
|
|
|
69
78
|
attr_reader handshake_timeout: Integer
|
|
70
79
|
attr_reader no_pmtud: bool
|
|
71
80
|
attr_reader alpn: Array[String]
|
|
81
|
+
attr_reader verify_mode: :peer | :none
|
|
82
|
+
attr_reader ca_file: (String | _ToPath)?
|
|
83
|
+
attr_reader ca_path: (String | _ToPath)?
|
|
72
84
|
|
|
73
85
|
def self.default: () -> Settings
|
|
74
86
|
end
|
|
75
87
|
|
|
76
88
|
module Connection
|
|
77
89
|
class Client
|
|
78
|
-
|
|
90
|
+
attr_reader remote_address: Addrinfo
|
|
91
|
+
|
|
92
|
+
def self.new: (host: String, port: Integer, ?address_family: Symbol?, ?transport_params: TransportParams?, ?settings: Settings?) -> Client
|
|
79
93
|
|
|
80
94
|
def self._open: (
|
|
81
95
|
local_sockaddr: String,
|
|
@@ -102,6 +116,24 @@ module Quic
|
|
|
102
116
|
def run: () -> void
|
|
103
117
|
def pump_once: (?timeout: Float?) -> void
|
|
104
118
|
def pump_until: () { () -> boolish } -> void
|
|
119
|
+
|
|
120
|
+
# Pops the next peer-initiated (server) stream off the accept queue,
|
|
121
|
+
# driving the I/O loop until one arrives. timeout: nil blocks forever
|
|
122
|
+
# (requires #bind), timeout: 0 returns immediately, timeout: Numeric
|
|
123
|
+
# blocks up to that many seconds. Returns nil on timeout.
|
|
124
|
+
def accept_stream: (?timeout: Numeric?) -> Stream?
|
|
125
|
+
|
|
126
|
+
# Non-blocking #accept_stream. Returns the next queued server stream,
|
|
127
|
+
# or raises QUIC::Error::WaitReadable when the queue is empty.
|
|
128
|
+
def accept_stream_nonblock: () -> Stream
|
|
129
|
+
|
|
130
|
+
# Sends an application CONNECTION_CLOSE (frame type 0x1d) to the peer
|
|
131
|
+
# carrying error_code and reason, then transitions the underlying
|
|
132
|
+
# ngtcp2_conn to the closing period. Requires Client#bind to have been
|
|
133
|
+
# called; raises QUIC::Error::NotBound otherwise. A second #close call
|
|
134
|
+
# after the connection has already entered the closing or draining
|
|
135
|
+
# period is a no-op.
|
|
136
|
+
def close: (?error_code: Integer, ?reason: String) -> nil
|
|
105
137
|
end
|
|
106
138
|
end
|
|
107
139
|
|
|
@@ -118,12 +150,12 @@ module Quic
|
|
|
118
150
|
# Blocks (by internally calling Client#pump_once) until the peer's flow
|
|
119
151
|
# control window has room for the full payload, then queues all bytes
|
|
120
152
|
# and returns data.bytesize. Requires the parent Client to be bound to
|
|
121
|
-
# a socket via Client#bind; raises
|
|
153
|
+
# a socket via Client#bind; raises QUIC::Error::NotBound otherwise.
|
|
122
154
|
def write: (String data, ?fin: bool) -> Integer
|
|
123
155
|
|
|
124
156
|
# Non-blocking variant. Queues as much of `data` as the current flow
|
|
125
157
|
# control window allows and returns the byte count actually queued.
|
|
126
|
-
# Raises
|
|
158
|
+
# Raises QUIC::Error::WaitWritable (mixed in with IO::WaitWritable) when
|
|
127
159
|
# the window is zero and the payload is non-empty. If only a partial
|
|
128
160
|
# prefix is accepted, `fin: true` is NOT propagated; the caller must
|
|
129
161
|
# re-issue with `fin: true` on the trailing chunk.
|
|
@@ -132,5 +164,10 @@ module Quic
|
|
|
132
164
|
def close_write: () -> nil
|
|
133
165
|
def close_read: () -> nil
|
|
134
166
|
def close: () -> nil
|
|
167
|
+
|
|
168
|
+
# Aborts the send side with RESET_STREAM carrying error_code (default 0).
|
|
169
|
+
# After #reset, #write / #write_nonblock raise QUIC::Error::StreamClosed.
|
|
170
|
+
# The read side is unaffected. Idempotent: a second #reset is a no-op.
|
|
171
|
+
def reset: (?Integer error_code) -> nil
|
|
135
172
|
end
|
|
136
173
|
end
|
metadata
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Nakamura
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: mini_portile2
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: A thin Ruby binding around ngtcp2 for the QUIC transport protocol, using
|
|
27
|
+
picotls for TLS 1.3. ngtcp2 and picotls are vendored via mini_portile2 at install
|
|
28
|
+
time and linked statically; the crypto primitives and X.509 come from the host's
|
|
29
|
+
OpenSSL (or LibreSSL), linked dynamically, so the process shares one libcrypto with
|
|
30
|
+
Ruby's openssl extension. The API is intentionally optimized for synchronous I/O
|
|
31
|
+
and String-based buffers. Public API is not yet stable.
|
|
16
32
|
email:
|
|
17
33
|
- yusuke1994525@gmail.com
|
|
18
34
|
executables: []
|
|
@@ -22,13 +38,18 @@ extra_rdoc_files: []
|
|
|
22
38
|
files:
|
|
23
39
|
- CHANGELOG.md
|
|
24
40
|
- CODE_OF_CONDUCT.md
|
|
41
|
+
- LICENSE-DEPENDENCIES.txt
|
|
25
42
|
- LICENSE.txt
|
|
26
43
|
- README.md
|
|
27
44
|
- Rakefile
|
|
45
|
+
- examples/doq_demo.rb
|
|
28
46
|
- examples/echo_demo.rb
|
|
29
47
|
- examples/handshake_demo.rb
|
|
48
|
+
- examples/io_loop_demo.rb
|
|
49
|
+
- examples/verify_demo.rb
|
|
30
50
|
- ext/quic/connection_client.c
|
|
31
51
|
- ext/quic/extconf.rb
|
|
52
|
+
- ext/quic/patches/picotls/0001-openssl-use-raw-public-key-api-for-X25519.patch
|
|
32
53
|
- ext/quic/quic.c
|
|
33
54
|
- ext/quic/quic.h
|
|
34
55
|
- ext/quic/stream.c
|
|
@@ -48,6 +69,7 @@ metadata:
|
|
|
48
69
|
homepage_uri: https://github.com/unasuke/quic-ruby
|
|
49
70
|
source_code_uri: https://github.com/unasuke/quic-ruby
|
|
50
71
|
changelog_uri: https://github.com/unasuke/quic-ruby/blob/main/CHANGELOG.md
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
51
73
|
rdoc_options: []
|
|
52
74
|
require_paths:
|
|
53
75
|
- lib
|
|
@@ -62,8 +84,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
62
84
|
- !ruby/object:Gem::Version
|
|
63
85
|
version: '0'
|
|
64
86
|
requirements: []
|
|
65
|
-
rubygems_version: 4.0.
|
|
87
|
+
rubygems_version: 4.0.16
|
|
66
88
|
specification_version: 4
|
|
67
|
-
summary: Thin Ruby binding around ngtcp2 for the QUIC transport protocol,
|
|
68
|
-
|
|
89
|
+
summary: Thin Ruby binding around ngtcp2 for the QUIC transport protocol, using picotls
|
|
90
|
+
for TLS 1.3 over the system OpenSSL.
|
|
69
91
|
test_files: []
|