async-http 0.45.4 → 0.45.6

Sign up to get free protection for your applications and to get access to all the features.
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