http3 0.0.3 → 0.0.5
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 +92 -0
- data/LICENSE-DEPENDENCIES.txt +2 -2
- data/README.md +124 -8
- data/ext/http3/extconf.rb +8 -5
- data/ext/http3/nghttp3.c +20 -1
- data/ext/http3/nghttp3.h +38 -3
- data/ext/http3/nghttp3_callbacks.c +8 -10
- data/ext/http3/nghttp3_connection.c +378 -206
- data/ext/http3/nghttp3_nv.c +53 -20
- data/ext/http3/nghttp3_qpack.c +66 -22
- data/ext/http3/session.c +429 -55
- data/lib/http3/body_pipe.rb +160 -0
- data/lib/http3/body_stream.rb +120 -0
- data/lib/http3/rack_adapter.rb +220 -38
- data/lib/http3/request.rb +4 -2
- data/lib/http3/server.rb +551 -41
- data/lib/http3/session.rb +614 -66
- data/lib/http3/version.rb +1 -1
- data/lib/http3/worker_pool.rb +71 -0
- data/lib/http3.rb +3 -0
- data/sig/http3/body_pipe.rbs +45 -0
- data/sig/http3/body_stream.rbs +41 -0
- 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 +29 -4
- data/sig/http3/request.rbs +5 -3
- data/sig/http3/response.rbs +2 -2
- data/sig/http3/server.rbs +14 -1
- data/sig/http3/session.rbs +136 -13
- data/sig/http3/worker_pool.rbs +19 -0
- data/sig/http3.rbs +3 -0
- metadata +9 -6
- data/.clang-format +0 -1
- data/.vscode/extensions.json +0 -6
- data/.vscode/settings.json +0 -10
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP3
|
|
4
|
+
# The stream a response was being sent on is gone: the client reset it or
|
|
5
|
+
# asked for no more of it (STOP_SENDING), the connection ended, the server
|
|
6
|
+
# stopped, or the response was aborted after waiting on its client for
|
|
7
|
+
# response_timeout. Body::Pipe's write, finish and flush raise it. It is
|
|
8
|
+
# an IOError, as a write to a closed socket raises one.
|
|
9
|
+
class StreamClosed < IOError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Body
|
|
13
|
+
# A response body written by the thread that runs the application and
|
|
14
|
+
# taken by the server's I/O loop, which sends it. On the writer's side,
|
|
15
|
+
# #write waits while capacity bytes or more are queued, and #finish ends
|
|
16
|
+
# the body, #fail ends it with an exception, each waiting until all of
|
|
17
|
+
# it has been taken. On the I/O loop's side, #take never waits, and
|
|
18
|
+
# #cancel gives up on the body: from then on the writer's write, finish
|
|
19
|
+
# and flush raise StreamClosed, those waiting included, and fail
|
|
20
|
+
# returns false. Since a writer waits for the
|
|
21
|
+
# end of its body to be taken, the pipes holding chunks are no more than
|
|
22
|
+
# the writers.
|
|
23
|
+
#
|
|
24
|
+
# wake is called, with no lock held, each time the body goes from
|
|
25
|
+
# having nothing to take to having something: a chunk, its end, or the
|
|
26
|
+
# exception that ended it.
|
|
27
|
+
class Pipe
|
|
28
|
+
def initialize(capacity:, wake:)
|
|
29
|
+
@capacity = capacity
|
|
30
|
+
@wake = wake
|
|
31
|
+
@lock = Thread::Mutex.new
|
|
32
|
+
@changed = Thread::ConditionVariable.new
|
|
33
|
+
@chunks = []
|
|
34
|
+
@bytes = 0
|
|
35
|
+
@finished = false
|
|
36
|
+
@end_taken = false
|
|
37
|
+
@error = nil
|
|
38
|
+
@cancelled = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Queue chunk, a String, first waiting while capacity bytes or more are
|
|
42
|
+
# queued. A String that can still change is copied. Returns its size.
|
|
43
|
+
def write(chunk)
|
|
44
|
+
raise TypeError, "chunk must be a String (got #{chunk.class})" unless chunk.is_a?(String)
|
|
45
|
+
|
|
46
|
+
chunk = chunk.dup.freeze unless chunk.frozen?
|
|
47
|
+
woke = @lock.synchronize do
|
|
48
|
+
@changed.wait(@lock) while @bytes >= @capacity && !@cancelled
|
|
49
|
+
check_writable
|
|
50
|
+
next false if chunk.empty?
|
|
51
|
+
|
|
52
|
+
was_empty = @chunks.empty?
|
|
53
|
+
@chunks << chunk
|
|
54
|
+
@bytes += chunk.bytesize
|
|
55
|
+
was_empty
|
|
56
|
+
end
|
|
57
|
+
@wake.call if woke
|
|
58
|
+
chunk.bytesize
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# End the body, and wait until the I/O loop has taken all of it, its
|
|
62
|
+
# end included.
|
|
63
|
+
def finish
|
|
64
|
+
woke = @lock.synchronize do
|
|
65
|
+
check_writable
|
|
66
|
+
@finished = true
|
|
67
|
+
@chunks.empty?
|
|
68
|
+
end
|
|
69
|
+
@wake.call if woke
|
|
70
|
+
@lock.synchronize do
|
|
71
|
+
wait_for_the_end
|
|
72
|
+
raise StreamClosed, "stream closed" unless @end_taken
|
|
73
|
+
end
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# End the body with error, for the I/O loop to abort the stream once
|
|
78
|
+
# it has taken what is queued, and wait until it has. Returns true
|
|
79
|
+
# once the I/O loop has taken error, and false when it never will:
|
|
80
|
+
# the body had ended or been given up on, and fail did nothing, or it
|
|
81
|
+
# was given up on while fail waited.
|
|
82
|
+
def fail(error)
|
|
83
|
+
woke = @lock.synchronize do
|
|
84
|
+
next nil if @cancelled || @finished || @error
|
|
85
|
+
|
|
86
|
+
@error = error
|
|
87
|
+
@chunks.empty?
|
|
88
|
+
end
|
|
89
|
+
return false if woke.nil?
|
|
90
|
+
|
|
91
|
+
@wake.call if woke
|
|
92
|
+
@lock.synchronize do
|
|
93
|
+
wait_for_the_end
|
|
94
|
+
@end_taken
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Wait until the I/O loop has taken every chunk queued.
|
|
99
|
+
def flush
|
|
100
|
+
@lock.synchronize do
|
|
101
|
+
@changed.wait(@lock) until @chunks.empty? || @cancelled
|
|
102
|
+
raise StreamClosed, "stream closed" if @cancelled
|
|
103
|
+
end
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The next of what the writer queued, without waiting: a chunk; once
|
|
108
|
+
# they have all been taken, :eof for a body that ended, or the
|
|
109
|
+
# exception given to #fail; nil while there is nothing yet, and after
|
|
110
|
+
# #cancel.
|
|
111
|
+
def take
|
|
112
|
+
@lock.synchronize do
|
|
113
|
+
return nil if @cancelled
|
|
114
|
+
|
|
115
|
+
if (chunk = @chunks.shift)
|
|
116
|
+
@bytes -= chunk.bytesize
|
|
117
|
+
@changed.broadcast
|
|
118
|
+
return chunk
|
|
119
|
+
end
|
|
120
|
+
return nil unless @finished || @error
|
|
121
|
+
|
|
122
|
+
@end_taken = true
|
|
123
|
+
@changed.broadcast
|
|
124
|
+
@error || :eof
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# True when #take has something new to return: a chunk, or the end.
|
|
129
|
+
def ready?
|
|
130
|
+
@lock.synchronize do
|
|
131
|
+
!@cancelled && (!@chunks.empty? || ((@finished || !@error.nil?) && !@end_taken))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Give up on the body: what is queued is dropped, and the writer's
|
|
136
|
+
# calls, those waiting included, raise StreamClosed.
|
|
137
|
+
def cancel
|
|
138
|
+
@lock.synchronize do
|
|
139
|
+
@cancelled = true
|
|
140
|
+
@chunks.clear
|
|
141
|
+
@bytes = 0
|
|
142
|
+
@changed.broadcast
|
|
143
|
+
end
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# With the lock held.
|
|
150
|
+
def wait_for_the_end
|
|
151
|
+
@changed.wait(@lock) until @end_taken || @cancelled
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def check_writable
|
|
155
|
+
raise StreamClosed, "stream closed" if @cancelled
|
|
156
|
+
raise IOError, "body already ended" if @finished || @error
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP3
|
|
4
|
+
module Body
|
|
5
|
+
# The stream a streaming body (Rack 3's call(stream)) is called with.
|
|
6
|
+
# What it writes goes to a Body::Pipe, and what it reads comes from the
|
|
7
|
+
# request body, which has arrived in full. Its methods behave as IO's
|
|
8
|
+
# do, as Rack 3 asks: write takes any objects and returns the bytes
|
|
9
|
+
# written, a write or flush after close_write raises IOError, as a read
|
|
10
|
+
# after close_read does, and the closes may be called again.
|
|
11
|
+
#
|
|
12
|
+
# Once the stream has gone away (the client reset it or sent
|
|
13
|
+
# STOP_SENDING, or it was aborted after response_timeout), write and
|
|
14
|
+
# flush raise StreamClosed, and so do close_write and close, once.
|
|
15
|
+
# Writing past length, the content-length, fails the pipe, so that the
|
|
16
|
+
# stream is aborted, and raises StreamClosed, so that the body stops
|
|
17
|
+
# there; ending short of it fails the pipe too.
|
|
18
|
+
class Stream
|
|
19
|
+
def initialize(pipe:, input:, length:)
|
|
20
|
+
@pipe = pipe
|
|
21
|
+
@input = input
|
|
22
|
+
@length = length
|
|
23
|
+
@size = 0
|
|
24
|
+
@failed = false
|
|
25
|
+
@read_closed = false
|
|
26
|
+
@write_closed = false
|
|
27
|
+
@lock = Thread::Mutex.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Read from the request body, as IO#read does.
|
|
31
|
+
def read(...)
|
|
32
|
+
raise IOError, "not opened for reading" if @read_closed
|
|
33
|
+
|
|
34
|
+
@input.read(...)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Write each of objects, as a String (to_s), first waiting while the
|
|
38
|
+
# pipe is full. Returns the bytes written.
|
|
39
|
+
def write(*objects)
|
|
40
|
+
@lock.synchronize do
|
|
41
|
+
objects.sum do |object|
|
|
42
|
+
chunk = object.to_s
|
|
43
|
+
check_writable
|
|
44
|
+
count(chunk.bytesize)
|
|
45
|
+
@pipe.write(chunk)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def <<(object)
|
|
51
|
+
write(object)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Wait until what has been written has been taken to be sent.
|
|
56
|
+
def flush
|
|
57
|
+
@lock.synchronize { check_writable }
|
|
58
|
+
@pipe.flush
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Stop reading: close the request body, unless it is closed already.
|
|
63
|
+
def close_read
|
|
64
|
+
@read_closed = true
|
|
65
|
+
@input.close unless @input.closed?
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# End the body, and wait until all of it has been taken to be sent;
|
|
70
|
+
# fail the pipe instead if it is short of length.
|
|
71
|
+
def close_write
|
|
72
|
+
@lock.synchronize do
|
|
73
|
+
next if @write_closed
|
|
74
|
+
|
|
75
|
+
@write_closed = true
|
|
76
|
+
next if @failed
|
|
77
|
+
|
|
78
|
+
if @length && @size != @length
|
|
79
|
+
@pipe.fail(length_mismatch("the body is #{@size} bytes"))
|
|
80
|
+
else
|
|
81
|
+
@pipe.finish
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def close
|
|
88
|
+
close_read
|
|
89
|
+
close_write
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def closed?
|
|
93
|
+
@read_closed && @write_closed
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# With the lock held.
|
|
99
|
+
def check_writable
|
|
100
|
+
raise IOError, "not opened for writing" if @write_closed
|
|
101
|
+
raise StreamClosed, "stream closed" if @failed
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# With the lock held. Past length, the pipe fails and StreamClosed is
|
|
105
|
+
# raised.
|
|
106
|
+
def count(bytes)
|
|
107
|
+
@size += bytes
|
|
108
|
+
return unless @length && @size > @length
|
|
109
|
+
|
|
110
|
+
@failed = true
|
|
111
|
+
@pipe.fail(length_mismatch("the body is at least #{@size} bytes"))
|
|
112
|
+
raise StreamClosed, "stream closed"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def length_mismatch(what)
|
|
116
|
+
RackAdapter::InvalidResponse.new("#{what} but content-length says otherwise")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/http3/rack_adapter.rb
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module HTTP3
|
|
6
7
|
# Calls a Rack 3 application for an HTTP3::Request and turns what it
|
|
7
|
-
# returns into an HTTP3::Response.
|
|
8
|
+
# returns into an HTTP3::Response. Several threads may call it at once,
|
|
9
|
+
# each for a request of its own.
|
|
8
10
|
class RackAdapter
|
|
9
11
|
# The application's response breaks the Rack 3 specification; the
|
|
10
12
|
# server answers 500 instead.
|
|
11
13
|
class InvalidResponse < StandardError; end
|
|
12
14
|
|
|
15
|
+
# The request cannot be put in a Rack env; it is answered with 400.
|
|
16
|
+
class BadRequest < StandardError; end
|
|
17
|
+
private_constant :BadRequest
|
|
18
|
+
|
|
19
|
+
# The schemes a Rack env may carry (Rack also allows ws and wss, which
|
|
20
|
+
# HTTP/3 has no use for).
|
|
21
|
+
SCHEMES = %w[http https].freeze
|
|
22
|
+
|
|
23
|
+
# An authority, "host" or "host:port" (RFC 3986 section 3.2), as Rack
|
|
24
|
+
# Lint checks HTTP_HOST: the port is digits, of any number, or none.
|
|
25
|
+
AUTHORITY = /\A(?<host>#{URI::RFC3986_Parser::HOST})(?::(?<port>\d*))?\z/
|
|
26
|
+
|
|
13
27
|
# Connection-specific fields, which HTTP/3 forbids (RFC 9114 section
|
|
14
28
|
# 4.2).
|
|
15
29
|
CONNECTION_FIELDS = %w[connection keep-alive transfer-encoding upgrade proxy-connection].freeze
|
|
@@ -21,20 +35,71 @@ module HTTP3
|
|
|
21
35
|
# specification).
|
|
22
36
|
FORBIDDEN_IN_VALUE = /[\0\r\n]/
|
|
23
37
|
|
|
24
|
-
|
|
38
|
+
# A body read with each, and a streaming body, go through a Body::Pipe
|
|
39
|
+
# of pipe_capacity bytes, whose wake is wake.
|
|
40
|
+
def initialize(app, pipe_capacity:, wake:)
|
|
25
41
|
@app = app
|
|
42
|
+
@pipe_capacity = pipe_capacity
|
|
43
|
+
@wake = wake
|
|
44
|
+
@refusals = Hash.new(0)
|
|
45
|
+
@refusals_lock = Thread::Mutex.new
|
|
26
46
|
end
|
|
27
47
|
|
|
28
|
-
# remote is the client's address, an
|
|
29
|
-
#
|
|
30
|
-
#
|
|
48
|
+
# Returns [response, fill]. remote is the client's address, an
|
|
49
|
+
# Addrinfo. CONNECT is answered with 501, and a request the Rack env
|
|
50
|
+
# cannot carry with 400, without calling the application;
|
|
51
|
+
# #take_refusals counts them. Raises whatever the application raises,
|
|
52
|
+
# and InvalidResponse for a response that cannot be sent; the Rack body
|
|
53
|
+
# is closed in both cases.
|
|
54
|
+
#
|
|
55
|
+
# fill is nil, the Rack body closed already, unless the response sends
|
|
56
|
+
# a body that is read with each or is a streaming body (call): its
|
|
57
|
+
# response has a Body::Pipe, and fill, called once the response has
|
|
58
|
+
# been handed over, writes the body to it and closes the Rack body.
|
|
59
|
+
# fill returns nil, or an exception the body raised that did not abort
|
|
60
|
+
# its stream, the body having ended or the server having given up on
|
|
61
|
+
# it, for the caller to log.
|
|
31
62
|
def call(request, remote:)
|
|
32
|
-
|
|
63
|
+
# The server cannot open a tunnel (RFC 9114 section 4.4), and a
|
|
64
|
+
# CONNECT request has neither :scheme nor :path, so the env would be
|
|
65
|
+
# made up. The Session leaves nghttp3's enable_connect_protocol off,
|
|
66
|
+
# so nghttp3 refuses the :protocol of an extended CONNECT (RFC 9220).
|
|
67
|
+
return [refuse(501), nil] if request.headers.assoc(":method")&.last == "CONNECT"
|
|
68
|
+
|
|
69
|
+
env = env_for(request, remote)
|
|
70
|
+
rescue BadRequest
|
|
71
|
+
# Not logged: the client sent it, the server did not fail.
|
|
72
|
+
[refuse(400), nil]
|
|
73
|
+
else
|
|
74
|
+
status, headers, body = @app.call(env)
|
|
33
75
|
response_for(request, status, headers, body)
|
|
34
76
|
end
|
|
35
77
|
|
|
78
|
+
# The requests answered without the application since the last call,
|
|
79
|
+
# as {status => count}; the count starts over.
|
|
80
|
+
def take_refusals
|
|
81
|
+
@refusals_lock.synchronize do
|
|
82
|
+
refusals = @refusals
|
|
83
|
+
@refusals = Hash.new(0)
|
|
84
|
+
refusals
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
36
88
|
private
|
|
37
89
|
|
|
90
|
+
def refuse(status)
|
|
91
|
+
@refusals_lock.synchronize { @refusals[status] += 1 }
|
|
92
|
+
Response.new(status: status, headers: [], body: nil)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Raises BadRequest for what Rack does not allow, or allows in a form
|
|
96
|
+
# the application might read differently from the client, in this
|
|
97
|
+
# order: a scheme other than http and https, a fragment in the path, an
|
|
98
|
+
# authority that is not one (RFC 3986 section 3.2), and an :authority
|
|
99
|
+
# and a host field that disagree (RFC 9114 section 4.3.1). nghttp3 has
|
|
100
|
+
# already refused a request without :scheme, :path, or both :authority
|
|
101
|
+
# and host (but CONNECT); the defaults are for calling the adapter
|
|
102
|
+
# directly.
|
|
38
103
|
def env_for(request, remote)
|
|
39
104
|
pseudo = {}
|
|
40
105
|
fields = {}
|
|
@@ -46,23 +111,38 @@ module HTTP3
|
|
|
46
111
|
end
|
|
47
112
|
end
|
|
48
113
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
114
|
+
# nghttp3 takes either case of http and https; Rack wants them in
|
|
115
|
+
# lowercase.
|
|
116
|
+
scheme = pseudo.fetch(":scheme", "https").downcase
|
|
117
|
+
raise BadRequest, "scheme #{scheme} is not http or https" unless SCHEMES.include?(scheme)
|
|
118
|
+
|
|
119
|
+
# nghttp3 lets a fragment through; Rack forbids one.
|
|
120
|
+
target = pseudo.fetch(":path", "/")
|
|
121
|
+
raise BadRequest, "the path has a fragment" if target.include?("#")
|
|
122
|
+
|
|
123
|
+
path, query = target.split("?", 2)
|
|
124
|
+
host = fields["host"]&.first
|
|
125
|
+
authority = pseudo[":authority"] || host || "localhost"
|
|
126
|
+
match = AUTHORITY.match(authority) or raise BadRequest, "the authority is malformed"
|
|
127
|
+
if pseudo[":authority"] && host && host != pseudo[":authority"]
|
|
128
|
+
raise BadRequest, ":authority and host differ"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
port = match[:port]
|
|
132
|
+
port = (scheme == "http") ? "80" : "443" if port.nil? || port.empty?
|
|
53
133
|
|
|
54
134
|
env = {
|
|
55
135
|
"REQUEST_METHOD" => pseudo.fetch(":method", "GET"),
|
|
56
136
|
"SCRIPT_NAME" => "",
|
|
57
137
|
"PATH_INFO" => path,
|
|
58
138
|
"QUERY_STRING" => query || "",
|
|
59
|
-
"SERVER_NAME" =>
|
|
60
|
-
"SERVER_PORT" =>
|
|
139
|
+
"SERVER_NAME" => match[:host],
|
|
140
|
+
"SERVER_PORT" => port,
|
|
61
141
|
"SERVER_PROTOCOL" => "HTTP/3",
|
|
62
142
|
"REMOTE_ADDR" => remote.ip_address,
|
|
63
143
|
"HTTP_HOST" => authority,
|
|
64
144
|
"rack.url_scheme" => scheme,
|
|
65
|
-
"rack.input" =>
|
|
145
|
+
"rack.input" => request.input,
|
|
66
146
|
"rack.errors" => $stderr
|
|
67
147
|
}
|
|
68
148
|
fields.each do |name, values|
|
|
@@ -88,33 +168,51 @@ module HTTP3
|
|
|
88
168
|
end
|
|
89
169
|
end
|
|
90
170
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
171
|
+
# [response, fill]. The Rack body is closed here, before anything is
|
|
172
|
+
# sent, unless fill goes on with it, or to_ary has closed it (body_for
|
|
173
|
+
# yields then). Rack 3 only says it is closed "typically after" the
|
|
174
|
+
# body has been sent, and the Body::File reads the file through a
|
|
175
|
+
# descriptor of its own.
|
|
104
176
|
def response_for(request, status, headers, body)
|
|
105
|
-
response = nil
|
|
106
177
|
status = Integer(status)
|
|
178
|
+
# A 1xx is no final response, which is all HTTP/3 can send here (Rack
|
|
179
|
+
# Lint only asks for 100 or more).
|
|
180
|
+
unless (200..599).cover?(status)
|
|
181
|
+
raise InvalidResponse, "status #{status} is not a final status (200..599)"
|
|
182
|
+
end
|
|
107
183
|
headers = header_pairs(headers)
|
|
184
|
+
# A 204 must not have one (RFC 9110 section 8.6); it is dropped as the
|
|
185
|
+
# connection-specific fields are.
|
|
186
|
+
headers = headers.reject { |pair| pair.first == "content-length" } if status == 204
|
|
187
|
+
length = content_length(headers)
|
|
108
188
|
|
|
109
189
|
method = request.headers.assoc(":method")&.last
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Response.new(status: status, headers: headers, body: nil)
|
|
113
|
-
else
|
|
114
|
-
Response.new(status: status, headers: headers, body: body_for(body))
|
|
190
|
+
if method == "HEAD" || status == 204 || status == 304
|
|
191
|
+
# No body is sent, so content-length is not held against one.
|
|
192
|
+
return [Response.new(status: status, headers: headers, body: nil), nil]
|
|
115
193
|
end
|
|
194
|
+
|
|
195
|
+
sent, fill = body_for(body, length, request.input) { body = nil }
|
|
196
|
+
[Response.new(status: status, headers: headers, body: sent), fill]
|
|
116
197
|
ensure
|
|
117
|
-
|
|
198
|
+
begin
|
|
199
|
+
close_rack_body(body)
|
|
200
|
+
rescue
|
|
201
|
+
# The response goes nowhere: a 500 replaces it.
|
|
202
|
+
sent.io.close if sent.is_a?(Body::File)
|
|
203
|
+
raise
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The content-length among headers, as an Integer, or nil if there is
|
|
208
|
+
# none. The message leaves the value out, as header_pairs's do.
|
|
209
|
+
def content_length(headers)
|
|
210
|
+
values = headers.filter_map { |name, value| value if name == "content-length" }
|
|
211
|
+
return nil if values.empty?
|
|
212
|
+
raise InvalidResponse, "content-length is given more than once" if values.size > 1
|
|
213
|
+
raise InvalidResponse, "content-length is not a number" unless values.first.match?(/\A\d+\z/)
|
|
214
|
+
|
|
215
|
+
Integer(values.first, 10)
|
|
118
216
|
end
|
|
119
217
|
|
|
120
218
|
# Rack 3 headers: lowercase names, each value a String or an Array of
|
|
@@ -141,20 +239,104 @@ module HTTP3
|
|
|
141
239
|
pairs
|
|
142
240
|
end
|
|
143
241
|
|
|
144
|
-
|
|
242
|
+
# [the body to send, fill]. One whose length is known before it is
|
|
243
|
+
# sent, a file or an Array, is held against length, the content-length,
|
|
244
|
+
# now; one read with each, and a streaming body, as fill writes it. A
|
|
245
|
+
# streaming body reads input, the request body. body_for yields when
|
|
246
|
+
# the Rack body is no longer for response_for to close.
|
|
247
|
+
def body_for(body, length, input)
|
|
145
248
|
if body.respond_to?(:to_path)
|
|
146
249
|
io = ::File.open(body.to_path, "rb")
|
|
147
|
-
|
|
250
|
+
if length && io.size != length
|
|
251
|
+
size = io.size
|
|
252
|
+
io.close
|
|
253
|
+
raise length_mismatch(size)
|
|
254
|
+
end
|
|
255
|
+
[Body::File.new(io: io, offset: 0, length: io.size, rack_body: nil), nil]
|
|
148
256
|
elsif body.respond_to?(:to_ary)
|
|
149
257
|
# Rack 3 has to_ary close the body itself.
|
|
150
|
-
|
|
258
|
+
chunks = body.to_ary
|
|
259
|
+
yield
|
|
260
|
+
# Checked before anything is sent, which Session#fill_bodies would
|
|
261
|
+
# only find out once the headers have gone.
|
|
262
|
+
chunks.each do |chunk|
|
|
263
|
+
raise InvalidResponse, "body chunk must be a String (got #{chunk.class})" unless chunk.is_a?(String)
|
|
264
|
+
end
|
|
265
|
+
size = chunks.sum(&:bytesize)
|
|
266
|
+
raise length_mismatch(size) if length && size != length
|
|
267
|
+
|
|
268
|
+
[Body::Chunks.new(enum: chunks.each, rack_body: nil), nil]
|
|
151
269
|
elsif body.respond_to?(:each)
|
|
152
|
-
|
|
270
|
+
# Rack 3: a body with both each and call is read with each.
|
|
271
|
+
pipe = Body::Pipe.new(capacity: @pipe_capacity, wake: @wake)
|
|
272
|
+
yield
|
|
273
|
+
[pipe, -> { fill_each(body, pipe, length) }]
|
|
274
|
+
elsif body.respond_to?(:call)
|
|
275
|
+
pipe = Body::Pipe.new(capacity: @pipe_capacity, wake: @wake)
|
|
276
|
+
yield
|
|
277
|
+
[pipe, -> { fill_call(body, pipe, length, input) }]
|
|
153
278
|
else
|
|
154
|
-
raise InvalidResponse, "
|
|
279
|
+
raise InvalidResponse, "the body (#{body.class}) responds to neither each nor call"
|
|
155
280
|
end
|
|
156
281
|
end
|
|
157
282
|
|
|
283
|
+
# Write the chunks of body's each to pipe, and end it. A chunk that is
|
|
284
|
+
# not a String, one that takes the body past length bytes, an end short
|
|
285
|
+
# of it, and an exception from each fail the pipe, and the stream is
|
|
286
|
+
# aborted then, since the headers have gone. A pipe the server has given
|
|
287
|
+
# up on (the client reset the stream, say) makes write raise
|
|
288
|
+
# StreamClosed, and the body stops there, without failing. Returns an
|
|
289
|
+
# exception the pipe did not take, or nil. The Rack body is closed in
|
|
290
|
+
# every case; an exception from its close is raised.
|
|
291
|
+
def fill_each(body, pipe, length)
|
|
292
|
+
size = 0
|
|
293
|
+
body.each do |chunk|
|
|
294
|
+
raise InvalidResponse, "body chunk must be a String (got #{chunk.class})" unless chunk.is_a?(String)
|
|
295
|
+
|
|
296
|
+
size += chunk.bytesize
|
|
297
|
+
if length && size > length
|
|
298
|
+
raise InvalidResponse, "the body is at least #{size} bytes but content-length says otherwise"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
pipe.write(chunk)
|
|
302
|
+
end
|
|
303
|
+
raise length_mismatch(size) if length && size < length
|
|
304
|
+
|
|
305
|
+
pipe.finish
|
|
306
|
+
rescue StreamClosed
|
|
307
|
+
nil
|
|
308
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
309
|
+
# Even exit, on a worker, ends only the body.
|
|
310
|
+
e unless pipe.fail(e)
|
|
311
|
+
ensure
|
|
312
|
+
close_rack_body(body)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Call the streaming body with a Body::Stream that writes to pipe and
|
|
316
|
+
# reads input, and end the body once call returns, unless it has ended
|
|
317
|
+
# already: a write after that, from another thread, raises IOError. As
|
|
318
|
+
# with fill_each, the stream is aborted once the body goes past length
|
|
319
|
+
# bytes or ends short of it, or call raises, and it stops without
|
|
320
|
+
# failing once the server has given up on the pipe. Returns an
|
|
321
|
+
# exception the pipe did not take, such as one call raised after it
|
|
322
|
+
# closed the stream, or nil. The Rack body is closed in every case; an
|
|
323
|
+
# exception from its close is raised.
|
|
324
|
+
def fill_call(body, pipe, length, input)
|
|
325
|
+
stream = Body::Stream.new(pipe: pipe, input: input, length: length)
|
|
326
|
+
body.call(stream)
|
|
327
|
+
stream.close_write
|
|
328
|
+
rescue StreamClosed
|
|
329
|
+
nil
|
|
330
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
331
|
+
e unless pipe.fail(e)
|
|
332
|
+
ensure
|
|
333
|
+
close_rack_body(body)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def length_mismatch(size)
|
|
337
|
+
InvalidResponse.new("the body is #{size} bytes but content-length says otherwise")
|
|
338
|
+
end
|
|
339
|
+
|
|
158
340
|
def close_rack_body(body)
|
|
159
341
|
body.close if body.respond_to?(:close)
|
|
160
342
|
end
|
data/lib/http3/request.rb
CHANGED
|
@@ -4,6 +4,8 @@ module HTTP3
|
|
|
4
4
|
# A request as a Session put it together. headers is an Array of
|
|
5
5
|
# [name, value] binary Strings with lowercase names, the pseudo-headers
|
|
6
6
|
# (:method, :scheme, :authority, :path) first; a repeated name keeps one
|
|
7
|
-
# pair per field.
|
|
8
|
-
|
|
7
|
+
# pair per field. input is the request body, a binary IO at its start,
|
|
8
|
+
# which becomes rack.input: a StringIO, or a temporary File once the body
|
|
9
|
+
# is past the Session's request_body_buffer_size.
|
|
10
|
+
Request = Data.define(:stream_id, :headers, :input)
|
|
9
11
|
end
|