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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af620d4e3e885af7b094634cd875459b5613999af8b80464dc52fbc92108d7cb
4
- data.tar.gz: a56bea4149a58c74f2830d31f62ef308720a51887574aab81087ebfb095502f6
3
+ metadata.gz: f10ee1ffdafaf1d8b663c53dcb8d4be3d9462206024a6415214a081e2574bee3
4
+ data.tar.gz: ee48372568c7563d0e273447299f1701d4dde0872136614a42df34ce26bb830b
5
5
  SHA512:
6
- metadata.gz: 16bd6f0a463a8fe62e14054fc9918df817a7937f7a172b9c4d5266e588143db6c86ea3d34b83e89dbf77bfe3fc0be3e76cb1b0e0a60e73f6620a251e5768caca
7
- data.tar.gz: bdce788539eab5560fb489b7ff9d631218f5fdbaf0c0602d8b054b9bbc43cea6743897c73ff332abd8138fb5e41e711539f96308291d9da6885dc3b291d14cfb
6
+ metadata.gz: d2fbfbd18899d1d773795afd014e8b1508564c6dc670c7290fd6b9cecd109fd0b2a3612e304352c353435320fcf703e99477ff9ece909baa0734c1e33e0a3f97
7
+ data.tar.gz: da9ca8b00e9fea4866c9e1806b0c24611f52be3dab459c2df424c00640f63db894f9b5e98e70720b9f244ade1155cb9b96776ccc6812e10272b42d29f01b5439
data/.gitignore CHANGED
@@ -12,3 +12,4 @@
12
12
 
13
13
  .rspec_status
14
14
  .covered.db
15
+ /h2spec
data/.travis.yml CHANGED
@@ -29,3 +29,6 @@ matrix:
29
29
  - rvm: ruby-head
30
30
  - rvm: jruby-head
31
31
  - rvm: truffleruby
32
+
33
+ # after_success:
34
+ # - bundle exec rake h2spec:all
data/Gemfile CHANGED
@@ -3,6 +3,9 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in async-io.gemspec
4
4
  gemspec
5
5
 
6
+ # gem "protocol-http2", path: "../protocol-http2"
7
+ # gem "protocol-hpack", path: "../protocol-hpack"
8
+
6
9
  group :development do
7
10
  gem 'pry'
8
11
  end
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.18")
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.6.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 => 32,
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,
@@ -39,7 +39,7 @@ module Async
39
39
  end
40
40
 
41
41
  def create_response
42
- Response.new(self, self.next_stream_id)
42
+ Response::Stream.create(self, self.next_stream_id).response
43
43
  end
44
44
 
45
45
  # Used by the client to send requests to the remote server.
@@ -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
- @remote_settings.maximum_concurrent_streams
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 'connection'
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
- def initialize(connection, stream_id)
30
- super(nil, nil, nil, nil, VERSION, ::Protocol::HTTP::Headers.new)
30
+ class Stream < HTTP2::Stream
31
+ def initialize(*)
32
+ super
33
+
34
+ @enqueued = false
35
+ @request = Request.new(self)
36
+ end
31
37
 
32
- @input = nil
33
- @length = nil
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
- @connection = connection
36
- @stream = Stream.new(self, connection, stream_id)
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
- attr :stream
40
-
41
- def hijack?
42
- false
109
+ def initialize(stream)
110
+ super(nil, nil, nil, nil, VERSION, nil)
111
+
112
+ @stream = stream
43
113
  end
44
114
 
45
- def push?
46
- @connection.enable_push?
47
- end
115
+ attr :stream
48
116
 
49
- def create_push_promise_stream(headers)
50
- @connection.create_push_promise_stream do |stream_id|
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
- # Stream state transition into `:closed`.
58
- def close!
121
+ def hijack?
122
+ false
59
123
  end
60
124
 
61
- def stream_closed(error)
62
- if @input
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, @scheme],
135
+ [SCHEME, scheme],
72
136
  [METHOD, ::Protocol::HTTP::Methods::GET],
73
137
  [PATH, path],
