plum 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +84 -12
- data/circle.yml +27 -0
- data/examples/client/large.rb +20 -0
- data/examples/client/twitter.rb +51 -0
- data/examples/non_tls_server.rb +15 -9
- data/examples/static_server.rb +30 -23
- data/lib/plum.rb +9 -2
- data/lib/plum/client.rb +198 -0
- data/lib/plum/client/client_session.rb +91 -0
- data/lib/plum/client/connection.rb +19 -0
- data/lib/plum/client/legacy_client_session.rb +118 -0
- data/lib/plum/client/response.rb +100 -0
- data/lib/plum/client/upgrade_client_session.rb +46 -0
- data/lib/plum/connection.rb +58 -65
- data/lib/plum/connection_utils.rb +1 -1
- data/lib/plum/errors.rb +7 -3
- data/lib/plum/flow_control.rb +3 -3
- data/lib/plum/rack/listener.rb +3 -3
- data/lib/plum/rack/server.rb +1 -0
- data/lib/plum/rack/session.rb +5 -2
- data/lib/plum/server/connection.rb +42 -0
- data/lib/plum/{http_connection.rb → server/http_connection.rb} +7 -14
- data/lib/plum/{https_connection.rb → server/https_connection.rb} +2 -9
- data/lib/plum/stream.rb +54 -24
- data/lib/plum/stream_utils.rb +0 -12
- data/lib/plum/version.rb +1 -1
- data/plum.gemspec +2 -2
- data/test/plum/client/test_client.rb +152 -0
- data/test/plum/client/test_connection.rb +11 -0
- data/test/plum/client/test_legacy_client_session.rb +90 -0
- data/test/plum/client/test_response.rb +74 -0
- data/test/plum/client/test_upgrade_client_session.rb +45 -0
- data/test/plum/connection/test_handle_frame.rb +4 -1
- data/test/plum/{test_http_connection.rb → server/test_http_connection.rb} +4 -4
- data/test/plum/{test_https_connection.rb → server/test_https_connection.rb} +14 -8
- data/test/plum/test_connection.rb +9 -2
- data/test/plum/test_connection_utils.rb +9 -0
- data/test/plum/test_error.rb +1 -2
- data/test/plum/test_frame_factory.rb +37 -0
- data/test/plum/test_stream.rb +24 -4
- data/test/plum/test_stream_utils.rb +0 -1
- data/test/test_helper.rb +5 -2
- data/test/utils/assertions.rb +9 -9
- data/test/utils/client.rb +19 -0
- data/test/utils/server.rb +6 -6
- data/test/utils/string_socket.rb +15 -0
- metadata +36 -12
@@ -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_id
|
24
24
|
send_immediately Frame.goaway(last_id, error_type)
|
25
25
|
end
|
26
26
|
|
data/lib/plum/errors.rb
CHANGED
@@ -31,9 +31,6 @@ module Plum
|
|
31
31
|
ERROR_CODES[@http2_error_type]
|
32
32
|
end
|
33
33
|
end
|
34
|
-
class ConnectionError < HTTPError; end
|
35
|
-
class StreamError < HTTPError; end
|
36
|
-
|
37
34
|
class LegacyHTTPError < Error
|
38
35
|
attr_reader :headers, :data, :parser
|
39
36
|
|
@@ -43,4 +40,11 @@ module Plum
|
|
43
40
|
@parser = parser
|
44
41
|
end
|
45
42
|
end
|
43
|
+
|
44
|
+
class RemoteHTTPError < HTTPError; end
|
45
|
+
class RemoteConnectionError < RemoteHTTPError; end
|
46
|
+
class RemoteStreamError < RemoteHTTPError; end
|
47
|
+
class LocalHTTPError < HTTPError; end
|
48
|
+
class LocalConnectionError < LocalHTTPError; end
|
49
|
+
class LocalStreamError < LocalHTTPError; end
|
46
50
|
end
|
data/lib/plum/flow_control.rb
CHANGED
@@ -65,7 +65,7 @@ module Plum
|
|
65
65
|
if frame.type == :data
|
66
66
|
@recv_remaining_window -= frame.length
|
67
67
|
if @recv_remaining_window < 0
|
68
|
-
local_error = (Connection === self) ?
|
68
|
+
local_error = (Connection === self) ? RemoteConnectionError : RemoteStreamError
|
69
69
|
raise local_error.new(:flow_control_error)
|
70
70
|
end
|
71
71
|
end
|
@@ -82,7 +82,7 @@ module Plum
|
|
82
82
|
|
83
83
|
def receive_window_update(frame)
|
84
84
|
if frame.length != 4
|
85
|
-
raise Plum::
|
85
|
+
raise Plum::RemoteConnectionError.new(:frame_size_error)
|
86
86
|
end
|
87
87
|
|
88
88
|
r_wsi = frame.payload.uint32
|
@@ -90,7 +90,7 @@ module Plum
|
|
90
90
|
wsi = r_wsi # & ~(1 << 31)
|
91
91
|
|
92
92
|
if wsi == 0
|
93
|
-
local_error = (Connection === self) ?
|
93
|
+
local_error = (Connection === self) ? RemoteConnectionError : RemoteStreamError
|
94
94
|
raise local_error.new(:protocol_error)
|
95
95
|
end
|
96
96
|
|
data/lib/plum/rack/listener.rb
CHANGED
@@ -25,7 +25,7 @@ module Plum
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def plum(sock)
|
28
|
-
::Plum::
|
28
|
+
::Plum::HTTPServerConnection.new(sock)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -56,7 +56,7 @@ module Plum
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def plum(sock)
|
59
|
-
::Plum::
|
59
|
+
::Plum::HTTPSServerConnection.new(sock)
|
60
60
|
end
|
61
61
|
|
62
62
|
private
|
@@ -116,7 +116,7 @@ module Plum
|
|
116
116
|
end
|
117
117
|
|
118
118
|
def plum(sock)
|
119
|
-
::Plum::
|
119
|
+
::Plum::HTTPSServerConnection.new(sock)
|
120
120
|
end
|
121
121
|
end
|
122
122
|
end
|
data/lib/plum/rack/server.rb
CHANGED
data/lib/plum/rack/session.rb
CHANGED
@@ -8,9 +8,10 @@ module Plum
|
|
8
8
|
class Session
|
9
9
|
attr_reader :app, :plum
|
10
10
|
|
11
|
-
def initialize(app:, plum:, logger:, server_push: true, remote_addr: "127.0.0.1")
|
11
|
+
def initialize(app:, plum:, sock:, logger:, server_push: true, remote_addr: "127.0.0.1")
|
12
12
|
@app = app
|
13
13
|
@plum = plum
|
14
|
+
@sock = sock
|
14
15
|
@logger = logger
|
15
16
|
@server_push = server_push
|
16
17
|
@remote_addr = remote_addr
|
@@ -24,7 +25,9 @@ module Plum
|
|
24
25
|
|
25
26
|
def run
|
26
27
|
begin
|
27
|
-
|
28
|
+
while !@sock.closed? && !@sock.eof?
|
29
|
+
@plum << @sock.readpartial(16384)
|
30
|
+
end
|
28
31
|
rescue Errno::EPIPE, Errno::ECONNRESET => e
|
29
32
|
rescue StandardError => e
|
30
33
|
@logger.error("#{e.class}: #{e.message}\n#{e.backtrace.map { |b| "\t#{b}" }.join("\n")}")
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- frozen-string-literal: true -*-
|
2
|
+
using Plum::BinaryString
|
3
|
+
module Plum
|
4
|
+
class ServerConnection < Connection
|
5
|
+
def initialize(writer, local_settings = {})
|
6
|
+
super(writer, local_settings)
|
7
|
+
|
8
|
+
@state = :waiting_preface
|
9
|
+
end
|
10
|
+
|
11
|
+
# Reserves a new stream to server push.
|
12
|
+
# @param args [Hash] The argument to pass to Stram.new.
|
13
|
+
def reserve_stream(**args)
|
14
|
+
next_id = @max_stream_id + (@max_stream_id.odd? ? 1 : 2)
|
15
|
+
stream = stream(next_id)
|
16
|
+
stream.set_state(:reserved_local)
|
17
|
+
stream.update_dependency(**args)
|
18
|
+
stream
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def consume_buffer
|
23
|
+
if @state == :waiting_preface
|
24
|
+
negotiate!
|
25
|
+
end
|
26
|
+
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def negotiate!
|
31
|
+
unless CLIENT_CONNECTION_PREFACE.start_with?(@buffer.byteslice(0, 24))
|
32
|
+
raise RemoteConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
|
33
|
+
end
|
34
|
+
|
35
|
+
if @buffer.bytesize >= 24
|
36
|
+
@buffer.byteshift(24)
|
37
|
+
settings(@local_settings)
|
38
|
+
@state = :waiting_settings
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
using Plum::BinaryString
|
3
3
|
|
4
4
|
module Plum
|
5
|
-
class
|
5
|
+
class HTTPServerConnection < ServerConnection
|
6
6
|
attr_reader :sock
|
7
7
|
|
8
8
|
def initialize(sock, local_settings = {})
|
@@ -14,13 +14,6 @@ module Plum
|
|
14
14
|
super(@sock.method(:write), local_settings)
|
15
15
|
end
|
16
16
|
|
17
|
-
# Starts communication with the peer. It blocks until the io is closed, or reaches EOF.
|
18
|
-
def run
|
19
|
-
while !@sock.closed? && !@sock.eof?
|
20
|
-
self << @sock.readpartial(1024)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
17
|
# Closes the socket.
|
25
18
|
def close
|
26
19
|
super
|
@@ -30,7 +23,7 @@ module Plum
|
|
30
23
|
private
|
31
24
|
def negotiate!
|
32
25
|
super
|
33
|
-
rescue
|
26
|
+
rescue RemoteConnectionError
|
34
27
|
# Upgrade from HTTP/1.1
|
35
28
|
offset = @_http_parser << @buffer
|
36
29
|
@buffer.byteshift(offset)
|
@@ -66,10 +59,10 @@ module Plum
|
|
66
59
|
process_first_request
|
67
60
|
}
|
68
61
|
|
69
|
-
resp = "HTTP/1.1 101 Switching Protocols\r\n"
|
70
|
-
"Connection: Upgrade\r\n"
|
71
|
-
"Upgrade: h2c\r\n"
|
72
|
-
"Server: plum/#{Plum::VERSION}\r\n"
|
62
|
+
resp = "HTTP/1.1 101 Switching Protocols\r\n" +
|
63
|
+
"Connection: Upgrade\r\n" +
|
64
|
+
"Upgrade: h2c\r\n" +
|
65
|
+
"Server: plum/#{Plum::VERSION}\r\n" +
|
73
66
|
"\r\n"
|
74
67
|
|
75
68
|
@sock.write(resp)
|
@@ -77,7 +70,7 @@ module Plum
|
|
77
70
|
|
78
71
|
def process_first_request
|
79
72
|
encoder = HPACK::Encoder.new(0, indexing: false) # don't pollute connection's HPACK context
|
80
|
-
stream =
|
73
|
+
stream = stream(1)
|
81
74
|
max_frame_size = local_settings[:max_frame_size]
|
82
75
|
headers = @_headers.merge({ ":method" => @_http_parser.http_method,
|
83
76
|
":path" => @_http_parser.request_url,
|
@@ -1,26 +1,19 @@
|
|
1
1
|
# -*- frozen-string-literal: true -*-
|
2
2
|
module Plum
|
3
|
-
class
|
3
|
+
class HTTPSServerConnection < ServerConnection
|
4
4
|
attr_reader :sock
|
5
5
|
|
6
6
|
def initialize(sock, local_settings = {})
|
7
7
|
@sock = sock
|
8
8
|
super(@sock.method(:write), local_settings)
|
9
|
-
end
|
10
9
|
|
11
|
-
# Starts communication with the peer. It blocks until the io is closed, or reaches EOF.
|
12
|
-
def run
|
13
10
|
if @sock.respond_to?(:cipher) # OpenSSL::SSL::SSLSocket-like
|
14
11
|
if CIPHER_BLACKLIST.include?(@sock.cipher.first) # [cipher-suite, ssl-version, keylen, alglen]
|
15
12
|
on(:negotiated) {
|
16
|
-
raise
|
13
|
+
raise RemoteConnectionError.new(:inadequate_security)
|
17
14
|
}
|
18
15
|
end
|
19
16
|
end
|
20
|
-
|
21
|
-
while !@sock.closed? && !@sock.eof?
|
22
|
-
self << @sock.readpartial(1024)
|
23
|
-
end
|
24
17
|
end
|
25
18
|
|
26
19
|
# Closes the socket.
|
data/lib/plum/stream.rb
CHANGED
@@ -44,30 +44,34 @@ module Plum
|
|
44
44
|
receive_window_update(frame)
|
45
45
|
when :continuation
|
46
46
|
receive_continuation(frame)
|
47
|
-
when :
|
48
|
-
|
47
|
+
when :push_promise
|
48
|
+
receive_push_promise(frame)
|
49
|
+
when :ping, :goaway, :settings
|
50
|
+
raise RemoteConnectionError.new(:protocol_error) # stream_id MUST be 0x00
|
49
51
|
else
|
50
52
|
# MUST ignore unknown frame
|
51
53
|
end
|
52
|
-
rescue
|
54
|
+
rescue RemoteStreamError => e
|
53
55
|
callback(:stream_error, e)
|
54
|
-
|
56
|
+
send_immediately Frame.rst_stream(id, e.http2_error_type)
|
57
|
+
close
|
55
58
|
end
|
56
59
|
|
57
60
|
# Closes this stream. Sends RST_STREAM frame to the peer.
|
58
61
|
# @param error_type [Symbol] The error type to be contained in the RST_STREAM frame.
|
59
|
-
def close
|
62
|
+
def close
|
60
63
|
@state = :closed
|
61
|
-
|
64
|
+
callback(:close)
|
62
65
|
end
|
63
66
|
|
64
|
-
private
|
65
|
-
def
|
66
|
-
@
|
67
|
+
# @api private
|
68
|
+
def set_state(state)
|
69
|
+
@state = state
|
67
70
|
end
|
68
71
|
|
72
|
+
# @api private
|
69
73
|
def update_dependency(weight: nil, parent: nil, exclusive: nil)
|
70
|
-
raise
|
74
|
+
raise RemoteStreamError.new(:protocol_error, "A stream cannot depend on itself.") if parent == self
|
71
75
|
|
72
76
|
if weight
|
73
77
|
@weight = weight
|
@@ -91,12 +95,17 @@ module Plum
|
|
91
95
|
end
|
92
96
|
end
|
93
97
|
|
98
|
+
private
|
99
|
+
def send_immediately(frame)
|
100
|
+
@connection.send(frame)
|
101
|
+
end
|
102
|
+
|
94
103
|
def validate_received_frame(frame)
|
95
104
|
if frame.length > @connection.local_settings[:max_frame_size]
|
96
105
|
if [:headers, :push_promise, :continuation].include?(frame.type)
|
97
|
-
raise
|
106
|
+
raise RemoteConnectionError.new(:frame_size_error)
|
98
107
|
else
|
99
|
-
raise
|
108
|
+
raise RemoteStreamError.new(:frame_size_error)
|
100
109
|
end
|
101
110
|
end
|
102
111
|
end
|
@@ -108,13 +117,13 @@ module Plum
|
|
108
117
|
|
109
118
|
def receive_data(frame)
|
110
119
|
if @state != :open && @state != :half_closed_local
|
111
|
-
raise
|
120
|
+
raise RemoteStreamError.new(:stream_closed)
|
112
121
|
end
|
113
122
|
|
114
123
|
if frame.padded?
|
115
124
|
padding_length = frame.payload.uint8
|
116
125
|
if padding_length >= frame.length
|
117
|
-
raise
|
126
|
+
raise RemoteConnectionError.new(:protocol_error, "padding is too long")
|
118
127
|
end
|
119
128
|
callback(:data, frame.payload.byteslice(1, frame.length - padding_length - 1))
|
120
129
|
else
|
@@ -141,7 +150,7 @@ module Plum
|
|
141
150
|
end
|
142
151
|
|
143
152
|
if padding_length > payload.bytesize
|
144
|
-
raise
|
153
|
+
raise RemoteConnectionError.new(:protocol_error, "padding is too long")
|
145
154
|
end
|
146
155
|
|
147
156
|
frames.each do |frame|
|
@@ -151,7 +160,7 @@ module Plum
|
|
151
160
|
begin
|
152
161
|
decoded_headers = @connection.hpack_decoder.decode(payload)
|
153
162
|
rescue => e
|
154
|
-
raise
|
163
|
+
raise RemoteConnectionError.new(:compression_error, e)
|
155
164
|
end
|
156
165
|
|
157
166
|
callback(:headers, decoded_headers)
|
@@ -161,11 +170,15 @@ module Plum
|
|
161
170
|
|
162
171
|
def receive_headers(frame)
|
163
172
|
if @state == :reserved_local
|
164
|
-
raise
|
173
|
+
raise RemoteConnectionError.new(:protocol_error)
|
165
174
|
elsif @state == :half_closed_remote
|
166
|
-
raise
|
175
|
+
raise RemoteStreamError.new(:stream_closed)
|
167
176
|
elsif @state == :closed
|
168
|
-
raise
|
177
|
+
raise RemoteConnectionError.new(:stream_closed)
|
178
|
+
elsif @state == :closed_implicitly
|
179
|
+
raise RemoteConnectionError.new(:protocol_error)
|
180
|
+
elsif @state == :idle && self.id.even?
|
181
|
+
raise RemoteConnectionError.new(:protocol_error)
|
169
182
|
end
|
170
183
|
|
171
184
|
@state = :open
|
@@ -178,6 +191,18 @@ module Plum
|
|
178
191
|
end
|
179
192
|
end
|
180
193
|
|
194
|
+
def receive_push_promise(frame)
|
195
|
+
raise NotImplementedError
|
196
|
+
|
197
|
+
if promised_stream.state == :closed_implicitly
|
198
|
+
# 5.1.1 An endpoint that receives an unexpected stream identifier MUST respond with a connection error of type PROTOCOL_ERROR.
|
199
|
+
raise RemoteConnectionError.new(:protocol_error)
|
200
|
+
elsif promised_id.odd?
|
201
|
+
# 5.1.1 Streams initiated by the server MUST use even-numbered stream identifiers.
|
202
|
+
raise RemoteConnectionError.new(:protocol_error)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
181
206
|
def receive_continuation(frame)
|
182
207
|
# state error mustn't happen: server_connection validates
|
183
208
|
@continuation << frame
|
@@ -190,7 +215,7 @@ module Plum
|
|
190
215
|
|
191
216
|
def receive_priority(frame)
|
192
217
|
if frame.length != 5
|
193
|
-
raise
|
218
|
+
raise RemoteStreamError.new(:frame_size_error)
|
194
219
|
end
|
195
220
|
receive_priority_payload(frame.payload)
|
196
221
|
end
|
@@ -206,13 +231,18 @@ module Plum
|
|
206
231
|
|
207
232
|
def receive_rst_stream(frame)
|
208
233
|
if frame.length != 4
|
209
|
-
raise
|
234
|
+
raise RemoteConnectionError.new(:frame_size_error)
|
210
235
|
elsif @state == :idle
|
211
|
-
raise
|
236
|
+
raise RemoteConnectionError.new(:protocol_error)
|
212
237
|
end
|
213
|
-
|
214
|
-
callback(:rst_stream, frame)
|
215
238
|
@state = :closed # MUST NOT send RST_STREAM
|
239
|
+
|
240
|
+
error_code = frame.payload.uint32
|
241
|
+
if error_code > 0
|
242
|
+
raise LocalStreamError.new(HTTPError::ERROR_CODES.key(error_code))
|
243
|
+
else
|
244
|
+
callback(:rst_stream, frame)
|
245
|
+
end
|
216
246
|
end
|
217
247
|
|
218
248
|
# override EventEmitter
|
data/lib/plum/stream_utils.rb
CHANGED
@@ -3,18 +3,6 @@ using Plum::BinaryString
|
|
3
3
|
|
4
4
|
module Plum
|
5
5
|
module StreamUtils
|
6
|
-
# Responds to a HTTP request.
|
7
|
-
# @param headers [Enumerable<String, String>] The response headers.
|
8
|
-
# @param body [String, IO] The response body.
|
9
|
-
def respond(headers, body = nil, end_stream: true) # TODO: priority, padding
|
10
|
-
if body
|
11
|
-
send_headers(headers, end_stream: false)
|
12
|
-
send_data(body, end_stream: end_stream)
|
13
|
-
else
|
14
|
-
send_headers(headers, end_stream: end_stream)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
6
|
# Reserves a stream to server push. Sends PUSH_PROMISE and create new stream.
|
19
7
|
# @param headers [Enumerable<String, String>] The *request* headers. It must contain all of them: ':authority', ':method', ':scheme' and ':path'.
|
20
8
|
# @return [Stream] The stream to send push response.
|
data/lib/plum/version.rb
CHANGED
data/plum.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["rhenium"]
|
10
10
|
spec.email = ["k@rhe.jp"]
|
11
11
|
|
12
|
-
spec.summary = %q{
|
12
|
+
spec.summary = %q{An HTTP/2 Library for Ruby}
|
13
13
|
spec.description = spec.summary
|
14
14
|
spec.homepage = "https://github.com/rhenium/plum"
|
15
15
|
spec.license = "MIT"
|
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_development_dependency "rack"
|
25
25
|
spec.add_development_dependency "rake"
|
26
26
|
spec.add_development_dependency "yard"
|
27
|
-
spec.add_development_dependency "minitest", "~> 5.
|
27
|
+
spec.add_development_dependency "minitest", "~> 5.8.0"
|
28
28
|
spec.add_development_dependency "simplecov"
|
29
29
|
spec.add_development_dependency "codeclimate-test-reporter"
|
30
30
|
spec.add_development_dependency "guard"
|