quic 0.0.2 → 0.0.3

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/lib/quic/settings.rb CHANGED
@@ -11,7 +11,8 @@ module QUIC
11
11
  :alpn,
12
12
  :verify_mode,
13
13
  :ca_file,
14
- :ca_path
14
+ :ca_path,
15
+ :qlog_dir
15
16
  )
16
17
 
17
18
  class Settings
@@ -25,7 +26,15 @@ module QUIC
25
26
  # it; later changes to those variables or to the CA bundle are not picked
26
27
  # up. Setting ca_file or ca_path trusts only the certificates found there.
27
28
  # 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
+ #
30
+ # Only a client uses verify_mode, ca_file and ca_path. A server ignores
31
+ # them, since it does not ask for a client certificate.
32
+ #
33
+ # qlog_dir (a String or Pathname) makes every connection write its qlog
34
+ # to <qlog_dir>/<its first source connection ID in hex>.sqlog. A file
35
+ # that cannot be opened when a connection is made only costs that
36
+ # connection's qlog.
37
+ def initialize(verify_mode: :peer, ca_file: nil, ca_path: nil, qlog_dir: nil, **rest)
29
38
  super
30
39
  end
31
40
 
@@ -40,8 +49,22 @@ module QUIC
40
49
  alpn: [],
41
50
  verify_mode: :peer,
42
51
  ca_file: nil,
43
- ca_path: nil
52
+ ca_path: nil,
53
+ qlog_dir: nil
44
54
  )
45
55
  end
56
+
57
+ # Raises ArgumentError unless qlog_dir is nil or a directory this
58
+ # process can write to. Client.new, Connection::Server.accept and
59
+ # Endpoint.new call it, so that a wrong qlog_dir shows up when they are
60
+ # set up rather than as qlog files that never appear.
61
+ def check_qlog_dir
62
+ return if qlog_dir.nil?
63
+
64
+ path = qlog_dir.respond_to?(:to_path) ? qlog_dir.to_path : qlog_dir
65
+ return if path.is_a?(String) && File.directory?(path) && File.writable?(path)
66
+
67
+ raise ArgumentError, "qlog_dir is not a writable directory: #{path}"
68
+ end
46
69
  end
47
70
  end
data/lib/quic/stream.rb CHANGED
@@ -4,7 +4,8 @@ module QUIC
4
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#pump_once loop.
7
+ # the C #read_nonblock with an internal #pump_once loop on the owning
8
+ # connection (@client, a Client or Server).
8
9
  class Stream
9
10
  INITIATORS = %i[client_bidi server_bidi client_uni server_uni].freeze
10
11
 
@@ -17,8 +18,8 @@ module QUIC
17
18
  INITIATORS[@id & 0b11]
18
19
  end
19
20
 
20
- # IO#read-compatible blocking read. Requires the parent Client to have
21
- # been #bind'ed to a socket (raised via Client#pump_once otherwise).
21
+ # IO#read-compatible blocking read. Requires the owning connection to
22
+ # have been #bind'ed to a socket (raised via its #pump_once otherwise).
22
23
  #
23
24
  # read(length): block until at least 1 byte is available or EOF,
24
25
  # return up to `length` bytes, return nil at EOF.
data/lib/quic/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QUIC
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.3"
5
5
  end
data/lib/quic.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "quic/version"
4
+ require_relative "quic/ext_include"
4
5
 
5
6
  module QUIC
6
7
  class Error < StandardError
@@ -12,4 +13,5 @@ require "quic/quic"
12
13
  require_relative "quic/transport_params"
13
14
  require_relative "quic/settings"
14
15
  require_relative "quic/connection"
16
+ require_relative "quic/endpoint"
15
17
  require_relative "quic/stream"
data/sig/quic.rbs CHANGED
@@ -1,6 +1,10 @@
1
1
  module QUIC
2
2
  VERSION: String
3
3
 
4
+ # The directory holding quic/ext.h, the C function table other native
5
+ # extensions build against. Defined without loading the extension.
6
+ EXT_INCLUDE_DIR: String
7
+
4
8
  def self.library_versions: () -> { ngtcp2: String, openssl: String, picotls: String }
5
9
 
6
10
  class Error < StandardError
@@ -19,7 +23,7 @@ module QUIC
19
23
  end
20
24
 
21
25
  class CryptoError < Error
22
- # The TLS alert this client sent when the handshake failed in
26
+ # The TLS alert this endpoint sent when the handshake failed in
23
27
  # #read_pkt, or nil. Alerts received from the peer are not recorded.
24
28
  attr_reader tls_alert: Integer?
25
29
  end
@@ -55,6 +59,12 @@ module QUIC
55
59
 