74
- [AUTHORITY, @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
- def initialize(connection, stream_id)
29
- @input = nil
30
- @length = nil
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
- super(connection.version, nil, ::Protocol::HTTP::Headers.new)
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
- @connection = connection
35
- @stream = Stream.new(self, connection, stream_id)
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
- @notification = Async::Notification.new
38
- @exception = nil
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
- def promises
46
- @promises ||= Async::Queue.new
47
- end
122
+ attr :request
48
123
 
49
- def accept_push_promise_stream(promised_stream_id, headers)
50
- @connection.accept_push_promise_stream(promised_stream_id) do
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
- # Stream state transition into `:closed`.
58
- def close!
59
- self.promises.enqueue(nil)
128
+ def valid?
129
+ !!@status
60
130
  end
61
131
 
62
- # Notify anyone waiting on the response headers to be received (or failure).
63
- protected def notify!
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
- # Wait for the headers to be received or for stream reset.
75
- def wait
76
- # If you call wait after the headers were already received, it should return immediately.
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 == STATUS
90
- @status = Integer(value)
91
- elsif key == CONTENT_LENGTH
92
- @length = Integer(value)
93
- elsif key == PROTOCOL
94
- @protocol = value
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
- @headers[key] = value
161
+ request.headers[key] = value
97
162
  end
98
163
  end
99
164
 
100
- unless end_stream
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.new(self, stream_id)
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
- @requests.async do |task, request|
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
- def initialize(delegate, *args)
29
- super(*args)
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
- @delegate = delegate
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
- # This is the body that is being sent.
34
- @body = nil
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
- # The remainder of the current chunk being sent.
37
- @remainder = nil
72
+ def push(chunk)
73
+ @remainder = chunk
74
+ end
38
75
 
39
- # The task that is handling sending the body.
40
- @task = nil
41
- end
42
-
43
- attr_accessor :delegate
44
- attr :body
45
-
46
- def create_push_promise_stream(headers)
47
- @delegate.create_push_promise_stream(headers)
48
- end
49
-
50
- def accept_push_promise_stream(headers, stream_id)
51
- @delegate.accept_push_promise_stream(headers, stream_id)
52
- end
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
- if maximum_size == 0
69
- return false
92
+ def end_stream
93
+ @stream.send_data(nil, ::Protocol::HTTP2::END_STREAM)
70
94
  end
71
95
 
72
- if @remainder
73
- chunk = @remainder
74
- @remainder = nil
75
- elsif chunk = @body.read
76
- # There was a new chunk of data to send
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
- # @body.read above might take a while and a stream reset might be received in the mean time.
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
- return false if closed?
113
+ @headers = nil
114
+ @trailers = nil
92
115
 
93
- if chunk.bytesize <= maximum_size
94
- send_data(chunk, maximum_size: maximum_size)
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
- send_data(chunk.byteslice(0, maximum_size), maximum_size: maximum_size)
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 window_updated
105
- return unless @body
106
-
107
- while send_chunk
108
- # There could be more data to send...
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 = super
114
-
115
- @delegate.receive_headers(self, headers, frame.end_stream?)
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
- return headers
165
+ send_reset_stream(error.code)
118
166
  end
119
167
 
120
- def receive_data(frame)
121
- data = super
168
+ def process_data(frame)
169
+ data = frame.unpack
122
170
 
123
- if data
124
- @delegate.receive_data(self, data, frame.end_stream?)
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
- def receive_reset_stream(frame)
131
- error_code = super
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 close!
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 @body
153
- @body.close(error)
154
- @body = nil
203
+ if @input
204
+ @input.close(error)
205
+ @input = nil
155
206
  end
156
207
 
157
- @delegate.stream_closed(error)
208
+ if @output
209
+ @output.close(error)
210
+ @output = nil
211
+ end
158
212
  end
159
213
  end
160
214
  end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.45.4"
23
+ VERSION = "0.45.6"
24
24
  end
25
25
  end
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
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-13 00:00:00.000000000 Z
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.18'
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.18'
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.6.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.6.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