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