async-http 0.45.4 → 0.45.6
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/.gitignore +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/async-http.gemspec +2 -2
- data/lib/async/http/protocol/http2.rb +1 -3
- data/lib/async/http/protocol/http2/client.rb +1 -1
- data/lib/async/http/protocol/http2/connection.rb +6 -3
- data/lib/async/http/protocol/http2/request.rb +94 -87
- data/lib/async/http/protocol/http2/response.rb +119 -82
- data/lib/async/http/protocol/http2/server.rb +6 -4
- data/lib/async/http/protocol/http2/stream.rb +142 -88
- data/lib/async/http/version.rb +1 -1
- data/tasks/h2spec.rake +45 -0
- metadata +7 -7
- data/lib/async/http/protocol/http2/promise.rb +0 -71
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f10ee1ffdafaf1d8b663c53dcb8d4be3d9462206024a6415214a081e2574bee3
|
4
|
+
data.tar.gz: ee48372568c7563d0e273447299f1701d4dde0872136614a42df34ce26bb830b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2fbfbd18899d1d773795afd014e8b1508564c6dc670c7290fd6b9cecd109fd0b2a3612e304352c353435320fcf703e99477ff9ece909baa0734c1e33e0a3f97
|
7
|
+
data.tar.gz: da9ca8b00e9fea4866c9e1806b0c24611f52be3dab459c2df424c00640f63db894f9b5e98e70720b9f244ade1155cb9b96776ccc6812e10272b42d29f01b5439
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/async-http.gemspec
CHANGED
@@ -16,12 +16,12 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f)}
|
17
17
|
spec.require_paths = ["lib"]
|
18
18
|
|
19
|
-
spec.add_dependency("async", "~> 1.
|
19
|
+
spec.add_dependency("async", "~> 1.19")
|
20
20
|
spec.add_dependency("async-io", "~> 1.18")
|
21
21
|
|
22
22
|
spec.add_dependency("protocol-http", "~> 0.8.0")
|
23
23
|
spec.add_dependency("protocol-http1", "~> 0.8.0")
|
24
|
-
spec.add_dependency("protocol-http2", "~> 0.
|
24
|
+
spec.add_dependency("protocol-http2", "~> 0.7.0")
|
25
25
|
|
26
26
|
# spec.add_dependency("openssl")
|
27
27
|
|
@@ -33,15 +33,13 @@ module Async
|
|
33
33
|
|
34
34
|
CLIENT_SETTINGS = {
|
35
35
|
::Protocol::HTTP2::Settings::ENABLE_PUSH => 0,
|
36
|
-
::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 256,
|
37
36
|
::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000,
|
38
37
|
::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 0x7FFFFFFF,
|
39
|
-
::Protocol::HTTP2::Settings::ENABLE_CONNECT_PROTOCOL => 1,
|
40
38
|
}
|
41
39
|
|
42
40
|
SERVER_SETTINGS = {
|
43
41
|
# We choose a lower maximum concurrent streams to avoid overloading a single connection/thread.
|
44
|
-
::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS =>
|
42
|
+
::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 10,
|
45
43
|
::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000,
|
46
44
|
::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 0x7FFFFFFF,
|
47
45
|
::Protocol::HTTP2::Settings::ENABLE_CONNECT_PROTOCOL => 1,
|
@@ -34,7 +34,9 @@ module Async
|
|
34
34
|
STATUS = ':status'.freeze
|
35
35
|
PROTOCOL = ':protocol'.freeze
|
36
36
|
|
37
|
-
CONTENT_LENGTH = 'content-length'
|
37
|
+
CONTENT_LENGTH = 'content-length'.freeze
|
38
|
+
CONNECTION = 'connection'.freeze
|
39
|
+
TRAILERS = 'trailers'.freeze
|
38
40
|
|
39
41
|
module Connection
|
40
42
|
def initialize(*)
|
@@ -80,9 +82,10 @@ module Async
|
|
80
82
|
|
81
83
|
begin
|
82
84
|
while !self.closed?
|
85
|
+
self.consume_window
|
83
86
|
self.read_frame
|
84
87
|
end
|
85
|
-
rescue EOFError, Async::Wrapper::Cancelled
|
88
|
+
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Async::Wrapper::Cancelled
|
86
89
|
# Ignore.
|
87
90
|
ensure
|
88
91
|
close($!)
|
@@ -99,7 +102,7 @@ module Async
|
|
99
102
|
attr :count
|
100
103
|
|
101
104
|
def multiplex
|
102
|
-
|
105
|
+
self.maximum_concurrent_streams
|
103
106
|
end
|
104
107
|
|
105
108
|
# Can we use this connection to make requests?
|
@@ -19,59 +19,123 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require_relative '../request'
|
22
|
-
require_relative '
|
22
|
+
require_relative 'stream'
|
23
23
|
|
24
24
|
module Async
|
25
25
|
module HTTP
|
26
26
|
module Protocol
|
27
27
|
module HTTP2
|
28
|
+
# Typically used on the server side to represent an incoming request, and write the response.
|
28
29
|
class Request < Protocol::Request
|
29
|
-
|
30
|
-
|
30
|
+
class Stream < HTTP2::Stream
|
31
|
+
def initialize(*)
|
32
|
+
super
|
33
|
+
|
34
|
+
@enqueued = false
|
35
|
+
@request = Request.new(self)
|
36
|
+
end
|
31
37
|
|
32
|
-
|
33
|
-
|
38
|
+
attr :request
|
39
|
+
|
40
|
+
# Create a fake request on the server, with the given headers.
|
41
|
+
def create_push_promise_stream(headers)
|
42
|
+
stream = @connection.create_push_promise_stream(&Stream.method(:create))
|
43
|
+
|
44
|
+
stream.headers = ::Protocol::HTTP::Headers.new
|
45
|
+
|
46
|
+
# This will ultimately enqueue the request to be processed by the server:
|
47
|
+
stream.receive_initial_headers(headers, false)
|
48
|
+
|
49
|
+
return stream
|
50
|
+
end
|
34
51
|
|
35
|
-
|
36
|
-
|
52
|
+
def receive_initial_headers(headers, end_stream)
|
53
|
+
headers.each do |key, value|
|
54
|
+
if key == SCHEME
|
55
|
+
raise ::Protocol::HTTP2::HeaderError, "Request scheme already specified!" if @request.scheme
|
56
|
+
|
57
|
+
@request.scheme = value
|
58
|
+
elsif key == AUTHORITY
|
59
|
+
raise ::Protocol::HTTP2::HeaderError, "Request authority already specified!" if @request.authority
|
60
|
+
|
61
|
+
@request.authority = value
|
62
|
+
elsif key == METHOD
|
63
|
+
raise ::Protocol::HTTP2::HeaderError, "Request method already specified!" if @request.method
|
64
|
+
|
65
|
+
@request.method = value
|
66
|
+
elsif key == PATH
|
67
|
+
raise ::Protocol::HTTP2::HeaderError, "Request path is empty!" if value.empty?
|
68
|
+
raise ::Protocol::HTTP2::HeaderError, "Request path already specified!" if @request.path
|
69
|
+
|
70
|
+
@request.path = value
|
71
|
+
elsif key == PROTOCOL
|
72
|
+
raise ::Protocol::HTTP2::HeaderError, "Request protocol already specified!" if @request.protocol
|
73
|
+
|
74
|
+
@request.protocol = value
|
75
|
+
elsif key == CONTENT_LENGTH
|
76
|
+
raise ::Protocol::HTTP2::HeaderError, "Request content length already specified!" if @length
|
77
|
+
|
78
|
+
@length = Integer(value)
|
79
|
+
elsif key == CONNECTION
|
80
|
+
raise ::Protocol::HTTP2::HeaderError, "Connection header is not allowed!"
|
81
|
+
elsif key.start_with? ':'
|
82
|
+
raise ::Protocol::HTTP2::HeaderError, "Invalid pseudo-header #{key}!"
|
83
|
+
elsif key =~ /[A-Z]/
|
84
|
+
raise ::Protocol::HTTP2::HeaderError, "Invalid characters in header #{key}!"
|
85
|
+
else
|
86
|
+
add_header(key, value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
@request.headers = @headers
|
91
|
+
|
92
|
+
unless @request.valid?
|
93
|
+
raise ::Protocol::HTTP2::HeaderError, "Request is missing required headers!"
|
94
|
+
else
|
95
|
+
# We only construct the input/body if data is coming.
|
96
|
+
unless end_stream
|
97
|
+
@input = Body::Writable.new(@length)
|
98
|
+
@request.body = @input
|
99
|
+
end
|
100
|
+
|
101
|
+
# We are ready for processing:
|
102
|
+
@connection.requests.enqueue(@request)
|
103
|
+
end
|
104
|
+
|
105
|
+
return headers
|
106
|
+
end
|
37
107
|
end
|
38
108
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
109
|
+
def initialize(stream)
|
110
|
+
super(nil, nil, nil, nil, VERSION, nil)
|
111
|
+
|
112
|
+
@stream = stream
|
43
113
|
end
|
44
114
|
|
45
|
-
|
46
|
-
@connection.enable_push?
|
47
|
-
end
|
115
|
+
attr :stream
|
48
116
|
|
49
|
-
def
|
50
|
-
@
|
51
|
-
request = self.class.new(@connection, stream_id)
|
52
|
-
|
53
|
-
request.receive_headers(self, headers, false)
|
54
|
-
end
|
117
|
+
def valid?
|
118
|
+
@scheme and @method and @path
|
55
119
|
end
|
56
120
|
|
57
|
-
|
58
|
-
|
121
|
+
def hijack?
|
122
|
+
false
|
59
123
|
end
|
60
124
|
|
61
|
-
def
|
62
|
-
|
63
|
-
@input.close(error)
|
64
|
-
@input = nil
|
65
|
-
end
|
125
|
+
def push?
|
126
|
+
@stream.connection.enable_push?
|
66
127
|
end
|
67
128
|
|
68
129
|
# @return [Stream] the promised stream, on which to send data.
|
69
|
-
def push(path, headers = nil)
|
130
|
+
def push(path, headers = nil, scheme = @scheme, authority = @authority)
|
131
|
+
raise ArgumentError, "Missing scheme!" unless scheme
|
132
|
+
raise ArgumentError, "Missing authority!" unless authority
|
133
|
+
|
70
134
|
push_headers = [
|
71
|
-
[SCHEME,
|
135
|
+
[SCHEME, scheme],
|
72
136
|
[METHOD, ::Protocol::HTTP::Methods::GET],
|
73
137
|
[PATH, path],
|
74
|
-
[AUTHORITY,
|
138
|
+
[AUTHORITY, authority]
|
75
139
|
]
|
76
140
|
|
77
141
|
if headers
|
@@ -84,63 +148,6 @@ module Async
|
|
84
148
|
@stream.send_push_promise(push_headers)
|
85
149
|
end
|
86
150
|
|
87
|
-
def receive_headers(stream, headers, end_stream)
|
88
|
-
headers.each do |key, value|
|
89
|
-
if key == SCHEME
|
90
|
-
return @stream.send_failure(400, "Request scheme already specified") if @scheme
|
91
|
-
|
92
|
-
@scheme = value
|
93
|
-
elsif key == AUTHORITY
|
94
|
-
return @stream.send_failure(400, "Request authority already specified") if @authority
|
95
|
-
|
96
|
-
@authority = value
|
97
|
-
elsif key == METHOD
|
98
|
-
return @stream.send_failure(400, "Request method already specified") if @method
|
99
|
-
|
100
|
-
@method = value
|
101
|
-
elsif key == PATH
|
102
|
-
return @stream.send_failure(400, "Request path already specified") if @path
|
103
|
-
|
104
|
-
@path = value
|
105
|
-
elsif key == PROTOCOL
|
106
|
-
return @stream.send_failure(400, "Request protocol already specified") if @protocol
|
107
|
-
|
108
|
-
@protocol = value
|
109
|
-
elsif key == CONTENT_LENGTH
|
110
|
-
return @stream.send_failure(400, "Request protocol already content length") if @length
|
111
|
-
|
112
|
-
@length = Integer(value)
|
113
|
-
else
|
114
|
-
@headers[key] = value
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
unless @scheme and @method and @path
|
119
|
-
send_reset_stream(PROTOCOL_ERROR)
|
120
|
-
else
|
121
|
-
# We only construct the input/body if data is coming.
|
122
|
-
unless end_stream
|
123
|
-
@body = @input = Body::Writable.new(@length)
|
124
|
-
end
|
125
|
-
|
126
|
-
# We are ready for processing:
|
127
|
-
@connection.requests.enqueue(self)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def receive_data(stream, data, end_stream)
|
132
|
-
unless data.empty?
|
133
|
-
@input.write(data)
|
134
|
-
end
|
135
|
-
|
136
|
-
if end_stream
|
137
|
-
@input.close
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def receive_reset_stream(stream, error_code)
|
142
|
-
end
|
143
|
-
|
144
151
|
NO_RESPONSE = [
|
145
152
|
[STATUS, '500'],
|
146
153
|
]
|
@@ -19,119 +19,156 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require_relative '../response'
|
22
|
+
require_relative 'stream'
|
22
23
|
|
23
24
|
module Async
|
24
25
|
module HTTP
|
25
26
|
module Protocol
|
26
27
|
module HTTP2
|
28
|
+
# Typically used on the client side to represent a request and the incoming response.
|
27
29
|
class Response < Protocol::Response
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
class Stream < HTTP2::Stream
|
31
|
+
def initialize(*)
|
32
|
+
super
|
33
|
+
|
34
|
+
@response = Response.new(self)
|
35
|
+
|
36
|
+
@notification = Async::Notification.new
|
37
|
+
@exception = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
attr :response
|
41
|
+
|
42
|
+
def accept_push_promise_stream(promised_stream_id, headers)
|
43
|
+
stream = @connection.accept_push_promise_stream(promised_stream_id, &Stream.method(:accept))
|
44
|
+
|
45
|
+
stream.response.build_request(headers)
|
46
|
+
|
47
|
+
@response.promises.enqueue(stream.response)
|
48
|
+
|
49
|
+
return stream
|
50
|
+
end
|
51
|
+
|
52
|
+
# This should be invoked from the background reader, and notifies the task waiting for the headers that we are done.
|
53
|
+
def receive_initial_headers(headers, end_stream)
|
54
|
+
headers.each do |key, value|
|
55
|
+
if key == STATUS
|
56
|
+
@response.status = Integer(value)
|
57
|
+
elsif key == PROTOCOL
|
58
|
+
@response.protocol = value
|
59
|
+
elsif key == CONTENT_LENGTH
|
60
|
+
@length = Integer(value)
|
61
|
+
else
|
62
|
+
add_header(key, value)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
@response.headers = @headers
|
67
|
+
|
68
|
+
unless @response.valid?
|
69
|
+
send_reset_stream(::Protocol::HTTP2::Error::PROTOCOL_ERROR)
|
70
|
+
else
|
71
|
+
# We only construct the input/body if data is coming.
|
72
|
+
unless end_stream
|
73
|
+
@input = Body::Writable.new(@length)
|
74
|
+
@response.body = @input
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
self.notify!
|
79
|
+
|
80
|
+
return headers
|
81
|
+
end
|
31
82
|
|
32
|
-
|
83
|
+
# Notify anyone waiting on the response headers to be received (or failure).
|
84
|
+
def notify!
|
85
|
+
if @notification
|
86
|
+
@notification.signal
|
87
|
+
@notification = nil
|
88
|
+
end
|
89
|
+
end
|
33
90
|
|
34
|
-
|
35
|
-
|
91
|
+
# Wait for the headers to be received or for stream reset.
|
92
|
+
def wait
|
93
|
+
# If you call wait after the headers were already received, it should return immediately:
|
94
|
+
@notification&.wait
|
95
|
+
|
96
|
+
if @exception
|
97
|
+
raise @exception
|
98
|
+
end
|
99
|
+
end
|
36
100
|
|
37
|
-
|
38
|
-
|
101
|
+
def close(error)
|
102
|
+
super
|
103
|
+
|
104
|
+
@response.promises.enqueue nil
|
105
|
+
|
106
|
+
@exception = error
|
107
|
+
|
108
|
+
notify!
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def initialize(stream)
|
113
|
+
super(stream.connection.version, nil, nil)
|
39
114
|
|
115
|
+
@stream = stream
|
116
|
+
@request = nil
|
40
117
|
@promises = nil
|
41
118
|
end
|
42
119
|
|
43
120
|
attr :stream
|
44
121
|
|
45
|
-
|
46
|
-
@promises ||= Async::Queue.new
|
47
|
-
end
|
122
|
+
attr :request
|
48
123
|
|
49
|
-
def
|
50
|
-
@
|
51
|
-
promise = Promise.new(@connection, headers, promised_stream_id)
|
52
|
-
|
53
|
-
self.promises.enqueue(promise)
|
54
|
-
end
|
124
|
+
def wait
|
125
|
+
@stream.wait
|
55
126
|
end
|
56
127
|
|
57
|
-
|
58
|
-
|
59
|
-
self.promises.enqueue(nil)
|
128
|
+
def valid?
|
129
|
+
!!@status
|
60
130
|
end
|
61
131
|
|
62
|
-
|
63
|
-
|
64
|
-
if @notification
|
65
|
-
@notification.signal
|
66
|
-
@notification = nil
|
67
|
-
end
|
68
|
-
|
69
|
-
# if @input
|
70
|
-
# @input.close(@exception)
|
71
|
-
# end
|
132
|
+
def promises
|
133
|
+
@promises ||= Async::Queue.new
|
72
134
|
end
|
73
135
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
if @notification
|
78
|
-
@notification.wait
|
79
|
-
end
|
136
|
+
def build_request(headers)
|
137
|
+
request = ::Protocol::HTTP::Request.new
|
138
|
+
request.headers = ::Protocol::HTTP::Headers.new
|
80
139
|
|
81
|
-
if @exception
|
82
|
-
raise @exception
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# This should be invoked from the background reader, and notifies the task waiting for the headers that we are done.
|
87
|
-
def receive_headers(stream, headers, end_stream)
|
88
140
|
headers.each do |key, value|
|
89
|
-
if key ==
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
elsif key ==
|
94
|
-
|
141
|
+
if key == SCHEME
|
142
|
+
raise ::Protocol::HTTP2::HeaderError, "Request scheme already specified!" if request.scheme
|
143
|
+
|
144
|
+
request.scheme = value
|
145
|
+
elsif key == AUTHORITY
|
146
|
+
raise ::Protocol::HTTP2::HeaderError, "Request authority already specified!" if request.authority
|
147
|
+
|
148
|
+
request.authority = value
|
149
|
+
elsif key == METHOD
|
150
|
+
raise ::Protocol::HTTP2::HeaderError, "Request method already specified!" if request.method
|
151
|
+
|
152
|
+
request.method = value
|
153
|
+
elsif key == PATH
|
154
|
+
raise ::Protocol::HTTP2::HeaderError, "Request path is empty!" if value.empty?
|
155
|
+
raise ::Protocol::HTTP2::HeaderError, "Request path already specified!" if request.path
|
156
|
+
|
157
|
+
request.path = value
|
158
|
+
elsif key.start_with? ':'
|
159
|
+
raise ::Protocol::HTTP2::HeaderError, "Invalid pseudo-header #{key}!"
|
95
160
|
else
|
96
|
-
|
161
|
+
request.headers[key] = value
|
97
162
|
end
|
98
163
|
end
|
99
164
|
|
100
|
-
|
101
|
-
@body = @input = Body::Writable.new(@length)
|
102
|
-
end
|
103
|
-
|
104
|
-
notify!
|
105
|
-
end
|
106
|
-
|
107
|
-
def receive_data(stream, data, end_stream)
|
108
|
-
unless data.empty?
|
109
|
-
@input.write(data)
|
110
|
-
end
|
111
|
-
|
112
|
-
if end_stream
|
113
|
-
@input.close
|
114
|
-
end
|
115
|
-
rescue
|
116
|
-
@stream.send_reset_stream(0)
|
117
|
-
end
|
118
|
-
|
119
|
-
def receive_reset_stream(stream, error_code)
|
120
|
-
if error_code > 0
|
121
|
-
@exception = EOFError.new("Stream reset: error_code=#{error_code}")
|
122
|
-
end
|
123
|
-
|
124
|
-
notify!
|
125
|
-
end
|
126
|
-
|
127
|
-
def stream_closed(error)
|
128
|
-
@exception = error
|
129
|
-
|
130
|
-
notify!
|
165
|
+
@request = request
|
131
166
|
end
|
132
167
|
|
133
168
|
# Send a request and read it into this response.
|
134
|
-
def send_request(request)
|
169
|
+
def send_request(request, task: Async::Task.current)
|
170
|
+
@request = request
|
171
|
+
|
135
172
|
# https://http2.github.io/http2-spec/#rfc.section.8.1.2.3
|
136
173
|
# All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header fields is malformed (Section 8.1.2.6).
|
137
174
|
pseudo_headers = [
|
@@ -20,7 +20,6 @@
|
|
20
20
|
|
21
21
|
require_relative 'connection'
|
22
22
|
require_relative 'request'
|
23
|
-
require_relative 'promise'
|
24
23
|
|
25
24
|
require 'protocol/http2/server'
|
26
25
|
|
@@ -32,6 +31,7 @@ module Async
|
|
32
31
|
include Connection
|
33
32
|
|
34
33
|
def initialize(stream)
|
34
|
+
# Used by some generic methods in Connetion:
|
35
35
|
@stream = stream
|
36
36
|
|
37
37
|
framer = ::Protocol::HTTP2::Framer.new(stream)
|
@@ -43,21 +43,23 @@ module Async
|
|
43
43
|
|
44
44
|
attr :requests
|
45
45
|
|
46
|
-
# A new request has come in:
|
47
46
|
def accept_stream(stream_id)
|
48
47
|
super do
|
49
|
-
Request.
|
48
|
+
Request::Stream.create(self, stream_id)
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
53
52
|
def close(error = nil)
|
54
53
|
super
|
55
54
|
|
55
|
+
# Stop the request loop:
|
56
56
|
@requests.enqueue nil
|
57
|
+
@requests = nil
|
57
58
|
end
|
58
59
|
|
59
60
|
def each
|
60
|
-
|
61
|
+
# It's possible the connection has died before we get here...
|
62
|
+
@requests&.async do |task, request|
|
61
63
|
@count += 1
|
62
64
|
|
63
65
|
begin
|
@@ -25,136 +25,190 @@ module Async
|
|
25
25
|
module Protocol
|
26
26
|
module HTTP2
|
27
27
|
class Stream < ::Protocol::HTTP2::Stream
|
28
|
-
|
29
|
-
|
28
|
+
class Buffer
|
29
|
+
def initialize(stream, body, task: Task.current)
|
30
|
+
@stream = stream
|
31
|
+
|
32
|
+
@body = body
|
33
|
+
@remainder = nil
|
34
|
+
|
35
|
+
@window_updated = Async::Condition.new
|
36
|
+
|
37
|
+
@task = task.async(&self.method(:passthrough))
|
38
|
+
end
|
30
39
|
|
31
|
-
|
40
|
+
def passthrough(task)
|
41
|
+
while chunk = self.read
|
42
|
+
maximum_size = @stream.available_frame_size
|
43
|
+
|
44
|
+
while maximum_size <= 0
|
45
|
+
@window_updated.wait
|
46
|
+
|
47
|
+
maximum_size = @stream.available_frame_size
|
48
|
+
end
|
49
|
+
|
50
|
+
self.send_data(chunk, maximum_size)
|
51
|
+
end
|
52
|
+
|
53
|
+
self.end_stream
|
54
|
+
rescue Async::Stop
|
55
|
+
# Ignore.
|
56
|
+
ensure
|
57
|
+
@body&.close($!)
|
58
|
+
@body = nil
|
59
|
+
end
|
32
60
|
|
33
|
-
|
34
|
-
|
61
|
+
def read
|
62
|
+
if @remainder
|
63
|
+
remainder = @remainder
|
64
|
+
@remainder = nil
|
65
|
+
|
66
|
+
return remainder
|
67
|
+
else
|
68
|
+
@body.read
|
69
|
+
end
|
70
|
+
end
|
35
71
|
|
36
|
-
|
37
|
-
|
72
|
+
def push(chunk)
|
73
|
+
@remainder = chunk
|
74
|
+
end
|
38
75
|
|
39
|
-
#
|
40
|
-
@
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
def send_body(body, task: Async::Task.current)
|
55
|
-
# TODO Might need to stop this task when body is cancelled.
|
56
|
-
@task = task.async do |subtask|
|
57
|
-
subtask.annotate "Sending body: #{body.class}"
|
58
|
-
|
59
|
-
@body = body
|
60
|
-
|
61
|
-
window_updated
|
76
|
+
# Send `maximum_size` bytes of data using the specified `stream`. If the buffer has no more chunks, `END_STREAM` will be sent on the final chunk.
|
77
|
+
# @param maximum_size [Integer] send up to this many bytes of data.
|
78
|
+
# @param stream [Stream] the stream to use for sending data frames.
|
79
|
+
def send_data(chunk, maximum_size)
|
80
|
+
if chunk.bytesize <= maximum_size
|
81
|
+
@stream.send_data(chunk, maximum_size: maximum_size)
|
82
|
+
else
|
83
|
+
@stream.send_data(chunk.byteslice(0, maximum_size), maximum_size: maximum_size)
|
84
|
+
|
85
|
+
# The window was not big enough to send all the data, so we save it for next time:
|
86
|
+
self.push(
|
87
|
+
chunk.byteslice(maximum_size, chunk.bytesize - maximum_size)
|
88
|
+
)
|
89
|
+
end
|
62
90
|
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def send_chunk
|
66
|
-
maximum_size = self.available_frame_size
|
67
91
|
|
68
|
-
|
69
|
-
|
92
|
+
def end_stream
|
93
|
+
@stream.send_data(nil, ::Protocol::HTTP2::END_STREAM)
|
70
94
|
end
|
71
95
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
else
|
96
|
+
def window_updated(size)
|
97
|
+
@window_updated.signal
|
98
|
+
end
|
99
|
+
|
100
|
+
def close(error)
|
78
101
|
if @body
|
79
|
-
@body.close
|
102
|
+
@body.close(error)
|
80
103
|
@body = nil
|
81
104
|
end
|
82
105
|
|
83
|
-
|
84
|
-
unless closed? or @connection.closed?
|
85
|
-
send_data(nil, ::Protocol::HTTP2::END_STREAM)
|
86
|
-
end
|
87
|
-
|
88
|
-
return false
|
106
|
+
@task&.stop
|
89
107
|
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def initialize(*)
|
111
|
+
super
|
90
112
|
|
91
|
-
|
113
|
+
@headers = nil
|
114
|
+
@trailers = nil
|
92
115
|
|
93
|
-
|
94
|
-
|
116
|
+
# Input buffer (receive_data):
|
117
|
+
@length = nil
|
118
|
+
@input = nil
|
119
|
+
|
120
|
+
# Output buffer (window_updated):
|
121
|
+
@output = nil
|
122
|
+
end
|
123
|
+
|
124
|
+
attr_accessor :headers
|
125
|
+
|
126
|
+
def add_header(key, value)
|
127
|
+
if key == CONNECTION
|
128
|
+
raise ::Protocol::HTTP2::HeaderError, "Connection header is not allowed!"
|
129
|
+
elsif key.start_with? ':'
|
130
|
+
raise ::Protocol::HTTP2::HeaderError, "Invalid pseudo-header #{key}!"
|
131
|
+
elsif key =~ /[A-Z]/
|
132
|
+
raise ::Protocol::HTTP2::HeaderError, "Invalid upper-case characters in header #{key}!"
|
95
133
|
else
|
96
|
-
|
97
|
-
|
98
|
-
@remainder = chunk.byteslice(maximum_size, chunk.bytesize - maximum_size)
|
134
|
+
@headers.add(key, value)
|
99
135
|
end
|
100
|
-
|
101
|
-
return true
|
102
136
|
end
|
103
137
|
|
104
|
-
def
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
#
|
138
|
+
def add_trailer(key, value)
|
139
|
+
if @trailers.include(key)
|
140
|
+
add_header(key, value)
|
141
|
+
else
|
142
|
+
raise ::Protocol::HTTP2::HeaderError, "Cannot add trailer #{key} as it was not specified in trailers!"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def receive_trailing_headers(headers, end_stream)
|
147
|
+
headers.each do |key, value|
|
148
|
+
add_trailer(key, value)
|
109
149
|
end
|
110
150
|
end
|
111
151
|
|
112
152
|
def receive_headers(frame)
|
113
|
-
headers
|
114
|
-
|
115
|
-
|
153
|
+
if @headers.nil?
|
154
|
+
@headers = ::Protocol::HTTP::Headers.new
|
155
|
+
self.receive_initial_headers(super, frame.end_stream?)
|
156
|
+
@trailers = @headers[TRAILERS]
|
157
|
+
elsif @trailers and frame.end_stream?
|
158
|
+
self.receive_trailing_headers(super, frame.end_stream?)
|
159
|
+
else
|
160
|
+
raise ::Protocol::HTTP2::HeaderError, "Unable to process headers!"
|
161
|
+
end
|
162
|
+
rescue ::Protocol::HTTP2::HeaderError => error
|
163
|
+
Async.logger.error(self, error)
|
116
164
|
|
117
|
-
|
165
|
+
send_reset_stream(error.code)
|
118
166
|
end
|
119
167
|
|
120
|
-
def
|
121
|
-
data =
|
168
|
+
def process_data(frame)
|
169
|
+
data = frame.unpack
|
122
170
|
|
123
|
-
if
|
124
|
-
|
171
|
+
if @input
|
172
|
+
unless data.empty?
|
173
|
+
@input.write(data)
|
174
|
+
end
|
175
|
+
|
176
|
+
if frame.end_stream?
|
177
|
+
@input.close
|
178
|
+
@input = nil
|
179
|
+
end
|
125
180
|
end
|
126
181
|
|
127
182
|
return data
|
183
|
+
rescue ::Protocol::HTTP2::ProtocolError
|
184
|
+
raise
|
185
|
+
rescue # Anything else...
|
186
|
+
send_reset_stream(::Protocol::HTTP2::Error::INTERNAL_ERROR)
|
128
187
|
end
|
129
188
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
if @body
|
134
|
-
@body.close(EOFError.new(error_code))
|
135
|
-
@body = nil
|
136
|
-
end
|
137
|
-
|
138
|
-
@delegate.receive_reset_stream(self, error_code)
|
139
|
-
|
140
|
-
return error_code
|
189
|
+
# Set the body and begin sending it.
|
190
|
+
def send_body(body)
|
191
|
+
@output = Buffer.new(self, body)
|
141
192
|
end
|
142
193
|
|
143
|
-
def
|
144
|
-
@delegate.close!
|
145
|
-
|
194
|
+
def window_updated(size)
|
146
195
|
super
|
196
|
+
|
197
|
+
@output&.window_updated(size)
|
147
198
|
end
|
148
199
|
|
149
200
|
def close(error = nil)
|
150
201
|
super
|
151
202
|
|
152
|
-
if @
|
153
|
-
@
|
154
|
-
@
|
203
|
+
if @input
|
204
|
+
@input.close(error)
|
205
|
+
@input = nil
|
155
206
|
end
|
156
207
|
|
157
|
-
@
|
208
|
+
if @output
|
209
|
+
@output.close(error)
|
210
|
+
@output = nil
|
211
|
+
end
|
158
212
|
end
|
159
213
|
end
|
160
214
|
end
|
data/lib/async/http/version.rb
CHANGED
data/tasks/h2spec.rake
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
namespace :h2spec do
|
3
|
+
task :build do
|
4
|
+
# Fetch the code:
|
5
|
+
sh "go get github.com/spf13/cobra"
|
6
|
+
sh "go get github.com/summerwind/h2spec"
|
7
|
+
|
8
|
+
# This builds `h2spec` into the current directory
|
9
|
+
sh "go build ~/go/src/github.com/summerwind/h2spec/cmd/h2spec/h2spec.go"
|
10
|
+
end
|
11
|
+
|
12
|
+
task :server do
|
13
|
+
require 'async/reactor'
|
14
|
+
require 'async/container'
|
15
|
+
require 'async/http/server'
|
16
|
+
require 'async/io/host_endpoint'
|
17
|
+
|
18
|
+
endpoint = Async::IO::Endpoint.tcp('127.0.0.1', 7272)
|
19
|
+
|
20
|
+
server = Async::HTTP::Server.for(endpoint, Async::HTTP::Protocol::HTTP2, "https") do |request|
|
21
|
+
Protocol::HTTP::Response[200, {'content-type' => 'text/plain'}, ["Hello World"]]
|
22
|
+
end
|
23
|
+
|
24
|
+
@container = Async::Container.new
|
25
|
+
|
26
|
+
Async.logger.info(self){"Starting server..."}
|
27
|
+
@container.run(count: 1) do
|
28
|
+
server.run
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
task :test => :server do
|
33
|
+
begin
|
34
|
+
if test = ENV['TEST']
|
35
|
+
sh("./h2spec", test, "-p", "7272")
|
36
|
+
else
|
37
|
+
sh("./h2spec", "-p", "7272")
|
38
|
+
end
|
39
|
+
ensure
|
40
|
+
@container.stop(false)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
task :all => [:build, :test]
|
45
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.45.
|
4
|
+
version: 0.45.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-06-
|
11
|
+
date: 2019-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.19'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
26
|
+
version: '1.19'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: async-io
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.
|
75
|
+
version: 0.7.0
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0.
|
82
|
+
version: 0.7.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: async-rspec
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -215,7 +215,6 @@ files:
|
|
215
215
|
- lib/async/http/protocol/http2.rb
|
216
216
|
- lib/async/http/protocol/http2/client.rb
|
217
217
|
- lib/async/http/protocol/http2/connection.rb
|
218
|
-
- lib/async/http/protocol/http2/promise.rb
|
219
218
|
- lib/async/http/protocol/http2/request.rb
|
220
219
|
- lib/async/http/protocol/http2/response.rb
|
221
220
|
- lib/async/http/protocol/http2/server.rb
|
@@ -228,6 +227,7 @@ files:
|
|
228
227
|
- lib/async/http/server.rb
|
229
228
|
- lib/async/http/statistics.rb
|
230
229
|
- lib/async/http/version.rb
|
230
|
+
- tasks/h2spec.rake
|
231
231
|
- tasks/server.rake
|
232
232
|
homepage: https://github.com/socketry/async-http
|
233
233
|
licenses: []
|
@@ -1,71 +0,0 @@
|
|
1
|
-
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
-
#
|
3
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
-
# of this software and associated documentation files (the "Software"), to deal
|
5
|
-
# in the Software without restriction, including without limitation the rights
|
6
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
-
# copies of the Software, and to permit persons to whom the Software is
|
8
|
-
# furnished to do so, subject to the following conditions:
|
9
|
-
#
|
10
|
-
# The above copyright notice and this permission notice shall be included in
|
11
|
-
# all copies or substantial portions of the Software.
|
12
|
-
#
|
13
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
-
# THE SOFTWARE.
|
20
|
-
|
21
|
-
require_relative '../response'
|
22
|
-
|
23
|
-
module Async
|
24
|
-
module HTTP
|
25
|
-
module Protocol
|
26
|
-
module HTTP2
|
27
|
-
class Promise < Response
|
28
|
-
def initialize(protocol, headers, stream_id)
|
29
|
-
super(protocol, stream_id)
|
30
|
-
|
31
|
-
@request = build_request(headers)
|
32
|
-
end
|
33
|
-
|
34
|
-
attr :stream
|
35
|
-
attr :request
|
36
|
-
|
37
|
-
private def build_request(headers)
|
38
|
-
request = ::Protocol::HTTP::Request.new
|
39
|
-
request.headers = ::Protocol::HTTP::Headers.new
|
40
|
-
|
41
|
-
headers.each do |key, value|
|
42
|
-
if key == SCHEME
|
43
|
-
return @stream.send_failure(400, "Request scheme already specified") if request.scheme
|
44
|
-
|
45
|
-
request.scheme = value
|
46
|
-
elsif key == AUTHORITY
|
47
|
-
return @stream.send_failure(400, "Request authority already specified") if request.authority
|
48
|
-
|
49
|
-
request.authority = value
|
50
|
-
elsif key == METHOD
|
51
|
-
return @stream.send_failure(400, "Request method already specified") if request.method
|
52
|
-
|
53
|
-
request.method = value
|
54
|
-
elsif key == PATH
|
55
|
-
return @stream.send_failure(400, "Request path already specified") if request.path
|
56
|
-
|
57
|
-
request.path = value
|
58
|
-
else
|
59
|
-
request.headers[key] = value
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
return request
|
64
|
-
end
|
65
|
-
|
66
|
-
undef send_request
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|