plum 0.1.3 → 0.2.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 +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"
|