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.
- checksums.yaml +7 -0
- data/.clang-format +1 -0
- data/.vscode/extensions.json +6 -0
- data/.vscode/settings.json +10 -0
- data/CHANGELOG.md +34 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE-DEPENDENCIES.txt +66 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +20 -0
- data/ext/http3/extconf.rb +48 -0
- data/ext/http3/nghttp3.c +347 -0
- data/ext/http3/nghttp3.h +105 -0
- data/ext/http3/nghttp3_callbacks.c +621 -0
- data/ext/http3/nghttp3_connection.c +1303 -0
- data/ext/http3/nghttp3_nv.c +87 -0
- data/ext/http3/nghttp3_qpack.c +656 -0
- data/ext/http3/nghttp3_settings.c +188 -0
- data/ext/http3/session.c +1017 -0
- data/lib/http3/rack_adapter.rb +162 -0
- data/lib/http3/request.rb +9 -0
- data/lib/http3/response.rb +21 -0
- data/lib/http3/server.rb +357 -0
- data/lib/http3/session.rb +193 -0
- data/lib/http3/version.rb +5 -0
- data/lib/http3.rb +16 -0
- data/sig/http3/nghttp3/callbacks.rbs +32 -0
- data/sig/http3/nghttp3/connection.rbs +101 -0
- data/sig/http3/nghttp3/error.rbs +48 -0
- data/sig/http3/nghttp3/info.rbs +9 -0
- data/sig/http3/nghttp3/nv.rbs +11 -0
- data/sig/http3/nghttp3/qpack.rbs +48 -0
- data/sig/http3/nghttp3/settings.rbs +27 -0
- data/sig/http3/rack_adapter.rbs +18 -0
- data/sig/http3/request.rbs +11 -0
- data/sig/http3/response.rbs +30 -0
- data/sig/http3/server.rbs +32 -0
- data/sig/http3/session.rbs +52 -0
- data/sig/http3.rbs +66 -0
- metadata +140 -0
|
@@ -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
|
data/lib/http3/server.rb
ADDED
|
@@ -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
|