56
60
  class NotBound < Error
57
61
  end
62
+
63
+ # Raised by #open_bidi_stream, #open_uni_stream, #accept_stream and
64
+ # #accept_stream_nonblock of a connection whose streams belong to a C
65
+ # stream handler registered through quic/ext.h.
66
+ class InvalidState < Error
67
+ end
58
68
  end
59
69
 
60
70
  class TransportParams < Data
@@ -78,15 +88,61 @@ module QUIC
78
88
  attr_reader handshake_timeout: Integer
79
89
  attr_reader no_pmtud: bool
80
90
  attr_reader alpn: Array[String]
91
+
92
+ # Client only: a server does not ask for a client certificate and
93
+ # ignores these three.
81
94
  attr_reader verify_mode: :peer | :none
82
95
  attr_reader ca_file: (String | _ToPath)?
83
96
  attr_reader ca_path: (String | _ToPath)?
84
97
 
98
+ # Every connection writes its qlog to <qlog_dir>/<its first source
99
+ # connection ID in hex>.sqlog. nil (the default) writes none. A file
100
+ # that cannot be opened when a connection is made only costs that
101
+ # connection's qlog.
102
+ attr_reader qlog_dir: (String | _ToPath)?
103
+
85
104
  def self.default: () -> Settings
105
+
106
+ # Raises ArgumentError unless qlog_dir is nil or a directory this
107
+ # process can write to. Client.new, Connection::Server.accept and
108
+ # Endpoint.new call it.
109
+ def check_qlog_dir: () -> nil
110
+ end
111
+
112
+ # A server's certificate chain, private key and ALPN protocols (in order
113
+ # of preference), read once and shared by every connection made from it.
114
+ # A connection keeps the context it started with even after this object
115
+ # is gone. Only .server makes one; a misconfiguration raises
116
+ # ArgumentError naming what could not be loaded or used.
117
+ class TLSContext
118
+ def self.server: (certificate_path: String | _ToPath, private_key_path: String | _ToPath, alpn: Array[String]) -> TLSContext
86
119
  end
87
120
 
88
121
  module Connection
122
+ # The I/O loop shared by Client and Server. An internal implementation
123
+ # detail: not meant to be included in other classes.
124
+ module Pump
125
+ def bind: (untyped sock) -> self
126
+ def bound_socket: () -> untyped
127
+ def run: () -> void
128
+ def pump_once: (?timeout: Float?) -> void
129
+ def pump_until: () { () -> boolish } -> void
130
+
131
+ # Pops the next peer-initiated stream off the accept queue, driving
132
+ # the I/O loop until one arrives. timeout: nil blocks forever
133
+ # (requires #bind), timeout: 0 returns immediately, timeout: Numeric
134
+ # blocks up to that many seconds. Returns nil on timeout. Raises
135
+ # QUIC::Error::InvalidState when a C stream handler owns the streams.
136
+ def accept_stream: (?timeout: Numeric?) -> Stream?
137
+
138
+ # Non-blocking #accept_stream. Returns the next queued peer-initiated
139
+ # stream, or raises QUIC::Error::WaitReadable when the queue is empty.
140
+ def accept_stream_nonblock: () -> Stream
141
+ end
142
+
89
143
  class Client
144
+ include Pump
145
+
90
146
  attr_reader remote_address: Addrinfo
91
147
 
92
148
  def self.new: (host: String, port: Integer, ?address_family: Symbol?, ?transport_params: TransportParams?, ?settings: Settings?) -> Client
@@ -111,32 +167,148 @@ module QUIC
111
167
  def in_closing_period?: () -> bool
112
168
  def in_draining_period?: () -> bool
113
169
 
114
- def bind: (untyped sock) -> Client
115
- def bound_socket: () -> untyped
116
- def run: () -> void
117
- def pump_once: (?timeout: Float?) -> void
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
170
  # Sends an application CONNECTION_CLOSE (frame type 0x1d) to the peer
131
171
  # carrying error_code and reason, then transitions the underlying
132
- # ngtcp2_conn to the closing period. Requires Client#bind to have been
172
+ # ngtcp2_conn to the closing period. Requires #bind to have been
133
173
  # called; raises QUIC::Error::NotBound otherwise. A second #close call
134
174
  # after the connection has already entered the closing or draining
135
175
  # period is a no-op.
136
176
  def close: (?error_code: Integer, ?reason: String) -> nil
