http3 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,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module HTTP3
6
+ # Calls a Rack 3 application for an HTTP3::Request and turns what it
7
+ # returns into an HTTP3::Response.
8
+ class RackAdapter
9
+ # The application's response breaks the Rack 3 specification; the
10
+ # server answers 500 instead.
11
+ class InvalidResponse < StandardError; end
12
+
13
+ # Connection-specific fields, which HTTP/3 forbids (RFC 9114 section
14
+ # 4.2).
15
+ CONNECTION_FIELDS = %w[connection keep-alive transfer-encoding upgrade proxy-connection].freeze
16
+
17
+ # A response field name: a token (RFC 9110 section 5.6.2), lowercase.
18
+ FIELD_NAME = /\A[!#$%&'*+\-.^_`|~0-9a-z]+\z/
19
+
20
+ # What a field value must not hold (RFC 9114 section 4.2, and the Rack 3
21
+ # specification).
22
+ FORBIDDEN_IN_VALUE = /[\0\r\n]/
23
+
24
+ def initialize(app)
25
+ @app = app
26
+ end
27
+
28
+ # remote is the client's address, an Addrinfo. Raises whatever the
29
+ # application raises, and InvalidResponse for a response that cannot be
30
+ # sent; the Rack body is closed in both cases.
31
+ def call(request, remote:)
32
+ status, headers, body = @app.call(env_for(request, remote))
33
+ response_for(request, status, headers, body)
34
+ end
35
+
36
+ private
37
+
38
+ def env_for(request, remote)
39
+ pseudo = {}
40
+ fields = {}
41
+ request.headers.each do |name, value|
42
+ if name.start_with?(":")
43
+ pseudo[name] ||= value
44
+ else
45
+ (fields[name] ||= []) << value
46
+ end
47
+ end
48
+
49
+ path, query = pseudo.fetch(":path", "/").split("?", 2)
50
+ scheme = pseudo.fetch(":scheme", "https")
51
+ authority = pseudo[":authority"] || fields["host"]&.first || "localhost"
52
+ server_name, server_port = split_authority(authority, scheme)
53
+
54
+ env = {
55
+ "REQUEST_METHOD" => pseudo.fetch(":method", "GET"),
56
+ "SCRIPT_NAME" => "",
57
+ "PATH_INFO" => path,
58
+ "QUERY_STRING" => query || "",
59
+ "SERVER_NAME" => server_name,
60
+ "SERVER_PORT" => server_port,
61
+ "SERVER_PROTOCOL" => "HTTP/3",
62
+ "REMOTE_ADDR" => remote.ip_address,
63
+ "HTTP_HOST" => authority,
64
+ "rack.url_scheme" => scheme,
65
+ "rack.input" => StringIO.new(request.body.b),
66
+ "rack.errors" => $stderr
67
+ }
68
+ fields.each do |name, values|
69
+ next if name == "host"
70
+ # "x_forwarded_for" would become HTTP_X_FORWARDED_FOR just like
71
+ # "x-forwarded-for", letting a client slip a value past a proxy that
72
+ # only removes or overwrites the latter. Such names are dropped, as
73
+ # nginx and Puma do.
74
+ next if name.include?("_")
75
+
76
+ # Cookies are split into one field each in HTTP/3 and joined with
77
+ # "; " again (RFC 9114 section 4.2.1).
78
+ env[env_key(name)] = values.join((name == "cookie") ? "; " : ", ")
79
+ end
80
+ env
81
+ end
82
+
83
+ def env_key(name)
84
+ case name
85
+ when "content-type" then "CONTENT_TYPE"
86
+ when "content-length" then "CONTENT_LENGTH"
87
+ else "HTTP_#{name.upcase.tr("-", "_")}"
88
+ end
89
+ end
90
+
91
+ # "host", "host:port", "[v6]" or "[v6]:port"; the port defaults to the
92
+ # scheme's.
93
+ def split_authority(authority, scheme)
94
+ default_port = (scheme == "http") ? "80" : "443"
95
+ if (match = authority.match(/\A(\[[^\]]*\])(?::(\d+))?\z/))
96
+ [match[1], match[2] || default_port]
97
+ else
98
+ host, port = authority.split(":", 2)
99
+ [host, port || default_port]
100
+ end
101
+ end
102
+
103
+ # The Rack body is closed here unless it goes on in the Response.
104
+ def response_for(request, status, headers, body)
105
+ response = nil
106
+ status = Integer(status)
107
+ headers = header_pairs(headers)
108
+
109
+ method = request.headers.assoc(":method")&.last
110
+ response = if method == "HEAD" || status < 200 || status == 204 || status == 304
111
+ close_rack_body(body)
112
+ Response.new(status: status, headers: headers, body: nil)
113
+ else
114
+ Response.new(status: status, headers: headers, body: body_for(body))
115
+ end
116
+ ensure
117
+ close_rack_body(body) if response.nil?
118
+ end
119
+
120
+ # Rack 3 headers: lowercase names, each value a String or an Array of
121
+ # them, one field per element. nghttp3 sends names and values as they
122
+ # are, so a name that is not a token (":status", say) or a value with
123
+ # NUL, CR or LF would reach the client, or a proxy that turns the
124
+ # response into HTTP/1.1, as fields the application never meant. The
125
+ # message leaves the value out, since it goes to the log.
126
+ def header_pairs(headers)
127
+ pairs = []
128
+ headers.each do |name, value|
129
+ name = name.to_s
130
+ raise InvalidResponse, "header name #{name.inspect} has uppercase letters" if name.match?(/[A-Z]/)
131
+ raise InvalidResponse, "header name #{name.inspect} is not a token" unless name.match?(FIELD_NAME)
132
+ next if CONNECTION_FIELDS.include?(name)
133
+
134
+ Array(value).each do |v|
135
+ v = v.to_s
136
+ raise InvalidResponse, "header #{name} has NUL, CR or LF in its value" if v.match?(FORBIDDEN_IN_VALUE)
137
+
138
+ pairs << [name, v]
139
+ end
140
+ end
141
+ pairs
142
+ end
143
+
144
+ def body_for(body)
145
+ if body.respond_to?(:to_path)
146
+ io = ::File.open(body.to_path, "rb")
147
+ Body::File.new(io: io, offset: 0, length: io.size, rack_body: body)
148
+ elsif body.respond_to?(:to_ary)
149
+ # Rack 3 has to_ary close the body itself.
150
+ Body::Chunks.new(enum: body.to_ary.each, rack_body: nil)
151
+ elsif body.respond_to?(:each)
152
+ Body::Chunks.new(enum: body.to_enum(:each), rack_body: body)
153
+ else
154
+ raise InvalidResponse, "streaming bodies (#{body.class}) are not supported"
155
+ end
156
+ end
157
+
158
+ def close_rack_body(body)
159
+ body.close if body.respond_to?(:close)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP3
4
+ # A request as a Session put it together. headers is an Array of
5
+ # [name, value] binary Strings with lowercase names, the pseudo-headers
6
+ # (:method, :scheme, :authority, :path) first; a repeated name keeps one
7
+ # pair per field. body is the request body, a binary String.
8
+ Request = Data.define(:stream_id, :headers, :body)
9
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP3
4
+ # A response for a Session to send. status is an Integer, headers an Array
5
+ # of [name, value] Strings without pseudo-headers, and body nil (no
6
+ # content) or one of the HTTP3::Body types.
7
+ Response = Data.define(:status, :headers, :body)
8
+
9
+ module Body
10
+ # length bytes of io from offset, read by the C part of the Session as
11
+ # the connection can take them, through a descriptor of its own. The
12
+ # Session closes io once the C part has that descriptor, and rack_body,
13
+ # unless it is nil, when the stream ends.
14
+ File = Data.define(:io, :offset, :length, :rack_body)
15
+
16
+ # Chunks pulled with enum.next as the send buffer drains; StopIteration
17
+ # ends the body. The Session closes rack_body, unless it is nil, when
18
+ # the stream ends.
19
+ Chunks = Data.define(:enum, :rack_body)
20
+ end
21
+ end
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "socket"
5
+
6
+ module HTTP3
7
+ # An HTTP/3 server for a Rack 3 application, on one UDP address:
8
+ #
9
+ # HTTP3::Server.new(
10
+ # app: app, bind: "192.0.2.1:443",
11
+ # certificate_path: "fullchain.pem", private_key_path: "privkey.pem"
12
+ # ).run
13
+ #
14
+ # #run serves until SIGTERM or SIGINT. It then sends GOAWAY to every
15
+ # client, refuses new connections, and waits up to shutdown_timeout
16
+ # seconds for the requests under way before closing every connection; a
17
+ # second signal stops the wait. SIGHUP reads the certificate and key
18
+ # again for new connections, and keeps the old ones if that fails.
19
+ #
20
+ # Only failures are logged, one line each: the event, then key=value
21
+ # pairs. Requests are not logged; use Rack::CommonLogger for that. Query
22
+ # strings and header values never appear in the log.
23
+ class Server
24
+ ALPN = ["h3"].freeze
25
+
26
+ # What the signal handlers write to the self-pipe.
27
+ SIGNALS = {"TERM" => "T", "INT" => "I", "HUP" => "H"}.freeze
28
+
29
+ # The largest UDP payload read at once.
30
+ RECV_SIZE = 65_535
31
+
32
+ # Datagrams read per turn of the loop at most, so that a flood cannot
33
+ # keep it from sending and running timers.
34
+ RECV_BATCH = 1024
35
+
36
+ # The address #run bound, an Addrinfo; nil until then. With port 0 in
37
+ # bind, this tells the port.
38
+ attr_reader :local_address
39
+
40
+ # bind is "host:port" or "[v6]:port", one address; 0.0.0.0 and [::] are
41
+ # refused, since the local address of each path has to be known.
42
+ # certificate_path is a PEM chain, leaf first; private_key_path a PEM key
43
+ # without passphrase. qlog_dir writes one qlog file per connection.
44
+ # max_request_body and max_field_section_size (bytes) bound a request's
45
+ # body (413 beyond) and header section (431 beyond); send_buffer_size
46
+ # bounds the bytes of a response body queued but unsent per stream.
47
+ def initialize(app:, bind:, certificate_path:, private_key_path:, transport_params: nil, settings: nil,
48
+ qlog_dir: nil, max_connections: 1024, max_request_body: 1_048_576, max_field_section_size: 65_536,
49
+ send_buffer_size: 65_536, shutdown_timeout: 30, logger: Logger.new($stderr))
50
+ @adapter = RackAdapter.new(app)
51
+ @bind_address = parse_bind(bind)
52
+ @certificate_path = certificate_path
53
+ @private_key_path = private_key_path
54
+ @tls_context = new_tls_context
55
+ @transport_params = transport_params || QUIC::TransportParams.default
56
+ settings ||= QUIC::Settings.default
57
+ settings = settings.with(qlog_dir: qlog_dir) if qlog_dir
58
+ settings.check_qlog_dir
59
+ @settings = settings
60
+ @max_connections = max_connections
61
+ @max_request_body = max_request_body
62
+ @max_field_section_size = max_field_section_size
63
+ @send_buffer_size = send_buffer_size
64
+ @shutdown_timeout = shutdown_timeout
65
+ @logger = logger
66
+ end
67
+
68
+ # Serve until told to stop (see the class comment). Returns nil.
69
+ def run
70
+ sig_r, sig_w = IO.pipe
71
+ @sock = UDPSocket.new(@bind_address.afamily)
72
+ @sock.bind(@bind_address.ip_address, @bind_address.ip_port)
73
+ @local_address = @sock.local_address
74
+ @endpoint = QUIC::Endpoint.new(
75
+ local_address: @local_address, tls_context: @tls_context, transport_params: @transport_params,
76
+ settings: @settings, max_connections: @max_connections
77
+ )
78
+ @sessions = {}
79
+ @remotes = {}
80
+ @blocked_send = nil
81
+ @phase = :serving
82
+ @drain_deadline = nil
83
+ old_traps = install_traps(sig_w)
84
+
85
+ turn(sig_r) until finished?
86
+ close_connections
87
+ nil
88
+ ensure
89
+ restore_traps(old_traps) if old_traps
90
+ @sessions&.each_value { |session| release_session(session) }
91
+ [@sock, sig_r, sig_w].each { |io| io.close if io && !io.closed? }
92
+ end
93
+
94
+ private
95
+
96
+ # One turn of the loop.
97
+ def turn(sig_r)
98
+ writers = @blocked_send ? [@sock] : nil
99
+ readable, writable, = IO.select([@sock, sig_r], writers, nil, select_timeout)
100
+ handle_signals(sig_r) if readable&.include?(sig_r)
101
+ flush if writable&.include?(@sock)
102
+ receive if readable&.include?(@sock)
103
+ serve_requests
104
+ fill_bodies
105
+ @endpoint.handle_expiry if due?(@endpoint.expiry)
106
+ flush
107
+ reap
108
+ end
109
+
110
+ def finished?
111
+ case @phase
112
+ when :serving then false
113
+ when :draining then now >= @drain_deadline || @sessions.each_value.none?(&:busy?)
114
+ else true
115
+ end
116
+ end
117
+
118
+ # Until the next timer or the end of the drain, but no wait at all while
119
+ # a body could take more.
120
+ def select_timeout
121
+ return 0 if @sessions.each_value.any? { |session| session.wants_fill?(@send_buffer_size) }
122
+
123
+ timeouts = []
124
+ expiry = @endpoint.expiry
125
+ timeouts << [(expiry - now_ns) / 1e9, 0.0].max if expiry
126
+ timeouts << [@drain_deadline - now, 0.0].max if @drain_deadline
127
+ timeouts.min
128
+ end
129
+
130
+ # Read into one buffer reused for every datagram: a buffer of RECV_SIZE
131
+ # per datagram would be garbage at once, and under a download the client
132
+ # sends thousands of ACKs, enough to grow the process by tens of MiB
133
+ # between GCs. read_pkt keeps nothing of the buffer.
134
+ def receive
135
+ @recv_buffer ||= String.new(capacity: RECV_SIZE, encoding: Encoding::BINARY)
136
+ RECV_BATCH.times do
137
+ result = @sock.recvfrom_nonblock(RECV_SIZE, 0, @recv_buffer, exception: false)
138
+ break if result == :wait_readable
139
+
140
+ data, sender_addr = result
141
+ sender = Addrinfo.new(sender_addr, nil, :DGRAM)
142
+ conn = @endpoint.read_pkt(data, remote: sender.to_sockaddr)
143
+ next if conn.nil? || @sessions.key?(conn)
144
+
145
+ @sessions[conn] = Session.attach(
146
+ conn, max_request_body: @max_request_body, max_field_section_size: @max_field_section_size
147
+ )
148
+ @remotes[conn] = sender
149
+ end
150
+ end
151
+
152
+ def serve_requests
153
+ @sessions.each do |conn, session|
154
+ session.take_requests.each do |request, status|
155
+ response = status ? Response.new(status: status, headers: [], body: nil) : call_app(conn, request)
156
+ begin
157
+ session.respond(request, response) { |error| log_body_error(conn, request, error) }
158
+ rescue Nghttp3::Error
159
+ # The stream went away while the application ran.
160
+ discard_body(response.body)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def call_app(conn, request)
167
+ @adapter.call(request, remote: @remotes[conn])
168
+ rescue => e
169
+ log_app_error(conn, request, e)
170
+ Response.new(status: 500, headers: [], body: nil)
171
+ end
172
+
173
+ def fill_bodies
174
+ @sessions.each do |conn, session|
175
+ session.fill_bodies(@send_buffer_size) { |request, error| log_app_error(conn, request, error) }
176
+ end
177
+ end
178
+
179
+ # Send what the Endpoint has, until the socket would block.
180
+ def flush
181
+ loop do
182
+ pkt, to = @blocked_send || @endpoint.write_pkt
183
+ break if pkt.nil?
184
+
185
+ result = begin
186
+ @sock.sendmsg_nonblock(pkt, 0, to, exception: false)
187
+ rescue SystemCallError
188
+ # Unreachable, too large, ...: the datagram is lost, as on the
189
+ # wire.
190
+ nil
191
+ end
192
+ if result == :wait_writable
193
+ @blocked_send = [pkt, to]
194
+ break
195
+ end
196
+ @blocked_send = nil
197
+ end
198
+ end
199
+
200
+ # Take out what has ended: streams within each Session, and whole
201
+ # connections.
202
+ def reap
203
+ @sessions.each do |conn, session|
204
+ if (error = session.take_error)
205
+ log(:error, "session_error", remote: remote_of(conn), **error_fields(error))
206
+ end
207
+ session.release_closed { |stream_id, e| log_body_close_error(stream_id, e) }
208
+ end
209
+ @endpoint.take_closed.each do |conn|
210
+ if conn.error
211
+ log(:warn, "connection_error", remote: remote_of(conn), error: conn.error.class, message: conn.error.message)
212
+ end
213
+ session = @sessions.delete(conn)
214
+ release_session(session) if session
215
+ @remotes.delete(conn)
216
+ end
217
+ end
218
+
219
+ def release_session(session)
220
+ session.release_all { |stream_id, e| log_body_close_error(stream_id, e) }
221
+ end
222
+
223
+ def handle_signals(sig_r)
224
+ bytes = sig_r.read_nonblock(64, exception: false)
225
+ return unless bytes.is_a?(String)
226
+
227
+ bytes.each_char { |byte| (byte == SIGNALS["HUP"]) ? reload_tls : begin_shutdown }
228
+ end
229
+
230
+ # First signal: GOAWAY and drain. Second: stop draining.
231
+ def begin_shutdown
232
+ if @phase == :serving
233
+ @phase = :draining
234
+ @drain_deadline = now + @shutdown_timeout
235
+ @sessions.each_value(&:shutdown_notice)
236
+ @endpoint.stop_accepting
237
+ else
238
+ @phase = :closing
239
+ end
240
+ end
241
+
242
+ def reload_tls
243
+ @endpoint.tls_context = new_tls_context
244
+ rescue => e
245
+ log(:error, "tls_reload_failed", certificate_path: @certificate_path, private_key_path: @private_key_path,
246
+ error: e.class, message: e.message)
247
+ end
248
+
249
+ # Close every connection and send the CONNECTION_CLOSEs, without
250
+ # waiting out the closing period.
251
+ def close_connections
252
+ @endpoint.connections.each do |conn|
253
+ conn.close(error_code: Nghttp3::H3_NO_ERROR)
254
+ rescue QUIC::Error
255
+ nil
256
+ end
257
+ deadline = now + 1
258
+ loop do
259
+ flush
260
+ break if @blocked_send.nil? || now >= deadline
261
+
262
+ IO.select(nil, [@sock], nil, [deadline - now, 0].max)
263
+ end
264
+ end
265
+
266
+ def new_tls_context
267
+ QUIC::TLSContext.server(certificate_path: @certificate_path, private_key_path: @private_key_path, alpn: ALPN)
268
+ end
269
+
270
+ def install_traps(sig_w)
271
+ SIGNALS.to_h do |name, byte|
272
+ previous = Signal.trap(name) do
273
+ sig_w.write_nonblock(byte, exception: false)
274
+ rescue IOError
275
+ nil
276
+ end
277
+ [name, previous]
278
+ end
279
+ end
280
+
281
+ def restore_traps(previous)
282
+ previous.each { |name, handler| Signal.trap(name, handler || "DEFAULT") }
283
+ end
284
+
285
+ def parse_bind(bind)
286
+ match = bind.match(/\A\[(?<host>[^\]]+)\]:(?<port>\d+)\z/) || bind.match(/\A(?<host>[^:\[\]]+):(?<port>\d+)\z/)
287
+ raise ArgumentError, "bind must be \"host:port\" or \"[ipv6]:port\" (got #{bind.inspect})" unless match
288
+
289
+ address = Addrinfo.udp(match[:host], Integer(match[:port], 10))
290
+ if address.ip_address == "0.0.0.0" || address.ip_address == "::"
291
+ raise ArgumentError, "bind must name the address to serve, not #{address.ip_address}"
292
+ end
293
+ address
294
+ end
295
+
296
+ def discard_body(body)
297
+ return if body.nil?
298
+
299
+ body.io.close if body.is_a?(Body::File) && !body.io.closed?
300
+ body.rack_body.close if body.rack_body.respond_to?(:close)
301
+ rescue => e
302
+ log(:error, "body_close_error", **error_fields(e))
303
+ end
304
+
305
+ def log_app_error(conn, request, error)
306
+ pseudo = request.headers.select { |name, _| name.start_with?(":") }.to_h
307
+ path = pseudo.fetch(":path", "")[/\A[^?]*/]
308
+ log(:error, "app_error", remote: remote_of(conn), stream: request.stream_id, method: pseudo[":method"],
309
+ path: path, **error_fields(error))
310
+ end
311
+
312
+ def log_body_error(conn, request, error)
313
+ log(:error, "body_error", remote: remote_of(conn), stream: request.stream_id, error: error.class,
314
+ message: error.message)
315
+ end
316
+
317
+ def log_body_close_error(stream_id, error)
318
+ log(:error, "body_close_error", stream: stream_id, error: error.class, message: error.message)
319
+ end
320
+
321
+ def error_fields(error)
322
+ fields = {error: error.class, message: error.message}
323
+ if (location = error.backtrace_locations&.first)
324
+ fields[:at] = "#{location.path}:#{location.lineno}"
325
+ end
326
+ fields
327
+ end
328
+
329
+ # The event, then key=value; a value with a space, '"' or '=' is
330
+ # written with String#inspect.
331
+ def log(level, event, **fields)
332
+ pairs = fields.map do |key, value|
333
+ text = value.to_s
334
+ "#{key}=#{text.match?(/[\s"=]/) ? text.inspect : text}"
335
+ end
336
+ @logger.public_send(level, [event, *pairs].join(" "))
337
+ end
338
+
339
+ def remote_of(conn)
340
+ address = @remotes[conn] or return "-"
341
+ host = address.ipv6? ? "[#{address.ip_address}]" : address.ip_address
342
+ "#{host}:#{address.ip_port}"
343
+ end
344
+
345
+ def due?(expiry)
346
+ expiry && expiry <= now_ns
347
+ end
348
+
349
+ def now
350
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
351
+ end
352
+
353
+ def now_ns
354
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
355
+ end
356
+ end
357
+ end