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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -12
  3. data/circle.yml +27 -0
  4. data/examples/client/large.rb +20 -0
  5. data/examples/client/twitter.rb +51 -0
  6. data/examples/non_tls_server.rb +15 -9
  7. data/examples/static_server.rb +30 -23
  8. data/lib/plum.rb +9 -2
  9. data/lib/plum/client.rb +198 -0
  10. data/lib/plum/client/client_session.rb +91 -0
  11. data/lib/plum/client/connection.rb +19 -0
  12. data/lib/plum/client/legacy_client_session.rb +118 -0
  13. data/lib/plum/client/response.rb +100 -0
  14. data/lib/plum/client/upgrade_client_session.rb +46 -0
  15. data/lib/plum/connection.rb +58 -65
  16. data/lib/plum/connection_utils.rb +1 -1
  17. data/lib/plum/errors.rb +7 -3
  18. data/lib/plum/flow_control.rb +3 -3
  19. data/lib/plum/rack/listener.rb +3 -3
  20. data/lib/plum/rack/server.rb +1 -0
  21. data/lib/plum/rack/session.rb +5 -2
  22. data/lib/plum/server/connection.rb +42 -0
  23. data/lib/plum/{http_connection.rb → server/http_connection.rb} +7 -14
  24. data/lib/plum/{https_connection.rb → server/https_connection.rb} +2 -9
  25. data/lib/plum/stream.rb +54 -24
  26. data/lib/plum/stream_utils.rb +0 -12
  27. data/lib/plum/version.rb +1 -1
  28. data/plum.gemspec +2 -2
  29. data/test/plum/client/test_client.rb +152 -0
  30. data/test/plum/client/test_connection.rb +11 -0
  31. data/test/plum/client/test_legacy_client_session.rb +90 -0
  32. data/test/plum/client/test_response.rb +74 -0
  33. data/test/plum/client/test_upgrade_client_session.rb +45 -0
  34. data/test/plum/connection/test_handle_frame.rb +4 -1
  35. data/test/plum/{test_http_connection.rb → server/test_http_connection.rb} +4 -4
  36. data/test/plum/{test_https_connection.rb → server/test_https_connection.rb} +14 -8
  37. data/test/plum/test_connection.rb +9 -2
  38. data/test/plum/test_connection_utils.rb +9 -0
  39. data/test/plum/test_error.rb +1 -2
  40. data/test/plum/test_frame_factory.rb +37 -0
  41. data/test/plum/test_stream.rb +24 -4
  42. data/test/plum/test_stream_utils.rb +0 -1
  43. data/test/test_helper.rb +5 -2
  44. data/test/utils/assertions.rb +9 -9
  45. data/test/utils/client.rb +19 -0
  46. data/test/utils/server.rb +6 -6
  47. data/test/utils/string_socket.rb +15 -0
  48. 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 = @max_odd_stream_id > @max_even_stream_id ? @max_odd_stream_id : @max_even_stream_id
23
+ last_id = @max_stream_id
24
24
  send_immediately Frame.goaway(last_id, error_type)
25
25
  end
26
26
 
@@ -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
@@ -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) ? ConnectionError : StreamError
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::ConnectionError.new(:frame_size_error)
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) ? ConnectionError : StreamError
93
+ local_error = (Connection === self) ? RemoteConnectionError : RemoteStreamError
94
94
  raise local_error.new(:protocol_error)
95
95
  end
96
96
 
@@ -25,7 +25,7 @@ module Plum
25
25
  end
26
26
 
27
27
  def plum(sock)
28
- ::Plum::HTTPConnection.new(sock)
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::HTTPSConnection.new(sock)
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::HTTPSConnection.new(sock)
119
+ ::Plum::HTTPSServerConnection.new(sock)
120
120
  end
121
121
  end
122
122
  end
@@ -52,6 +52,7 @@ module Plum
52
52
 
53
53
  con = Session.new(app: @app,
54
54
  plum: plum,
55
+ sock: sock,
55
56
  logger: @logger,
56
57
  server_push: @config[:server_push],
57
58
  remote_addr: sock.peeraddr.last)
@@ -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
- @plum.run
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 HTTPConnection < Connection
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 ConnectionError
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 = new_stream(1)
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 HTTPSConnection < Connection
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 ConnectionError.new(:inadequate_security)
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.
@@ -44,30 +44,34 @@ module Plum
44
44
  receive_window_update(frame)
45
45
  when :continuation
46
46
  receive_continuation(frame)
47
- when :ping, :goaway, :settings, :push_promise
48
- raise ConnectionError.new(:protocol_error) # stream_id MUST be 0x00
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 StreamError => e
54
+ rescue RemoteStreamError => e
53
55
  callback(:stream_error, e)
54
- close(e.http2_error_type)
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(error_type = :no_error)
62
+ def close
60
63
  @state = :closed
61
- send_immediately Frame.rst_stream(id, error_type)
64
+ callback(:close)
62
65
  end
63
66
 
64
- private
65
- def send_immediately(frame)
66
- @connection.send(frame)
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 StreamError.new(:protocol_error, "A stream cannot depend on itself.") if parent == self
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 ConnectionError.new(:frame_size_error)
106
+ raise RemoteConnectionError.new(:frame_size_error)
98
107
  else
99
- raise StreamError.new(:frame_size_error)
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 StreamError.new(:stream_closed)
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 ConnectionError.new(:protocol_error, "padding is too long")
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 ConnectionError.new(:protocol_error, "padding is too long")
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 ConnectionError.new(:compression_error, e)
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 ConnectionError.new(:protocol_error)
173
+ raise RemoteConnectionError.new(:protocol_error)
165
174
  elsif @state == :half_closed_remote
166
- raise StreamError.new(:stream_closed)
175
+ raise RemoteStreamError.new(:stream_closed)
167
176
  elsif @state == :closed
168
- raise ConnectionError.new(:stream_closed)
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 StreamError.new(:frame_size_error)
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 ConnectionError.new(:frame_size_error)
234
+ raise RemoteConnectionError.new(:frame_size_error)
210
235
  elsif @state == :idle
211
- raise ConnectionError.new(:protocol_error)
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
@@ -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.
@@ -1,4 +1,4 @@
1
1
  # -*- frozen-string-literal: true -*-
2
2
  module Plum
3
- VERSION = "0.1.3"
3
+ VERSION = "0.2.0"
4
4
  end
@@ -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{A minimal implementation of HTTP/2 server.}
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.7.0"
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"