177
+
178
+ # Always nil: only a connection an Endpoint made has a closing reason.
179
+ def error: () -> Exception?
180
+ end
181
+
182
+ class Server
183
+ include Pump
184
+
185
+ attr_reader remote_address: Addrinfo
186
+
187
+ # Waits for the first datagram on sock (bound to the listen address,
188
+ # not connected), connects sock to its sender, and returns a Server
189
+ # for that one client; call #run to complete the handshake.
190
+ # - The wait has no timeout; close sock from another thread to give up.
191
+ # - If anything fails after the first datagram arrived, sock stays
192
+ # connected to its sender: discard it and accept on a new socket.
193
+ # - settings must name at least one ALPN protocol; leaving it out
194
+ # raises ArgumentError, since QUIC::Settings.default has none.
195
+ # - The certificate file is PEM, the leaf certificate first and then
196
+ # any intermediates.
197
+ def self.accept: (
198
+ sock: untyped,
199
+ certificate_path: String | _ToPath,
200
+ private_key_path: String | _ToPath,
201
+ ?transport_params: TransportParams?,
202
+ ?settings: Settings?
203
+ ) -> Server
204
+
205
+ # Builds a server connection from initial_packet, the first datagram a
206
+ # client sent; feed that datagram to #read_pkt afterwards. The
207
+ # certificate file is PEM, the leaf certificate first and then any
208
+ # intermediates. settings.alpn must not be empty (ArgumentError).
209
+ # A first packet that cannot start a connection raises
210
+ # QUIC::Error::Proto.
211
+ def self._accept: (
212
+ initial_packet: String,
213
+ local_sockaddr: String,
214
+ remote_sockaddr: String,
215
+ certificate_path: String | _ToPath,
216
+ private_key_path: String | _ToPath,
217
+ transport_params: TransportParams,
218
+ settings: Settings
219
+ ) -> Server
220
+
221
+ def open_bidi_stream: () -> Stream
222
+ def open_uni_stream: () -> Stream
223
+
224
+ def write_pkt: (?String? buffer) -> String?
225
+ def read_pkt: (String packet, local_sockaddr: String, remote_sockaddr: String) -> nil
226
+
227
+ def expiry: () -> Integer?
228
+ def handle_expiry: () -> nil
229
+ def handshake_completed?: () -> bool
230
+ def in_closing_period?: () -> bool
231
+ def in_draining_period?: () -> bool
232
+
233
+ # Sends an application CONNECTION_CLOSE (frame type 0x1d) to the peer
234
+ # carrying error_code and reason, then transitions the underlying
235
+ # ngtcp2_conn to the closing period. Requires #bind to have been
236
+ # called; raises QUIC::Error::NotBound otherwise. A connection an
237
+ # Endpoint made needs no #bind: the CONNECTION_CLOSE goes out through
238
+ # Endpoint#write_pkt. A second #close call after the connection has
239
+ # already entered the closing or draining period, or after it left
240
+ # its Endpoint, is a no-op.
241
+ def close: (?error_code: Integer, ?reason: String) -> nil
242
+
243
+ # Why the Endpoint closed this connection: nil when the peer closed it,
244
+ # it timed out idle or #close was called; the exception itself when
245
+ # Ruby code in a callback raised; otherwise a QUIC::Error made (not
246
+ # raised) from ngtcp2's error code. Always nil for a connection no
247
+ # Endpoint made.
248
+ def error: () -> Exception?
137
249
  end
138
250
  end
139
251
 
252
+ # Many server connections on one UDP socket, which must not be connected
253
+ # and must be bound to the address it serves. The Endpoint routes each
254
+ # datagram to its connection, makes connections for new clients and
255
+ # answers what no connection can (Version Negotiation, Stateless Reset,
256
+ # CONNECTION_REFUSED); the socket and the I/O loop stay with the caller.
257
+ # A connection's failure never raises from these methods; it closes that
258
+ # connection alone (see Connection::Server#error). The connections have
259
+ # no socket, so only their non-blocking methods work.
260
+ class Endpoint
261
+ attr_reader local_address: Addrinfo
262
+
263
+ # tls_context's ALPN list is the server's; settings.alpn is not read.
264
+ # max_connections counts closing and draining connections too.
265
+ def self.new: (
266
+ local_address: Addrinfo,
267
+ tls_context: TLSContext,
268
+ ?transport_params: TransportParams?,
269
+ ?settings: Settings?,
270
+ ?max_connections: Integer
271
+ ) -> Endpoint
272
+
273
+ def self._new: (
274
+ local_sockaddr: String,
275
+ tls_context: TLSContext,
276
+ transport_params: TransportParams,
277
+ settings: Settings,
278
+ max_connections: Integer
279
+ ) -> Endpoint
280
+
281
+ # Routes one datagram from remote (an AF_INET or AF_INET6 sockaddr, as
282
+ # Addrinfo#to_sockaddr makes; ArgumentError otherwise); local defaults
283
+ # to local_address. Returns the connection it went to, which may be a
284
+ # new one, or nil when the Endpoint answered or dropped it.
285
+ def read_pkt: (String packet, remote: String, ?local: String?) -> Connection::Server?
286
+
287
+ # The next datagram to send and its destination sockaddr, or nil when
288
+ # there is none left: the Endpoint's own answers first (at most 1024
289
+ # wait; any beyond are dropped), then each connection's packets in turn.
290
+ def write_pkt: () -> [String, String]?
291
+
292
+ # The earliest CLOCK_MONOTONIC time (ns) #handle_expiry is due, or nil.
293
+ def expiry: () -> Integer?
294
+
295
+ # Runs the timers that are due and takes out of the table the
296
+ # connections that are finished.
297
+ def handle_expiry: () -> nil
298
+
299
+ # The connections that left the table since the last call.
300
+ def take_closed: () -> Array[Connection::Server]
301
+
302
+ # The connections in the table, closing and draining ones included.
303
+ def connections: () -> Array[Connection::Server]
304
+
305
+ # Refuses every new connection from now on with CONNECTION_REFUSED.
306
+ def stop_accepting: () -> nil
307
+
308
+ # The context new connections are made with; existing ones keep theirs.
309
+ def tls_context=: (TLSContext) -> TLSContext
310
+ end
311
+
140
312
  class Stream
