quic 0.0.1 → 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.
@@ -0,0 +1,260 @@
1
+ #include "tls_context.h"
2
+ #include "connection_common.h"
3
+
4
+ #include <stddef.h>
5
+ #include <string.h>
6
+
7
+ #include <openssl/bio.h>
8
+ #include <openssl/err.h>
9
+ #include <openssl/pem.h>
10
+ #include <openssl/x509.h>
11
+
12
+ VALUE rb_cQUICTLSContext;
13
+
14
+ void
15
+ quic_tls_ctx_ref(quic_tls_ctx_t *t)
16
+ {
17
+ t->refcnt++;
18
+ }
19
+
20
+ void
21
+ quic_tls_ctx_unref(quic_tls_ctx_t *t)
22
+ {
23
+ if (--t->refcnt > 0) return;
24
+ if (t->sign_cert_initialized) ptls_openssl_dispose_sign_certificate(&t->sign_cert);
25
+ /* picotls malloc()ed the list and each entry, so free(), not xfree(). */
26
+ if (t->ctx.certificates.list != NULL) {
27
+ for (size_t i = 0; i < t->ctx.certificates.count; i++) free(t->ctx.certificates.list[i].base);
28
+ free(t->ctx.certificates.list);
29
+ }
30
+ xfree(t->alpn);
31
+ xfree(t->alpn_buf);
32
+ xfree(t);
33
+ }
34
+
35
+ /* The QUIC::TLSContext object holds one reference. A context it never
36
+ finished building (an exception mid-way) is released the same way. */
37
+ static void
38
+ quic_tls_context_free(void *ptr)
39
+ {
40
+ if (ptr != NULL) quic_tls_ctx_unref((quic_tls_ctx_t *)ptr);
41
+ }
42
+
43
+ static size_t
44
+ quic_tls_context_size(const void *ptr)
45
+ {
46
+ (void)ptr;
47
+ return sizeof(quic_tls_ctx_t);
48
+ }
49
+
50
+ static const rb_data_type_t quic_tls_context_type = {
51
+ "QUIC::TLSContext",
52
+ {NULL, quic_tls_context_free, quic_tls_context_size,},
53
+ NULL, NULL,
54
+ RUBY_TYPED_FREE_IMMEDIATELY,
55
+ };
56
+
57
+ quic_tls_ctx_t *
58
+ quic_tls_context_get(VALUE tls_context)
59
+ {
60
+ return (quic_tls_ctx_t *)rb_check_typeddata(tls_context, &quic_tls_context_type);
61
+ }
62
+
63
+ /* Pick the first of the server's ALPN protocols, in the server's order of
64
+ preference, that the client offered. Runs inside read_pkt with the GVL
65
+ held and never touches Ruby objects. */
66
+ static int
67
+ quic_tls_on_client_hello_cb(ptls_on_client_hello_t *self, ptls_t *tls,
68
+ ptls_on_client_hello_parameters_t *params)
69
+ {
70
+ quic_tls_ctx_t *t = (quic_tls_ctx_t *)((char *)self - offsetof(quic_tls_ctx_t, on_client_hello));
71
+ for (size_t i = 0; i < t->alpn_count; i++) {
72
+ for (size_t j = 0; j < params->negotiated_protocols.count; j++) {
73
+ ptls_iovec_t offered = params->negotiated_protocols.list[j];
74
+ if (offered.len == t->alpn[i].len &&
75
+ memcmp(offered.base, t->alpn[i].base, offered.len) == 0) {
76
+ return ptls_set_negotiated_protocol(tls, (const char *)offered.base, offered.len) == 0
77
+ ? 0 : PTLS_ERROR_NO_MEMORY;
78
+ }
79
+ }
80
+ }
81
+ return PTLS_ALERT_NO_APPLICATION_PROTOCOL;
82
+ }
83
+
84
+ /* Never supply a passphrase. Without a callback, PEM_read_bio_PrivateKey
85
+ falls back to PEM_def_callback, which prompts on the controlling terminal;
86
+ this way an encrypted key fails to load like any other unreadable one. */
87
+ static int
88
+ quic_tls_no_passphrase_cb(char *buf, int size, int rwflag, void *userdata)
89
+ {
90
+ (void)buf;
91
+ (void)size;
92
+ (void)rwflag;
93
+ (void)userdata;
94
+ return -1;
95
+ }
96
+
97
+ /* Load the certificate chain and private key into t->ctx and install the
98
+ signer. The certificate file is PEM, the leaf first and then any
99
+ intermediates; there is no limit on their number. A misconfiguration
100
+ raises ArgumentError naming the step that failed. Everything allocated
101
+ here is released before raising, and libcrypto's error queue, which is
102
+ shared with Ruby's openssl extension, is left empty on every return. */
103
+ static void
104
+ quic_tls_ctx_load_certificate_and_key(quic_tls_ctx_t *t, VALUE certificate_path,
105
+ VALUE private_key_path)
106
+ {
107
+ VALUE failure_class = rb_eArgError;
108
+ const char *failure = NULL;
109
+ VALUE failed_path = Qnil;
110
+ BIO *cert_bio = NULL;
111
+ BIO *key_bio = NULL;
112
+ X509 *leaf = NULL;
113
+ X509 *x;
114
+ STACK_OF(X509) *chain = NULL;
115
+ EVP_PKEY *pkey = NULL;
116
+ unsigned long err;
117
+
118
+ ERR_clear_error();
119
+
120
+ cert_bio = BIO_new_file(RSTRING_PTR(certificate_path), "r");
121
+ if (cert_bio == NULL || (leaf = PEM_read_bio_X509(cert_bio, NULL, NULL, NULL)) == NULL) {
122
+ failure = "failed to load certificate";
123
+ failed_path = certificate_path;
124
+ goto done;
125
+ }
126
+
127
+ if ((chain = sk_X509_new_null()) == NULL) {
128
+ failure_class = rb_eRuntimeError;
129
+ failure = "sk_X509_new_null failed";
130
+ goto done;
131
+ }
132
+ while ((x = PEM_read_bio_X509(cert_bio, NULL, NULL, NULL)) != NULL) {
133
+ if (!sk_X509_push(chain, x)) {
134
+ X509_free(x);
135
+ failure_class = rb_eRuntimeError;
136
+ failure = "sk_X509_push failed";
137
+ goto done;
138
+ }
139
+ }
140
+ /* The loop ends with PEM_R_NO_START_LINE once no PEM block is left. Any
141
+ other error means a broken block, as in SSL_CTX_use_certificate_chain_file. */
142
+ err = ERR_peek_last_error();
143
+ if (ERR_GET_LIB(err) != ERR_LIB_PEM || ERR_GET_REASON(err) != PEM_R_NO_START_LINE) {
144
+ failure = "failed to load certificate";
145
+ failed_path = certificate_path;
146
+ goto done;
147
+ }
148
+
149
+ key_bio = BIO_new_file(RSTRING_PTR(private_key_path), "r");
150
+ if (key_bio == NULL ||
151
+ (pkey = PEM_read_bio_PrivateKey(key_bio, NULL, quic_tls_no_passphrase_cb, NULL)) == NULL) {
152
+ failure = "failed to load private key";
153
+ failed_path = private_key_path;
154
+ goto done;
155
+ }
156
+
157
+ /* picotls has no counterpart of SSL_CTX_check_private_key. */
158
+ if (X509_check_private_key(leaf, pkey) != 1) {
159
+ failure = "private key does not match certificate";
160
+ failed_path = private_key_path;
161
+ goto done;
162
+ }
163
+
164
+ /* Takes its own reference on pkey. */
165
+ if (ptls_openssl_init_sign_certificate(&t->sign_cert, pkey) != 0) {
166
+ failure = "unsupported private key type";
167
+ failed_path = private_key_path;
168
+ goto done;
169
+ }
170
+ t->sign_cert_initialized = true;
171
+ t->ctx.sign_certificate = &t->sign_cert.super;
172
+
173
+ /* Copies the DER of each certificate into a list quic_tls_ctx_unref
174
+ releases. It fails only on allocation, and frees what it allocated then. */
175
+ if (ptls_openssl_load_certificates(&t->ctx, leaf, chain) != 0) {
176
+ failure_class = rb_eRuntimeError;
177
+ failure = "ptls_openssl_load_certificates failed";
178
+ goto done;
179
+ }
180
+
181
+ done:
182
+ sk_X509_pop_free(chain, X509_free);
183
+ X509_free(leaf);
184
+ EVP_PKEY_free(pkey);
185
+ BIO_free(cert_bio);
186
+ BIO_free(key_bio);
187
+ ERR_clear_error();
188
+
189
+ if (failure == NULL) return;
190
+ if (NIL_P(failed_path)) rb_raise(failure_class, "%s", failure);
191
+ rb_raise(failure_class, "%s: %"PRIsVALUE, failure, failed_path);
192
+ }
193
+
194
+ VALUE
195
+ quic_tls_context_server_new(VALUE certificate_path, VALUE private_key_path, VALUE alpn_ary)
196
+ {
197
+ certificate_path = quic_path_value(certificate_path, "certificate_path");
198
+ private_key_path = quic_path_value(private_key_path, "private_key_path");
199
+ if (NIL_P(certificate_path)) rb_raise(rb_eArgError, "certificate_path must not be nil");
200
+ if (NIL_P(private_key_path)) rb_raise(rb_eArgError, "private_key_path must not be nil");
201
+
202
+ /* The object owns the context from the start, so that an exception below
203
+ releases whatever was built so far. */
204
+ VALUE self = TypedData_Wrap_Struct(rb_cQUICTLSContext, &quic_tls_context_type, NULL);
205
+ quic_tls_ctx_t *t = ZALLOC(quic_tls_ctx_t);
206
+ t->refcnt = 1;
207
+ DATA_PTR(self) = t;
208
+
209
+ /* Without an ALPN to pick, every handshake would end in
210
+ no_application_protocol. */
211
+ quic_copy_alpn(alpn_ary, &t->alpn, &t->alpn_count, &t->alpn_buf);
212
+ if (t->alpn_count == 0) rb_raise(rb_eArgError, "alpn must not be empty for a server");
213
+
214
+ t->ctx = (ptls_context_t){
215
+ .random_bytes = ptls_openssl_random_bytes,
216
+ .get_time = &ptls_get_time,
217
+ .key_exchanges = quic_key_exchanges,
218
+ .cipher_suites = quic_cipher_suites,
219
+ /* No session resumption: encrypt_ticket stays NULL, so no ticket is
220
+ ever issued. Keep this so that PSK-only resumption is refused once
221
+ resumption is added. */
222
+ .require_dhe_on_psk = 1,
223
+ .server_cipher_preference = 1,
224
+ };
225
+ if (ngtcp2_crypto_picotls_configure_server_context(&t->ctx) != 0) {
226
+ rb_raise(rb_eRuntimeError, "ngtcp2_crypto_picotls_configure_server_context failed");
227
+ }
228
+ /* verify_certificate stays NULL: no client certificate is requested. */
229
+ quic_tls_ctx_load_certificate_and_key(t, certificate_path, private_key_path);
230
+ t->on_client_hello.cb = quic_tls_on_client_hello_cb;
231
+ t->ctx.on_client_hello = &t->on_client_hello;
232
+ return self;
233
+ }
234
+
235
+ /* QUIC::TLSContext.server(certificate_path:, private_key_path:, alpn:). A
236
+ missing or unknown keyword raises ArgumentError as a Ruby method would. */
237
+ static VALUE
238
+ quic_tls_context_s_server(int argc, VALUE *argv, VALUE klass)
239
+ {
240
+ (void)klass;
241
+ VALUE opts = Qnil;
242
+ rb_scan_args(argc, argv, "0:", &opts);
243
+ if (NIL_P(opts)) opts = rb_hash_new();
244
+
245
+ ID keywords[3];
246
+ VALUE values[3];
247
+ keywords[0] = rb_intern("certificate_path");
248
+ keywords[1] = rb_intern("private_key_path");
249
+ keywords[2] = rb_intern("alpn");
250
+ rb_get_kwargs(opts, keywords, 3, 0, values);
251
+ return quic_tls_context_server_new(values[0], values[1], values[2]);
252
+ }
253
+
254
+ void
255
+ Init_quic_tls_context(VALUE rb_mQUICArg)
256
+ {
257
+ rb_cQUICTLSContext = rb_define_class_under(rb_mQUICArg, "TLSContext", rb_cObject);
258
+ rb_undef_alloc_func(rb_cQUICTLSContext);
259
+ rb_define_singleton_method(rb_cQUICTLSContext, "server", quic_tls_context_s_server, -1);
260
+ }
@@ -0,0 +1,46 @@
1
+ #ifndef QUIC_TLS_CONTEXT_H
2
+ #define QUIC_TLS_CONTEXT_H 1
3
+
4
+ #include "quic.h"
5
+
6
+ /* A server's TLS context: the certificate chain, the signer and the ALPN
7
+ list, read once and shared by every connection made from it. Reference
8
+ counted: the QUIC::TLSContext object holds one reference and each server
9
+ connection another, so a context replaced on an Endpoint lives on until
10
+ the last connection that uses it is freed. */
11
+ typedef struct {
12
+ /* Handed to ptls_server_new. certificates.list is malloc()ed by picotls
13
+ and freed with the context. */
14
+ ptls_context_t ctx;
15
+ /* Referenced by ctx.sign_certificate; holds a reference to the key. */
16
+ ptls_openssl_sign_certificate_t sign_cert;
17
+ bool sign_cert_initialized;
18
+ /* ctx.on_client_hello points here; the callback recovers this struct
19
+ from its self argument with offsetof, so it needs no connection. */
20
+ ptls_on_client_hello_t on_client_hello;
21
+ /* The ALPN protocols the server accepts, in its order of preference. The
22
+ entries point into alpn_buf. */
23
+ ptls_iovec_t *alpn;
24
+ size_t alpn_count;
25
+ uint8_t *alpn_buf;
26
+ long refcnt;
27
+ } quic_tls_ctx_t;
28
+
29
+ extern VALUE rb_cQUICTLSContext;
30
+
31
+ void Init_quic_tls_context(VALUE rb_mQUICArg);
32
+
33
+ /* QUIC::TLSContext.server, callable from C: a new QUIC::TLSContext object
34
+ holding the one reference to a new context. alpn_ary must be a non-empty
35
+ Array of 1-255 byte Strings. Raises ArgumentError naming what could not
36
+ be loaded or used. */
37
+ VALUE quic_tls_context_server_new(VALUE certificate_path, VALUE private_key_path, VALUE alpn_ary);
38
+
39
+ /* The context behind a QUIC::TLSContext object. Raises TypeError for any
40
+ other object. */
41
+ quic_tls_ctx_t *quic_tls_context_get(VALUE tls_context);
42
+
43
+ void quic_tls_ctx_ref(quic_tls_ctx_t *t);
44
+ void quic_tls_ctx_unref(quic_tls_ctx_t *t);
45
+
46
+ #endif /* QUIC_TLS_CONTEXT_H */
@@ -2,25 +2,13 @@
2
2
 
