plum 0.0.1
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 +11 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/Guardfile +7 -0
- data/LICENSE +21 -0
- data/README.md +14 -0
- data/Rakefile +12 -0
- data/bin/.gitkeep +0 -0
- data/examples/local_server.rb +206 -0
- data/examples/static_server.rb +157 -0
- data/lib/plum.rb +21 -0
- data/lib/plum/binary_string.rb +74 -0
- data/lib/plum/connection.rb +201 -0
- data/lib/plum/connection_utils.rb +38 -0
- data/lib/plum/errors.rb +35 -0
- data/lib/plum/event_emitter.rb +19 -0
- data/lib/plum/flow_control.rb +97 -0
- data/lib/plum/frame.rb +163 -0
- data/lib/plum/frame_factory.rb +53 -0
- data/lib/plum/frame_utils.rb +50 -0
- data/lib/plum/hpack/constants.rb +331 -0
- data/lib/plum/hpack/context.rb +55 -0
- data/lib/plum/hpack/decoder.rb +145 -0
- data/lib/plum/hpack/encoder.rb +105 -0
- data/lib/plum/hpack/huffman.rb +42 -0
- data/lib/plum/http_connection.rb +33 -0
- data/lib/plum/https_connection.rb +24 -0
- data/lib/plum/stream.rb +217 -0
- data/lib/plum/stream_utils.rb +58 -0
- data/lib/plum/version.rb +3 -0
- data/plum.gemspec +29 -0
- data/test/plum/connection/test_handle_frame.rb +70 -0
- data/test/plum/hpack/test_context.rb +63 -0
- data/test/plum/hpack/test_decoder.rb +291 -0
- data/test/plum/hpack/test_encoder.rb +49 -0
- data/test/plum/hpack/test_huffman.rb +36 -0
- data/test/plum/stream/test_handle_frame.rb +262 -0
- data/test/plum/test_binary_string.rb +64 -0
- data/test/plum/test_connection.rb +96 -0
- data/test/plum/test_connection_utils.rb +29 -0
- data/test/plum/test_error.rb +13 -0
- data/test/plum/test_flow_control.rb +167 -0
- data/test/plum/test_frame.rb +59 -0
- data/test/plum/test_frame_factory.rb +56 -0
- data/test/plum/test_frame_utils.rb +46 -0
- data/test/plum/test_https_connection.rb +37 -0
- data/test/plum/test_stream.rb +32 -0
- data/test/plum/test_stream_utils.rb +16 -0
- data/test/server.crt +19 -0
- data/test/server.csr +16 -0
- data/test/server.key +27 -0
- data/test/test_helper.rb +28 -0
- data/test/utils/assertions.rb +60 -0
- data/test/utils/server.rb +63 -0
- metadata +234 -0
data/lib/plum.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "socket"
|
3
|
+
require "plum/version"
|
4
|
+
require "plum/errors"
|
5
|
+
require "plum/binary_string"
|
6
|
+
require "plum/event_emitter"
|
7
|
+
require "plum/hpack/constants"
|
8
|
+
require "plum/hpack/huffman"
|
9
|
+
require "plum/hpack/context"
|
10
|
+
require "plum/hpack/decoder"
|
11
|
+
require "plum/hpack/encoder"
|
12
|
+
require "plum/frame_utils"
|
13
|
+
require "plum/frame_factory"
|
14
|
+
require "plum/frame"
|
15
|
+
require "plum/flow_control"
|
16
|
+
require "plum/connection_utils"
|
17
|
+
require "plum/connection"
|
18
|
+
require "plum/https_connection"
|
19
|
+
require "plum/http_connection"
|
20
|
+
require "plum/stream_utils"
|
21
|
+
require "plum/stream"
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Plum
|
2
|
+
module BinaryString
|
3
|
+
refine String do
|
4
|
+
# Reads a 8-bit unsigned integer.
|
5
|
+
# @param pos [Integer] The start position to read.
|
6
|
+
def uint8(pos = 0)
|
7
|
+
byteslice(pos, 1).unpack("C")[0]
|
8
|
+
end
|
9
|
+
|
10
|
+
# Reads a 16-bit unsigned integer.
|
11
|
+
# @param pos [Integer] The start position to read.
|
12
|
+
def uint16(pos = 0)
|
13
|
+
byteslice(pos, 2).unpack("n")[0]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Reads a 24-bit unsigned integer.
|
17
|
+
# @param pos [Integer] The start position to read.
|
18
|
+
def uint24(pos = 0)
|
19
|
+
(uint16(pos) << 8) | uint8(pos + 2)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Reads a 32-bit unsigned integer.
|
23
|
+
# @param pos [Integer] The start position to read.
|
24
|
+
def uint32(pos = 0)
|
25
|
+
byteslice(pos, 4).unpack("N")[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Appends a 8-bit unsigned integer to this string.
|
29
|
+
def push_uint8(val)
|
30
|
+
self << [val].pack("C")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Appends a 16-bit unsigned integer to this string.
|
34
|
+
def push_uint16(val)
|
35
|
+
self << [val].pack("n")
|
36
|
+
end
|
37
|
+
|
38
|
+
# Appends a 24-bit unsigned integer to this string.
|
39
|
+
def push_uint24(val)
|
40
|
+
push_uint16(val >> 8)
|
41
|
+
push_uint8(val & ((1 << 8) - 1))
|
42
|
+
end
|
43
|
+
|
44
|
+
# Appends a 32-bit unsigned integer to this string.
|
45
|
+
def push_uint32(val)
|
46
|
+
self << [val].pack("N")
|
47
|
+
end
|
48
|
+
|
49
|
+
alias push <<
|
50
|
+
|
51
|
+
# Takes from beginning and cut specified *octets* from this String.
|
52
|
+
# @param count [Integer] The amount.
|
53
|
+
def byteshift(count)
|
54
|
+
force_encoding(Encoding::BINARY)
|
55
|
+
slice!(0, count)
|
56
|
+
end
|
57
|
+
|
58
|
+
def each_byteslice(n, &blk)
|
59
|
+
if block_given?
|
60
|
+
pos = 0
|
61
|
+
while pos < self.bytesize
|
62
|
+
yield byteslice(pos, n)
|
63
|
+
pos += n
|
64
|
+
end
|
65
|
+
else
|
66
|
+
Enumerator.new do |y|
|
67
|
+
each_byteslice(n) {|ss| y << ss }
|
68
|
+
end
|
69
|
+
# I want to write `enum_for(__method__, n)`!
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
using Plum::BinaryString
|
2
|
+
|
3
|
+
module Plum
|
4
|
+
class Connection
|
5
|
+
include EventEmitter
|
6
|
+
include FlowControl
|
7
|
+
include ConnectionUtils
|
8
|
+
|
9
|
+
CLIENT_CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
10
|
+
|
11
|
+
DEFAULT_SETTINGS = {
|
12
|
+
header_table_size: 4096, # octets
|
13
|
+
enable_push: 1, # 1: enabled, 0: disabled
|
14
|
+
max_concurrent_streams: 1 << 30, # (1 << 31) / 2
|
15
|
+
initial_window_size: 65535, # octets; <= 2 ** 31 - 1
|
16
|
+
max_frame_size: 16384, # octets; <= 2 ** 24 - 1
|
17
|
+
max_header_list_size: (1 << 32) - 1 # Fixnum
|
18
|
+
}
|
19
|
+
|
20
|
+
attr_reader :hpack_encoder, :hpack_decoder
|
21
|
+
attr_reader :local_settings, :remote_settings
|
22
|
+
attr_reader :state, :streams, :io
|
23
|
+
|
24
|
+
def initialize(io, local_settings = {})
|
25
|
+
@io = io
|
26
|
+
@local_settings = Hash.new {|hash, key| DEFAULT_SETTINGS[key] }.merge!(local_settings)
|
27
|
+
@remote_settings = Hash.new {|hash, key| DEFAULT_SETTINGS[key] }
|
28
|
+
@buffer = "".force_encoding(Encoding::BINARY)
|
29
|
+
@streams = {}
|
30
|
+
@state = :negotiation
|
31
|
+
@hpack_decoder = HPACK::Decoder.new(@local_settings[:header_table_size])
|
32
|
+
@hpack_encoder = HPACK::Encoder.new(@remote_settings[:header_table_size])
|
33
|
+
initialize_flow_control(send: @remote_settings[:initial_window_size],
|
34
|
+
recv: @local_settings[:initial_window_size])
|
35
|
+
end
|
36
|
+
private :initialize
|
37
|
+
|
38
|
+
# Starts communication with the peer. It blocks until the io is closed, or reaches EOF.
|
39
|
+
def run
|
40
|
+
while !@io.closed? && !@io.eof?
|
41
|
+
receive @io.readpartial(1024)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Closes the io.
|
46
|
+
def close
|
47
|
+
# TODO: server MAY wait streams
|
48
|
+
@io.close
|
49
|
+
end
|
50
|
+
|
51
|
+
# Receives the specified data and process.
|
52
|
+
#
|
53
|
+
# @param new_data [String] The data received from the peer.
|
54
|
+
def receive(new_data)
|
55
|
+
return if new_data.empty?
|
56
|
+
@buffer << new_data
|
57
|
+
|
58
|
+
if @state == :negotiation
|
59
|
+
negotiate!
|
60
|
+
end
|
61
|
+
|
62
|
+
if @state != :negotiation
|
63
|
+
while frame = Frame.parse!(@buffer)
|
64
|
+
callback(:frame, frame)
|
65
|
+
receive_frame(frame)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
rescue ConnectionError => e
|
69
|
+
callback(:connection_error, e)
|
70
|
+
goaway(e.http2_error_type)
|
71
|
+
close
|
72
|
+
end
|
73
|
+
alias << receive
|
74
|
+
|
75
|
+
# Reserves a new stream to server push.
|
76
|
+
#
|
77
|
+
# @param args [Hash] The argument to pass to Stram.new.
|
78
|
+
def reserve_stream(**args)
|
79
|
+
next_id = ((@streams.keys.last / 2).to_i + 1) * 2
|
80
|
+
stream = new_stream(next_id, state: :reserved_local, **args)
|
81
|
+
stream
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
def send_immediately(frame)
|
86
|
+
callback(:send_frame, frame)
|
87
|
+
@io.write(frame.assemble)
|
88
|
+
end
|
89
|
+
|
90
|
+
def new_stream(stream_id, **args)
|
91
|
+
if @streams.size > 0 && @streams.keys.last >= stream_id
|
92
|
+
raise Plum::ConnectionError.new(:protocol_error)
|
93
|
+
end
|
94
|
+
|
95
|
+
stream = Stream.new(self, stream_id, **args)
|
96
|
+
callback(:stream, stream)
|
97
|
+
@streams[stream_id] = stream
|
98
|
+
stream
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_received_frame(frame)
|
102
|
+
case @state
|
103
|
+
when :waiting_settings
|
104
|
+
raise ConnectionError.new(:protocol_error) if frame.type != :settings
|
105
|
+
@state = :negotiated
|
106
|
+
callback(:negotiated)
|
107
|
+
when :waiting_continuation
|
108
|
+
if frame.type != :continuation || frame.stream_id != @continuation_id
|
109
|
+
raise Plum::ConnectionError.new(:protocol_error)
|
110
|
+
end
|
111
|
+
|
112
|
+
if frame.flags.include?(:end_headers)
|
113
|
+
@state = :open
|
114
|
+
@continuation_id = nil
|
115
|
+
end
|
116
|
+
else
|
117
|
+
if [:headers].include?(frame.type)
|
118
|
+
if !frame.flags.include?(:end_headers)
|
119
|
+
@state = :waiting_continuation
|
120
|
+
@continuation_id = frame.stream_id
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def receive_frame(frame)
|
127
|
+
validate_received_frame(frame)
|
128
|
+
consume_recv_window(frame)
|
129
|
+
|
130
|
+
if frame.stream_id == 0
|
131
|
+
receive_control_frame(frame)
|
132
|
+
else
|
133
|
+
if @streams.key?(frame.stream_id)
|
134
|
+
stream = @streams[frame.stream_id]
|
135
|
+
else
|
136
|
+
if frame.stream_id.even? # stream started by client must have odd ID
|
137
|
+
raise Plum::ConnectionError.new(:protocol_error)
|
138
|
+
end
|
139
|
+
stream = new_stream(frame.stream_id)
|
140
|
+
end
|
141
|
+
stream.receive_frame(frame)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def receive_control_frame(frame)
|
146
|
+
if frame.length > @local_settings[:max_frame_size]
|
147
|
+
raise ConnectionError.new(:frame_size_error)
|
148
|
+
end
|
149
|
+
|
150
|
+
case frame.type
|
151
|
+
when :settings
|
152
|
+
receive_settings(frame)
|
153
|
+
when :window_update
|
154
|
+
receive_window_update(frame)
|
155
|
+
when :ping
|
156
|
+
receive_ping(frame)
|
157
|
+
when :goaway
|
158
|
+
goaway
|
159
|
+
close
|
160
|
+
when :data, :headers, :priority, :rst_stream, :push_promise, :continuation
|
161
|
+
raise Plum::ConnectionError.new(:protocol_error)
|
162
|
+
else
|
163
|
+
# MUST ignore unknown frame type.
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def receive_settings(frame)
|
168
|
+
if frame.flags.include?(:ack)
|
169
|
+
raise ConnectionError.new(:frame_size_error) if frame.length != 0
|
170
|
+
return
|
171
|
+
end
|
172
|
+
|
173
|
+
raise ConnectionError.new(:frame_size_error) if frame.length % 6 != 0
|
174
|
+
|
175
|
+
old_remote_settings = @remote_settings.dup
|
176
|
+
@remote_settings.merge!(frame.parse_settings)
|
177
|
+
apply_remote_settings(old_remote_settings)
|
178
|
+
|
179
|
+
callback(:remote_settings, @remote_settings, old_remote_settings)
|
180
|
+
|
181
|
+
send_immediately Frame.settings(:ack)
|
182
|
+
end
|
183
|
+
|
184
|
+
def apply_remote_settings(old_remote_settings)
|
185
|
+
@hpack_encoder.limit = @remote_settings[:header_table_size]
|
186
|
+
update_send_initial_window_size(@remote_settings[:initial_window_size] - old_remote_settings[:initial_window_size])
|
187
|
+
end
|
188
|
+
|
189
|
+
def receive_ping(frame)
|
190
|
+
raise Plum::ConnectionError.new(:frame_size_error) if frame.length != 8
|
191
|
+
|
192
|
+
if frame.flags.include?(:ack)
|
193
|
+
on(:ping_ack)
|
194
|
+
else
|
195
|
+
on(:ping)
|
196
|
+
opaque_data = frame.payload
|
197
|
+
send_immediately Frame.ping(:ack, opaque_data)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
using Plum::BinaryString
|
2
|
+
|
3
|
+
module Plum
|
4
|
+
module ConnectionUtils
|
5
|
+
# Sends local settings to the peer.
|
6
|
+
#
|
7
|
+
# @param kwargs [Hash<Symbol, Integer>]
|
8
|
+
def settings(**kwargs)
|
9
|
+
send_immediately Frame.settings(**kwargs)
|
10
|
+
update_local_settings(kwargs)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Sends a PING frame to the peer.
|
14
|
+
#
|
15
|
+
# @param data [String] Must be 8 octets.
|
16
|
+
# @raise [ArgumentError] If the data is not 8 octets.
|
17
|
+
def ping(data = "plum\x00\x00\x00\x00")
|
18
|
+
send_immediately Frame.ping(data)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Sends GOAWAY frame to the peer and closes the connection.
|
22
|
+
#
|
23
|
+
# @param error_type [Symbol] The error type to be contained in the GOAWAY frame.
|
24
|
+
def goaway(error_type = :no_error)
|
25
|
+
last_id = @streams.keys.max || 0
|
26
|
+
send_immediately Frame.goaway(last_id, error_type)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def update_local_settings(new_settings)
|
31
|
+
old_settings = @local_settings.dup
|
32
|
+
@local_settings.merge!(new_settings)
|
33
|
+
|
34
|
+
@hpack_decoder.limit = @local_settings[:header_table_size]
|
35
|
+
update_recv_initial_window_size(@local_settings[:initial_window_size] - old_settings[:initial_window_size])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/plum/errors.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Plum
|
2
|
+
class Error < StandardError; end
|
3
|
+
class HPACKError < Error; end
|
4
|
+
class HTTPError < Error
|
5
|
+
ERROR_CODES = {
|
6
|
+
no_error: 0x00,
|
7
|
+
protocol_error: 0x01,
|
8
|
+
internal_error: 0x02,
|
9
|
+
flow_control_error: 0x03,
|
10
|
+
settings_timeout: 0x04,
|
11
|
+
stream_closed: 0x05,
|
12
|
+
frame_size_error: 0x06,
|
13
|
+
refused_stream: 0x07,
|
14
|
+
cancel: 0x08,
|
15
|
+
compression_error: 0x09,
|
16
|
+
connect_error: 0x0a,
|
17
|
+
enhance_your_calm: 0x0b,
|
18
|
+
inadequate_security: 0x0c,
|
19
|
+
http_1_1_required: 0x0d
|
20
|
+
}
|
21
|
+
|
22
|
+
attr_reader :http2_error_type
|
23
|
+
|
24
|
+
def initialize(type, message = nil)
|
25
|
+
@http2_error_type = type
|
26
|
+
super(message)
|
27
|
+
end
|
28
|
+
|
29
|
+
def http2_error_code
|
30
|
+
ERROR_CODES[@http2_error_type]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
class ConnectionError < HTTPError; end
|
34
|
+
class StreamError < HTTPError; end
|
35
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Plum
|
2
|
+
module EventEmitter
|
3
|
+
# Registers an event handler to specified event. An event can have multiple handlers.
|
4
|
+
# @param name [String] The name of event.
|
5
|
+
# @yield Gives event-specific parameters.
|
6
|
+
def on(name, &blk)
|
7
|
+
callbacks[name] << blk
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def callback(name, *args)
|
12
|
+
callbacks[name].each {|cb| cb.call(*args) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def callbacks
|
16
|
+
@callbacks ||= Hash.new {|hash, key| hash[key] = [] }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
using Plum::BinaryString
|
2
|
+
|
3
|
+
module Plum
|
4
|
+
module FlowControl
|
5
|
+
attr_reader :send_remaining_window, :recv_remaining_window
|
6
|
+
|
7
|
+
# Sends frame respecting inner-stream flow control.
|
8
|
+
#
|
9
|
+
# @param frame [Frame] The frame to be sent.
|
10
|
+
def send(frame)
|
11
|
+
case frame.type
|
12
|
+
when :data
|
13
|
+
@send_buffer << frame
|
14
|
+
callback(:send_deferred, frame) if @send_remaining_window < frame.length
|
15
|
+
consume_send_buffer
|
16
|
+
else
|
17
|
+
send_immediately frame
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Increases receiving window size. Sends WINDOW_UPDATE frame to the peer.
|
22
|
+
#
|
23
|
+
# @param wsi [Integer] The amount to increase receiving window size. The legal range is 1 to 2^32-1.
|
24
|
+
def window_update(wsi)
|
25
|
+
@recv_remaining_window += wsi
|
26
|
+
payload = "".push_uint32(wsi & ~(1 << 31))
|
27
|
+
sid = (Stream === self) ? self.id : 0
|
28
|
+
send_immediately Frame.new(type: :window_update, stream_id: sid, payload: payload)
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
def update_send_initial_window_size(diff)
|
33
|
+
@send_remaining_window += diff
|
34
|
+
consume_send_buffer
|
35
|
+
|
36
|
+
if Connection === self
|
37
|
+
@streams.values.each do |stream|
|
38
|
+
stream.update_send_initial_window_size(diff)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_recv_initial_window_size(diff)
|
44
|
+
@recv_remaining_window += diff
|
45
|
+
if Connection === self
|
46
|
+
@streams.values.each do |stream|
|
47
|
+
stream.update_recv_initial_window_size(diff)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def initialize_flow_control(send:, recv:)
|
54
|
+
@send_buffer = []
|
55
|
+
@send_remaining_window = send
|
56
|
+
@recv_remaining_window = recv
|
57
|
+
end
|
58
|
+
|
59
|
+
def consume_recv_window(frame)
|
60
|
+
case frame.type
|
61
|
+
when :data
|
62
|
+
@recv_remaining_window -= frame.length
|
63
|
+
if @recv_remaining_window < 0
|
64
|
+
local_error = (Connection === self) ? ConnectionError : StreamError
|
65
|
+
raise local_error.new(:flow_control_error)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def consume_send_buffer
|
71
|
+
while frame = @send_buffer.first
|
72
|
+
break if frame.length > @send_remaining_window
|
73
|
+
@send_buffer.shift
|
74
|
+
@send_remaining_window -= frame.length
|
75
|
+
send_immediately frame
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def receive_window_update(frame)
|
80
|
+
if frame.length != 4
|
81
|
+
raise Plum::ConnectionError.new(:frame_size_error)
|
82
|
+
end
|
83
|
+
|
84
|
+
r_wsi = frame.payload.uint32
|
85
|
+
r = r_wsi >> 31
|
86
|
+
wsi = r_wsi & ~(1 << 31)
|
87
|
+
|
88
|
+
if wsi == 0
|
89
|
+
local_error = (Connection === self) ? ConnectionError : StreamError
|
90
|
+
raise local_error.new(:protocol_error)
|
91
|
+
end
|
92
|
+
|
93
|
+
@send_remaining_window += wsi
|
94
|
+
consume_send_buffer
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|