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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7edaa74a52403fc469e096512924e275a1e0b2dc
4
- data.tar.gz: b0e40798462d83baff094cbb4664401afe711694
3
+ metadata.gz: 3671dbbd1de29adc6a254e5c95cef0a20cce6970
4
+ data.tar.gz: efd31623c389dbefb5d8d46de2f4f1233d72470e
5
5
  SHA512:
6
- metadata.gz: 1fda3ee328f9de55f22d575cbdde788cd388fb1df15ead696cc4f86f8ce02532c458b50dc24866d3e9032345b752dccedbb6d58c5b1bfe70606c85adeeb529ff
7
- data.tar.gz: fa4810540a9d8f81a4611cd91fb24d5de3d38543b2c366474388959e6006f99bce46846abe4834b8566a6e6470d4ec2ab3215c1ee3020401d76b79af6665b819
6
+ metadata.gz: e8dd0b9146d5261854136d69c4bcbbe471f31daeb1785b3219779c5ad8daa76bfab82763a383e308b2c17f2fe6ec127a7c8b293ab59a33c22bb7123ecd9ccaab
7
+ data.tar.gz: 035f11f0270f7fb7c7c95cae95993edcf4e924d6f10643928a53f5bcbee8501b13f66b537eb2875607abf2b87f9c0fd3fd16de1241c525d91742505f2b6909f8
@@ -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" => "identity;q=1" }) { |res| # plum doesn't have built-in gzip/deflate decoder
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
 
@@ -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
@@ -12,7 +12,7 @@ module Plum
12
12
 
13
13
  # Create a new stream for HTTP request.
14
14
  def open_stream
15
- next_id = @max_stream_id + (@max_stream_id.even? ? 1 : 2)
15
+ next_id = @max_stream_ids[1] + 2
16
16
  stream(next_id)
17
17
  end
18
18
  end
@@ -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
- @max_stream_id = 0
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 && stream.id < @max_stream_id
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 > @max_stream_id
73
- @max_stream_id = stream_id
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
- @writer.call(frame.assemble)
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 = @max_stream_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 :headers, :data, :parser
47
+ attr_reader :buf
44
48
 
45
- def initialize(headers, data, parser)
46
- @headers = headers
47
- @data = data
48
- @parser = parser
49
+ def initialize(message, buf = nil)
50
+ super(message)
51
+ @buf = buf
49
52
  end
50
53
  end
51
54
 
@@ -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 flags [Array<Symbol>] Flags.
58
- def data(stream_id, payload, *flags)
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
- Frame.new(type: :data, stream_id: stream_id, flags: flags, payload: payload)
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 flags [Array<Symbol>] Flags.
67
- def headers(stream_id, encoded, *flags)
68
- Frame.new(type: :headers, stream_id: stream_id, flags: flags, payload: encoded)
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 flags [Array<Symbol>] Flags.
76
- def push_promise(stream_id, new_id, encoded, *flags)
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
- Frame.new(type: :push_promise, stream_id: stream_id, flags: flags, payload: payload)
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 flags [Array<Symbol>] Flags.
86
- def continuation(stream_id, payload, *flags)
87
- Frame.new(type: :continuation, stream_id: stream_id, flags: flags, payload: payload)
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
@@ -3,33 +3,28 @@ using Plum::BinaryString
3
3
 
4
4
  module Plum
5
5
  module FrameUtils
6
- # Splits the DATA frame into multiple frames if the payload size exceeds max size.
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
- # @return [Array<Frame>] The splitted frames.
9
- def split_data(max)
10
- return [self] if self.length <= max
11
- raise "Frame type must be DATA" unless self.type == :data
12
-
13
- fragments = self.payload.each_byteslice(max).to_a
14
- frames = fragments.map {|fragment| Frame.new(type: :data, flags: [], stream_id: self.stream_id, payload: fragment) }
15
- frames.first.flags = self.flags - [:end_stream]
16
- frames.last.flags = self.flags & [:end_stream]
17
- frames
18
- end
19
-
20
- # Splits the HEADERS or PUSH_PROMISE frame into multiple frames if the payload size exceeds max size.
21
- # @param max [Integer] The maximum size of a frame payload.
22
- # @return [Array<Frame>] The splitted frames.
23
- def split_headers(max)
24
- return [self] if self.length <= max
25
- raise "Frame type must be HEADERS or PUSH_PROMISE" unless [:headers, :push_promise].include?(self.type)
26
-
27
- fragments = self.payload.each_byteslice(max).to_a
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).
@@ -67,6 +67,8 @@ module Plum
67
67
  ["www-authenticate", ""],
68
68
  ].freeze
69
69
 
70
+ STATIC_TABLE_SIZE = STATIC_TABLE.size
71
+
70
72
  HUFFMAN_TABLE = [
71
73
  "1111111111000",
72
74
  "11111111111111111011000",
@@ -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 <= STATIC_TABLE.size
30
- STATIC_TABLE[index - 1]
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 - STATIC_TABLE.size - 1]
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 + STATIC_TABLE.size + 1 if 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] && File.read(@options[:cert]),
63
- certificate_key: @options[:cert] && File.read(@options[:key]) }
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)
@@ -6,7 +6,8 @@ module Plum
6
6
  listeners: [],
7
7
  debug: false,
8
8
  log: nil, # $stdout
9
- server_push: true
9
+ server_push: true,
10
+ threaded: false
10
11
  }.freeze
11
12
 
12
13
  def initialize(config = {})
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
@@ -31,17 +31,22 @@ module Plum
31
31
 
32
32
  class TLSListener < BaseListener
