protocol-http2 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e2c3c6ed8ca4bc4aeb3e92d524a9e4882c5b4d57ccaaaabfbc162bd59d45f197
4
+ data.tar.gz: 11ffce80cc6471753a248724eca4be38eab3574ff45045d32237615590770781
5
+ SHA512:
6
+ metadata.gz: 7ff24f4b935c7db73d1d00852e2b82ff2ec74a0f1c9d952cacf81ab50f945cb179148195b86921870f4cb3d6139d6a3a18cbbe6b0f137593726f65aed38f8fd8
7
+ data.tar.gz: 92ab47f67f2f9fa3ededa811c71ebf70c2bddd0bad95adea72d0fdf4ba1951c8c37284389e85e595b4f0302989255b26384adeee6111625dcbdd92fe4f9e6359
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --warnings
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ dist: xenial
3
+ cache: bundler
4
+
5
+ matrix:
6
+ include:
7
+ - rvm: 2.4
8
+ - rvm: 2.5
9
+ - rvm: 2.6
10
+ - rvm: 2.6
11
+ env: COVERAGE=PartialSummary,Coveralls
12
+ - rvm: truffleruby
13
+ - rvm: jruby-head
14
+ env: JRUBY_OPTS="--debug -X+O"
15
+ - rvm: ruby-head
16
+ allow_failures:
17
+ - rvm: truffleruby
18
+ - rvm: ruby-head
19
+ - rvm: jruby-head
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in protocol-http2.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Protocol::HTTP2
2
+
3
+ Provides a low-level implementation of the HTTP/2 protocol.
4
+
5
+ [![Build Status](https://secure.travis-ci.com/socketry/protocol-http2.svg)](http://travis-ci.com/socketry/protocol-http2)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'protocol-http2'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install protocol-http2
22
+
23
+ ## Usage
24
+
25
+ Here is a basic HTTP/2 client:
26
+
27
+ ```ruby
28
+ require 'async'
29
+ require 'async/io/stream'
30
+ require 'async/http/url_endpoint'
31
+ require 'protocol/http2/client'
32
+ require 'pry'
33
+
34
+ Async.run do
35
+ endpoint = Async::HTTP::URLEndpoint.parse("https://www.google.com/search?q=kittens")
36
+
37
+ peer = endpoint.connect
38
+
39
+ puts "Connected to #{peer.inspect}"
40
+
41
+ # IO Buffering...
42
+ stream = Async::IO::Stream.new(peer)
43
+
44
+ framer = Protocol::HTTP2::Framer.new(stream)
45
+ client = Protocol::HTTP2::Client.new(framer)
46
+
47
+ puts "Sending connection preface..."
48
+ client.send_connection_preface
49
+
50
+ puts "Creating stream..."
51
+ stream = client.create_stream
52
+
53
+ headers = [
54
+ [":scheme", endpoint.scheme],
55
+ [":method", "GET"],
56
+ [":authority", "www.google.com"],
57
+ [":path", endpoint.path],
58
+ ["accept", "*/*"],
59
+ ]
60
+
61
+ puts "Sending request on stream id=#{stream.id} state=#{stream.state}..."
62
+ stream.send_headers(nil, headers, Protocol::HTTP2::END_STREAM)
63
+
64
+ puts "Waiting for response..."
65
+ $count = 0
66
+
67
+ def stream.process_headers(frame)
68
+ headers = super
69
+ puts "Got response headers: #{headers} (#{frame.end_stream?})"
70
+ end
71
+
72
+ def stream.receive_data(frame)
73
+ data = super
74
+
75
+ $count += data.scan(/kittens/).count
76
+
77
+ puts "Got response data: #{data.bytesize}"
78
+ end
79
+
80
+ until stream.closed?
81
+ frame = client.read_frame
82
+ end
83
+
84
+ puts "Got #{$count} kittens!"
85
+
86
+ binding.pry
87
+
88
+ puts "Closing client..."
89
+ client.close
90
+ end
91
+ ```
92
+
93
+ ## Contributing
94
+
95
+ 1. Fork it
96
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
97
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
98
+ 4. Push to the branch (`git push origin my-new-feature`)
99
+ 5. Create new Pull Request
100
+
101
+ ## License
102
+
103
+ Released under the MIT license.
104
+
105
+ Copyright, 2019, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
106
+
107
+ Permission is hereby granted, free of charge, to any person obtaining a copy
108
+ of this software and associated documentation files (the "Software"), to deal
109
+ in the Software without restriction, including without limitation the rights
110
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
111
+ copies of the Software, and to permit persons to whom the Software is
112
+ furnished to do so, subject to the following conditions:
113
+
114
+ The above copyright notice and this permission notice shall be included in
115
+ all copies or substantial portions of the Software.
116
+
117
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
118
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
119
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
120
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
121
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
122
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
123
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,68 @@
1
+
2
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
3
+
4
+ require 'async'
5
+ require 'async/io/stream'
6
+ require 'async/http/url_endpoint'
7
+ require 'protocol/http2/client'
8
+ require 'pry'
9
+
10
+ Async.run do
11
+ endpoint = Async::HTTP::URLEndpoint.parse("https://www.google.com/search?q=kittens")
12
+
13
+ peer = endpoint.connect
14
+
15
+ puts "Connected to #{peer.inspect}"
16
+
17
+ # IO Buffering...
18
+ stream = Async::IO::Stream.new(peer)
19
+
20
+ framer = Protocol::HTTP2::Framer.new(stream)
21
+ client = Protocol::HTTP2::Client.new(framer)
22
+
23
+ puts "Sending connection preface..."
24
+ client.send_connection_preface
25
+
26
+ puts "Creating stream..."
27
+ stream = client.create_stream
28
+
29
+ headers = [
30
+ [":scheme", endpoint.scheme],
31
+ [":method", "GET"],
32
+ [":authority", "www.google.com"],
33
+ [":path", endpoint.path],
34
+ ["accept", "*/*"],
35
+ ]
36
+
37
+ puts "Sending request on stream id=#{stream.id} state=#{stream.state}..."
38
+ stream.send_headers(nil, headers, Protocol::HTTP2::END_STREAM)
39
+
40
+ puts "Waiting for response..."
41
+ $count = 0
42
+
43
+ def stream.process_headers(frame)
44
+ headers = super
45
+ puts "Got response headers: #{headers} (#{frame.end_stream?})"
46
+ end
47
+
48
+ def stream.receive_data(frame)
49
+ data = super
50
+
51
+ $count += data.scan(/kittens/).count
52
+
53
+ puts "Got response data: #{data.bytesize}"
54
+ end
55
+
56
+ until stream.closed?
57
+ frame = client.read_frame
58
+ end
59
+
60
+ puts "Got #{$count} kittens!"
61
+
62
+ binding.pry
63
+
64
+ puts "Closing client..."
65
+ client.close
66
+ end
67
+
68
+ puts "Exiting."
@@ -0,0 +1,57 @@
1
+
2
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
3
+
4
+ require 'async'
5
+ require 'async/io/stream'
6
+ require 'async/http/url_endpoint'
7
+ require 'protocol/http2/client'
8
+ require 'pry'
9
+
10
+ queries = ["apple", "orange", "teapot", "async"]
11
+
12
+ Async.run do
13
+ endpoint = Async::HTTP::URLEndpoint.parse("https://www.google.com")
14
+
15
+ peer = endpoint.connect
16
+ stream = Async::IO::Stream.new(peer)
17
+ framer = Protocol::HTTP2::Framer.new(stream)
18
+ client = Protocol::HTTP2::Client.new(framer)
19
+
20
+ puts "Sending connection preface..."
21
+ client.send_connection_preface
22
+
23
+ puts "Creating stream..."
24
+ streams = queries.collect do |keyword|
25
+ client.create_stream.tap do |stream|
26
+ headers = [
27
+ [":scheme", endpoint.scheme],
28
+ [":method", "GET"],
29
+ [":authority", "www.google.com"],
30
+ [":path", "/search?q=#{keyword}"],
31
+ ["accept", "*/*"],
32
+ ]
33
+
34
+ puts "Sending request on stream id=#{stream.id} state=#{stream.state}..."
35
+ stream.send_headers(nil, headers, Protocol::HTTP2::END_STREAM)
36
+
37
+ def stream.process_headers(frame)
38
+ headers = super
39
+ puts "Stream #{self.id}: Got response headers: #{headers} (#{frame.end_stream?})"
40
+ end
41
+
42
+ def stream.receive_data(frame)
43
+ data = super
44
+ puts "Stream #{self.id}: Got response data: #{data.bytesize}"
45
+ end
46
+ end
47
+ end
48
+
49
+ until streams.all?{|stream| stream.closed?}
50
+ frame = client.read_frame
51
+ end
52
+
53
+ puts "Closing client..."
54
+ client.close
55
+ end
56
+
57
+ puts "Exiting."
@@ -0,0 +1,64 @@
1
+ # Copyright, 2018, 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 'connection'
22
+
23
+ module Protocol
24
+ module HTTP2
25
+ class Client < Connection
26
+ def initialize(framer)
27
+ super(framer, 1)
28
+ end
29
+
30
+ def send_connection_preface(settings = [])
31
+ if @state == :new
32
+ @framer.write_connection_preface
33
+
34
+ send_settings(settings)
35
+
36
+ yield if block_given?
37
+
38
+ read_frame do |frame|
39
+ raise ProtocolError, "First frame must be SettingsFrame, but got #{frame.class}" unless frame.is_a? SettingsFrame
40
+ end
41
+ else
42
+ raise ProtocolError, "Cannot send connection preface in state #{@state}"
43
+ end
44
+ end
45
+
46
+ def receive_push_promise(frame)
47
+ if frame.stream_id == 0
48
+ raise ProtocolError, "Cannot receive headers for stream 0!"
49
+ end
50
+
51
+ if stream = @streams[frame.stream_id]
52
+ # This is almost certainly invalid:
53
+ promised_stream, request_headers = stream.receive_push_promise(frame)
54
+
55
+ if stream.closed?
56
+ @streams.delete(stream.id)
57
+ end
58
+
59
+ return promised_stream, request_headers
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,336 @@
1
+ # Copyright, 2018, 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 'framer'
22
+ require_relative 'flow_control'
23
+
24
+ require 'protocol/hpack'
25
+
26
+ module Protocol
27
+ module HTTP2
28
+ class Connection
29
+ include FlowControl
30
+
31
+ def initialize(framer, local_stream_id)
32
+ @state = :new
33
+ @streams = {}
34
+
35
+ @framer = framer
36
+ @local_stream_id = local_stream_id
37
+ @remote_stream_id = 0
38
+
39
+ @local_settings = PendingSettings.new
40
+ @remote_settings = Settings.new
41
+
42
+ @decoder = HPACK::Context.new
43
+ @encoder = HPACK::Context.new
44
+
45
+ @local_window = Window.new(@local_settings.initial_window_size)
46
+ @remote_window = Window.new(@remote_settings.initial_window_size)
47
+ end
48
+
49
+ def id
50
+ 0
51
+ end
52
+
53
+ def maximum_frame_size
54
+ @remote_settings.maximum_frame_size
55
+ end
56
+
57
+ def maximum_concurrent_streams
58
+ [@local_settings.maximum_concurrent_streams, @remote_settings.maximum_concurrent_streams].min
59
+ end
60
+
61
+ attr :framer
62
+
63
+ # Connection state (:new, :open, :closed).
64
+ attr_accessor :state
65
+
66
+ # Current settings value for local and peer
67
+ attr_accessor :local_settings
68
+ attr_accessor :remote_settings
69
+
70
+ # Our window for receiving data. When we receive data, it reduces this window.
71
+ # If the window gets too small, we must send a window update.
72
+ attr :local_window
73
+
74
+ # Our window for sending data. When we send data, it reduces this window.
75
+ attr :remote_window
76
+
77
+ def closed?
78
+ @state == :closed
79
+ end
80
+
81
+ def close
82
+ send_goaway
83
+
84
+ @framer.close
85
+ end
86
+
87
+ def encode_headers(headers, buffer = String.new.b)
88
+ HPACK::Compressor.new(buffer, @encoder).encode(headers)
89
+
90
+ return buffer
91
+ end
92
+
93
+ def decode_headers(data)
94
+ HPACK::Decompressor.new(data, @decoder).decode
95
+ end
96
+
97
+ # Streams are identified with an unsigned 31-bit integer. Streams initiated by a client MUST use odd-numbered stream identifiers; those initiated by the server MUST use even-numbered stream identifiers. A stream identifier of zero (0x0) is used for connection control messages; the stream identifier of zero cannot be used to establish a new stream.
98
+ def next_stream_id
99
+ id = @local_stream_id
100
+
101
+ @local_stream_id += 2
102
+
103
+ return id
104
+ end
105
+
106
+ attr :streams
107
+
108
+ def read_frame
109
+ frame = @framer.read_frame(@local_settings.maximum_frame_size)
110
+ # puts "#{self.class} #{@state} read_frame: class=#{frame.class} flags=#{frame.flags} length=#{frame.length}"
111
+ # puts "Windows: local_window=#{@local_window.inspect}; remote_window=#{@remote_window.inspect}"
112
+
113
+ yield frame if block_given?
114
+
115
+ frame.apply(self)
116
+
117
+ return frame
118
+ rescue ProtocolError => error
119
+ send_goaway(error.code || PROTOCOL_ERROR, error.message)
120
+
121
+ raise
122
+ rescue HTTP::HPACK::CompressionError => error
123
+ send_goaway(COMPRESSION_ERROR, error.message)
124
+
125
+ raise
126
+ rescue
127
+ send_goaway(PROTOCOL_ERROR, $!.message)
128
+
129
+ raise
130
+ end
131
+
132
+ def send_settings(changes)
133
+ @local_settings.append(changes)
134
+
135
+ frame = SettingsFrame.new
136
+ frame.pack(changes)
137
+
138
+ write_frame(frame)
139
+ end
140
+
141
+ def send_goaway(error_code = 0, message = "")
142
+ frame = GoawayFrame.new
143
+ frame.pack @remote_stream_id, error_code, message
144
+
145
+ write_frame(frame)
146
+
147
+ @state = :closed
148
+ end
149
+
150
+ def receive_goaway(frame)
151
+ @state = :closed
152
+ end
153
+
154
+ def write_frame(frame)
155
+ # puts "#{self.class} #{@state} write_frame: class=#{frame.class} flags=#{frame.flags} length=#{frame.length}"
156
+ @framer.write_frame(frame)
157
+ end
158
+
159
+ def send_ping(data)
160
+ if @state != :closed
161
+ frame = PingFrame.new
162
+ frame.pack data
163
+
164
+ write_frame(frame)
165
+ else
166
+ raise ProtocolError, "Cannot send ping in state #{@state}"
167
+ end
168
+ end
169
+
170
+ def update_local_settings(changes)
171
+ capacity = @local_settings.initial_window_size
172
+
173
+ @streams.each_value do |stream|
174
+ stream.local_window.capacity = capacity
175
+ end
176
+ end
177
+
178
+ def update_remote_settings(changes)
179
+ capacity = @remote_settings.initial_window_size
180
+
181
+ @streams.each_value do |stream|
182
+ stream.remote_window.capacity = capacity
183
+ end
184
+ end
185
+
186
+ # In addition to changing the flow-control window for streams that are not yet active, a SETTINGS frame can alter the initial flow-control window size for streams with active flow-control windows (that is, streams in the "open" or "half-closed (remote)" state). When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream flow-control windows that it maintains by the difference between the new value and the old value.
187
+ #
188
+ # @return [Boolean] whether the frame was an acknowledgement
189
+ def process_settings(frame)
190
+ if frame.acknowledgement?
191
+ # The remote end has confirmed the settings have been received:
192
+ changes = @local_settings.acknowledge
193
+
194
+ update_local_settings(changes)
195
+
196
+ return true
197
+ else
198
+ # The remote end is updating the settings, we reply with acknowledgement:
199
+ reply = frame.acknowledge
200
+
201
+ write_frame(reply)
202
+
203
+ changes = frame.unpack
204
+ @remote_settings.update(changes)
205
+
206
+ update_remote_settings(changes)
207
+
208
+ return false
209
+ end
210
+ end
211
+
212
+ def open!
213
+ @state = :open
214
+
215
+ return self
216
+ end
217
+
218
+ def receive_settings(frame)
219
+ if @state == :new
220
+ # We transition to :open when we receive acknowledgement of first settings frame:
221
+ open! if process_settings(frame)
222
+ elsif @state != :closed
223
+ process_settings(frame)
224
+ else
225
+ raise ProtocolError, "Cannot receive settings in state #{@state}"
226
+ end
227
+ end
228
+
229
+ def receive_ping(frame)
230
+ if @state != :closed
231
+ unless frame.acknowledgement?
232
+ reply = frame.acknowledge
233
+
234
+ write_frame(reply)
235
+ end
236
+ else
237
+ raise ProtocolError, "Cannot receive ping in state #{@state}"
238
+ end
239
+ end
240
+
241
+ def receive_data(frame)
242
+ consume_local_window(frame)
243
+
244
+ if stream = @streams[frame.stream_id]
245
+ stream.receive_data(frame)
246
+
247
+ if stream.closed?
248
+ @streams.delete(stream.id)
249
+ end
250
+ else
251
+ raise ProtocolError, "Bad stream"
252
+ end
253
+ end
254
+
255
+ def create_stream(stream_id = next_stream_id)
256
+ Stream.new(self, stream_id)
257
+ end
258
+
259
+ def receive_headers(frame)
260
+ if frame.stream_id == 0
261
+ raise ProtocolError, "Cannot receive headers for stream 0!"
262
+ end
263
+
264
+ if stream = @streams[frame.stream_id]
265
+ stream.receive_headers(frame)
266
+
267
+ if stream.closed?
268
+ @streams.delete(stream.id)
269
+ end
270
+ elsif frame.stream_id > @remote_stream_id
271
+ if @streams.count < self.maximum_concurrent_streams
272
+ stream = create_stream(frame.stream_id)
273
+ stream.receive_headers(frame)
274
+
275
+ @remote_stream_id = stream.id
276
+ @streams[stream.id] = stream
277
+ else
278
+ raise ProtocolError, "Exceeded maximum concurrent streams"
279
+ end
280
+ end
281
+ end
282
+
283
+ def deleted_stream? frame
284
+ frame.stream_id <= @local_stream_id or frame.stream_id <= @remote_stream_id
285
+ end
286
+
287
+ def receive_priority(frame)
288
+ if stream = @streams[frame.stream_id]
289
+ stream.receive_priority(frame)
290
+ elsif deleted_stream? frame
291
+ # ignore
292
+ else
293
+ stream = create_stream(frame.stream_id)
294
+ stream.receive_priority(frame)
295
+
296
+ @streams[frame.stream_id] = stream
297
+ end
298
+ end
299
+
300
+ def receive_reset_stream(frame)
301
+ if stream = @streams[frame.stream_id]
302
+ stream.receive_reset_stream(frame)
303
+
304
+ @streams.delete(stream.id)
305
+ elsif deleted_stream? frame
306
+ # ignore
307
+ else
308
+ raise ProtocolError, "Bad stream"
309
+ end
310
+ end
311
+
312
+ def receive_window_update(frame)
313
+ if frame.connection?
314
+ super
315
+ elsif stream = @streams[frame.stream_id]
316
+ stream.receive_window_update(frame)
317
+ elsif deleted_stream? frame
318
+ # ignore
319
+ else
320
+ raise ProtocolError, "Cannot update window of non-existant stream: #{frame.stream_id}"
321
+ end
322
+ end
323
+
324
+ def window_updated
325
+ # This is very inefficient, but workable.
326
+ @streams.each_value do |stream|
327
+ stream.window_updated unless stream.closed?
328
+ end
329
+ end
330
+
331
+ def receive_frame(frame)
332
+ warn "Unhandled frame #{frame.inspect}"
333
+ end
334
+ end
335
+ end
336
+ end