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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +19 -0
- data/Gemfile +4 -0
- data/README.md +123 -0
- data/Rakefile +6 -0
- data/examples/http2/request.rb +68 -0
- data/examples/http2/requests.rb +57 -0
- data/lib/protocol/http2/client.rb +64 -0
- data/lib/protocol/http2/connection.rb +336 -0
- data/lib/protocol/http2/continuation_frame.rb +98 -0
- data/lib/protocol/http2/data_frame.rb +62 -0
- data/lib/protocol/http2/error.rb +36 -0
- data/lib/protocol/http2/flow_control.rb +84 -0
- data/lib/protocol/http2/frame.rb +193 -0
- data/lib/protocol/http2/framer.rb +113 -0
- data/lib/protocol/http2/goaway_frame.rb +60 -0
- data/lib/protocol/http2/headers_frame.rb +83 -0
- data/lib/protocol/http2/padded.rb +79 -0
- data/lib/protocol/http2/ping_frame.rb +80 -0
- data/lib/protocol/http2/priority_frame.rb +80 -0
- data/lib/protocol/http2/push_promise_frame.rb +62 -0
- data/lib/protocol/http2/reset_stream_frame.rb +72 -0
- data/lib/protocol/http2/server.rb +53 -0
- data/lib/protocol/http2/settings_frame.rb +237 -0
- data/lib/protocol/http2/stream.rb +372 -0
- data/lib/protocol/http2/version.rb +25 -0
- data/lib/protocol/http2/window_update_frame.rb +104 -0
- data/lib/protocol/http2.rb +22 -0
- data/protocol-http2.gemspec +28 -0
- metadata +157 -0
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
data/.rspec
ADDED
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
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
|
+
[](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,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
|