3
3
  require "socket"
4
4
 
5
- module Quic
5
+ module QUIC
6
6
  module Connection
7
- class Client
8
- def self.new(host:, port:, transport_params: nil, settings: nil)
9
- remote_sockaddr = Addrinfo.udp(host, port).to_sockaddr
10
- local_sockaddr = Addrinfo.udp("0.0.0.0", 0).to_sockaddr
11
-
12
- client = _open(
13
- local_sockaddr: local_sockaddr,
14
- remote_sockaddr: remote_sockaddr,
15
- server_name: host,
16
- transport_params: transport_params || Quic::TransportParams.default,
17
- settings: settings || Quic::Settings.default
18
- )
19
- client.instance_variable_set(:@host, host)
20
- client.instance_variable_set(:@port, port)
21
- client
22
- end
23
-
7
+ # The Ruby side of the I/O loop, shared by Client and Server. It is an
8
+ # internal implementation detail: not meant to be included in other
9
+ # classes, and the ivars and methods it relies on are not a public
10
+ # contract.
11
+ module Pump
24
12
  # Attach an IO.select / #send / #recvfrom-compatible UDP socket so that
25
13
  # #run and Stream blocking I/O can drive the wire transparently. Caller
26
14
  # is responsible for socket lifetime; we do not close it. The socket