141
313
  INITIATORS: Array[Symbol]
142
314
 
@@ -147,10 +319,11 @@ module QUIC
147
319
  def read: (?Integer? length) -> String?
148
320
  def read_nonblock: (Integer length) -> String
149
321
 
150
- # Blocks (by internally calling Client#pump_once) until the peer's flow
151
- # control window has room for the full payload, then queues all bytes
152
- # and returns data.bytesize. Requires the parent Client to be bound to
153
- # a socket via Client#bind; raises QUIC::Error::NotBound otherwise.
322
+ # Blocks (by internally calling #pump_once on the owning Client or
323
+ # Server) until the peer's flow control window has room for the full
324
+ # payload, then queues all bytes and returns data.bytesize. Requires the
325
+ # owning connection to be bound to a socket via #bind; raises
326
+ # QUIC::Error::NotBound otherwise.
154
327
  def write: (String data, ?fin: bool) -> Integer
155
328
 
156
329
  # Non-blocking variant. Queues as much of `data` as the current flow
@@ -161,6 +334,14 @@ module QUIC
161
334
  # re-issue with `fin: true` on the trailing chunk.
162
335
  def write_nonblock: (String data, ?fin: bool) -> Integer
163
336
  def eof?: () -> bool
337
+
338
+ # True once the stream is fully closed: both directions have finished
339
+ # (FIN or reset) and everything sent has been acknowledged. Stays false
340
+ # if the connection closes first, since ngtcp2 does not report stream
341
+ # closure then. Keeps returning true after the stream has left the
342
+ # connection. A bare Stream is never closed.
343
+ def closed?: () -> bool
344
+
164
345
  def close_write: () -> nil
165
346
  def close_read: () -> nil
166
347
  def close: () -> nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Nakamura
@@ -44,18 +44,30 @@ files:
44
44
  - Rakefile
45
45
  - examples/doq_demo.rb
46
46
  - examples/echo_demo.rb
47
+ - examples/echo_server_demo.rb
47
48
  - examples/handshake_demo.rb
48
49
  - examples/io_loop_demo.rb
49
50
  - examples/verify_demo.rb
50
51
  - ext/quic/connection_client.c
52
+ - ext/quic/connection_common.c
53
+ - ext/quic/connection_common.h
54
+ - ext/quic/connection_server.c
55
+ - ext/quic/endpoint.c
56
+ - ext/quic/endpoint.h
57
+ - ext/quic/ext_api.c
51
58
  - ext/quic/extconf.rb
59
+ - ext/quic/include/quic/ext.h
52
60
  - ext/quic/patches/picotls/0001-openssl-use-raw-public-key-api-for-X25519.patch
53
61
  - ext/quic/quic.c
54
62
  - ext/quic/quic.h
55
63
  - ext/quic/stream.c
56
64
  - ext/quic/stream.h
65
+ - ext/quic/tls_context.c
66
+ - ext/quic/tls_context.h
57
67
  - lib/quic.rb
58
68
  - lib/quic/connection.rb
69
+ - lib/quic/endpoint.rb
70
+ - lib/quic/ext_include.rb
59
71
  - lib/quic/settings.rb
60
72
  - lib/quic/stream.rb
61
73
  - lib/quic/transport_params.rb