plum 0.1.3 → 0.2.0

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