@@ -35,10 +23,10 @@ module Quic
35
23
  end
36
24
 
37
25
  # Drive the handshake to completion using the bound socket. Raises
38
- # Quic::Error::NotBound if #bind has not been called. Propagates any
39
- # Quic::Error subclass raised by #read_pkt / #handle_expiry.
26
+ # QUIC::Error::NotBound if #bind has not been called. Propagates any
27
+ # QUIC::Error subclass raised by #read_pkt / #handle_expiry.
40
28
  def run
41
- raise Quic::Error::NotBound, "Quic::Connection::Client#bind(sock) has not been called" if @sock.nil?
29
+ raise QUIC::Error::NotBound, "#{self.class}#bind(sock) has not been called" if @sock.nil?
42
30
 
43
31
  pump_until { handshake_completed? }
44
32
  end
@@ -49,7 +37,7 @@ module Quic
49
37
  # fire. Public-ish so Stream blocking ops can share it; documented as
50
38
  # "internal" for users.
51
39
  def pump_once(timeout: nil)
52
- raise Quic::Error::NotBound, "Quic::Connection::Client#bind(sock) has not been called" if @sock.nil?
40
+ raise QUIC::Error::NotBound, "#{self.class}#bind(sock) has not been called" if @sock.nil?
53
41
 
