plum 0.2.1 → 0.2.2
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 +4 -4
- data/examples/client/twitter.rb +2 -1
- data/lib/plum/binary_string.rb +14 -0
- data/lib/plum/client/connection.rb +1 -1
- data/lib/plum/connection.rb +14 -7
- data/lib/plum/connection_utils.rb +1 -1
- data/lib/plum/errors.rb +8 -5
- data/lib/plum/frame_factory.rb +20 -12
- data/lib/plum/frame_utils.rb +21 -26
- data/lib/plum/hpack/constants.rb +2 -0
- data/lib/plum/hpack/context.rb +4 -4
- data/lib/plum/rack/cli.rb +17 -2
- data/lib/plum/rack/config.rb +2 -1
- data/lib/plum/rack/dsl.rb +10 -0
- data/lib/plum/rack/listener.rb +11 -5
- data/lib/plum/rack/server.rb +39 -11
- data/lib/plum/rack/session.rb +55 -31
- data/lib/plum/server/connection.rb +1 -1
- data/lib/plum/server/http_connection.rb +26 -24
- data/lib/plum/stream.rb +0 -1
- data/lib/plum/stream_utils.rb +8 -14
- data/lib/plum/version.rb +1 -1
- data/lib/rack/handler/plum.rb +2 -0
- data/test/plum/client/test_upgrade_client_session.rb +2 -2
- data/test/plum/test_binary_string.rb +6 -0
- data/test/plum/test_connection.rb +11 -0
- data/test/plum/test_connection_utils.rb +1 -1
- data/test/plum/test_flow_control.rb +6 -6
- data/test/plum/test_frame_factory.rb +3 -3
- data/test/plum/test_frame_utils.rb +7 -8
- data/test/plum/test_stream.rb +2 -2
- data/test/utils/server.rb +4 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3671dbbd1de29adc6a254e5c95cef0a20cce6970
|
4
|
+
data.tar.gz: efd31623c389dbefb5d8d46de2f4f1233d72470e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8dd0b9146d5261854136d69c4bcbbe471f31daeb1785b3219779c5ad8daa76bfab82763a383e308b2c17f2fe6ec127a7c8b293ab59a33c22bb7123ecd9ccaab
|
7
|
+
data.tar.gz: 035f11f0270f7fb7c7c95cae95993edcf4e924d6f10643928a53f5bcbee8501b13f66b537eb2875607abf2b87f9c0fd3fd16de1241c525d91742505f2b6909f8
|
data/examples/client/twitter.rb
CHANGED
@@ -17,7 +17,7 @@ rest = Plum::Client.start("api.twitter.com", 443)
|
|
17
17
|
Plum::Client.start("userstream.twitter.com", 443) { |streaming|
|
18
18
|
streaming.get("/1.1/user.json",
|
19
19
|
headers: { "authorization" => SimpleOAuth::Header.new(:get, "https://userstream.twitter.com/1.1/user.json", {}, credentials).to_s,
|
20
|
-
"accept-encoding" => "
|
20
|
+
"accept-encoding" => "gzip, deflate" }) { |res|
|
21
21
|
if res.status != "200"
|
22
22
|
puts "failed userstream"
|
23
23
|
exit
|
@@ -25,6 +25,7 @@ Plum::Client.start("userstream.twitter.com", 443) { |streaming|
|
|
25
25
|
|
26
26
|
buf = String.new
|
27
27
|
res.on_chunk { |chunk| # when received DATA frame
|
28
|
+
next if chunk.empty?
|
28
29
|
buf << chunk
|
29
30
|
*msgs, buf = buf.split("\r\n", -1)
|
30
31
|
|
data/lib/plum/binary_string.rb
CHANGED
@@ -70,6 +70,20 @@ module Plum
|
|
70
70
|
# I want to write `enum_for(__method__, n)`!
|
71
71
|
end
|
72
72
|
end
|
73
|
+
|
74
|
+
# Splits this String into chunks.
|
75
|
+
# @param n [Integer] max chunk bytesize
|
76
|
+
# @return [Array<String>] the slices
|
77
|
+
def chunk(n)
|
78
|
+
res = []
|
79
|
+
pos = 0
|
80
|
+
lim = bytesize
|
81
|
+
while pos < lim
|
82
|
+
res << byteslice(pos, n)
|
83
|
+
pos += n
|
84
|
+
end
|
85
|
+
res
|
86
|
+
end
|
73
87
|
end
|
74
88
|
end
|
75
89
|
end
|
data/lib/plum/connection.rb
CHANGED
@@ -33,7 +33,7 @@ module Plum
|
|
33
33
|
@hpack_encoder = HPACK::Encoder.new(@remote_settings[:header_table_size])
|
34
34
|
initialize_flow_control(send: @remote_settings[:initial_window_size],
|
35
35
|
recv: @local_settings[:initial_window_size])
|
36
|
-
@
|
36
|
+
@max_stream_ids = [0, -1] # [even, odd]
|
37
37
|
end
|
38
38
|
|
39
39
|
# Emits :close event. Doesn't actually close socket.
|
@@ -61,16 +61,16 @@ module Plum
|
|
61
61
|
# Returns a Stream object with the specified ID.
|
62
62
|
# @param stream_id [Integer] the stream id
|
63
63
|
# @return [Stream] the stream
|
64
|
-
def stream(stream_id)
|
64
|
+
def stream(stream_id, update_max_id = true)
|
65
65
|
raise ArgumentError, "stream_id can't be 0" if stream_id == 0
|
66
66
|
|
67
67
|
stream = @streams[stream_id]
|
68
68
|
if stream
|
69
|
-
if stream.state == :idle &&
|
69
|
+
if stream.state == :idle && stream_id < @max_stream_ids[stream_id % 2]
|
70
70
|
stream.set_state(:closed_implicitly)
|
71
71
|
end
|
72
|
-
elsif stream_id > @
|
73
|
-
@
|
72
|
+
elsif stream_id > @max_stream_ids[stream_id % 2]
|
73
|
+
@max_stream_ids[stream_id % 2] = stream_id if update_max_id
|
74
74
|
stream = Stream.new(self, stream_id, state: :idle)
|
75
75
|
callback(:stream, stream)
|
76
76
|
@streams[stream_id] = stream
|
@@ -92,7 +92,14 @@ module Plum
|
|
92
92
|
|
93
93
|
def send_immediately(frame)
|
94
94
|
callback(:send_frame, frame)
|
95
|
-
|
95
|
+
|
96
|
+
if frame.length <= @remote_settings[:max_frame_size]
|
97
|
+
@writer.call(frame.assemble)
|
98
|
+
else
|
99
|
+
frame.split(@remote_settings[:max_frame_size]) { |splitted|
|
100
|
+
@writer.call(splitted.assemble)
|
101
|
+
}
|
102
|
+
end
|
96
103
|
end
|
97
104
|
|
98
105
|
def validate_received_frame(frame)
|
@@ -124,7 +131,7 @@ module Plum
|
|
124
131
|
if frame.stream_id == 0
|
125
132
|
receive_control_frame(frame)
|
126
133
|
else
|
127
|
-
stream(frame.stream_id).receive_frame(frame)
|
134
|
+
stream(frame.stream_id, frame.type == :headers).receive_frame(frame)
|
128
135
|
end
|
129
136
|
end
|
130
137
|
|
@@ -20,7 +20,7 @@ module Plum
|
|
20
20
|
# Sends GOAWAY frame to the peer and closes the connection.
|
21
21
|
# @param error_type [Symbol] The error type to be contained in the GOAWAY frame.
|
22
22
|
def goaway(error_type = :no_error)
|
23
|
-
last_id = @
|
23
|
+
last_id = @max_stream_ids.max
|
24
24
|
send_immediately Frame.goaway(last_id, error_type)
|
25
25
|
end
|
26
26
|
|
data/lib/plum/errors.rb
CHANGED
@@ -30,6 +30,10 @@ module Plum
|
|
30
30
|
def http2_error_code
|
31
31
|
ERROR_CODES[@http2_error_type]
|
32
32
|
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"#{@http2_error_type.to_s.upcase}: #{super}"
|
36
|
+
end
|
33
37
|
end
|
34
38
|
|
35
39
|
class RemoteHTTPError < HTTPError; end
|
@@ -40,12 +44,11 @@ module Plum
|
|
40
44
|
class LocalStreamError < LocalHTTPError; end
|
41
45
|
|
42
46
|
class LegacyHTTPError < Error
|
43
|
-
attr_reader :
|
47
|
+
attr_reader :buf
|
44
48
|
|
45
|
-
def initialize(
|
46
|
-
|
47
|
-
@
|
48
|
-
@parser = parser
|
49
|
+
def initialize(message, buf = nil)
|
50
|
+
super(message)
|
51
|
+
@buf = buf
|
49
52
|
end
|
50
53
|
end
|
51
54
|
|
data/lib/plum/frame_factory.rb
CHANGED
@@ -54,37 +54,45 @@ module Plum
|
|
54
54
|
# Creates a DATA frame.
|
55
55
|
# @param stream_id [Integer] The stream ID.
|
56
56
|
# @param payload [String] Payload.
|
57
|
-
# @param
|
58
|
-
def data(stream_id, payload,
|
57
|
+
# @param end_stream [Boolean] add END_STREAM flag
|
58
|
+
def data(stream_id, payload, end_stream: false)
|
59
59
|
payload = payload.b if payload && payload.encoding != Encoding::BINARY
|
60
|
-
|
60
|
+
fval = 0
|
61
|
+
fval += 1 if end_stream
|
62
|
+
Frame.new(type_value: 0, stream_id: stream_id, flags_value: fval, payload: payload)
|
61
63
|
end
|
62
64
|
|
63
65
|
# Creates a HEADERS frame.
|
64
66
|
# @param stream_id [Integer] The stream ID.
|
65
67
|
# @param encoded [String] Headers.
|
66
|
-
# @param
|
67
|
-
|
68
|
-
|
68
|
+
# @param end_stream [Boolean] add END_STREAM flag
|
69
|
+
# @param end_headers [Boolean] add END_HEADERS flag
|
70
|
+
def headers(stream_id, encoded, end_stream: false, end_headers: false)
|
71
|
+
fval = 0
|
72
|
+
fval += 1 if end_stream
|
73
|
+
fval += 4 if end_headers
|
74
|
+
Frame.new(type_value: 1, stream_id: stream_id, flags_value: fval, payload: encoded)
|
69
75
|
end
|
70
76
|
|
71
77
|
# Creates a PUSH_PROMISE frame.
|
72
78
|
# @param stream_id [Integer] The stream ID.
|
73
79
|
# @param new_id [Integer] The stream ID to create.
|
74
80
|
# @param encoded [String] Request headers.
|
75
|
-
# @param
|
76
|
-
def push_promise(stream_id, new_id, encoded,
|
81
|
+
# @param end_headers [Boolean] add END_HEADERS flag
|
82
|
+
def push_promise(stream_id, new_id, encoded, end_headers: false)
|
77
83
|
payload = String.new.push_uint32(new_id)
|
78
84
|
.push(encoded)
|
79
|
-
|
85
|
+
fval = 0
|
86
|
+
fval += 4 if end_headers
|
87
|
+
Frame.new(type: :push_promise, stream_id: stream_id, flags_value: fval, payload: payload)
|
80
88
|
end
|
81
89
|
|
82
90
|
# Creates a CONTINUATION frame.
|
83
91
|
# @param stream_id [Integer] The stream ID.
|
84
92
|
# @param payload [String] Payload.
|
85
|
-
# @param
|
86
|
-
def continuation(stream_id, payload,
|
87
|
-
Frame.new(type: :continuation, stream_id: stream_id,
|
93
|
+
# @param end_headers [Boolean] add END_HEADERS flag
|
94
|
+
def continuation(stream_id, payload, end_headers: false)
|
95
|
+
Frame.new(type: :continuation, stream_id: stream_id, flags_value: (end_headers && 4 || 0), payload: payload)
|
88
96
|
end
|
89
97
|
end
|
90
98
|
end
|
data/lib/plum/frame_utils.rb
CHANGED
@@ -3,33 +3,28 @@ using Plum::BinaryString
|
|
3
3
|
|
4
4
|
module Plum
|
5
5
|
module FrameUtils
|
6
|
-
# Splits
|
6
|
+
# Splits this frame into multiple frames not to exceed MAX_FRAME_SIZE.
|
7
7
|
# @param max [Integer] The maximum size of a frame payload.
|
8
|
-
# @
|
9
|
-
def
|
10
|
-
return
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
frames = fragments.map {|fragment| Frame.new(type: :continuation, flags: [], stream_id: self.stream_id, payload: fragment) }
|
29
|
-
frames.first.type_value = self.type_value
|
30
|
-
frames.first.flags = self.flags - [:end_headers]
|
31
|
-
frames.last.flags = self.flags & [:end_headers]
|
32
|
-
frames
|
8
|
+
# @yield [Frame] The splitted frames.
|
9
|
+
def split(max)
|
10
|
+
return yield self if @length <= max
|
11
|
+
first, *mid, last = @payload.chunk(max)
|
12
|
+
case type
|
13
|
+
when :data
|
14
|
+
yield Frame.new(type_value: 0, stream_id: @stream_id, payload: first, flags_value: @flags_value & ~1)
|
15
|
+
mid.each { |slice|
|
16
|
+
yield Frame.new(type_value: 0, stream_id: @stream_id, payload: slice, flags_value: 0)
|
17
|
+
}
|
18
|
+
yield Frame.new(type_value: 0, stream_id: @stream_id, payload: last, flags_value: @flags_value & 1)
|
19
|
+
when :headers, :push_promise
|
20
|
+
yield Frame.new(type_value: @type_value, stream_id: @stream_id, payload: first, flags_value: @flags_value & ~4)
|
21
|
+
mid.each { |slice|
|
22
|
+
yield Frame.new(type: :continuation, stream_id: @stream_id, payload: slice, flags_value: 0)
|
23
|
+
}
|
24
|
+
yield Frame.new(type: :continuation, stream_id: @stream_id, payload: last, flags_value: @flags_value & 4)
|
25
|
+
else
|
26
|
+
raise NotImplementedError.new("frame split of frame with type #{type} is not supported")
|
27
|
+
end
|
33
28
|
end
|
34
29
|
|
35
30
|
# Parses SETTINGS frame payload. Ignores unknown settings type (see RFC7540 6.5.2).
|
data/lib/plum/hpack/constants.rb
CHANGED
data/lib/plum/hpack/context.rb
CHANGED
@@ -26,10 +26,10 @@ module Plum
|
|
26
26
|
def fetch(index)
|
27
27
|
if index == 0
|
28
28
|
raise HPACKError.new("index can't be 0")
|
29
|
-
elsif index <=
|
30
|
-
|
29
|
+
elsif index <= STATIC_TABLE_SIZE
|
30
|
+
STATIC_TABLE[index - 1]
|
31
31
|
elsif index <= STATIC_TABLE.size + @dynamic_table.size
|
32
|
-
@dynamic_table[index -
|
32
|
+
@dynamic_table[index - STATIC_TABLE_SIZE - 1]
|
33
33
|
else
|
34
34
|
raise HPACKError.new("invalid index: #{index}")
|
35
35
|
end
|
@@ -43,7 +43,7 @@ module Plum
|
|
43
43
|
si = STATIC_TABLE.index &pr
|
44
44
|
return si + 1 if si
|
45
45
|
di = @dynamic_table.index &pr
|
46
|
-
return di +
|
46
|
+
return di + STATIC_TABLE_SIZE + 1 if di
|
47
47
|
end
|
48
48
|
|
49
49
|
def evict
|
data/lib/plum/rack/cli.rb
CHANGED
@@ -44,6 +44,13 @@ module Plum
|
|
44
44
|
ENV["RACK_ENV"] = @options[:env] if @options[:env]
|
45
45
|
config[:debug] = @options[:debug] unless @options[:debug].nil?
|
46
46
|
config[:server_push] = @options[:server_push] unless @options[:server_push].nil?
|
47
|
+
config[:threaded] = @options[:threaded] unless @options[:threaded].nil?
|
48
|
+
|
49
|
+
if @options[:fallback_legacy]
|
50
|
+
h, p = @options[:fallback_legacy].split(":")
|
51
|
+
config[:fallback_legacy_host] = h
|
52
|
+
config[:fallback_legacy_port] = p.to_i
|
53
|
+
end
|
47
54
|
|
48
55
|
if @options[:socket]
|
49
56
|
config[:listeners] << { listener: UNIXListener,
|
@@ -59,8 +66,8 @@ module Plum
|
|
59
66
|
config[:listeners] << { listener: TLSListener,
|
60
67
|
hostname: @options[:host] || "0.0.0.0",
|
61
68
|
port: @options[:port] || 8080,
|
62
|
-
certificate: @options[:cert]
|
63
|
-
certificate_key: @options[:cert] &&
|
69
|
+
certificate: @options[:cert],
|
70
|
+
certificate_key: @options[:cert] && @options[:key] }
|
64
71
|
end
|
65
72
|
end
|
66
73
|
|
@@ -113,6 +120,14 @@ module Plum
|
|
113
120
|
@options[:key] = arg
|
114
121
|
end
|
115
122
|
|
123
|
+
o.on "--threaded", "Call the Rack application in threads (experimental)" do
|
124
|
+
@options[:threaded] = true
|
125
|
+
end
|
126
|
+
|
127
|
+
o.on "--fallback-legacy HOST:PORT", "Fallbacks if the client doesn't support HTTP/2" do |arg|
|
128
|
+
@options[:fallback_legacy] = arg
|
129
|
+
end
|
130
|
+
|
116
131
|
o.on "-v", "--version", "Show version" do
|
117
132
|
puts "plum version #{::Plum::VERSION}"
|
118
133
|
exit(0)
|
data/lib/plum/rack/config.rb
CHANGED
data/lib/plum/rack/dsl.rb
CHANGED
@@ -39,6 +39,16 @@ module Plum
|
|
39
39
|
def server_push(bool)
|
40
40
|
@config[:server_push] = !!bool
|
41
41
|
end
|
42
|
+
|
43
|
+
def threaded(bool)
|
44
|
+
@config[:threaded] = !!bool
|
45
|
+
end
|
46
|
+
|
47
|
+
def fallback_legacy(str)
|
48
|
+
h, p = str.split(":")
|
49
|
+
@config[:fallback_legacy_host] = h
|
50
|
+
@config[:fallback_legacy_port] = p.to_i
|
51
|
+
end
|
42
52
|
end
|
43
53
|
end
|
44
54
|
end
|
data/lib/plum/rack/listener.rb
CHANGED
@@ -31,17 +31,22 @@ module Plum
|
|
31
31
|
|
32
32
|
class TLSListener < BaseListener
|
33
33
|
def initialize(lc)
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
if lc[:certificate] && lc[:certificate_key]
|
35
|
+
cert = File.read(lc[:certificate])
|
36
|
+
key = File.read(lc[:certificate_key])
|
37
|
+
else
|
38
|
+
STDERR.puts "WARNING: using dummy certificate"
|
37
39
|
cert, key = dummy_key
|
38
40
|
end
|
39
41
|
|
40
42
|
ctx = OpenSSL::SSL::SSLContext.new
|
41
43
|
ctx.ssl_version = :TLSv1_2
|
42
44
|
ctx.alpn_select_cb = -> protocols {
|
43
|
-
|
44
|
-
|
45
|
+
if protocols.include?("h2")
|
46
|
+
"h2"
|
47
|
+
else
|
48
|
+
protocols.first
|
49
|
+
end
|
45
50
|
}
|
46
51
|
ctx.tmp_ecdh_callback = -> (sock, ise, keyl) { OpenSSL::PKey::EC.new("prime256v1") }
|
47
52
|
ctx.cert = OpenSSL::X509::Certificate.new(cert)
|
@@ -56,6 +61,7 @@ module Plum
|
|
56
61
|
end
|
57
62
|
|
58
63
|
def plum(sock)
|
64
|
+
raise ::Plum::LegacyHTTPError.new("client doesn't offered h2 with ALPN", nil) unless sock.alpn_protocol == "h2"
|
59
65
|
::Plum::HTTPSServerConnection.new(sock)
|
60
66
|
end
|
61
67
|
|
data/lib/plum/rack/server.rb
CHANGED
@@ -47,31 +47,59 @@ module Plum
|
|
47
47
|
sock = svr.accept
|
48
48
|
Thread.new {
|
49
49
|
begin
|
50
|
-
|
51
|
-
|
50
|
+
begin
|
51
|
+
sock = sock.accept if sock.respond_to?(:accept)
|
52
|
+
plum = svr.plum(sock)
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
54
|
+
con = Session.new(app: @app,
|
55
|
+
plum: plum,
|
56
|
+
sock: sock,
|
57
|
+
logger: @logger,
|
58
|
+
config: @config,
|
59
|
+
remote_addr: sock.peeraddr.last)
|
60
|
+
con.run
|
61
|
+
rescue ::Plum::LegacyHTTPError => e
|
62
|
+
@logger.info "legacy HTTP client: #{e}"
|
63
|
+
handle_legacy(e, sock)
|
64
|
+
end
|
65
|
+
rescue Errno::ECONNRESET, Errno::EPROTO, Errno::EINVAL, EOFError => e # closed
|
62
66
|
rescue StandardError => e
|
63
67
|
log_exception(e)
|
68
|
+
ensure
|
64
69
|
sock.close if sock
|
65
70
|
end
|
66
71
|
}
|
67
72
|
rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINVAL => e # closed
|
73
|
+
sock.close if sock
|
68
74
|
rescue StandardError => e
|
69
75
|
log_exception(e)
|
76
|
+
sock.close if sock
|
70
77
|
end
|
71
78
|
|
72
79
|
def log_exception(e)
|
73
80
|
@logger.error("#{e.class}: #{e.message}\n#{e.backtrace.map { |b| "\t#{b}" }.join("\n")}")
|
74
81
|
end
|
82
|
+
|
83
|
+
def handle_legacy(e, sock)
|
84
|
+
if @config[:fallback_legacy_host]
|
85
|
+
@logger.info "legacy HTTP: fallbacking to: #{@config[:fallback_legacy_host]}:#{@config[:fallback_legacy_port]}"
|
86
|
+
upstream = TCPSocket.open(@config[:fallback_legacy_host], @config[:fallback_legacy_port])
|
87
|
+
upstream.write(e.buf) if e.buf
|
88
|
+
loop do
|
89
|
+
ret = IO.select([sock, upstream])
|
90
|
+
ret[0].each { |s|
|
91
|
+
a = s.readpartial(65536)
|
92
|
+
if s == upstream
|
93
|
+
sock.write(a)
|
94
|
+
else
|
95
|
+
upstream.write(a)
|
96
|
+
end
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
ensure
|
101
|
+
upstream.close if upstream
|
102
|
+
end
|
75
103
|
end
|
76
104
|
end
|
77
105
|
end
|
data/lib/plum/rack/session.rb
CHANGED
@@ -8,13 +8,14 @@ module Plum
|
|
8
8
|
class Session
|
9
9
|
attr_reader :app, :plum
|
10
10
|
|
11
|
-
def initialize(app:, plum:, sock:, logger:,
|
11
|
+
def initialize(app:, plum:, sock:, logger:, config:, remote_addr: "127.0.0.1")
|
12
12
|
@app = app
|
13
13
|
@plum = plum
|
14
14
|
@sock = sock
|
15
15
|
@logger = logger
|
16
|
-
@
|
16
|
+
@config = config
|
17
17
|
@remote_addr = remote_addr
|
18
|
+
@request_thread = {} # used if threaded
|
18
19
|
|
19
20
|
setup_plum
|
20
21
|
end
|
@@ -24,22 +25,33 @@ module Plum
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def run
|
27
|
-
|
28
|
-
|
29
|
-
@plum << @sock.readpartial(16384)
|
30
|
-
end
|
31
|
-
rescue Errno::EPIPE, Errno::ECONNRESET => e
|
32
|
-
rescue StandardError => e
|
33
|
-
@logger.error("#{e.class}: #{e.message}\n#{e.backtrace.map { |b| "\t#{b}" }.join("\n")}")
|
28
|
+
while !@sock.closed? && !@sock.eof?
|
29
|
+
@plum << @sock.readpartial(1024)
|
34
30
|
end
|
31
|
+
rescue Errno::EPIPE, Errno::ECONNRESET => e
|
32
|
+
rescue StandardError => e
|
33
|
+
@logger.error("#{e.class}: #{e.message}\n#{e.backtrace.map { |b| "\t#{b}" }.join("\n")}")
|
34
|
+
ensure
|
35
|
+
@request_thread.each { |stream, thread| thread.kill }
|
36
|
+
stop
|
35
37
|
end
|
36
38
|
|
37
39
|
private
|
38
40
|
def setup_plum
|
41
|
+
@plum.on(:frame) { |f| puts "recv:#{f.inspect}" }
|
42
|
+
@plum.on(:send_frame) { |f|
|
43
|
+
puts "send:#{f.inspect}" unless f.type == :data
|
44
|
+
}
|
39
45
|
@plum.on(:connection_error) { |ex| @logger.error(ex) }
|
40
46
|
|
41
47
|
# @plum.on(:stream) { |stream| @logger.debug("new stream: #{stream}") }
|
42
|
-
@plum.on(:stream_error) { |stream, ex|
|
48
|
+
@plum.on(:stream_error) { |stream, ex|
|
49
|
+
if [:cancel, :refused_stream].include?(ex.http2_error_type)
|
50
|
+
@logger.debug("stream was cancelled: #{stream}")
|
51
|
+
else
|
52
|
+
@logger.error(ex)
|
53
|
+
end
|
54
|
+
}
|
43
55
|
|
44
56
|
reqs = {}
|
45
57
|
@plum.on(:headers) { |stream, h|
|
@@ -48,25 +60,34 @@ module Plum
|
|
48
60
|
|
49
61
|
@plum.on(:data) { |stream, d|
|
50
62
|
reqs[stream][:data] << d # TODO: store to file?
|
63
|
+
check_window(stream)
|
51
64
|
}
|
52
65
|
|
53
66
|
@plum.on(:end_stream) { |stream|
|
54
|
-
|
67
|
+
if @config[:threaded]
|
68
|
+
@request_thread[stream] = Thread.new {
|
69
|
+
handle_request(stream, reqs[stream][:headers], reqs[stream][:data])
|
70
|
+
}
|
71
|
+
else
|
72
|
+
handle_request(stream, reqs[stream][:headers], reqs[stream][:data])
|
73
|
+
end
|
55
74
|
}
|
56
75
|
end
|
57
76
|
|
77
|
+
def check_window(stream)
|
78
|
+
ws = @plum.local_settings[:initial_window_size]
|
79
|
+
stream.window_update(ws) if stream.recv_remaining_window < (ws / 2)
|
80
|
+
@plum.window_update(ws) if @plum.recv_remaining_window < (ws / 2)
|
81
|
+
end
|
82
|
+
|
58
83
|
def send_body(stream, body)
|
59
84
|
begin
|
60
|
-
if body.
|
85
|
+
if body.respond_to?(:to_str)
|
61
86
|
stream.send_data(body, end_stream: true)
|
62
|
-
elsif body.respond_to?(:
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
stream.send_data(part, end_stream: last == i)
|
67
|
-
i += 1
|
68
|
-
}
|
69
|
-
stream.send_data(nil, end_stream: true) if i == 0
|
87
|
+
elsif body.respond_to?(:readpartial) && body.respond_to?(:eof?)
|
88
|
+
until body.eof?
|
89
|
+
stream.send_data(body.readpartial(65536), end_stream: body.eof?)
|
90
|
+
end
|
70
91
|
else
|
71
92
|
body.each { |part| stream.send_data(part, end_stream: false) }
|
72
93
|
stream.send_data(nil, end_stream: true)
|
@@ -77,9 +98,7 @@ module Plum
|
|
77
98
|
end
|
78
99
|
|
79
100
|
def extract_push(reqheaders, extheaders)
|
80
|
-
if @server_push &&
|
81
|
-
@plum.push_enabled? &&
|
82
|
-
pushs = extheaders["plum.serverpush"]
|
101
|
+
if @config[:server_push] && @plum.push_enabled? && pushs = extheaders["plum.serverpush"]
|
83
102
|
authority = reqheaders.find { |k, v| k == ":authority" }[1]
|
84
103
|
scheme = reqheaders.find { |k, v| k == ":scheme" }[1]
|
85
104
|
|
@@ -87,7 +106,7 @@ module Plum
|
|
87
106
|
method, path = push.split(" ", 2)
|
88
107
|
{
|
89
108
|
":authority" => authority,
|
90
|
-
":method" => method.
|
109
|
+
":method" => method.upcase,
|
91
110
|
":scheme" => scheme,
|
92
111
|
":path" => path
|
93
112
|
}
|
@@ -102,21 +121,26 @@ module Plum
|
|
102
121
|
r_status, r_rawheaders, r_body = @app.call(env)
|
103
122
|
r_headers, r_extheaders = extract_headers(r_status, r_rawheaders)
|
104
123
|
|
105
|
-
|
124
|
+
no_body = r_body.respond_to?(:empty?) && r_body.empty?
|
125
|
+
|
126
|
+
stream.send_headers(r_headers, end_stream: no_body)
|
106
127
|
|
107
128
|
push_sts = extract_push(headers, r_extheaders).map { |preq|
|
108
129
|
[stream.promise(preq), preq]
|
109
130
|
}
|
110
131
|
|
111
|
-
send_body(stream, r_body)
|
132
|
+
send_body(stream, r_body) unless no_body
|
112
133
|
|
113
134
|
push_sts.each { |st, preq|
|
114
|
-
penv = new_env(preq, "")
|
135
|
+
penv = new_env(preq, "".b)
|
115
136
|
p_status, p_h, p_body = @app.call(penv)
|
116
|
-
p_headers = extract_headers(p_status, p_h)
|
117
|
-
|
118
|
-
|
137
|
+
p_headers, _ = extract_headers(p_status, p_h)
|
138
|
+
pno_body = p_body.respond_to?(:empty?) && p_body.empty?
|
139
|
+
st.send_headers(p_headers, end_stream: pno_body)
|
140
|
+
send_body(st, p_body) unless pno_body
|
119
141
|
}
|
142
|
+
|
143
|
+
@request_thread.delete(stream)
|
120
144
|
end
|
121
145
|
|
122
146
|
def new_env(h, data)
|
@@ -130,6 +154,7 @@ module Plum
|
|
130
154
|
"rack.hijack?" => false,
|
131
155
|
"SCRIPT_NAME" => "",
|
132
156
|
"REMOTE_ADDR" => @remote_addr,
|
157
|
+
"HTTP_VERSION" => "HTTP/2.0", # Rack::CommonLogger uses
|
133
158
|
}
|
134
159
|
|
135
160
|
h.each { |k, v|
|
@@ -180,7 +205,6 @@ module Plum
|
|
180
205
|
if "set-cookie" == key
|
181
206
|
rbase[key] = v_.gsub("\n", "; ") # RFC 7540 8.1.2.5
|
182
207
|
elsif !INVALID_HEADERS.member?(key)
|
183
|
-
key.byteshift(2) if key.start_with?("x-")
|
184
208
|
rbase[key] = v_.tr("\n", ",") # RFC 7230 7
|
185
209
|
end
|
186
210
|
end
|
@@ -11,7 +11,7 @@ module Plum
|
|
11
11
|
# Reserves a new stream to server push.
|
12
12
|
# @param args [Hash] The argument to pass to Stram.new.
|
13
13
|
def reserve_stream(**args)
|
14
|
-
next_id = @
|
14
|
+
next_id = @max_stream_ids[0] + 2
|
15
15
|
stream = stream(next_id)
|
16
16
|
stream.set_state(:reserved_local)
|
17
17
|
stream.update_dependency(**args)
|
@@ -7,10 +7,9 @@ module Plum
|
|
7
7
|
|
8
8
|
def initialize(sock, local_settings = {})
|
9
9
|
require "http/parser"
|
10
|
-
@_headers = nil
|
11
|
-
@_body = String.new
|
12
|
-
@_http_parser = setup_parser
|
13
10
|
@sock = sock
|
11
|
+
@negobuf = String.new
|
12
|
+
@_http_parser = setup_parser
|
14
13
|
super(@sock.method(:write), local_settings)
|
15
14
|
end
|
16
15
|
|
@@ -23,40 +22,44 @@ module Plum
|
|
23
22
|
private
|
24
23
|
def negotiate!
|
25
24
|
super
|
26
|
-
rescue RemoteConnectionError
|
27
|
-
|
25
|
+
rescue RemoteConnectionError # Upgrade from HTTP/1.1 or legacy
|
26
|
+
@negobuf << @buffer
|
28
27
|
offset = @_http_parser << @buffer
|
29
28
|
@buffer.byteshift(offset)
|
30
29
|
end
|
31
30
|
|
32
31
|
def setup_parser
|
32
|
+
headers = nil
|
33
|
+
body = String.new
|
34
|
+
|
33
35
|
parser = HTTP::Parser.new
|
34
|
-
parser.on_headers_complete = proc {|_headers|
|
35
|
-
|
36
|
+
parser.on_headers_complete = proc { |_headers|
|
37
|
+
headers = _headers.map {|n, v| [n.downcase, v] }.to_h
|
36
38
|
}
|
37
|
-
parser.on_body = proc {|chunk|
|
38
|
-
parser.on_message_complete = proc {|env|
|
39
|
-
connection =
|
40
|
-
upgrade =
|
41
|
-
settings =
|
39
|
+
parser.on_body = proc { |chunk| body << chunk }
|
40
|
+
parser.on_message_complete = proc { |env|
|
41
|
+
connection = headers["connection"] || ""
|
42
|
+
upgrade = headers["upgrade"] || ""
|
43
|
+
settings = headers["http2-settings"]
|
42
44
|
|
43
45
|
if (connection.split(", ").sort == ["Upgrade", "HTTP2-Settings"].sort &&
|
44
46
|
upgrade.split(", ").include?("h2c") &&
|
45
47
|
settings != nil)
|
46
|
-
switch_protocol(settings)
|
48
|
+
switch_protocol(settings, parser, headers, body)
|
49
|
+
@negobuf = @_http_parser = nil
|
47
50
|
else
|
48
|
-
raise LegacyHTTPError.new(
|
51
|
+
raise LegacyHTTPError.new("request doesn't Upgrade", @negobuf)
|
49
52
|
end
|
50
53
|
}
|
51
54
|
|
52
55
|
parser
|
53
56
|
end
|
54
57
|
|
55
|
-
def switch_protocol(settings)
|
58
|
+
def switch_protocol(settings, parser, headers, data)
|
56
59
|
self.on(:negotiated) {
|
57
60
|
_frame = Frame.new(type: :settings, stream_id: 0, payload: Base64.urlsafe_decode64(settings))
|
58
61
|
receive_settings(_frame, send_ack: false) # HTTP2-Settings
|
59
|
-
process_first_request
|
62
|
+
process_first_request(parser, headers, data)
|
60
63
|
}
|
61
64
|
|
62
65
|
resp = "HTTP/1.1 101 Switching Protocols\r\n" +
|
@@ -68,18 +71,17 @@ module Plum
|
|
68
71
|
@sock.write(resp)
|
69
72
|
end
|
70
73
|
|
71
|
-
def process_first_request
|
74
|
+
def process_first_request(parser, headers, body)
|
72
75
|
encoder = HPACK::Encoder.new(0, indexing: false) # don't pollute connection's HPACK context
|
73
76
|
stream = stream(1)
|
74
77
|
max_frame_size = local_settings[:max_frame_size]
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
78
|
+
nheaders = headers.merge({ ":method" => parser.http_method,
|
79
|
+
":path" => parser.request_url,
|
80
|
+
":authority" => headers["host"] })
|
81
|
+
.reject {|n, v| ["connection", "http2-settings", "upgrade", "host"].include?(n) }
|
79
82
|
|
80
|
-
|
81
|
-
|
82
|
-
(headers_s + data_s).each {|frag| stream.receive_frame(frag) }
|
83
|
+
stream.receive_frame Frame.headers(1, encoder.encode(nheaders), end_headers: true)
|
84
|
+
stream.receive_frame Frame.data(1, body, end_stream: true)
|
83
85
|
end
|
84
86
|
end
|
85
87
|
end
|
data/lib/plum/stream.rb
CHANGED
data/lib/plum/stream_utils.rb
CHANGED
@@ -9,10 +9,8 @@ module Plum
|
|
9
9
|
def promise(headers)
|
10
10
|
stream = @connection.reserve_stream(weight: self.weight + 1, parent: self)
|
11
11
|
encoded = @connection.hpack_encoder.encode(headers)
|
12
|
-
|
13
|
-
|
14
|
-
send frame
|
15
|
-
end
|
12
|
+
frame = Frame.push_promise(id, stream.id, encoded, end_headers: true)
|
13
|
+
send frame
|
16
14
|
stream
|
17
15
|
end
|
18
16
|
|
@@ -22,10 +20,8 @@ module Plum
|
|
22
20
|
def send_headers(headers, end_stream:)
|
23
21
|
max = @connection.remote_settings[:max_frame_size]
|
24
22
|
encoded = @connection.hpack_encoder.encode(headers)
|
25
|
-
|
26
|
-
|
27
|
-
send frame
|
28
|
-
end
|
23
|
+
frame = Frame.headers(id, encoded, end_headers: true, end_stream: end_stream)
|
24
|
+
send frame
|
29
25
|
@state = :half_closed_local if end_stream
|
30
26
|
end
|
31
27
|
|
@@ -35,14 +31,12 @@ module Plum
|
|
35
31
|
def send_data(data, end_stream: true)
|
36
32
|
max = @connection.remote_settings[:max_frame_size]
|
37
33
|
if data.is_a?(IO)
|
38
|
-
|
39
|
-
|
34
|
+
until data.eof?
|
35
|
+
fragment = data.readpartial(max)
|
36
|
+
send Frame.data(id, fragment, end_stream: end_stream && data.eof?)
|
40
37
|
end
|
41
38
|
else
|
42
|
-
|
43
|
-
original.split_data(max).each do |frame|
|
44
|
-
send frame
|
45
|
-
end
|
39
|
+
send Frame.data(id, data, end_stream: end_stream)
|
46
40
|
end
|
47
41
|
@state = :half_closed_local if end_stream
|
48
42
|
end
|
data/lib/plum/version.rb
CHANGED
data/lib/rack/handler/plum.rb
CHANGED
@@ -24,8 +24,8 @@ class UpgradeClientSessionTest < Minitest::Test
|
|
24
24
|
sock.rio.string << Frame.settings().assemble
|
25
25
|
sock.rio.string << Frame.settings(:ack).assemble
|
26
26
|
res = session.request({ ":method" => "GET", ":path" => "/aa" }, "aa", {})
|
27
|
-
sock.rio.string << Frame.headers(3, HPACK::Encoder.new(3).encode(":status" => "200", "content-length" => "3"), :
|
28
|
-
sock.rio.string << Frame.data(3, "aaa", :
|
27
|
+
sock.rio.string << Frame.headers(3, HPACK::Encoder.new(3).encode(":status" => "200", "content-length" => "3"), end_headers: true).assemble
|
28
|
+
sock.rio.string << Frame.data(3, "aaa", end_stream: true).assemble
|
29
29
|
session.succ until res.finished?
|
30
30
|
assert(res.finished?)
|
31
31
|
assert_equal("aaa", res.body)
|
@@ -100,4 +100,15 @@ class ConnectionTest < Minitest::Test
|
|
100
100
|
}
|
101
101
|
}
|
102
102
|
end
|
103
|
+
|
104
|
+
def test_send_immediately_split
|
105
|
+
io = StringIO.new
|
106
|
+
con = Connection.new(io.method(:write))
|
107
|
+
fs = parse_frames(io) {
|
108
|
+
con.__send__(:send_immediately, Frame.new(type: :data, stream_id: 1, payload: "a"*16385))
|
109
|
+
}
|
110
|
+
assert_equal(2, fs.size)
|
111
|
+
assert_equal(16384, fs.first.length)
|
112
|
+
assert_equal(1, fs.last.length)
|
113
|
+
end
|
103
114
|
end
|
@@ -16,7 +16,7 @@ class ServerConnectionUtilsTest < Minitest::Test
|
|
16
16
|
|
17
17
|
def test_server_goaway
|
18
18
|
open_server_connection {|con|
|
19
|
-
con << Frame.headers(3, "", :
|
19
|
+
con << Frame.headers(3, "", end_stream: true, end_headers: true).assemble
|
20
20
|
con.goaway(:stream_closed)
|
21
21
|
|
22
22
|
last = sent_frames.last
|
@@ -135,17 +135,17 @@ class FlowControlTest < Minitest::Test
|
|
135
135
|
|
136
136
|
prepare.call {|con, stream|
|
137
137
|
con.window_update(500) # extend only connection
|
138
|
-
con << Frame.headers(stream.id, "", :
|
138
|
+
con << Frame.headers(stream.id, "", end_headers: true).assemble
|
139
139
|
assert_stream_error(:flow_control_error) {
|
140
|
-
con << Frame.data(stream.id, "\x00" * 30, :
|
140
|
+
con << Frame.data(stream.id, "\x00" * 30, end_stream: true).assemble
|
141
141
|
}
|
142
142
|
}
|
143
143
|
|
144
144
|
prepare.call {|con, stream|
|
145
145
|
stream.window_update(500) # extend only stream
|
146
|
-
con << Frame.headers(stream.id, "", :
|
146
|
+
con << Frame.headers(stream.id, "", end_headers: true).assemble
|
147
147
|
assert_connection_error(:flow_control_error) {
|
148
|
-
con << Frame.data(stream.id, "\x00" * 30, :
|
148
|
+
con << Frame.data(stream.id, "\x00" * 30, end_stream: true).assemble
|
149
149
|
}
|
150
150
|
}
|
151
151
|
end
|
@@ -155,8 +155,8 @@ class FlowControlTest < Minitest::Test
|
|
155
155
|
con = stream.connection
|
156
156
|
con.settings(initial_window_size: 24)
|
157
157
|
stream.window_update(1)
|
158
|
-
con << Frame.headers(stream.id, "", :
|
159
|
-
con << Frame.data(stream.id, "\x00" * 20, :
|
158
|
+
con << Frame.headers(stream.id, "", end_headers: true).assemble
|
159
|
+
con << Frame.data(stream.id, "\x00" * 20, end_stream: true).assemble
|
160
160
|
assert_equal(4, con.recv_remaining_window)
|
161
161
|
assert_equal(5, stream.recv_remaining_window)
|
162
162
|
con.settings(initial_window_size: 60)
|
@@ -55,7 +55,7 @@ class FrameFactoryTest < Minitest::Test
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def test_continuation
|
58
|
-
frame = Frame.continuation(123, "abc", :
|
58
|
+
frame = Frame.continuation(123, "abc", end_headers: true)
|
59
59
|
assert_frame(frame,
|
60
60
|
type: :continuation,
|
61
61
|
stream_id: 123,
|
@@ -74,7 +74,7 @@ class FrameFactoryTest < Minitest::Test
|
|
74
74
|
end
|
75
75
|
|
76
76
|
def test_headers
|
77
|
-
frame = Frame.headers(123, "abc", :
|
77
|
+
frame = Frame.headers(123, "abc", end_stream: true)
|
78
78
|
assert_frame(frame,
|
79
79
|
type: :headers,
|
80
80
|
stream_id: 123,
|
@@ -83,7 +83,7 @@ class FrameFactoryTest < Minitest::Test
|
|
83
83
|
end
|
84
84
|
|
85
85
|
def test_push_promise
|
86
|
-
frame = Frame.push_promise(345, 2, "abc", :
|
86
|
+
frame = Frame.push_promise(345, 2, "abc", end_headers: true)
|
87
87
|
assert_frame(frame,
|
88
88
|
type: :push_promise,
|
89
89
|
stream_id: 345,
|
@@ -3,30 +3,29 @@ require "test_helper"
|
|
3
3
|
class FrameUtilsTest < Minitest::Test
|
4
4
|
def test_frame_enough_short
|
5
5
|
frame = Frame.new(type: :data, stream_id: 1, payload: "123")
|
6
|
-
ret = frame.
|
6
|
+
ret = frame.to_enum(:split, 3).to_a
|
7
7
|
assert_equal(1, ret.size)
|
8
8
|
assert_equal("123", ret.first.payload)
|
9
9
|
end
|
10
10
|
|
11
11
|
def test_frame_unknown
|
12
12
|
frame = Frame.new(type: :settings, stream_id: 1, payload: "123")
|
13
|
-
assert_raises { frame.
|
14
|
-
assert_raises { frame.split_headers(2) }
|
13
|
+
assert_raises(NotImplementedError) { frame.split(2) }
|
15
14
|
end
|
16
15
|
|
17
16
|
def test_frame_data
|
18
17
|
frame = Frame.new(type: :data, flags: [:end_stream], stream_id: 1, payload: "12345")
|
19
|
-
ret = frame.
|
20
|
-
assert_equal(
|
21
|
-
assert_equal("
|
18
|
+
ret = frame.to_enum(:split, 2).to_a
|
19
|
+
assert_equal(3, ret.size)
|
20
|
+
assert_equal("12", ret.first.payload)
|
22
21
|
assert_equal([], ret.first.flags)
|
23
|
-
assert_equal("
|
22
|
+
assert_equal("5", ret.last.payload)
|
24
23
|
assert_equal([:end_stream], ret.last.flags)
|
25
24
|
end
|
26
25
|
|
27
26
|
def test_frame_headers
|
28
27
|
frame = Frame.new(type: :headers, flags: [:priority, :end_stream, :end_headers], stream_id: 1, payload: "1234567")
|
29
|
-
ret = frame.
|
28
|
+
ret = frame.to_enum(:split, 3).to_a
|
30
29
|
assert_equal(3, ret.size)
|
31
30
|
assert_equal("123", ret[0].payload)
|
32
31
|
assert_equal([:end_stream, :priority], ret[0].flags)
|
data/test/plum/test_stream.rb
CHANGED
@@ -28,7 +28,7 @@ class StreamTest < Minitest::Test
|
|
28
28
|
}
|
29
29
|
|
30
30
|
assert_stream_error(:frame_size_error) {
|
31
|
-
con << Frame.headers(1, "", :
|
31
|
+
con << Frame.headers(1, "", end_headers: true).assemble
|
32
32
|
}
|
33
33
|
|
34
34
|
last = sent_frames.last
|
@@ -43,7 +43,7 @@ class StreamTest < Minitest::Test
|
|
43
43
|
stream = nil
|
44
44
|
con.on(:headers) { |s| stream = s }
|
45
45
|
|
46
|
-
con << Frame.headers(1, "", :
|
46
|
+
con << Frame.headers(1, "", end_headers: true).assemble
|
47
47
|
assert_raises(LocalStreamError) {
|
48
48
|
con << Frame.rst_stream(1, :frame_size_error).assemble
|
49
49
|
}
|
data/test/utils/server.rb
CHANGED
@@ -20,7 +20,7 @@ module ServerUtils
|
|
20
20
|
con = open_server_connection
|
21
21
|
end
|
22
22
|
|
23
|
-
@_stream = con.instance_eval { stream(
|
23
|
+
@_stream = con.instance_eval { stream(@max_stream_ids[1] + 2) }
|
24
24
|
@_stream.set_state(state)
|
25
25
|
@_stream.update_dependency(**kwargs)
|
26
26
|
if block_given?
|
@@ -39,8 +39,7 @@ module ServerUtils
|
|
39
39
|
frames
|
40
40
|
end
|
41
41
|
|
42
|
-
def
|
43
|
-
io = (con || @_con).sock
|
42
|
+
def parse_frames(io, &blk)
|
44
43
|
pos = io.string.bytesize
|
45
44
|
blk.call
|
46
45
|
resp = io.string.byteslice(pos, io.string.bytesize - pos).force_encoding(Encoding::BINARY)
|
@@ -51,8 +50,8 @@ module ServerUtils
|
|
51
50
|
frames
|
52
51
|
end
|
53
52
|
|
54
|
-
def
|
55
|
-
frames = capture_frames(
|
53
|
+
def parse_frame(io, &blk)
|
54
|
+
frames = capture_frames(io, &blk)
|
56
55
|
assert_equal(1, frames.size, "Supplied block sent no frames or more than 1 frame")
|
57
56
|
frames.first
|
58
57
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- rhenium
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-11-
|
11
|
+
date: 2015-11-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|