plum 0.2.1 → 0.2.2

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