54
42
  while (pkt = write_pkt)
55
43
  @sock.send(pkt, 0)
@@ -59,10 +47,11 @@ module Quic
59
47
  ready = IO.select([@sock], nil, nil, wait_for)
60
48
  if ready
61
49
  data, _addr = @sock.recvfrom(2048)
62
- # Reuse the same path tuple that Client.new / Client._open passed to
63
- # ngtcp2_conn_client_new so the read_pkt path matches the stored
64
- # connection path. local is 0.0.0.0:0 (Client.new convention) and
65
- # remote comes from the connected socket (assumes #connect was used).
50
+ # Reuse the same path tuple the connection was built with so the
51
+ # read_pkt path matches the stored connection path. A Client leaves
52
+ # @local_sockaddr unset, so local is 0.0.0.0:0 (Client.new
53
+ # convention); remote comes from the connected socket (assumes
54
+ # #connect was used).
66
55
  read_pkt(
67
56
  data,
68
57
  local_sockaddr: pump_local_sockaddr,
@@ -79,8 +68,64 @@ module Quic
79
68
  pump_once until pred.call
80
69
  end
81
70
 
71
+ # Pop the next peer-initiated stream off the accept queue, driving the
72
+ # I/O loop until one arrives. The queue is fed by the recv_stream_data
73
+ # callback the first time a peer-initiated stream carries data.
74
+ #
75
+ # timeout: nil block until a stream is available (requires #bind)
76
+ # timeout: 0 return immediately; nil if the queue is empty
77
+ # timeout: Numeric block up to that many seconds, then return nil
78
+ #
79
+ # Returns a QUIC::Stream, or nil on timeout.
80
+ def accept_stream(timeout: nil)
81
+ require_stream_mode
82
+ stream = take_accepted_stream
83
+ return stream unless stream.nil?
84
+ return nil if timeout == 0
85
+
86
+ deadline = timeout && Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
87
+ loop do
88
+ if deadline
89
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
90
+ return nil if remaining <= 0.0
91
+ pump_once(timeout: remaining)
92
+ else
93
+ pump_once
94
+ end
95
+ stream = take_accepted_stream
96
+ return stream unless stream.nil?
97
+ end
98
+ end
99
+
100
+ # Non-blocking variant of #accept_stream. Returns the next queued
101
+ # peer-initiated stream, or raises QUIC::Error::WaitReadable
102
+ # (IO::WaitReadable mixin) when the queue is empty. Does not require
103
+ # #bind.
104
+ def accept_stream_nonblock
105
+ require_stream_mode
106
+ stream = take_accepted_stream
107
+ raise QUIC::Error::WaitReadable, "no peer-initiated stream available" if stream.nil?
108
+ stream
109
+ end
110
+
82
111
  private
83
112
 
113
+ # A C stream handler (quic/ext.h) registered on the connection owns
114
+ # every stream, so no QUIC::Stream is ever queued for #accept_stream.
115
+ def require_stream_mode
116
+ return unless @stream_handler
117
+ raise QUIC::Error::InvalidState, "the connection's streams belong to a C stream handler"
118
+ end
119
+
120
+ # The next stream off the accept queue, or nil. Telling the stream it
121
+ # has been accepted lets the peer open another one in place of a
122
+ # stream that closed while it waited in the queue.
123
+ def take_accepted_stream
124
+ stream = @accept_queue.shift
125
+ stream&.__send__(:_accepted)
126
+ stream
127
+ end
128
+
84
129
  def pump_timeout(caller_timeout)
85
130
  ngtcp2_expiry_ns = expiry
86
131
  if ngtcp2_expiry_ns.nil?
@@ -93,11 +138,111 @@ module Quic
93
138
  end
94
139
 
95
140
  def pump_local_sockaddr
96
- @pump_local_sockaddr ||= Addrinfo.udp("0.0.0.0", 0).to_sockaddr
141
+ @local_sockaddr ||= Addrinfo.udp("0.0.0.0", 0).to_sockaddr
97
142
  end
98
143
 
99
144
  def pump_remote_sockaddr
100
- @pump_remote_sockaddr ||= @sock.remote_address.to_sockaddr
145
+ @remote_sockaddr ||= @sock.remote_address.to_sockaddr
146
+ end
147
+ end
148
+
149
+ class Client
150
+ include Pump
151
+
152
+ attr_reader :remote_address
153
+
154
+ # Build a Client and pin a single peer address. address_family takes a
155
+ # Symbol (:inet for IPv4, :inet6 for IPv6) or nil to defer family
156
+ # selection to Addrinfo.udp's implicit resolution. Pin the resolved
157
+ # Addrinfo into @remote_address so callers can match their UDPSocket's
158
+ # connected sockaddr (otherwise DNS round-robin between Client.new and
159
+ # sock.connect can produce a path that ngtcp2 silently drops).
160
+ def self.new(host:, port:, address_family: nil, transport_params: nil, settings: nil)
161
+ family = case address_family
162
+ when nil then nil
163
+ when :inet then Socket::AF_INET
164
+ when :inet6 then Socket::AF_INET6
165
+ else
166
+ raise ArgumentError, "unknown address_family: #{address_family.inspect}"
167
+ end
168
+
169
+ remote_address = if family
170
+ Addrinfo.getaddrinfo(host, port, family, :DGRAM, Socket::IPPROTO_UDP).first
171
+ else
172
+ Addrinfo.udp(host, port)
173
+ end
174
+
175
+ remote_sockaddr = remote_address.to_sockaddr
176
+ local_sockaddr = Addrinfo.udp("0.0.0.0", 0).to_sockaddr
177
+ settings ||= QUIC::Settings.default
178
+ settings.check_qlog_dir
179
+
180
+ client = _open(
181
+ local_sockaddr: local_sockaddr,
182
+ remote_sockaddr: remote_sockaddr,
183
+ server_name: host,
184
+ transport_params: transport_params || QUIC::TransportParams.default,
185
+ settings: settings
186
+ )
187
+ client.instance_variable_set(:@host, host)
188
+ client.instance_variable_set(:@port, port)
189
+ client.instance_variable_set(:@remote_address, remote_address)
190
+ client
191
+ end
192
+ end
193
+
194
+ # One server-side connection. Built by Server.accept (or Server._accept)
195
+ # from the first datagram a client sent; there is no public Server.new,
196
+ # since an object without an ngtcp2 connection behind it would be
197
+ # unusable.
198
+ class Server
199
+ include Pump
200
+
201
+ attr_reader :remote_address
202
+
203
+ private_class_method :new
204
+
205
+ # Wait for the first datagram on sock and build a connection for its
206
+ # sender. sock must be bound to the listen address and not connected.
207
+ # It is connected to the first sender, so this Server serves exactly
208
+ # that one client and shares the I/O loop with Client as is. The
209
+ # handshake is left to #run.
210
+ #
211
+ # The wait for the first datagram has no timeout; close sock from
212
+ # another thread to give up. If anything fails after that datagram
213
+ # arrived, sock stays connected to its sender: discard it and accept on
214
+ # a new socket. accept never closes sock; its lifetime is the caller's.
215
+ #
216
+ # settings must name at least one ALPN protocol, so the default
217
+ # (QUIC::Settings.default) raises ArgumentError. A qlog_dir that is not
218
+ # a writable directory raises ArgumentError before the wait.
219
+ def self.accept(sock:, certificate_path:, private_key_path:, transport_params: nil, settings: nil)
220
+ settings ||= QUIC::Settings.default
221
+ settings.check_qlog_dir
222
+
223
+ data, sender = sock.recvfrom(2048)
224
+ sock.connect(sender[3], sender[1])
225
+ # Read both addresses back from the connected socket so that the path
226
+ # given to ngtcp2 here and every later read_pkt path come from one
227
+ # source and always match.
228
+ local_sockaddr = sock.local_address.to_sockaddr
229
+ remote_sockaddr = sock.remote_address.to_sockaddr
230
+
231
+ server = _accept(
232
+ initial_packet: data,
233
+ local_sockaddr: local_sockaddr,
234
+ remote_sockaddr: remote_sockaddr,
235
+ certificate_path: certificate_path,
236
+ private_key_path: private_key_path,
237
+ transport_params: transport_params || QUIC::TransportParams.default,
238
+ settings: settings
239
+ )
240
+ server.instance_variable_set(:@local_sockaddr, local_sockaddr)
241
+ server.instance_variable_set(:@remote_sockaddr, remote_sockaddr)
242
+ server.instance_variable_set(:@remote_address, sock.remote_address)
243
+ server.bind(sock)
244
+ server.read_pkt(data, local_sockaddr: local_sockaddr, remote_sockaddr: remote_sockaddr)
245
+ server
101
246
  end
102
247
  end
103
248
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module QUIC
6
+ # Many server connections on one UDP socket. The Endpoint routes each
7
+ # datagram to its connection by connection ID, makes connections for new
8
+ # clients and answers what no connection can (Version Negotiation,
9
+ # Stateless Reset, CONNECTION_REFUSED). The socket and the I/O loop stay
10
+ # with the caller:
11
+ #
12
+ # loop do
13
+ # data, addr = sock.recvfrom(65_535) # or recvmsg_nonblock
14
+ # endpoint.read_pkt(data, remote: addr.to_sockaddr)
15
+ # endpoint.handle_expiry # when endpoint.expiry is due
16
+ # while (pkt, to = endpoint.write_pkt)
17
+ # sock.send(pkt, 0, to)
18
+ # end
19
+ # endpoint.take_closed.each { |conn| ... }
20
+ # end
21
+ #
22
+ # The socket must not be connected, and must be bound to the address it
23
+ # serves (not 0.0.0.0): every path's local address is local_address.
24
+ #
25
+ # The Endpoint's own answers wait for #write_pkt in a queue of at most
26
+ # 1024; any beyond are dropped, so call #write_pkt often under load.
27
+ #
28
+ # A connection's failure never raises from the Endpoint's methods. It
29
+ # closes that connection alone, and Connection#error tells why. The
30
+ # connections have no socket, so only their non-blocking methods work;
31
+ # #close queues its CONNECTION_CLOSE for #write_pkt.
32
+ class Endpoint
33
+ attr_reader :local_address
34
+
35
+ # tls_context is a QUIC::TLSContext; its ALPN list is the server's, and
36
+ # settings.alpn is not read. max_connections counts connections in their
37
+ # closing or draining period too.
38
+ def self.new(local_address:, tls_context:, transport_params: nil, settings: nil, max_connections: 1024)
39
+ raise TypeError, "local_address must be an Addrinfo (got #{local_address.class})" unless local_address.is_a?(Addrinfo)
40
+ unless max_connections.is_a?(Integer)
41
+ raise TypeError, "max_connections must be an Integer (got #{max_connections.class})"
42
+ end
43
+ settings ||= QUIC::Settings.default
44
+ settings.check_qlog_dir
45
+
46
+ endpoint = _new(
47
+ local_sockaddr: local_address.to_sockaddr,
48
+ tls_context: tls_context,
49
+ transport_params: transport_params || QUIC::TransportParams.default,
50
+ settings: settings,
51
+ max_connections: max_connections
52
+ )
53
+ endpoint.instance_variable_set(:@local_address, local_address)
54
+ endpoint
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QUIC
4
+ # The directory holding quic/ext.h, the C function table other native
5
+ # extensions build against. An extconf.rb adds it to its include path:
6
+ #
7
+ # require "quic/ext_include"
8
+ # append_cppflags("-I#{QUIC::EXT_INCLUDE_DIR}")
9
+ #
10
+ # This file does not load the extension, so extconf.rb can require it.
11
+ EXT_INCLUDE_DIR = File.expand_path("../../ext/quic/include", __dir__)
12
+ end