http3 0.0.3 → 0.0.4
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/README.md +66 -9
- data/ext/http3/nghttp3.c +20 -1
- data/ext/http3/nghttp3.h +35 -2
- data/ext/http3/nghttp3_connection.c +377 -205
- data/ext/http3/nghttp3_nv.c +53 -20
- data/ext/http3/nghttp3_qpack.c +64 -20
- data/ext/http3/session.c +197 -48
- data/lib/http3/rack_adapter.rb +141 -26
- data/lib/http3/server.rb +187 -17
- data/lib/http3/session.rb +211 -32
- data/lib/http3/version.rb +1 -1
- data/sig/http3/nghttp3/connection.rbs +43 -9
- data/sig/http3/nghttp3/nv.rbs +4 -0
- data/sig/http3/nghttp3/qpack.rbs +10 -3
- data/sig/http3/rack_adapter.rbs +17 -2
- data/sig/http3/server.rbs +6 -0
- data/sig/http3/session.rbs +52 -8
- data/sig/http3.rbs +3 -0
- metadata +3 -3
data/lib/http3/server.rb
CHANGED
|
@@ -19,7 +19,11 @@ module HTTP3
|
|
|
19
19
|
#
|
|
20
20
|
# Only failures are logged, one line each: the event, then key=value
|
|
21
21
|
# pairs. Requests are not logged; use Rack::CommonLogger for that. Query
|
|
22
|
-
# strings and header values never appear in the log.
|
|
22
|
+
# strings and header values never appear in the log. Those answered
|
|
23
|
+
# without the application (400, 408, 413, 431, 501, 503) are counted
|
|
24
|
+
# instead: refusal_log_interval seconds after the first of them, and when
|
|
25
|
+
# #run ends, a warn line per status tells how many there were since,
|
|
26
|
+
# such as "requests_refused status=503 count=1234 seconds=60".
|
|
23
27
|
class Server
|
|
24
28
|
ALPN = ["h3"].freeze
|
|
25
29
|
|
|
@@ -42,11 +46,29 @@ module HTTP3
|
|
|
42
46
|
# certificate_path is a PEM chain, leaf first; private_key_path a PEM key
|
|
43
47
|
# without passphrase. qlog_dir writes one qlog file per connection.
|
|
44
48
|
# max_request_body and max_field_section_size (bytes) bound a request's
|
|
45
|
-
# body (413 beyond) and header section (431 beyond)
|
|
46
|
-
# bounds the bytes of a
|
|
49
|
+
# body (413 beyond) and header section (431 beyond).
|
|
50
|
+
# max_connection_request_buffer bounds the bytes the requests of a
|
|
51
|
+
# connection hold before they reach the application, and
|
|
52
|
+
# max_total_request_buffer those of every connection; a request that
|
|
53
|
+
# would pass either gets 503. They have to leave room for a request at
|
|
54
|
+
# the other two limits, or ArgumentError is raised. send_buffer_size
|
|
55
|
+
# bounds the bytes of a response body queued but unsent per stream,
|
|
56
|
+
# max_connection_send_buffer those of the array and each bodies of a
|
|
57
|
+
# connection, and max_total_send_buffer those of every connection; a
|
|
58
|
+
# body waits for room. Each has to be at least the one before. A request
|
|
59
|
+
# not received in full within request_timeout seconds of the start of
|
|
60
|
+
# its headers gets 408; nil is no limit. refusal_log_interval is the
|
|
61
|
+
# seconds between the requests_refused lines (see above).
|
|
47
62
|
def initialize(app:, bind:, certificate_path:, private_key_path:, transport_params: nil, settings: nil,
|
|
48
63
|
qlog_dir: nil, max_connections: 1024, max_request_body: 1_048_576, max_field_section_size: 65_536,
|
|
49
|
-
|
|
64
|
+
max_connection_request_buffer: 4_194_304, max_total_request_buffer: 67_108_864,
|
|
65
|
+
send_buffer_size: 65_536, max_connection_send_buffer: 1_048_576, max_total_send_buffer: 67_108_864,
|
|
66
|
+
request_timeout: 30, refusal_log_interval: 60, shutdown_timeout: 30, logger: Logger.new($stderr))
|
|
67
|
+
check_request_limits(max_request_body, max_field_section_size, max_connection_request_buffer,
|
|
68
|
+
max_total_request_buffer)
|
|
69
|
+
check_send_limits(send_buffer_size, max_connection_send_buffer, max_total_send_buffer)
|
|
70
|
+
check_request_timeout(request_timeout)
|
|
71
|
+
check_refusal_log_interval(refusal_log_interval)
|
|
50
72
|
@adapter = RackAdapter.new(app)
|
|
51
73
|
@bind_address = parse_bind(bind)
|
|
52
74
|
@certificate_path = certificate_path
|
|
@@ -60,7 +82,13 @@ module HTTP3
|
|
|
60
82
|
@max_connections = max_connections
|
|
61
83
|
@max_request_body = max_request_body
|
|
62
84
|
@max_field_section_size = max_field_section_size
|
|
85
|
+
@max_connection_request_buffer = max_connection_request_buffer
|
|
86
|
+
@max_total_request_buffer = max_total_request_buffer
|
|
63
87
|
@send_buffer_size = send_buffer_size
|
|
88
|
+
@max_connection_send_buffer = max_connection_send_buffer
|
|
89
|
+
@max_total_send_buffer = max_total_send_buffer
|
|
90
|
+
@request_timeout = request_timeout
|
|
91
|
+
@refusal_log_interval = refusal_log_interval
|
|
64
92
|
@shutdown_timeout = shutdown_timeout
|
|
65
93
|
@logger = logger
|
|
66
94
|
end
|
|
@@ -77,13 +105,19 @@ module HTTP3
|
|
|
77
105
|
)
|
|
78
106
|
@sessions = {}
|
|
79
107
|
@remotes = {}
|
|
108
|
+
@request_buffer = Session.shared_buffer(@max_total_request_buffer)
|
|
80
109
|
@blocked_send = nil
|
|
81
110
|
@phase = :serving
|
|
82
111
|
@drain_deadline = nil
|
|
112
|
+
# {status => count} of the requests answered without the
|
|
113
|
+
# application, since @refusals_since.
|
|
114
|
+
@refusals = Hash.new(0)
|
|
115
|
+
@refusals_since = nil
|
|
83
116
|
old_traps = install_traps(sig_w)
|
|
84
117
|
|
|
85
118
|
turn(sig_r) until finished?
|
|
86
119
|
close_connections
|
|
120
|
+
log_refusals
|
|
87
121
|
nil
|
|
88
122
|
ensure
|
|
89
123
|
restore_traps(old_traps) if old_traps
|
|
@@ -93,15 +127,21 @@ module HTTP3
|
|
|
93
127
|
|
|
94
128
|
private
|
|
95
129
|
|
|
96
|
-
# One turn of the loop.
|
|
130
|
+
# One turn of the loop. The room left in max_total_send_buffer is
|
|
131
|
+
# counted once, at the start. Only fill_bodies queues more, so until
|
|
132
|
+
# then the room can only have grown (nghttp3 taking what flush sends),
|
|
133
|
+
# and filling by the room counted queues no more than the limit allows.
|
|
97
134
|
def turn(sig_r)
|
|
135
|
+
room = send_room
|
|
98
136
|
writers = @blocked_send ? [@sock] : nil
|
|
99
|
-
readable, writable, = IO.select([@sock, sig_r], writers, nil, select_timeout)
|
|
137
|
+
readable, writable, = IO.select([@sock, sig_r], writers, nil, select_timeout(room))
|
|
100
138
|
handle_signals(sig_r) if readable&.include?(sig_r)
|
|
101
139
|
flush if writable&.include?(@sock)
|
|
102
140
|
receive if readable&.include?(@sock)
|
|
141
|
+
expire_requests
|
|
103
142
|
serve_requests
|
|
104
|
-
|
|
143
|
+
log_refusals if refusals_due?
|
|
144
|
+
fill_bodies(room)
|
|
105
145
|
@endpoint.handle_expiry if due?(@endpoint.expiry)
|
|
106
146
|
flush
|
|
107
147
|
reap
|
|
@@ -115,14 +155,21 @@ module HTTP3
|
|
|
115
155
|
end
|
|
116
156
|
end
|
|
117
157
|
|
|
118
|
-
# Until the next timer
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
158
|
+
# Until the next timer, the next request to run out of time, the next
|
|
159
|
+
# requests_refused lines or the end of the drain, but no wait at all
|
|
160
|
+
# while a body could take more within the send buffers.
|
|
161
|
+
def select_timeout(room)
|
|
162
|
+
wants_fill = @sessions.each_value.any? { |session|
|
|
163
|
+
session.wants_fill?(@send_buffer_size, connection_limit: @max_connection_send_buffer, room: room)
|
|
164
|
+
}
|
|
165
|
+
return 0 if wants_fill
|
|
122
166
|
|
|
123
167
|
timeouts = []
|
|
124
168
|
expiry = @endpoint.expiry
|
|
125
169
|
timeouts << [(expiry - now_ns) / 1e9, 0.0].max if expiry
|
|
170
|
+
deadline = @sessions.each_value.filter_map(&:next_request_deadline).min
|
|
171
|
+
timeouts << [deadline - now, 0.0].max if deadline
|
|
172
|
+
timeouts << [refusal_log_at - now, 0.0].max if refusal_log_at
|
|
126
173
|
timeouts << [@drain_deadline - now, 0.0].max if @drain_deadline
|
|
127
174
|
timeouts.min
|
|
128
175
|
end
|
|
@@ -142,16 +189,39 @@ module HTTP3
|
|
|
142
189
|
conn = @endpoint.read_pkt(data, remote: sender.to_sockaddr)
|
|
143
190
|
next if conn.nil? || @sessions.key?(conn)
|
|
144
191
|
|
|
145
|
-
|
|
146
|
-
conn, max_request_body: @max_request_body, max_field_section_size: @max_field_section_size
|
|
147
|
-
)
|
|
148
|
-
@remotes[conn] = sender
|
|
192
|
+
attach_session(conn, sender)
|
|
149
193
|
end
|
|
150
194
|
end
|
|
151
195
|
|
|
196
|
+
# Serve a new connection. One whose Session cannot be set up is logged
|
|
197
|
+
# and closed, and the loop goes on with the others.
|
|
198
|
+
def attach_session(conn, sender)
|
|
199
|
+
@sessions[conn] = Session.attach(
|
|
200
|
+
conn, max_request_body: @max_request_body, max_field_section_size: @max_field_section_size,
|
|
201
|
+
max_connection_request_buffer: @max_connection_request_buffer, shared_buffer: @request_buffer,
|
|
202
|
+
request_timeout: @request_timeout
|
|
203
|
+
)
|
|
204
|
+
@remotes[conn] = sender
|
|
205
|
+
rescue => e
|
|
206
|
+
log(:error, "session_error", remote: format_remote(sender), **error_fields(e))
|
|
207
|
+
begin
|
|
208
|
+
conn.close(error_code: Nghttp3::H3_INTERNAL_ERROR)
|
|
209
|
+
rescue QUIC::Error
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Refuse with 408 the requests that ran out of time; serve_requests
|
|
215
|
+
# answers them.
|
|
216
|
+
def expire_requests
|
|
217
|
+
at = now
|
|
218
|
+
@sessions.each_value { |session| session.expire_requests(at) }
|
|
219
|
+
end
|
|
220
|
+
|
|
152
221
|
def serve_requests
|
|
153
222
|
@sessions.each do |conn, session|
|
|
154
223
|
session.take_requests.each do |request, status|
|
|
224
|
+
count_refusal(status) if status
|
|
155
225
|
response = status ? Response.new(status: status, headers: [], body: nil) : call_app(conn, request)
|
|
156
226
|
begin
|
|
157
227
|
session.respond(request, response) { |error| log_body_error(conn, request, error) }
|
|
@@ -161,6 +231,7 @@ module HTTP3
|
|
|
161
231
|
end
|
|
162
232
|
end
|
|
163
233
|
end
|
|
234
|
+
@adapter.take_refusals.each { |status, count| count_refusal(status, count) }
|
|
164
235
|
end
|
|
165
236
|
|
|
166
237
|
def call_app(conn, request)
|
|
@@ -170,12 +241,49 @@ module HTTP3
|
|
|
170
241
|
Response.new(status: 500, headers: [], body: nil)
|
|
171
242
|
end
|
|
172
243
|
|
|
173
|
-
def
|
|
244
|
+
def count_refusal(status, count = 1)
|
|
245
|
+
@refusals_since ||= now
|
|
246
|
+
@refusals[status] += count
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# When the refusals counted are due to be logged; nil while there are
|
|
250
|
+
# none.
|
|
251
|
+
def refusal_log_at
|
|
252
|
+
@refusals_since && @refusals_since + @refusal_log_interval
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def refusals_due?
|
|
256
|
+
at = refusal_log_at
|
|
257
|
+
!at.nil? && at <= now
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# One line per status, in ascending order, with the seconds since the
|
|
261
|
+
# first of them; the count starts over.
|
|
262
|
+
def log_refusals
|
|
263
|
+
return if @refusals_since.nil?
|
|
264
|
+
|
|
265
|
+
seconds = (now - @refusals_since).round
|
|
266
|
+
@refusals.sort.each do |status, count|
|
|
267
|
+
log(:warn, "requests_refused", status: status, count: count, seconds: seconds)
|
|
268
|
+
end
|
|
269
|
+
@refusals.clear
|
|
270
|
+
@refusals_since = nil
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# The connections are filled in the order they came, so that while the
|
|
274
|
+
# total is short the older ones get its room first.
|
|
275
|
+
def fill_bodies(room)
|
|
174
276
|
@sessions.each do |conn, session|
|
|
175
|
-
session.fill_bodies(@send_buffer_size
|
|
277
|
+
room -= session.fill_bodies(@send_buffer_size, connection_limit: @max_connection_send_buffer,
|
|
278
|
+
room: room) { |request, error| log_app_error(conn, request, error) }
|
|
176
279
|
end
|
|
177
280
|
end
|
|
178
281
|
|
|
282
|
+
# What max_total_send_buffer has left for array and each bodies.
|
|
283
|
+
def send_room
|
|
284
|
+
@max_total_send_buffer - @sessions.each_value.sum(&:unsent_total)
|
|
285
|
+
end
|
|
286
|
+
|
|
179
287
|
# Send what the Endpoint has, until the socket would block.
|
|
180
288
|
def flush
|
|
181
289
|
loop do
|
|
@@ -282,6 +390,64 @@ module HTTP3
|
|
|
282
390
|
previous.each { |name, handler| Signal.trap(name, handler || "DEFAULT") }
|
|
283
391
|
end
|
|
284
392
|
|
|
393
|
+
# Raise ArgumentError for the first request limit found wrong: each has
|
|
394
|
+
# to be an Integer in range, and the buffers have to let a request at
|
|
395
|
+
# max_request_body and max_field_section_size through on a server with
|
|
396
|
+
# nothing else under way.
|
|
397
|
+
def check_request_limits(max_request_body, max_field_section_size, max_connection_request_buffer,
|
|
398
|
+
max_total_request_buffer)
|
|
399
|
+
unless max_request_body.is_a?(Integer) && max_request_body >= 0
|
|
400
|
+
raise ArgumentError, "max_request_body must be an Integer of 0 or more (got #{max_request_body.inspect})"
|
|
401
|
+
end
|
|
402
|
+
# The range Session._attach takes, which would otherwise raise only
|
|
403
|
+
# on the first connection.
|
|
404
|
+
unless max_field_section_size.is_a?(Integer) && (1...2**62).cover?(max_field_section_size)
|
|
405
|
+
raise ArgumentError, "max_field_section_size must be 1..2**62-1 (got #{max_field_section_size.inspect})"
|
|
406
|
+
end
|
|
407
|
+
{max_connection_request_buffer:, max_total_request_buffer:}.each do |name, value|
|
|
408
|
+
raise ArgumentError, "#{name} must be a positive Integer (got #{value.inspect})" unless value.is_a?(Integer) && value.positive?
|
|
409
|
+
end
|
|
410
|
+
request = max_request_body + max_field_section_size
|
|
411
|
+
if max_connection_request_buffer < request
|
|
412
|
+
raise ArgumentError, "max_connection_request_buffer (#{max_connection_request_buffer}) must be at least " \
|
|
413
|
+
"max_request_body + max_field_section_size (#{request})"
|
|
414
|
+
end
|
|
415
|
+
if max_total_request_buffer < max_connection_request_buffer
|
|
416
|
+
raise ArgumentError, "max_total_request_buffer (#{max_total_request_buffer}) must be at least " \
|
|
417
|
+
"max_connection_request_buffer (#{max_connection_request_buffer})"
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Raise ArgumentError for the first send limit found wrong: each has to
|
|
422
|
+
# be a positive Integer, at least the one before.
|
|
423
|
+
def check_send_limits(send_buffer_size, max_connection_send_buffer, max_total_send_buffer)
|
|
424
|
+
{send_buffer_size:, max_connection_send_buffer:, max_total_send_buffer:}.each do |name, value|
|
|
425
|
+
raise ArgumentError, "#{name} must be a positive Integer (got #{value.inspect})" unless value.is_a?(Integer) && value.positive?
|
|
426
|
+
end
|
|
427
|
+
if max_connection_send_buffer < send_buffer_size
|
|
428
|
+
raise ArgumentError, "max_connection_send_buffer (#{max_connection_send_buffer}) must be at least " \
|
|
429
|
+
"send_buffer_size (#{send_buffer_size})"
|
|
430
|
+
end
|
|
431
|
+
if max_total_send_buffer < max_connection_send_buffer
|
|
432
|
+
raise ArgumentError, "max_total_send_buffer (#{max_total_send_buffer}) must be at least " \
|
|
433
|
+
"max_connection_send_buffer (#{max_connection_send_buffer})"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def check_request_timeout(request_timeout)
|
|
438
|
+
return if request_timeout.nil?
|
|
439
|
+
return if (request_timeout.is_a?(Integer) || request_timeout.is_a?(Float)) && request_timeout.positive?
|
|
440
|
+
|
|
441
|
+
raise ArgumentError, "request_timeout must be nil or a positive number of seconds (got #{request_timeout.inspect})"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def check_refusal_log_interval(refusal_log_interval)
|
|
445
|
+
return if (refusal_log_interval.is_a?(Integer) || refusal_log_interval.is_a?(Float)) && refusal_log_interval.positive?
|
|
446
|
+
|
|
447
|
+
raise ArgumentError,
|
|
448
|
+
"refusal_log_interval must be a positive number of seconds (got #{refusal_log_interval.inspect})"
|
|
449
|
+
end
|
|
450
|
+
|
|
285
451
|
def parse_bind(bind)
|
|
286
452
|
match = bind.match(/\A\[(?<host>[^\]]+)\]:(?<port>\d+)\z/) || bind.match(/\A(?<host>[^:\[\]]+):(?<port>\d+)\z/)
|
|
287
453
|
raise ArgumentError, "bind must be \"host:port\" or \"[ipv6]:port\" (got #{bind.inspect})" unless match
|
|
@@ -338,6 +504,10 @@ module HTTP3
|
|
|
338
504
|
|
|
339
505
|
def remote_of(conn)
|
|
340
506
|
address = @remotes[conn] or return "-"
|
|
507
|
+
format_remote(address)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def format_remote(address)
|
|
341
511
|
host = address.ipv6? ? "[#{address.ip_address}]" : address.ip_address
|
|
342
512
|
"#{host}:#{address.ip_port}"
|
|
343
513
|
end
|
data/lib/http3/session.rb
CHANGED
|
@@ -10,49 +10,136 @@ module HTTP3
|
|
|
10
10
|
# requests, answers them, keeps the bodies flowing and lets go of what
|
|
11
11
|
# the streams no longer need.
|
|
12
12
|
class Session
|
|
13
|
-
# A request still coming in
|
|
14
|
-
#
|
|
15
|
-
Incoming = Struct.new(:pseudo, :fields, :body, :
|
|
13
|
+
# A request still coming in; started is when its headers began
|
|
14
|
+
# (CLOCK_MONOTONIC).
|
|
15
|
+
Incoming = Struct.new(:pseudo, :fields, :body, :started)
|
|
16
16
|
|
|
17
17
|
# A response whose body is being sent.
|
|
18
18
|
Outgoing = Struct.new(:request, :body, :done)
|
|
19
19
|
|
|
20
|
+
# What a field counts for besides its name and value, as RFC 9114
|
|
21
|
+
# section 4.2.2 counts it, and as the C part does for 431.
|
|
22
|
+
FIELD_OVERHEAD = 32
|
|
23
|
+
|
|
24
|
+
# Bytes the requests coming in may hold: a connection's, and through
|
|
25
|
+
# parent the server's. Only the server's I/O loop, one thread, calls
|
|
26
|
+
# it, so it takes no lock.
|
|
27
|
+
class Budget
|
|
28
|
+
attr_reader :limit, :used
|
|
29
|
+
|
|
30
|
+
def initialize(limit, parent: nil)
|
|
31
|
+
@limit = limit
|
|
32
|
+
@used = 0
|
|
33
|
+
@parent = parent
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Count n bytes and return true if they fit both here and in the
|
|
37
|
+
# parent; count nothing and return false if not.
|
|
38
|
+
def reserve(n)
|
|
39
|
+
return false if @used + n > @limit
|
|
40
|
+
return false if @parent && !@parent.reserve(n)
|
|
41
|
+
|
|
42
|
+
@used += n
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def release(n)
|
|
47
|
+
@used -= n
|
|
48
|
+
@parent&.release(n)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
private_constant :Budget
|
|
52
|
+
|
|
20
53
|
# Take over quic_conn's streams. A request body over max_request_body
|
|
21
54
|
# bytes is answered with 413, a header section over
|
|
22
|
-
# max_field_section_size bytes (RFC 9114 section 4.2.2) with 431
|
|
23
|
-
|
|
55
|
+
# max_field_section_size bytes (RFC 9114 section 4.2.2) with 431, as
|
|
56
|
+
# soon as the request passes the limit; the rest of it is not read.
|
|
57
|
+
#
|
|
58
|
+
# The requests not yet handed out (#take_requests) may hold
|
|
59
|
+
# max_connection_request_buffer bytes in all, header fields counted as
|
|
60
|
+
# for 431 and bodies by their bytes, and those of every Session given
|
|
61
|
+
# the same shared_buffer (Session.shared_buffer) its limit; a request
|
|
62
|
+
# that would pass either is answered with 503.
|
|
63
|
+
#
|
|
64
|
+
# A request not received in full within request_timeout seconds of the
|
|
65
|
+
# start of its headers is answered with 408 (#expire_requests); nil is
|
|
66
|
+
# no limit.
|
|
67
|
+
def self.attach(quic_conn, max_request_body:, max_field_section_size:,
|
|
68
|
+
max_connection_request_buffer: 4_194_304, shared_buffer: nil, request_timeout: nil)
|
|
24
69
|
session = _attach(quic_conn, max_field_section_size)
|
|
25
|
-
session.send(:setup, max_request_body)
|
|
70
|
+
session.send(:setup, max_request_body, Budget.new(max_connection_request_buffer, parent: shared_buffer),
|
|
71
|
+
request_timeout)
|
|
26
72
|
session
|
|
27
73
|
end
|
|
28
74
|
|
|
75
|
+
# A limit of limit bytes for the requests coming in on every Session
|
|
76
|
+
# given it as attach's shared_buffer.
|
|
77
|
+
def self.shared_buffer(limit)
|
|
78
|
+
Budget.new(limit)
|
|
79
|
+
end
|
|
80
|
+
|
|
29
81
|
# The requests received in full since the last call, each as
|
|
30
82
|
# [request, status]: status is nil for the application to answer the
|
|
31
|
-
# request, or the status to answer it with directly.
|
|
83
|
+
# request, or the status to answer it with directly. A request answered
|
|
84
|
+
# directly comes out as soon as it is refused, before the rest of it
|
|
85
|
+
# has arrived, with neither headers nor body. The bytes they held no
|
|
86
|
+
# longer count for the limits.
|
|
32
87
|
def take_requests
|
|
33
88
|
done = @done
|
|
34
89
|
@done = []
|
|
90
|
+
done.each { |request, _| release_held(request.stream_id) }
|
|
35
91
|
done
|
|
36
92
|
end
|
|
37
93
|
|
|
94
|
+
# Refuse with 408 the requests still coming in whose request_timeout
|
|
95
|
+
# has passed at now (CLOCK_MONOTONIC); #take_requests hands them out.
|
|
96
|
+
def expire_requests(now)
|
|
97
|
+
return if @request_timeout.nil?
|
|
98
|
+
|
|
99
|
+
@incoming.each do |id, request|
|
|
100
|
+
next if @rejected[id] || request.started + @request_timeout > now
|
|
101
|
+
|
|
102
|
+
reject(id, request, 408)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# When the next request still coming in runs out of time
|
|
107
|
+
# (CLOCK_MONOTONIC), or nil.
|
|
108
|
+
def next_request_deadline
|
|
109
|
+
return nil if @request_timeout.nil?
|
|
110
|
+
|
|
111
|
+
started = @incoming.filter_map { |id, request| request.started unless @rejected[id] }.min
|
|
112
|
+
started && started + @request_timeout
|
|
113
|
+
end
|
|
114
|
+
|
|
38
115
|
# Send response to request. Its body, if any, is kept until the stream
|
|
39
116
|
# closes, but the IO of a Body::File is closed at once: the C part reads
|
|
40
117
|
# the file through a descriptor of its own. When that descriptor cannot
|
|
41
118
|
# be had (EMFILE, say), the stream is aborted with H3_INTERNAL_ERROR and
|
|
42
|
-
# the block gets the exception.
|
|
119
|
+
# the block gets the exception. A request is answered once: nghttp3
|
|
120
|
+
# would send a second response after the first, so it raises
|
|
121
|
+
# Nghttp3::InvalidStateError.
|
|
43
122
|
def respond(request, response)
|
|
123
|
+
id = request.stream_id
|
|
124
|
+
raise Nghttp3::InvalidStateError, "stream #{id} has been answered" if @answered[id]
|
|
125
|
+
|
|
126
|
+
stop_reading(id) if @rejected[id]
|
|
44
127
|
headers = [[":status", response.status.to_s], *response.headers]
|
|
45
128
|
body = response.body
|
|
46
|
-
submit_response(
|
|
129
|
+
submit_response(id, headers, !body.nil?)
|
|
130
|
+
@answered[id] = true
|
|
47
131
|
return if body.nil?
|
|
48
132
|
|
|
49
|
-
|
|
133
|
+
# Only the pseudo-headers stay, for the log, while the body is sent:
|
|
134
|
+
# the request body would otherwise be held all along, uncounted.
|
|
135
|
+
pseudo = request.headers.select { |name, _| name.start_with?(":") }
|
|
136
|
+
@outgoing[id] = Outgoing.new(Request.new(stream_id: id, headers: pseudo, body: "".b), body, false)
|
|
50
137
|
return unless body.is_a?(Body::File)
|
|
51
138
|
|
|
52
139
|
begin
|
|
53
|
-
attach_file(
|
|
140
|
+
attach_file(id, body.io, body.offset, body.length)
|
|
54
141
|
rescue SystemCallError => e
|
|
55
|
-
abort_stream(
|
|
142
|
+
abort_stream(id, Nghttp3::H3_INTERNAL_ERROR)
|
|
56
143
|
yield e if block_given?
|
|
57
144
|
ensure
|
|
58
145
|
body.io.close
|
|
@@ -60,24 +147,44 @@ module HTTP3
|
|
|
60
147
|
end
|
|
61
148
|
|
|
62
149
|
# Queue more of each Body::Chunks body until limit bytes wait unsent on
|
|
63
|
-
# its stream
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
150
|
+
# its stream, connection_limit on the whole connection (#unsent_total),
|
|
151
|
+
# and room more bytes have been queued; nil is no limit. A chunk may
|
|
152
|
+
# take any of them past its limit, by less than its size. Returns the
|
|
153
|
+
# bytes queued.
|
|
154
|
+
#
|
|
155
|
+
# A body whose enum raises is the application failing after its headers
|
|
156
|
+
# went out: the stream is aborted with H3_INTERNAL_ERROR and the block
|
|
157
|
+
# gets (request, exception). That request has only the pseudo-headers,
|
|
158
|
+
# and no body.
|
|
159
|
+
def fill_bodies(limit, connection_limit: nil, room: nil)
|
|
160
|
+
left = [room, connection_limit && connection_limit - unsent_total].compact.min
|
|
161
|
+
queued = 0
|
|
67
162
|
@outgoing.each_value do |out|
|
|
68
163
|
next if out.done || !out.body.is_a?(Body::Chunks)
|
|
164
|
+
break if left && queued >= left
|
|
69
165
|
|
|
70
|
-
fill_body(out, limit) { |error| yield out.request, error if block_given? }
|
|
166
|
+
queued += fill_body(out, limit, left && left - queued) { |error| yield out.request, error if block_given? }
|
|
71
167
|
end
|
|
168
|
+
queued
|
|
72
169
|
end
|
|
73
170
|
|
|
74
|
-
# True when a Body::Chunks body could take more right now
|
|
75
|
-
|
|
171
|
+
# True when a Body::Chunks body could take more right now, with the
|
|
172
|
+
# limits of #fill_bodies.
|
|
173
|
+
def wants_fill?(limit, connection_limit: nil, room: nil)
|
|
174
|
+
return false if room && room <= 0
|
|
175
|
+
return false if connection_limit && unsent_total >= connection_limit
|
|
176
|
+
|
|
76
177
|
@outgoing.any? do |id, out|
|
|
77
178
|
!out.done && out.body.is_a?(Body::Chunks) && unsent_bytes(id) < limit
|
|
78
179
|
end
|
|
79
180
|
end
|
|
80
181
|
|
|
182
|
+
# Bytes of the Body::Chunks bodies queued but not taken by nghttp3 yet,
|
|
183
|
+
# on every stream; a body that has ended may still have some.
|
|
184
|
+
def unsent_total
|
|
185
|
+
@outgoing.sum { |id, out| out.body.is_a?(Body::Chunks) ? unsent_bytes(id) : 0 }
|
|
186
|
+
end
|
|
187
|
+
|
|
81
188
|
# True while a request stream is open: received in part, waiting for
|
|
82
189
|
# its answer, or with a response still being sent.
|
|
83
190
|
def busy?
|
|
@@ -86,8 +193,13 @@ module HTTP3
|
|
|
86
193
|
|
|
87
194
|
# Let go of the streams nghttp3 closed: their requests are dropped and
|
|
88
195
|
# their bodies closed. The block gets (stream_id, exception) for a body
|
|
89
|
-
# whose close raised.
|
|
196
|
+
# whose close raised. First, the requests the client reset before their
|
|
197
|
+
# end (#on_reset) get our side of their streams reset too, with
|
|
198
|
+
# H3_REQUEST_INCOMPLETE as RFC 9114 section 4.1 says, so that the
|
|
199
|
+
# streams close.
|
|
90
200
|
def release_closed(&block)
|
|
201
|
+
@incomplete.each { |id| abort_stream(id, Nghttp3::H3_REQUEST_INCOMPLETE) }
|
|
202
|
+
@incomplete.clear
|
|
91
203
|
take_closed.each { |id| release(id, &block) }
|
|
92
204
|
end
|
|
93
205
|
|
|
@@ -100,17 +212,22 @@ module HTTP3
|
|
|
100
212
|
|
|
101
213
|
def on_begin_headers(id)
|
|
102
214
|
@open[id] = true
|
|
103
|
-
@incoming[id] = Incoming.new([], [], "".b,
|
|
215
|
+
@incoming[id] = Incoming.new([], [], "".b, Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
104
216
|
end
|
|
105
217
|
|
|
106
218
|
def on_header(id, name, value)
|
|
107
219
|
request = @incoming[id] or return
|
|
220
|
+
return if @rejected[id]
|
|
221
|
+
return reject(id, request, 503) unless hold(id, name.bytesize + value.bytesize + FIELD_OVERHEAD)
|
|
222
|
+
|
|
108
223
|
(name.start_with?(":") ? request.pseudo : request.fields) << [name, value]
|
|
109
224
|
end
|
|
110
225
|
|
|
111
226
|
def on_fields_too_large(id)
|
|
112
227
|
request = @incoming[id] or return
|
|
113
|
-
|
|
228
|
+
return if @rejected[id]
|
|
229
|
+
|
|
230
|
+
reject(id, request, 431)
|
|
114
231
|
end
|
|
115
232
|
|
|
116
233
|
def on_end_headers(id)
|
|
@@ -118,64 +235,126 @@ module HTTP3
|
|
|
118
235
|
|
|
119
236
|
def on_data(id, bytes)
|
|
120
237
|
request = @incoming[id] or return
|
|
121
|
-
return if
|
|
238
|
+
return if @rejected[id]
|
|
122
239
|
|
|
123
240
|
if request.body.bytesize + bytes.bytesize > @max_request_body
|
|
124
|
-
|
|
125
|
-
|
|
241
|
+
reject(id, request, 413)
|
|
242
|
+
elsif !hold(id, bytes.bytesize)
|
|
243
|
+
reject(id, request, 503)
|
|
126
244
|
else
|
|
127
245
|
request.body << bytes
|
|
128
246
|
end
|
|
129
247
|
end
|
|
130
248
|
|
|
131
249
|
def on_end_stream(id)
|
|
250
|
+
return if @rejected[id]
|
|
251
|
+
|
|
132
252
|
request = @incoming.delete(id) or return
|
|
133
253
|
headers = request.pseudo + request.fields
|
|
134
|
-
@done << [Request.new(stream_id: id, headers: headers, body: request.body),
|
|
254
|
+
@done << [Request.new(stream_id: id, headers: headers, body: request.body), nil]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# The client reset its side of the stream. A request it had not
|
|
258
|
+
# finished is dropped with the bytes it held, and #release_closed
|
|
259
|
+
# resets our side; one received in full, or refused, is answered as it
|
|
260
|
+
# would have been. Inside a callback, so it calls no C.
|
|
261
|
+
def on_reset(id)
|
|
262
|
+
return unless @incoming.key?(id) && !@rejected[id]
|
|
263
|
+
|
|
264
|
+
@incoming.delete(id)
|
|
265
|
+
release_held(id)
|
|
266
|
+
@incomplete << id
|
|
135
267
|
end
|
|
136
268
|
|
|
137
269
|
private
|
|
138
270
|
|
|
139
|
-
def setup(max_request_body)
|
|
271
|
+
def setup(max_request_body, budget, request_timeout)
|
|
140
272
|
@max_request_body = max_request_body
|
|
273
|
+
@budget = budget
|
|
274
|
+
@request_timeout = request_timeout
|
|
275
|
+
@held = {}
|
|
141
276
|
@incoming = {}
|
|
142
277
|
@done = []
|
|
143
278
|
@outgoing = {}
|
|
144
279
|
@open = {}
|
|
280
|
+
@rejected = {}
|
|
281
|
+
@answered = {}
|
|
282
|
+
@incomplete = []
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Count n more bytes for the request on id, if the budget has room.
|
|
286
|
+
def hold(id, n)
|
|
287
|
+
return false unless @budget.reserve(n)
|
|
288
|
+
|
|
289
|
+
@held[id] = @held.fetch(id, 0) + n
|
|
290
|
+
true
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# The request on id holds nothing any more. Calling it again does
|
|
294
|
+
# nothing.
|
|
295
|
+
def release_held(id)
|
|
296
|
+
n = @held.delete(id) or return
|
|
297
|
+
@budget.release(n)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Answer the request on id with status, at once, instead of passing it
|
|
301
|
+
# on: what has come of it is dropped, with the bytes it held, and
|
|
302
|
+
# #respond stops reading the rest. Its Incoming stays in @incoming,
|
|
303
|
+
# emptied, until the stream closes, and the callbacks still to come for
|
|
304
|
+
# it are ignored. 431 may come before all of the pseudo-headers, so the
|
|
305
|
+
# request goes out without any headers. A refusal is not logged: only
|
|
306
|
+
# failures are, and a flood of refusals would flood the log.
|
|
307
|
+
def reject(id, request, status)
|
|
308
|
+
request.pseudo.clear
|
|
309
|
+
request.fields.clear
|
|
310
|
+
request.body = "".b
|
|
311
|
+
release_held(id)
|
|
312
|
+
@rejected[id] = true
|
|
313
|
+
@done << [Request.new(stream_id: id, headers: [], body: "".b), status]
|
|
145
314
|
end
|
|
146
315
|
|
|
147
|
-
|
|
316
|
+
# Queue chunks of out's body while its stream has less than limit bytes
|
|
317
|
+
# unsent and less than room (nil for no limit) has been queued. Returns
|
|
318
|
+
# the bytes queued.
|
|
319
|
+
def fill_body(out, limit, room)
|
|
320
|
+
queued = 0
|
|
148
321
|
id = out.request.stream_id
|
|
149
|
-
while unsent_bytes(id) < limit
|
|
322
|
+
while unsent_bytes(id) < limit && (room.nil? || queued < room)
|
|
150
323
|
begin
|
|
151
324
|
chunk = out.body.enum.next
|
|
152
325
|
rescue StopIteration
|
|
153
326
|
out.done = true
|
|
154
327
|
push_eof(id)
|
|
155
|
-
return
|
|
328
|
+
return queued
|
|
156
329
|
rescue => e
|
|
157
330
|
out.done = true
|
|
158
331
|
abort_stream(id, Nghttp3::H3_INTERNAL_ERROR)
|
|
159
332
|
yield e
|
|
160
|
-
return
|
|
333
|
+
return queued
|
|
161
334
|
end
|
|
162
335
|
unless chunk.is_a?(String)
|
|
163
336
|
out.done = true
|
|
164
337
|
abort_stream(id, Nghttp3::H3_INTERNAL_ERROR)
|
|
165
338
|
yield TypeError.new("body chunk must be a String (got #{chunk.class})")
|
|
166
|
-
return
|
|
339
|
+
return queued
|
|
167
340
|
end
|
|
168
341
|
push_chunk(id, chunk)
|
|
342
|
+
queued += chunk.bytesize
|
|
169
343
|
end
|
|
344
|
+
queued
|
|
170
345
|
rescue Nghttp3::Error
|
|
171
346
|
# The stream went away under the body (the peer reset it); it is
|
|
172
347
|
# released when its close comes through.
|
|
173
348
|
out.done = true
|
|
349
|
+
queued
|
|
174
350
|
end
|
|
175
351
|
|
|
176
352
|
def release(id)
|
|
177
353
|
@open.delete(id)
|
|
178
354
|
@incoming.delete(id)
|
|
355
|
+
release_held(id)
|
|
356
|
+
@rejected.delete(id)
|
|
357
|
+
@answered.delete(id)
|
|
179
358
|
@done.reject! { |request, _| request.stream_id == id }
|
|
180
359
|
out = @outgoing.delete(id) or return
|
|
181
360
|
|