33
33
  def initialize(lc)
34
- cert, key = lc[:certificate], lc[:certificate_key]
35
- unless cert && key
36
- puts "WARNING: using dummy certificate"
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
- raise "Client does not support HTTP/2: #{protocols}" unless protocols.include?("h2")
44
- "h2"
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
 
@@ -47,31 +47,59 @@ module Plum
47
47
  sock = svr.accept
48
48
  Thread.new {
49
49
  begin
50
- sock = sock.accept if sock.respond_to?(:accept)
51
- plum = svr.plum(sock)
50
+ begin
51
+ sock = sock.accept if sock.respond_to?(:accept)
52
+ plum = svr.plum(sock)
52
53
 
53
- con = Session.new(app: @app,
54
- plum: plum,
55
- sock: sock,
56
- logger: @logger,
57
- server_push: @config[:server_push],
58
- remote_addr: sock.peeraddr.last)
59
- con.run
60
- rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINVAL => e # closed
61
- sock.close if sock
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
@@ -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:, server_push: true, remote_addr: "127.0.0.1")
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
- @server_push = server_push
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
- begin
28
- while !@sock.closed? && !@sock.eof?
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| @logger.error(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
- handle_request(stream, reqs[stream][:headers], reqs[stream][:data])
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.is_a?(IO)
85
+ if body.respond_to?(:to_str)
61
86
  stream.send_data(body, end_stream: true)
62
- elsif body.respond_to?(:size)
63
- last = body.size - 1
64
- i = 0
65
- body.each { |part|
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.to_s.upcase,
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
- stream.send_headers(r_headers, end_stream: false)
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
- st.send_headers(p_headers, end_stream: false)
118
- send_body(st, p_body)
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 = @max_stream_id + (@max_stream_id.odd? ? 1 : 2)
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
- # Upgrade from HTTP/1.1
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
- @_headers = _headers.map {|n, v| [n.downcase, v] }.to_h
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| @_body << chunk }
38
- parser.on_message_complete = proc {|env|
39
- connection = @_headers["connection"] || ""
40
- upgrade = @_headers["upgrade"] || ""
41
- settings = @_headers["http2-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(@_headers, @_body, parser)
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
- headers = @_headers.merge({ ":method" => @_http_parser.http_method,
76
- ":path" => @_http_parser.request_url,
77
- ":authority" => @_headers["host"] })
78
- .reject {|n, v| ["connection", "http2-settings", "upgrade", "host"].include?(n) }
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
- headers_s = Frame.headers(1, encoder.encode(headers), :end_headers).split_headers(max_frame_size) # stream ID is 1
81
- data_s = Frame.data(1, @_body, :end_stream).split_data(max_frame_size)
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
@@ -58,7 +58,6 @@ module Plum
58
58
  end
59
59
 
60
60
  # Closes this stream. Sends RST_STREAM frame to the peer.
61
- # @param error_type [Symbol] The error type to be contained in the RST_STREAM frame.
62
61
  def close
63
62
  @state = :closed
64
63
  callback(:close)
@@ -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
- original = Frame.push_promise(id, stream.id, encoded, :end_headers)
13
- original.split_headers(@connection.remote_settings[:max_frame_size]).each do |frame|
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
- original_frame = Frame.headers(id, encoded, :end_headers, (end_stream && :end_stream || nil))
26
- original_frame.split_headers(max).each do |frame|
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
- while !data.eof? && fragment = data.readpartial(max)
39
- send Frame.data(id, fragment, (end_stream && data.eof? && :end_stream))
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
- original = Frame.data(id, data, (end_stream && :end_stream))
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
@@ -1,4 +1,4 @@
1
1
  # -*- frozen-string-literal: true -*-
2
2
  module Plum
3
- VERSION = "0.2.1"
3
+ VERSION = "0.2.2"
4
4
  end
@@ -1,4 +1,6 @@
1
1
  # -*- frozen-string-literal: true -*-
2
+ require "plum/rack"
3
+
2
4
  module Rack
3
5
  module Handler
4
6
  class Plum
@@ -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"), :end_headers).assemble
28
- sock.rio.string << Frame.data(3, "aaa", :end_stream).assemble
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)
@@ -61,4 +61,10 @@ class BinaryStringTest < Minitest::Test
61
61
  ret = string.each_byteslice(3)
62
62
  assert_equal(["123", "456", "78"], ret.to_a)
63
63
  end
64
+
65
+ def test_chunk
66
+ string = "12345678"
67
+ ret = string.chunk(3)
68
+ assert_equal(["123", "456", "78"], ret)
69
+ end
64
70
  end
@@ -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, "", :end_stream, :end_headers).assemble
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, "", :end_headers).assemble
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, :end_stream).assemble
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, "", :end_headers).assemble
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, :end_stream).assemble
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, "", :end_headers).assemble
159
- con << Frame.data(stream.id, "\x00" * 20, :end_stream).assemble
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", :end_headers)
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", :end_stream)
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", :end_headers)
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.split_data(3)
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.split_data(2) }
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.split_data(3)
20
- assert_equal(2, ret.size)
21
- assert_equal("123", ret.first.payload)
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("45", ret.last.payload)
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.split_headers(3)
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)
@@ -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, "", :end_headers).assemble
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, "", :end_headers).assemble
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(((@max_stream_id+1)/2)*2+1) }
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 capture_frames(con = nil, &blk)
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 capture_frame(con = nil, &blk)
55
- frames = capture_frames(con, &blk)
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.1
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-10 00:00:00.000000000 Z
11
+ date: 2015-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler