protocol-http2 0.1.0

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