aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKazuki Yamaguchi <k@rhe.jp>2015-08-13 22:25:20 +0900
committerKazuki Yamaguchi <k@rhe.jp>2015-08-13 22:25:20 +0900
commiteb5facc1549cf044bf474b52a6a78dc55ea5fa0a (patch)
tree7014c7cb540bd9ae931ffe3aff41ae84bca9aae4
parent799da829bcc62bc92c1cb3088591bdb9a92d1799 (diff)
parent6b0787d8ceb85f4eb6feeba091529e71c82dd07d (diff)
downloadplum-eb5facc1549cf044bf474b52a6a78dc55ea5fa0a.tar.gz
Merge branch 'master' of github.com:rhenium/plum
-rw-r--r--.gitignore2
-rw-r--r--.travis.yml1
-rw-r--r--README.md8
-rw-r--r--examples/local_server.rb6
-rw-r--r--examples/static_server.rb4
-rw-r--r--lib/plum.rb13
-rw-r--r--lib/plum/binary_string.rb15
-rw-r--r--lib/plum/connection.rb (renamed from lib/plum/server_connection.rb)166
-rw-r--r--lib/plum/connection_utils.rb38
-rw-r--r--lib/plum/error.rb34
-rw-r--r--lib/plum/errors.rb35
-rw-r--r--lib/plum/flow_control.rb8
-rw-r--r--lib/plum/frame.rb56
-rw-r--r--lib/plum/frame_factory.rb53
-rw-r--r--lib/plum/frame_helper.rb64
-rw-r--r--lib/plum/frame_utils.rb50
-rw-r--r--lib/plum/hpack/constants.rb517
-rw-r--r--lib/plum/hpack/context.rb21
-rw-r--r--lib/plum/hpack/decoder.rb48
-rw-r--r--lib/plum/hpack/encoder.rb79
-rw-r--r--lib/plum/hpack/huffman.rb18
-rw-r--r--lib/plum/http_connection.rb33
-rw-r--r--lib/plum/https_connection.rb24
-rw-r--r--lib/plum/server_connection_helper.rb31
-rw-r--r--lib/plum/stream.rb81
-rw-r--r--lib/plum/stream_utils.rb (renamed from lib/plum/stream_helper.rb)29
-rw-r--r--lib/plum/version.rb2
-rw-r--r--plum.gemspec19
-rw-r--r--test/plum/connection/test_handle_frame.rb (renamed from test/plum/server_connection/test_handle_frame.rb)23
-rw-r--r--test/plum/hpack/test_context.rb18
-rw-r--r--test/plum/hpack/test_decoder.rb27
-rw-r--r--test/plum/hpack/test_huffman.rb7
-rw-r--r--test/plum/stream/test_handle_frame.rb66
-rw-r--r--test/plum/test_binary_string.rb13
-rw-r--r--test/plum/test_connection.rb (renamed from test/plum/test_server_connection.rb)42
-rw-r--r--test/plum/test_connection_utils.rb29
-rw-r--r--test/plum/test_flow_control.rb66
-rw-r--r--test/plum/test_frame.rb37
-rw-r--r--test/plum/test_frame_factory.rb56
-rw-r--r--test/plum/test_frame_utils.rb (renamed from test/plum/test_frame_helper.rb)12
-rw-r--r--test/plum/test_https_connection.rb (renamed from test/plum/server_connection/test_negotiation.rb)23
-rw-r--r--test/plum/test_server_connection_helper.rb16
-rw-r--r--test/plum/test_stream.rb8
-rw-r--r--test/plum/test_stream_utils.rb (renamed from test/plum/test_stream_helper.rb)2
-rw-r--r--test/test_helper.rb1
-rw-r--r--test/utils/assertions.rb62
-rw-r--r--test/utils/server.rb68
47 files changed, 1207 insertions, 824 deletions
diff --git a/.gitignore b/.gitignore
index 16edcc8..6c58a7a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,5 @@
/pkg/
/spec/reports/
/tmp/
-.*.swp
+.*.sw*
.*.local
diff --git a/.travis.yml b/.travis.yml
index 6cc5e11..7415747 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,6 +3,7 @@ addons:
code_climate:
repo_token: f5092ab344fac7f2de9d7332e00597642a4d24e3d560f7d7f329172a2e5a2def
install:
+ - echo openssl_url=https://www.openssl.org/source >> $rvm_path/user/db
- echo openssl_version=1.0.2d >> $rvm_path/user/db
- rvm pkg install openssl
- $rvm_path/usr/bin/openssl version
diff --git a/README.md b/README.md
index f25433c..2d9cb52 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,14 @@
# Plum [![Build Status](https://travis-ci.org/rhenium/plum.png?branch=master)](https://travis-ci.org/rhenium/plum) [![Code Climate](https://codeclimate.com/github/rhenium/plum/badges/gpa.svg)](https://codeclimate.com/github/rhenium/plum) [![Test Coverage](https://codeclimate.com/github/rhenium/plum/badges/coverage.svg)](https://codeclimate.com/github/rhenium/plum/coverage)
-A minimal implementation of HTTP/2 server. (WIP)
+A minimal implementation of HTTP/2 server.
## Requirements
* OpenSSL 1.0.2+
-* Ruby 2.2 with [ALPN support](https://gist.github.com/rhenium/b1711edcc903e8887a51) and [ECDH support (r51348)](https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/51348/diff?format=diff).
+* Ruby 2.2 with [ALPN support](https://gist.github.com/rhenium/b1711edcc903e8887a51) and [ECDH support (r51348)](https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/51348/diff?format=diff) or latest Ruby 2.3.0-dev.
## TODO
* "http" URIs support (upgrade from HTTP/1.1)
* Stream Priority (RFC 7540 5.3)
-* Better HPACK encoding (RFC 7541)
-* SNI support
* Better API
-* Better Code Climate
-* More test code
## License
MIT License
diff --git a/examples/local_server.rb b/examples/local_server.rb
index 7a38d07..c844a87 100644
--- a/examples/local_server.rb
+++ b/examples/local_server.rb
@@ -98,7 +98,7 @@ loop do
next
end
- plum = Plum::ServerConnection.new(sock)
+ plum = Plum::HTTPSConnection.new(sock)
plum.on(:frame) do |frame|
log(id, frame.stream_id, "recv: #{frame.inspect}")
@@ -141,7 +141,7 @@ loop do
data << data_
end
- stream.on(:complete) do
+ stream.on(:end_stream) do
if headers[":method"] == "GET"
file = File.expand_path(DOCUMENT_ROOT + headers[":path"])
file << "/index.html" if Dir.exist?(file)
@@ -197,7 +197,7 @@ loop do
Thread.new {
begin
- plum.start
+ plum.run
rescue
puts $!
puts $!.backtrace
diff --git a/examples/static_server.rb b/examples/static_server.rb
index e62a009..36c8504 100644
--- a/examples/static_server.rb
+++ b/examples/static_server.rb
@@ -42,7 +42,7 @@ loop do
next
end
- plum = Plum::ServerConnection.new(sock)
+ plum = Plum::HTTPSConnection.new(sock)
plum.on(:frame) do |frame|
log(id, frame.stream_id, "recv: #{frame.inspect}")
@@ -146,7 +146,7 @@ loop do
Thread.new {
begin
- plum.start
+ plum.run
rescue
puts $!
puts $!.backtrace
diff --git a/lib/plum.rb b/lib/plum.rb
index 59e6a9e..8842d6b 100644
--- a/lib/plum.rb
+++ b/lib/plum.rb
@@ -1,7 +1,7 @@
require "openssl"
require "socket"
require "plum/version"
-require "plum/error"
+require "plum/errors"
require "plum/binary_string"
require "plum/event_emitter"
require "plum/hpack/constants"
@@ -9,10 +9,13 @@ require "plum/hpack/huffman"
require "plum/hpack/context"
require "plum/hpack/decoder"
require "plum/hpack/encoder"
-require "plum/frame_helper"
+require "plum/frame_utils"
+require "plum/frame_factory"
require "plum/frame"
require "plum/flow_control"
-require "plum/stream_helper"
+require "plum/connection_utils"
+require "plum/connection"
+require "plum/https_connection"
+require "plum/http_connection"
+require "plum/stream_utils"
require "plum/stream"
-require "plum/server_connection_helper"
-require "plum/server_connection"
diff --git a/lib/plum/binary_string.rb b/lib/plum/binary_string.rb
index 2d3ed76..72b38b1 100644
--- a/lib/plum/binary_string.rb
+++ b/lib/plum/binary_string.rb
@@ -54,6 +54,21 @@ module Plum
force_encoding(Encoding::BINARY)
slice!(0, count)
end
+
+ def each_byteslice(n, &blk)
+ if block_given?
+ pos = 0
+ while pos < self.bytesize
+ yield byteslice(pos, n)
+ pos += n
+ end
+ else
+ Enumerator.new do |y|
+ each_byteslice(n) {|ss| y << ss }
+ end
+ # I want to write `enum_for(__method__, n)`!
+ end
+ end
end
end
end
diff --git a/lib/plum/server_connection.rb b/lib/plum/connection.rb
index 12bbe4f..32a7d2e 100644
--- a/lib/plum/server_connection.rb
+++ b/lib/plum/connection.rb
@@ -1,10 +1,10 @@
using Plum::BinaryString
module Plum
- class ServerConnection
+ class Connection
include EventEmitter
include FlowControl
- include ServerConnectionHelper
+ include ConnectionUtils
CLIENT_CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
@@ -19,100 +19,91 @@ module Plum
attr_reader :hpack_encoder, :hpack_decoder
attr_reader :local_settings, :remote_settings
- attr_reader :state, :socket, :streams
+ attr_reader :state, :streams, :io
- def initialize(socket, local_settings = {})
- @socket = socket
+ def initialize(io, local_settings = {})
+ @io = io
@local_settings = Hash.new {|hash, key| DEFAULT_SETTINGS[key] }.merge!(local_settings)
@remote_settings = Hash.new {|hash, key| DEFAULT_SETTINGS[key] }
@buffer = "".force_encoding(Encoding::BINARY)
@streams = {}
- @state = :waiting_connetion_preface
+ @state = :negotiation
@hpack_decoder = HPACK::Decoder.new(@local_settings[:header_table_size])
@hpack_encoder = HPACK::Encoder.new(@remote_settings[:header_table_size])
initialize_flow_control(send: @remote_settings[:initial_window_size],
recv: @local_settings[:initial_window_size])
end
+ private :initialize
- # Starts communication with the peer. It blocks until the socket is closed, or reaches EOF.
- def start
- settings(@local_settings)
- while !@socket.closed? && !@socket.eof?
- self << @socket.readpartial(@local_settings[:max_frame_size])
+ # Starts communication with the peer. It blocks until the io is closed, or reaches EOF.
+ def run
+ while !@io.closed? && !@io.eof?
+ receive @io.readpartial(1024)
end
- rescue Plum::ConnectionError => e
- callback(:connection_error, e)
- close(e.http2_error_code)
end
- # Closes the connection and closes the socket. Sends GOAWAY frame to the peer.
- #
- # @param error_code [Integer] The error code to be contained in the GOAWAY frame.
- def close(error_code = 0)
- last_id = @streams.keys.reverse_each.find {|id| id.odd? }
- data = ""
- data.push_uint32((last_id || 0) & ~(1 << 31))
- data.push_uint32(error_code)
- data.push("") # debug message
- send_immediately Frame.new(type: :goaway,
- stream_id: 0,
- payload: data)
+ # Closes the io.
+ def close
# TODO: server MAY wait streams
- @socket.close
- end
-
- # Reserves a new stream to server push.
- #
- # @param args [Hash] The argument to pass to Stram.new.
- def reserve_stream(**args)
- next_id = ((@streams.keys.last / 2).to_i + 1) * 2
- stream = new_stream(next_id, state: :reserved_local, **args)
- stream
+ @io.close
end
# Receives the specified data and process.
#
# @param new_data [String] The data received from the peer.
- def <<(new_data)
+ def receive(new_data)
return if new_data.empty?
@buffer << new_data
- if @state == :waiting_connetion_preface
- if @buffer.bytesize >= 24
- if @buffer.byteshift(24) == CLIENT_CONNECTION_PREFACE
- @state = :waiting_settings
- else
- raise Plum::ConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
- end
- else
- if CLIENT_CONNECTION_PREFACE.start_with?(@buffer)
- return # not complete
- else
- raise Plum::ConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
- end
- end
+ if @state == :negotiation
+ negotiate!
end
- while frame = Frame.parse!(@buffer)
- callback(:frame, frame)
- process_frame(frame)
+ if @state != :negotiation
+ while frame = Frame.parse!(@buffer)
+ callback(:frame, frame)
+ receive_frame(frame)
+ end
end
+ rescue ConnectionError => e
+ callback(:connection_error, e)
+ goaway(e.http2_error_type)
+ close
+ end
+ alias << receive
+
+ # Reserves a new stream to server push.
+ #
+ # @param args [Hash] The argument to pass to Stram.new.
+ def reserve_stream(**args)
+ next_id = ((@streams.keys.last / 2).to_i + 1) * 2
+ stream = new_stream(next_id, state: :reserved_local, **args)
+ stream
end
private
def send_immediately(frame)
callback(:send_frame, frame)
- @socket.write(frame.assemble)
+ @io.write(frame.assemble)
+ end
+
+ def new_stream(stream_id, **args)
+ if @streams.size > 0 && @streams.keys.last >= stream_id
+ raise Plum::ConnectionError.new(:protocol_error)
+ end
+
+ stream = Stream.new(self, stream_id, **args)
+ callback(:stream, stream)
+ @streams[stream_id] = stream
+ stream
end
def validate_received_frame(frame)
case @state
when :waiting_settings
- if frame.type == :settings
- @state = :open
- else
- raise Plum::ConnectionError.new(:protocol_error)
- end
+ raise ConnectionError.new(:protocol_error) if frame.type != :settings
+ @state = :negotiated
+ callback(:negotiated)
when :waiting_continuation
if frame.type != :continuation || frame.stream_id != @continuation_id
raise Plum::ConnectionError.new(:protocol_error)
@@ -123,9 +114,8 @@ module Plum
@continuation_id = nil
end
else
- case frame.type
- when :headers
- unless frame.flags.include?(:end_headers)
+ if [:headers].include?(frame.type)
+ if !frame.flags.include?(:end_headers)
@state = :waiting_continuation
@continuation_id = frame.stream_id
end
@@ -133,11 +123,12 @@ module Plum
end
end
- def process_frame(frame)
+ def receive_frame(frame)
validate_received_frame(frame)
+ consume_recv_window(frame)
if frame.stream_id == 0
- process_control_frame(frame)
+ receive_control_frame(frame)
else
if @streams.key?(frame.stream_id)
stream = @streams[frame.stream_id]
@@ -147,23 +138,24 @@ module Plum
end
stream = new_stream(frame.stream_id)
end
- stream.process_frame(frame)
+ stream.receive_frame(frame)
end
end
- def process_control_frame(frame)
+ def receive_control_frame(frame)
if frame.length > @local_settings[:max_frame_size]
raise ConnectionError.new(:frame_size_error)
end
case frame.type
when :settings
- process_settings(frame)
+ receive_settings(frame)
when :window_update
- process_window_update(frame)
+ receive_window_update(frame)
when :ping
- process_ping(frame)
+ receive_ping(frame)
when :goaway
+ goaway
close
when :data, :headers, :priority, :rst_stream, :push_promise, :continuation
raise Plum::ConnectionError.new(:protocol_error)
@@ -172,7 +164,7 @@ module Plum
end
end
- def process_settings(frame)
+ def receive_settings(frame)
if frame.flags.include?(:ack)
raise ConnectionError.new(:frame_size_error) if frame.length != 0
return
@@ -186,15 +178,7 @@ module Plum
callback(:remote_settings, @remote_settings, old_remote_settings)
- send_immediately Frame.new(type: :settings, stream_id: 0x00, flags: [:ack])
- end
-
- def update_local_settings(new_settings)
- old_settings = @local_settings.dup
- @local_settings.merge!(new_settings)
-
- @hpack_decoder.limit = @local_settings[:header_table_size]
- update_recv_initial_window_size(@local_settings[:initial_window_size] - old_settings[:initial_window_size])
+ send_immediately Frame.settings(:ack)
end
def apply_remote_settings(old_remote_settings)
@@ -202,7 +186,7 @@ module Plum
update_send_initial_window_size(@remote_settings[:initial_window_size] - old_remote_settings[:initial_window_size])
end
- def process_ping(frame)
+ def receive_ping(frame)
raise Plum::ConnectionError.new(:frame_size_error) if frame.length != 8
if frame.flags.include?(:ack)
@@ -210,26 +194,8 @@ module Plum
else
on(:ping)
opaque_data = frame.payload
- send_immediately Frame.new(type: :ping,
- stream_id: 0,
- flags: [:ack],
- payload: opaque_data)
- end
- end
-
- def new_stream(stream_id, **args)
- if @streams.size > 0 && @streams.keys.last >= stream_id
- raise Plum::ConnectionError.new(:protocol_error)
+ send_immediately Frame.ping(:ack, opaque_data)
end
-
- stream = Stream.new(self, stream_id, **args)
- callback(:stream, stream)
- @streams[stream_id] = stream
- stream
- end
-
- def local_error
- ConnectionError
end
end
end
diff --git a/lib/plum/connection_utils.rb b/lib/plum/connection_utils.rb
new file mode 100644
index 0000000..da89edf
--- /dev/null
+++ b/lib/plum/connection_utils.rb
@@ -0,0 +1,38 @@
+using Plum::BinaryString
+
+module Plum
+ module ConnectionUtils
+ # Sends local settings to the peer.
+ #
+ # @param kwargs [Hash<Symbol, Integer>]
+ def settings(**kwargs)
+ send_immediately Frame.settings(**kwargs)
+ update_local_settings(kwargs)
+ end
+
+ # Sends a PING frame to the peer.
+ #
+ # @param data [String] Must be 8 octets.
+ # @raise [ArgumentError] If the data is not 8 octets.
+ def ping(data = "plum\x00\x00\x00\x00")
+ send_immediately Frame.ping(data)
+ end
+
+ # Sends GOAWAY frame to the peer and closes the connection.
+ #
+ # @param error_type [Symbol] The error type to be contained in the GOAWAY frame.
+ def goaway(error_type = :no_error)
+ last_id = @streams.keys.max || 0
+ send_immediately Frame.goaway(last_id, error_type)
+ end
+
+ private
+ def update_local_settings(new_settings)
+ old_settings = @local_settings.dup
+ @local_settings.merge!(new_settings)
+
+ @hpack_decoder.limit = @local_settings[:header_table_size]
+ update_recv_initial_window_size(@local_settings[:initial_window_size] - old_settings[:initial_window_size])
+ end
+ end
+end
diff --git a/lib/plum/error.rb b/lib/plum/error.rb
deleted file mode 100644
index c4c0e39..0000000
--- a/lib/plum/error.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module Plum
- ERROR_CODES = {
- no_error: 0x00,
- protocol_error: 0x01,
- internal_error: 0x02,
- flow_control_error: 0x03,
- settings_timeout: 0x04,
- stream_closed: 0x05,
- frame_size_error: 0x06,
- refused_stream: 0x07,
- cancel: 0x08,
- compression_error: 0x09,
- connect_error: 0x0a,
- enhance_your_calm: 0x0b,
- inadequate_security: 0x0c,
- http_1_1_required: 0x0d
- }
-
- class Error < StandardError; end
- class HPACKError < Error; end
- class HTTPError < Error
- attr_reader :http2_error_type
- def initialize(type, message = nil)
- @http2_error_type = type
- super(message)
- end
-
- def http2_error_code
- ERROR_CODES[@http2_error_type]
- end
- end
- class ConnectionError < HTTPError; end
- class StreamError < HTTPError; end
-end
diff --git a/lib/plum/errors.rb b/lib/plum/errors.rb
new file mode 100644
index 0000000..5b4b565
--- /dev/null
+++ b/lib/plum/errors.rb
@@ -0,0 +1,35 @@
+module Plum
+ class Error < StandardError; end
+ class HPACKError < Error; end
+ class HTTPError < Error
+ ERROR_CODES = {
+ no_error: 0x00,
+ protocol_error: 0x01,
+ internal_error: 0x02,
+ flow_control_error: 0x03,
+ settings_timeout: 0x04,
+ stream_closed: 0x05,
+ frame_size_error: 0x06,
+ refused_stream: 0x07,
+ cancel: 0x08,
+ compression_error: 0x09,
+ connect_error: 0x0a,
+ enhance_your_calm: 0x0b,
+ inadequate_security: 0x0c,
+ http_1_1_required: 0x0d
+ }
+
+ attr_reader :http2_error_type
+
+ def initialize(type, message = nil)
+ @http2_error_type = type
+ super(message)
+ end
+
+ def http2_error_code
+ ERROR_CODES[@http2_error_type]
+ end
+ end
+ class ConnectionError < HTTPError; end
+ class StreamError < HTTPError; end
+end
diff --git a/lib/plum/flow_control.rb b/lib/plum/flow_control.rb
index b62b39e..27f39bf 100644
--- a/lib/plum/flow_control.rb
+++ b/lib/plum/flow_control.rb
@@ -33,7 +33,7 @@ module Plum
@send_remaining_window += diff
consume_send_buffer
- if ServerConnection === self
+ if Connection === self
@streams.values.each do |stream|
stream.update_send_initial_window_size(diff)
end
@@ -42,7 +42,7 @@ module Plum
def update_recv_initial_window_size(diff)
@recv_remaining_window += diff
- if ServerConnection === self
+ if Connection === self
@streams.values.each do |stream|
stream.update_recv_initial_window_size(diff)
end
@@ -61,6 +61,7 @@ module Plum
when :data
@recv_remaining_window -= frame.length
if @recv_remaining_window < 0
+ local_error = (Connection === self) ? ConnectionError : StreamError
raise local_error.new(:flow_control_error)
end
end
@@ -75,7 +76,7 @@ module Plum
end
end
- def process_window_update(frame)
+ def receive_window_update(frame)
if frame.length != 4
raise Plum::ConnectionError.new(:frame_size_error)
end
@@ -85,6 +86,7 @@ module Plum
wsi = r_wsi & ~(1 << 31)
if wsi == 0
+ local_error = (Connection === self) ? ConnectionError : StreamError
raise local_error.new(:protocol_error)
end
diff --git a/lib/plum/frame.rb b/lib/plum/frame.rb
index 67316b5..ae7a532 100644
--- a/lib/plum/frame.rb
+++ b/lib/plum/frame.rb
@@ -2,7 +2,8 @@ using Plum::BinaryString
module Plum
class Frame
- include FrameHelper
+ extend FrameFactory
+ include FrameUtils
FRAME_TYPES = {
data: 0x00,
@@ -67,35 +68,53 @@ module Plum
# | Frame Payload (0...) ...
# +---------------------------------------------------------------+
- # [Integer] The length of payload. unsigned 24-bit integer
- attr_reader :length
# [Integer] Frame type. 8-bit
- attr_reader :type_value
+ attr_accessor :type_value
# [Integer] Flags. 8-bit
- attr_reader :flags_value
+ attr_accessor :flags_value
# [Integer] Stream Identifier. unsigned 31-bit integer
- attr_reader :stream_id
+ attr_accessor :stream_id
# [String] The payload.
- attr_reader :payload
-
- def initialize(length: nil, type: nil, type_value: nil, flags: nil, flags_value: nil, stream_id: nil, payload: nil)
- @payload = (payload || "").freeze
- @length = length || @payload.bytesize
- @type_value = type_value || FRAME_TYPES[type] or raise ArgumentError.new("type_value or type is necessary")
- @flags_value = flags_value || (flags && flags.map {|flag| FRAME_FLAGS[self.type][flag] }.inject(:|)) || 0
- @stream_id = stream_id or raise ArgumentError.new("stream_id is necessary")
+ attr_accessor :payload
+
+ def initialize(type: nil, type_value: nil, flags: nil, flags_value: nil, stream_id: nil, payload: nil)
+ self.payload = (payload || "")
+ self.type_value = type_value or self.type = type
+ self.flags_value = flags_value or self.flags = flags
+ self.stream_id = stream_id or raise ArgumentError.new("stream_id is necessary")
+ end
+
+ # Returns the length of payload.
+ # @return [Integer] The length.
+ def length
+ @payload.bytesize
end
# Returns the type of the frame in Symbol.
# @return [Symbol] The type.
def type
- @_type ||= FRAME_TYPES.key(type_value) || ("unknown_%01A" % type_value).to_sym
+ FRAME_TYPES.key(type_value) || ("unknown_%02x" % type_value).to_sym
+ end
+
+ # Sets the frame type.
+ # @param value [Symbol] The type.
+ def type=(value)
+ self.type_value = FRAME_TYPES[value] or raise ArgumentError.new("unknown frame type: #{value}")
end
# Returns the set flags on the frame.
# @return [Array<Symbol>] The flags.
def flags
- @_flags ||= FRAME_FLAGS[type].select {|name, value| value & flags_value > 0 }.map {|name, value| name }.freeze
+ fs = FRAME_FLAGS[type]
+ [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]
+ .select {|v| flags_value & v > 0 }
+ .map {|val| fs && fs.key(val) || ("unknown_%02x" % val).to_sym }
+ end
+
+ # Sets the frame flags.
+ # @param value [Array<Symbol>] The flags.
+ def flags=(value)
+ self.flags_value = (value && value.map {|flag| FRAME_FLAGS[self.type][flag] }.inject(:|) || 0)
end
# Assembles the frame into binary representation.
@@ -135,7 +154,10 @@ module Plum
r = r_sid >> 31
stream_id = r_sid & ~(1 << 31)
- self.new(length: length, type_value: type_value, flags_value: flags_value, stream_id: stream_id, payload: payload)
+ self.new(type_value: type_value,
+ flags_value: flags_value,
+ stream_id: stream_id,
+ payload: payload).freeze
end
end
end
diff --git a/lib/plum/frame_factory.rb b/lib/plum/frame_factory.rb
new file mode 100644
index 0000000..b88cfab
--- /dev/null
+++ b/lib/plum/frame_factory.rb
@@ -0,0 +1,53 @@
+using Plum::BinaryString
+
+module Plum
+ module FrameFactory
+ def rst_stream(stream_id, error_type)
+ payload = "".push_uint32(HTTPError::ERROR_CODES[error_type])
+ Frame.new(type: :rst_stream, stream_id: stream_id, payload: payload)
+ end
+
+ def goaway(last_id, error_type, message = "")
+ payload = "".push_uint32((last_id || 0) | (0 << 31))
+ .push_uint32(HTTPError::ERROR_CODES[error_type])
+ .push(message)
+ Frame.new(type: :goaway, stream_id: 0, payload: payload)
+ end
+
+ def settings(ack = nil, **args)
+ payload = args.inject("") {|payload, (key, value)|
+ id = Frame::SETTINGS_TYPE[key] or raise ArgumentError.new("invalid settings type")
+ payload.push_uint16(id)
+ payload.push_uint32(value)
+ }
+ Frame.new(type: :settings, stream_id: 0, flags: [ack].compact, payload: payload)
+ end
+
+ def ping(arg1 = "plum\x00\x00\x00\x00", arg2 = nil)
+ if !arg2
+ raise ArgumentError.new("data must be 8 octets") if arg1.bytesize != 8
+ Frame.new(type: :ping, stream_id: 0, payload: arg1)
+ else
+ Frame.new(type: :ping, stream_id: 0, flags: [:ack], payload: arg2)
+ end
+ end
+
+ def data(stream_id, payload, *flags)
+ Frame.new(type: :data, stream_id: stream_id, flags: flags.compact, payload: payload.to_s)
+ end
+
+ def headers(stream_id, encoded, *flags)
+ Frame.new(type: :headers, stream_id: stream_id, flags: flags.compact, payload: encoded)
+ end
+
+ def push_promise(stream_id, new_id, encoded, *flags)
+ payload = "".push_uint32(0 << 31 | new_id)
+ .push(encoded)
+ Frame.new(type: :push_promise, stream_id: stream_id, flags: flags.compact, payload: payload)
+ end
+
+ def continuation(stream_id, payload, *flags)
+ Frame.new(type: :continuation, stream_id: stream_id, flags: flags.compact, payload: payload)
+ end
+ end
+end
diff --git a/lib/plum/frame_helper.rb b/lib/plum/frame_helper.rb
deleted file mode 100644
index dd46ddd..0000000
--- a/lib/plum/frame_helper.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-using Plum::BinaryString
-
-module Plum
- module FrameHelper
- # Splits the frame into multiple frames if the payload size exceeds max size.
- #
- # @param max [Integer] The maximum size of a frame payload.
- # @return [Array<Frame>] The splitted frames.
- def split_data(max)
- return [self] if self.length <= max
- raise "Frame type must be DATA" unless self.type == :data
-
- fragments = []
- pos = 0
- while pos <= self.length # data may be empty
- fragments << self.payload.byteslice(pos, max)
- pos += max
- end
-
- frames = []
- last = Frame.new(type: :data, flags: self.flags & [:end_stream], stream_id: self.stream_id, payload: fragments.pop)
- fragments.each do |fragment|
- frames << Frame.new(type: :data, flags: self.flags - [:end_stream], stream_id: self.stream_id, payload: fragment)
- end
- frames << last
- frames
- end
-
- def split_headers(max)
- return [self] if self.length <= max
- raise "Frame type must be DATA" unless [:headers, :push_promise].include?(self.type)
-
- fragments = []
- pos = 0
- while pos < self.length
- fragments << self.payload.byteslice(pos, max)
- pos += max
- end
-
- frames = []
- frames << Frame.new(type_value: self.type_value, flags: self.flags - [:end_headers], stream_id: self.stream_id, payload: fragments.shift)
- if fragments.size > 0
- last = Frame.new(type: :continuation, flags: self.flags & [:end_headers], stream_id: self.stream_id, payload: fragments.pop)
- fragments.each do |fragment|
- frames << Frame.new(type: :continuation, stream_id: self.stream_id, payload: fragment)
- end
- frames << last
- end
- frames
- end
-
- # Parses SETTINGS frame payload. Ignores unknown settings type (see RFC7540 6.5.2).
- #
- # @return [Hash<Symbol, Integer>] The parsed strings.
- def parse_settings
- (self.length / 6).times.map {|i|
- id = self.payload.uint16(6 * i)
- val = self.payload.uint32(6 * i + 2)
- name = Frame::SETTINGS_TYPE.key(id)
- [name, val]
- }.select {|k, v| k }.to_h
- end
- end
-end
diff --git a/lib/plum/frame_utils.rb b/lib/plum/frame_utils.rb
new file mode 100644
index 0000000..69ae3d2
--- /dev/null
+++ b/lib/plum/frame_utils.rb
@@ -0,0 +1,50 @@
+using Plum::BinaryString
+
+module Plum
+ module FrameUtils
+ # Splits the DATA frame into multiple frames if the payload size exceeds max size.
+ #
+ # @param max [Integer] The maximum size of a frame payload.
+ # @return [Array<Frame>] The splitted frames.
+ def split_data(max)
+ return [self] if self.length <= max
+ raise "Frame type must be DATA" unless self.type == :data
+
+ fragments = self.payload.each_byteslice(max).to_a
+ frames = fragments.map {|fragment| Frame.new(type: :data, flags: [], stream_id: self.stream_id, payload: fragment) }
+ frames.first.flags = self.flags - [:end_stream]
+ frames.last.flags = self.flags & [:end_stream]
+ frames
+ end
+
+ # Splits the HEADERS or PUSH_PROMISE frame into multiple frames if the payload size exceeds max size.
+ #
+ # @param max [Integer] The maximum size of a frame payload.
+ # @return [Array<Frame>] The splitted frames.
+ def split_headers(max)
+ return [self] if self.length <= max
+ raise "Frame type must be HEADERS or PUSH_PROMISE" unless [:headers, :push_promise].include?(self.type)
+
+ fragments = self.payload.each_byteslice(max).to_a
+ frames = fragments.map {|fragment| Frame.new(type: :continuation, flags: [], stream_id: self.stream_id, payload: fragment) }
+ frames.first.type_value = self.type_value
+ frames.first.flags = self.flags - [:end_headers]
+ frames.last.flags = self.flags & [:end_headers]
+ frames
+ end
+
+ # Parses SETTINGS frame payload. Ignores unknown settings type (see RFC7540 6.5.2).
+ #
+ # @return [Hash<Symbol, Integer>] The parsed strings.
+ def parse_settings
+ settings = {}
+ payload.each_byteslice(6) do |param|
+ id = param.uint16
+ name = Frame::SETTINGS_TYPE.key(id)
+ # ignore unknown settings type
+ settings[name] = param.uint32(2) if name
+ end
+ settings
+ end
+ end
+end
diff --git a/lib/plum/hpack/constants.rb b/lib/plum/hpack/constants.rb
index 8a4f114..5c09a38 100644
--- a/lib/plum/hpack/constants.rb
+++ b/lib/plum/hpack/constants.rb
@@ -67,266 +67,265 @@ module Plum
]
HUFFMAN_TABLE = [
- [0x1ff8, 13],
- [0x7fffd8, 23],
- [0xfffffe2, 28],
- [0xfffffe3, 28],
- [0xfffffe4, 28],
- [0xfffffe5, 28],
- [0xfffffe6, 28],
- [0xfffffe7, 28],
- [0xfffffe8, 28],
- [0xffffea, 24],
- [0x3ffffffc, 30],
- [0xfffffe9, 28],
- [0xfffffea, 28],
- [0x3ffffffd, 30],
- [0xfffffeb, 28],
- [0xfffffec, 28],
- [0xfffffed, 28],
- [0xfffffee, 28],
- [0xfffffef, 28],
- [0xffffff0, 28],
- [0xffffff1, 28],
- [0xffffff2, 28],
- [0x3ffffffe, 30],
- [0xffffff3, 28],
- [0xffffff4, 28],
- [0xffffff5, 28],
- [0xffffff6, 28],
- [0xffffff7, 28],
- [0xffffff8, 28],
- [0xffffff9, 28],
- [0xffffffa, 28],
- [0xffffffb, 28],
- [0x14, 6],
- [0x3f8, 10],
- [0x3f9, 10],
- [0xffa, 12],
- [0x1ff9, 13],
- [0x15, 6],
- [0xf8, 8],
- [0x7fa, 11],
- [0x3fa, 10],
- [0x3fb, 10],
- [0xf9, 8],
- [0x7fb, 11],
- [0xfa, 8],
- [0x16, 6],
- [0x17, 6],
- [0x18, 6],
- [0x0, 5],
- [0x1, 5],
- [0x2, 5],
- [0x19, 6],
- [0x1a, 6],
- [0x1b, 6],
- [0x1c, 6],
- [0x1d, 6],
- [0x1e, 6],
- [0x1f, 6],
- [0x5c, 7],
- [0xfb, 8],
- [0x7ffc, 15],
- [0x20, 6],
- [0xffb, 12],
- [0x3fc, 10],
- [0x1ffa, 13],
- [0x21, 6],
- [0x5d, 7],
- [0x5e, 7],
- [0x5f, 7],
- [0x60, 7],
- [0x61, 7],
- [0x62, 7],
- [0x63, 7],
- [0x64, 7],
- [0x65, 7],
- [0x66, 7],
- [0x67, 7],
- [0x68, 7],
- [0x69, 7],
- [0x6a, 7],
- [0x6b, 7],
- [0x6c, 7],
- [0x6d, 7],
- [0x6e, 7],
- [0x6f, 7],
- [0x70, 7],
- [0x71, 7],
- [0x72, 7],
- [0xfc, 8],
- [0x73, 7],
- [0xfd, 8],
- [0x1ffb, 13],
- [0x7fff0, 19],
- [0x1ffc, 13],
- [0x3ffc, 14],
- [0x22, 6],
- [0x7ffd, 15],
- [0x3, 5],
- [0x23, 6],
- [0x4, 5],
- [0x24, 6],
- [0x5, 5],
- [0x25, 6],
- [0x26, 6],
- [0x27, 6],
- [0x6, 5],
- [0x74, 7],
- [0x75, 7],
- [0x28, 6],
- [0x29, 6],
- [0x2a, 6],
- [0x7, 5],
- [0x2b, 6],
- [0x76, 7],
- [0x2c, 6],
- [0x8, 5],
- [0x9, 5],
- [0x2d, 6],
- [0x77, 7],
- [0x78, 7],
- [0x79, 7],
- [0x7a, 7],
- [0x7b, 7],
- [0x7ffe, 15],
- [0x7fc, 11],
- [0x3ffd, 14],
- [0x1ffd, 13],
- [0xffffffc, 28],
- [0xfffe6, 20],
- [0x3fffd2, 22],
- [0xfffe7, 20],
- [0xfffe8, 20],
- [0x3fffd3, 22],
- [0x3fffd4, 22],
- [0x3fffd5, 22],
- [0x7fffd9, 23],
- [0x3fffd6, 22],
- [0x7fffda, 23],
- [0x7fffdb, 23],
- [0x7fffdc, 23],
- [0x7fffdd, 23],
- [0x7fffde, 23],
- [0xffffeb, 24],
- [0x7fffdf, 23],
- [0xffffec, 24],
- [0xffffed, 24],
- [0x3fffd7, 22],
- [0x7fffe0, 23],
- [0xffffee, 24],
- [0x7fffe1, 23],
- [0x7fffe2, 23],
- [0x7fffe3, 23],
- [0x7fffe4, 23],
- [0x1fffdc, 21],
- [0x3fffd8, 22],
- [0x7fffe5, 23],
- [0x3fffd9, 22],
- [0x7fffe6, 23],
- [0x7fffe7, 23],
- [0xffffef, 24],
- [0x3fffda, 22],
- [0x1fffdd, 21],
- [0xfffe9, 20],
- [0x3fffdb, 22],
- [0x3fffdc, 22],
- [0x7fffe8, 23],
- [0x7fffe9, 23],
- [0x1fffde, 21],
- [0x7fffea, 23],
- [0x3fffdd, 22],
- [0x3fffde, 22],
- [0xfffff0, 24],
- [0x1fffdf, 21],
- [0x3fffdf, 22],
- [0x7fffeb, 23],
- [0x7fffec, 23],
- [0x1fffe0, 21],
- [0x1fffe1, 21],
- [0x3fffe0, 22],
- [0x1fffe2, 21],
- [0x7fffed, 23],
- [0x3fffe1, 22],
- [0x7fffee, 23],
- [0x7fffef, 23],
- [0xfffea, 20],
- [0x3fffe2, 22],
- [0x3fffe3, 22],
- [0x3fffe4, 22],
- [0x7ffff0, 23],
- [0x3fffe5, 22],
- [0x3fffe6, 22],
- [0x7ffff1, 23],
- [0x3ffffe0, 26],
- [0x3ffffe1, 26],
- [0xfffeb, 20],
- [0x7fff1, 19],
- [0x3fffe7, 22],
- [0x7ffff2, 23],
- [0x3fffe8, 22],
- [0x1ffffec, 25],
- [0x3ffffe2, 26],
- [0x3ffffe3, 26],
- [0x3ffffe4, 26],
- [0x7ffffde, 27],
- [0x7ffffdf, 27],
- [0x3ffffe5, 26],
- [0xfffff1, 24],
- [0x1ffffed, 25],
- [0x7fff2, 19],
- [0x1fffe3, 21],
- [0x3ffffe6, 26],
- [0x7ffffe0, 27],
- [0x7ffffe1, 27],
- [0x3ffffe7, 26],
- [0x7ffffe2, 27],
- [0xfffff2, 24],
- [0x1fffe4, 21],
- [0x1fffe5, 21],
- [0x3ffffe8, 26],
- [0x3ffffe9, 26],
- [0xffffffd, 28],
- [0x7ffffe3, 27],
- [0x7ffffe4, 27],
- [0x7ffffe5, 27],
- [0xfffec, 20],
- [0xfffff3, 24],
- [0xfffed, 20],
- [0x1fffe6, 21],
- [0x3fffe9, 22],
- [0x1fffe7, 21],
- [0x1fffe8, 21],
- [0x7ffff3, 23],
- [0x3fffea, 22],
- [0x3fffeb, 22],
- [0x1ffffee, 25],
- [0x1ffffef, 25],
- [0xfffff4, 24],
- [0xfffff5, 24],
- [0x3ffffea, 26],
- [0x7ffff4, 23],
- [0x3ffffeb, 26],
- [0x7ffffe6, 27],
- [0x3ffffec, 26],
- [0x3ffffed, 26],
- [0x7ffffe7, 27],
- [0x7ffffe8, 27],
- [0x7ffffe9, 27],
- [0x7ffffea, 27],
- [0x7ffffeb, 27],
- [0xffffffe, 28],
- [0x7ffffec, 27],
- [0x7ffffed, 27],
- [0x7ffffee, 27],
- [0x7ffffef, 27],
- [0x7fffff0, 27],
- [0x3ffffee, 26],
- [0x3fffffff, 30] # EOS
+ "1111111111000",
+ "11111111111111111011000",
+ "1111111111111111111111100010",
+ "1111111111111111111111100011",
+ "1111111111111111111111100100",
+ "1111111111111111111111100101",
+ "1111111111111111111111100110",
+ "1111111111111111111111100111",
+ "1111111111111111111111101000",
+ "111111111111111111101010",
+ "111111111111111111111111111100",
+ "1111111111111111111111101001",
+ "1111111111111111111111101010",
+ "111111111111111111111111111101",
+ "1111111111111111111111101011",
+ "1111111111111111111111101100",
+ "1111111111111111111111101101",
+ "1111111111111111111111101110",
+ "1111111111111111111111101111",
+ "1111111111111111111111110000",
+ "1111111111111111111111110001",
+ "1111111111111111111111110010",
+ "111111111111111111111111111110",
+ "1111111111111111111111110011",
+ "1111111111111111111111110100",
+ "1111111111111111111111110101",
+ "1111111111111111111111110110",
+ "1111111111111111111111110111",
+ "1111111111111111111111111000",
+ "1111111111111111111111111001",
+ "1111111111111111111111111010",
+ "1111111111111111111111111011",
+ "010100",
+ "1111111000",
+ "1111111001",
+ "111111111010",
+ "1111111111001",
+ "010101",
+ "11111000",
+ "11111111010",
+ "1111111010",
+ "1111111011",
+ "11111001",
+ "11111111011",
+ "11111010",
+ "010110",
+ "010111",
+ "011000",
+ "00000",
+ "00001",
+ "00010",
+ "011001",
+ "011010",
+ "011011",
+ "011100",
+ "011101",
+ "011110",
+ "011111",
+ "1011100",
+ "11111011",
+ "111111111111100",
+ "100000",
+ "111111111011",
+ "1111111100",
+ "1111111111010",
+ "100001",
+ "1011101",
+ "1011110",
+ "1011111",
+ "1100000",
+ "1100001",
+ "1100010",
+ "1100011",
+ "1100100",
+ "1100101",
+ "1100110",
+ "1100111",
+ "1101000",
+ "1101001",
+ "1101010",
+ "1101011",
+ "1101100",
+ "1101101",
+ "1101110",
+ "1101111",
+ "1110000",
+ "1110001",
+ "1110010",
+ "11111100",
+ "1110011",
+ "11111101",
+ "1111111111011",
+ "1111111111111110000",
+ "1111111111100",
+ "11111111111100",
+ "100010",
+ "111111111111101",
+ "00011",
+ "100011",
+ "00100",
+ "100100",
+ "00101",
+ "100101",
+ "100110",
+ "100111",
+ "00110",
+ "1110100",
+ "1110101",
+ "101000",
+ "101001",
+ "101010",
+ "00111",
+ "101011",
+ "1110110",
+ "101100",
+ "01000",
+ "01001",
+ "101101",
+ "1110111",
+ "1111000",
+ "1111001",
+ "1111010",
+ "1111011",
+ "111111111111110",
+ "11111111100",
+ "11111111111101",
+ "1111111111101",
+ "1111111111111111111111111100",
+ "11111111111111100110",
+ "1111111111111111010010",
+ "11111111111111100111",
+ "11111111111111101000",
+ "1111111111111111010011",
+ "1111111111111111010100",
+ "1111111111111111010101",
+ "11111111111111111011001",
+ "1111111111111111010110",
+ "11111111111111111011010",
+ "11111111111111111011011",
+ "11111111111111111011100",
+ "11111111111111111011101",
+ "11111111111111111011110",
+ "111111111111111111101011",
+ "11111111111111111011111",
+ "111111111111111111101100",
+ "111111111111111111101101",
+ "1111111111111111010111",
+ "11111111111111111100000",
+ "111111111111111111101110",
+ "11111111111111111100001",
+ "11111111111111111100010",
+ "11111111111111111100011",
+ "11111111111111111100100",
+ "111111111111111011100",
+ "1111111111111111011000",
+ "11111111111111111100101",
+ "1111111111111111011001",
+ "11111111111111111100110",
+ "11111111111111111100111",
+ "111111111111111111101111",
+ "1111111111111111011010",
+ "111111111111111011101",
+ "11111111111111101001",
+ "1111111111111111011011",
+ "1111111111111111011100",
+ "11111111111111111101000",
+ "11111111111111111101001",
+ "111111111111111011110",
+ "11111111111111111101010",
+ "1111111111111111011101",
+ "1111111111111111011110",
+ "111111111111111111110000",
+ "111111111111111011111",
+ "1111111111111111011111",
+ "11111111111111111101011",
+ "11111111111111111101100",
+ "111111111111111100000",
+ "111111111111111100001",
+ "1111111111111111100000",
+ "111111111111111100010",
+ "11111111111111111101101",
+ "1111111111111111100001",
+ "11111111111111111101110",
+ "11111111111111111101111",
+ "11111111111111101010",
+ "1111111111111111100010",
+ "1111111111111111100011",
+ "1111111111111111100100",
+ "11111111111111111110000",
+ "1111111111111111100101",
+ "1111111111111111100110",
+ "11111111111111111110001",
+ "11111111111111111111100000",
+ "11111111111111111111100001",
+ "11111111111111101011",
+ "1111111111111110001",
+ "1111111111111111100111",
+ "11111111111111111110010",
+ "1111111111111111101000",
+ "1111111111111111111101100",
+ "11111111111111111111100010",
+ "11111111111111111111100011",
+ "11111111111111111111100100",
+ "111111111111111111111011110",
+ "111111111111111111111011111",
+ "11111111111111111111100101",
+ "111111111111111111110001",
+ "1111111111111111111101101",
+ "1111111111111110010",
+ "111111111111111100011",
+ "11111111111111111111100110",
+ "111111111111111111111100000",
+ "111111111111111111111100001",
+ "11111111111111111111100111",
+ "111111111111111111111100010",
+ "111111111111111111110010",
+ "111111111111111100100",
+ "111111111111111100101",
+ "11111111111111111111101000",
+ "11111111111111111111101001",
+ "1111111111111111111111111101",
+ "111111111111111111111100011",
+ "111111111111111111111100100",
+ "111111111111111111111100101",
+ "11111111111111101100",
+ "111111111111111111110011",
+ "11111111111111101101",
+ "111111111111111100110",
+ "1111111111111111101001",
+ "111111111111111100111",
+ "111111111111111101000",
+ "11111111111111111110011",
+ "1111111111111111101010",
+ "1111111111111111101011",
+ "1111111111111111111101110",
+ "1111111111111111111101111",
+ "111111111111111111110100",
+ "111111111111111111110101",
+ "11111111111111111111101010",
+ "11111111111111111110100",
+ "11111111111111111111101011",
+ "111111111111111111111100110",
+ "11111111111111111111101100",
+ "11111111111111111111101101",
+ "111111111111111111111100111",
+ "111111111111111111111101000",
+ "111111111111111111111101001",
+ "111111111111111111111101010",
+ "111111111111111111111101011",
+ "1111111111111111111111111110",
+ "111111111111111111111101100",
+ "111111111111111111111101101",
+ "111111111111111111111101110",
+ "111111111111111111111101111",
+ "111111111111111111111110000",
+ "11111111111111111111101110",
+ "111111111111111111111111111111"
]
- HUFFMAN_ENCODE_TABLE = HUFFMAN_TABLE.map {|val, len| "%0#{len}b" % val }
- HUFFMAN_DECODE_TABLE = HUFFMAN_ENCODE_TABLE.each_with_index.to_h
+ HUFFMAN_TABLE_INVERSED = HUFFMAN_TABLE.each_with_index.to_h
end
end
diff --git a/lib/plum/hpack/context.rb b/lib/plum/hpack/context.rb
index 0a4f6c2..71f07cb 100644
--- a/lib/plum/hpack/context.rb
+++ b/lib/plum/hpack/context.rb
@@ -22,7 +22,26 @@ module Plum
end
def fetch(index)
- STATIC_TABLE[index - 1] || @dynamic_table[index - STATIC_TABLE.size - 1] or raise HPACKError.new("invalid index: #{index}")
+ if index == 0
+ raise HPACKError.new("index can't be 0")
+ elsif index <= STATIC_TABLE.size
+ STATIC_TABLE[index - 1]
+ elsif index <= STATIC_TABLE.size + @dynamic_table.size
+ @dynamic_table[index - STATIC_TABLE.size - 1]
+ else
+ raise HPACKError.new("invalid index: #{index}")
+ end
+ end
+
+ def search(name, value)
+ pr = proc {|n, v|
+ n == name && (!value || v == value)
+ }
+
+ si = STATIC_TABLE.index &pr
+ return si + 1 if si
+ di = @dynamic_table.index &pr
+ return di + STATIC_TABLE.size + 1 if di
end
def evict
diff --git a/lib/plum/hpack/decoder.rb b/lib/plum/hpack/decoder.rb
index 01149e6..13833c1 100644
--- a/lib/plum/hpack/decoder.rb
+++ b/lib/plum/hpack/decoder.rb
@@ -19,38 +19,50 @@ module Plum
private
def parse!(str)
first_byte = str.uint8
- if first_byte & 0b10000000 == 0b10000000
+ if first_byte >= 128 # 0b1XXXXXXX
parse_indexed!(str)
- elsif first_byte & 0b11000000 == 0b01000000
+ elsif first_byte >= 64 # 0b01XXXXXX
parse_indexing!(str)
- elsif first_byte & 0b11110000 == 0b00000000 || # without indexing
- first_byte & 0b11110000 == 0b00010000 # never indexing
- parse_no_indexing!(str)
- elsif first_byte & 0b11100000 == 0b00100000
+ elsif first_byte >= 32 # 0b001XXXXX
self.limit = read_integer!(str, 5)
nil
- end # all match
+ else # 0b0000XXXX (without indexing) or 0b0001XXXX (never indexing)
+ parse_no_indexing!(str)
+ end
end
def read_integer!(str, prefix_length)
+ first_byte = str.byteshift(1).uint8
+ raise HPACKError.new("integer: end of buffer") unless first_byte
+
mask = (1 << prefix_length) - 1
- ret = str.byteshift(1).uint8 & mask
+ ret = first_byte & mask
+ return ret if ret < mask
+
+ octets = 0
+ while next_value = str.byteshift(1).uint8
+ ret += (next_value & 0b01111111) << (7 * octets)
+ octets += 1
- if ret == mask
- loop.with_index do |_, i|
- next_value = str.byteshift(1).uint8
- ret += (next_value & ~(0b10000000)) << (7 * i)
- break if next_value & 0b10000000 == 0
+ if next_value < 128
+ return ret
+ elsif octets == 4 # RFC 7541 5.1 tells us that we MUST have limitation. at least > 2 ** 28
+ raise HPACKError.new("integer: too large integer")
end
end
- ret
+ raise HPACKError.new("integer: end of buffer")
end
def read_string!(str)
- huffman = (str.uint8 >> 7) == 1
+ first_byte = str.uint8
+ raise HPACKError.new("string: end of buffer") unless first_byte
+
+ huffman = (first_byte >> 7) == 1
length = read_integer!(str, 7)
bin = str.byteshift(length)
+
+ raise HTTPError.new("string: end of buffer") if bin.bytesize < length
bin = Huffman.decode(bin) if huffman
bin
end
@@ -61,11 +73,7 @@ module Plum
# | 1 | Index (7+) |
# +---+---------------------------+
index = read_integer!(str, 7)
- if index == 0
- raise HPACKError.new("index can't be 0 in indexed heaeder field representation")
- else
- fetch(index)
- end
+ fetch(index)
end
def parse_indexing!(str)
diff --git a/lib/plum/hpack/encoder.rb b/lib/plum/hpack/encoder.rb
index a82c9b1..58d13be 100644
--- a/lib/plum/hpack/encoder.rb
+++ b/lib/plum/hpack/encoder.rb
@@ -9,10 +9,24 @@ module Plum
super
end
- # currently only support 0x0000XXXX type (without indexing)
- # and not huffman
+ def encode(headers)
+ out = ""
+ headers.each do |name, value|
+ name = name.to_s; value = value.to_s
+ if index = search(name, value)
+ out << encode_indexed(index)
+ elsif index = search(name, nil)
+ out << encode_half_indexed(index, value, true) # incremental indexing
+ else
+ out << encode_literal(name, value, true) # incremental indexing
+ end
+ end
+ out.force_encoding(Encoding::BINARY)
+ end
+
+ private
# +---+---+---+---+---+---+---+---+
- # | 0 | 0 | 0 | 0 | 0 |
+ # | 0 | 1 | 0 |
# +---+---+-----------------------+
# | H | Name Length (7+) |
# +---+---------------------------+
@@ -22,28 +36,50 @@ module Plum
# +---+---------------------------+
# | Value String (Length octets) |
# +-------------------------------+
- def encode(headers)
- out = "".force_encoding(Encoding::BINARY)
- headers.each do |name, value|
- name = name.to_s; value = value.to_s
- out << "\x00"
- out << encode_integer(name.bytesize, 7)
- out << name
- out << encode_integer(value.bytesize, 7)
- out << value
+ def encode_literal(name, value, indexing = true)
+ if indexing
+ store(name, value)
+ fb = "\x40"
+ else
+ fb = "\x00"
end
- out
+ fb << encode_string(name) << encode_string(value)
+ end
+
+ # +---+---+---+---+---+---+---+---+
+ # | 0 | 1 | Index (6+) |
+ # +---+---+-----------------------+
+ # | H | Value Length (7+) |
+ # +---+---------------------------+
+ # | Value String (Length octets) |
+ # +-------------------------------+
+ def encode_half_indexed(index, value, indexing = true)
+ if indexing
+ store(fetch(index)[0], value)
+ fb = encode_integer(index, 6)
+ fb.setbyte(0, fb.uint8 | 0b01000000)
+ else
+ fb = encode_integer(index, 4)
+ end
+ fb << encode_string(value)
+ end
+
+ # +---+---+---+---+---+---+---+---+
+ # | 1 | Index (7+) |
+ # +---+---------------------------+
+ def encode_indexed(index)
+ s = encode_integer(index, 7)
+ s.setbyte(0, s.uint8 | 0b10000000)
+ s
end
- private
def encode_integer(value, prefix_length)
mask = (1 << prefix_length) - 1
- out = "".force_encoding(Encoding::BINARY)
+ out = ""
if value < mask
out.push_uint8(value)
else
- bytes = [mask]
value -= mask
out.push_uint8(mask)
while value >= mask
@@ -53,6 +89,17 @@ module Plum
out.push_uint8(value)
end
end
+
+ def encode_string(str)
+ huffman_str = Huffman.encode(str).force_encoding(__ENCODING__)
+ if huffman_str.bytesize < str.bytesize
+ lenstr = encode_integer(huffman_str.bytesize, 7)
+ lenstr.setbyte(0, lenstr.uint8(0) | 0b10000000)
+ lenstr << huffman_str
+ else
+ encode_integer(str.bytesize, 7) << str
+ end
+ end
end
end
end
diff --git a/lib/plum/hpack/huffman.rb b/lib/plum/hpack/huffman.rb
index 70bb67c..40fae59 100644
--- a/lib/plum/hpack/huffman.rb
+++ b/lib/plum/hpack/huffman.rb
@@ -8,22 +8,24 @@ module Plum
# Static-Huffman-encodes the specified String.
def encode(bytestr)
out = ""
- bytestr.bytes.each do |b|
- out << HUFFMAN_ENCODE_TABLE[b]
+ bytestr.each_byte do |b|
+ out << HUFFMAN_TABLE[b]
end
- out << "1" * (8 - (out.bytesize % 8))
+ out << "1" * ((8 - out.bytesize) % 8)
[out].pack("B*")
end
# Static-Huffman-decodes the specified String.
def decode(encoded)
bits = encoded.unpack("B*")[0]
+ out = []
buf = ""
- outl = []
- while (n = bits.byteshift(1)).bytesize > 0
- if c = HUFFMAN_DECODE_TABLE[buf << n]
+ bits.each_char do |cb|
+ buf << cb
+ if c = HUFFMAN_TABLE_INVERSED[buf]
+ raise HPACKError.new("huffman: EOS detected") if c == 256
+ out << c
buf = ""
- outl << c
end
end
@@ -32,7 +34,7 @@ module Plum
elsif buf != "1" * buf.bytesize
raise HPACKError.new("huffman: unknown suffix: #{buf}")
else
- outl.pack("C*")
+ out.pack("C*")
end
end
end
diff --git a/lib/plum/http_connection.rb b/lib/plum/http_connection.rb
new file mode 100644
index 0000000..bde8b7d
--- /dev/null
+++ b/lib/plum/http_connection.rb
@@ -0,0 +1,33 @@
+module Plum
+ class HTTPConnection < Connection
+ def initialize(io, local_settings = {})
+ super
+ end
+
+ private
+ def negotiate!
+ if @buffer.bytesize >= 4
+ if CLIENT_CONNECTION_PREFACE.start_with?(@buffer)
+ negotiate_with_knowledge
+ else
+ negotiate_with_upgrade
+ end
+ end
+ # next
+ end
+
+ def negotiate_with_knowledge
+ if @buffer.bytesize >= 24
+ if @buffer.byteshift(24) == CLIENT_CONNECTION_PREFACE
+ @state = :waiting_settings
+ settings(@local_settings)
+ end
+ end
+ # next
+ end
+
+ def negotiate_with_upgrade
+ raise NotImplementedError, "Parsing HTTP/1.1 is hard..."
+ end
+ end
+end
diff --git a/lib/plum/https_connection.rb b/lib/plum/https_connection.rb
new file mode 100644
index 0000000..d05da40
--- /dev/null
+++ b/lib/plum/https_connection.rb
@@ -0,0 +1,24 @@
+using Plum::BinaryString
+
+module Plum
+ class HTTPSConnection < Connection
+ def initialize(io, local_settings = {})
+ super
+ end
+
+ private
+ def negotiate!
+ return if @buffer.empty?
+
+ if CLIENT_CONNECTION_PREFACE.start_with?(@buffer.byteslice(0, 24))
+ if @buffer.bytesize >= 24
+ @buffer.byteshift(24)
+ @state = :waiting_settings
+ settings(@local_settings)
+ end
+ else
+ raise ConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
+ end
+ end
+ end
+end
diff --git a/lib/plum/server_connection_helper.rb b/lib/plum/server_connection_helper.rb
deleted file mode 100644
index d799cf6..0000000
--- a/lib/plum/server_connection_helper.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-using Plum::BinaryString
-
-module Plum
- module ServerConnectionHelper
- # Sends local settings to the peer.
- #
- # @param kwargs [Hash<Symbol, Integer>]
- def settings(**kwargs)
- payload = kwargs.inject("") {|payload, (key, value)|
- id = Frame::SETTINGS_TYPE[key] or raise ArgumentError.new("invalid settings type")
- payload.push_uint16(id)
- payload.push_uint32(value)
- }
- send Frame.new(type: :settings,
- stream_id: 0,
- payload: payload)
- update_local_settings(kwargs)
- end
-
- # Sends a PING frame to the peer.
- #
- # @param data [String] Must be 8 octets.
- # @raise [ArgumentError] If the data is not 8 octets.
- def ping(data = "plum\x00\x00\x00\x00")
- raise ArgumentError.new("data must be 8 octets") if data.bytesize != 8
- send Frame.new(type: :ping,
- stream_id: 0,
- payload: data)
- end
- end
-end
diff --git a/lib/plum/stream.rb b/lib/plum/stream.rb
index 67e2550..46fad40 100644
--- a/lib/plum/stream.rb
+++ b/lib/plum/stream.rb
@@ -4,7 +4,7 @@ module Plum
class Stream
include EventEmitter
include FlowControl
- include StreamHelper
+ include StreamUtils
attr_reader :id, :state, :connection
attr_reader :weight, :exclusive
@@ -30,43 +30,39 @@ module Plum
# Processes received frames for this stream. Internal use.
# @private
- def process_frame(frame)
+ def receive_frame(frame)
validate_received_frame(frame)
consume_recv_window(frame)
case frame.type
when :data
- process_data(frame)
+ receive_data(frame)
when :headers
- process_headers(frame)
+ receive_headers(frame)
when :priority
- process_priority(frame)
+ receive_priority(frame)
when :rst_stream
- process_rst_stream(frame)
+ receive_rst_stream(frame)
when :window_update
- process_window_update(frame)
+ receive_window_update(frame)
when :continuation
- process_continuation(frame)
+ receive_continuation(frame)
when :ping, :goaway, :settings, :push_promise
- raise Plum::ConnectionError.new(:protocol_error) # stream_id MUST be 0x00
+ raise ConnectionError.new(:protocol_error) # stream_id MUST be 0x00
else
# MUST ignore unknown frame
end
- rescue Plum::StreamError => e
+ rescue StreamError => e
callback(:stream_error, e)
- close(e.http2_error_code)
+ close(e.http2_error_type)
end
# Closes this stream. Sends RST_STREAM frame to the peer.
#
- # @param error_code [Integer] The error code to be contained in the RST_STREAM frame.
- def close(error_code = 0)
+ # @param error_type [Symbol] The error type to be contained in the RST_STREAM frame.
+ def close(error_type = :no_error)
@state = :closed
- data = "".force_encoding(Encoding::BINARY)
- data.push_uint32(error_code)
- send_immediately Frame.new(type: :rst_stream,
- stream_id: id,
- payload: data)
+ send_immediately Frame.rst_stream(id, error_type)
end
private
@@ -99,7 +95,12 @@ module Plum
end
end
- def process_data(frame)
+ def receive_end_stream
+ callback(:end_stream)
+ @state = :half_closed_remote
+ end
+
+ def receive_data(frame)
if @state != :open && @state != :half_closed_local
raise StreamError.new(:stream_closed)
end
@@ -107,7 +108,7 @@ module Plum
if frame.flags.include?(:padded)
padding_length = frame.payload.uint8(0)
if padding_length >= frame.length
- raise Plum::ConnectionError.new(:protocol_error, "padding is too long")
+ raise ConnectionError.new(:protocol_error, "padding is too long")
end
body = frame.payload.byteslice(1, frame.length - padding_length - 1)
else
@@ -115,13 +116,10 @@ module Plum
end
callback(:data, body)
- if frame.flags.include?(:end_stream) # :data, :headers
- callback(:end_stream)
- @state = :half_closed_remote
- end
+ receive_end_stream if frame.flags.include?(:end_stream)
end
- def process_complete_headers(frames)
+ def receive_complete_headers(frames)
frames = frames.dup
first = frames.shift
@@ -138,12 +136,12 @@ module Plum
end
if first.flags.include?(:priority)
- process_priority_payload(payload.byteshift(5))
+ receive_priority_payload(payload.byteshift(5))
first_length -= 5
end
if padding_length > first_length
- raise Plum::ConnectionError.new(:protocol_error, "padding is too long")
+ raise ConnectionError.new(:protocol_error, "padding is too long")
end
frames.each do |frame|
@@ -158,13 +156,10 @@ module Plum
callback(:headers, decoded_headers)
- if first.flags.include?(:end_stream)
- callback(:end_stream)
- @state = :half_closed_remote
- end
+ receive_end_stream if first.flags.include?(:end_stream)
end
- def process_headers(frame)
+ def receive_headers(frame)
if @state == :reserved_local
raise ConnectionError.new(:protocol_error)
elsif @state == :half_closed_remote
@@ -177,30 +172,30 @@ module Plum
callback(:open)
if frame.flags.include?(:end_headers)
- process_complete_headers([frame])
+ receive_complete_headers([frame])
else
@continuation << frame
end
end
- def process_continuation(frame)
+ def receive_continuation(frame)
# state error mustn't happen: server_connection validates
@continuation << frame
if frame.flags.include?(:end_headers)
- process_complete_headers(@continuation)
+ receive_complete_headers(@continuation)
@continuation.clear
end
end
- def process_priority(frame)
+ def receive_priority(frame)
if frame.length != 5
- raise Plum::StreamError.new(:frame_size_error)
+ raise StreamError.new(:frame_size_error)
end
- process_priority_payload(frame.payload)
+ receive_priority_payload(frame.payload)
end
- def process_priority_payload(payload)
+ def receive_priority_payload(payload)
esd = payload.uint32
e = esd >> 31
dependency_id = e & ~(1 << 31)
@@ -209,18 +204,14 @@ module Plum
update_dependency(weight: weight, parent: @connection.streams[dependency_id], exclusive: e == 1)
end
- def process_rst_stream(frame)
+ def receive_rst_stream(frame)
if frame.length != 4
- raise Plum::ConnectionError.new(:frame_size_error)
+ raise ConnectionError.new(:frame_size_error)
elsif @state == :idle
raise ConnectionError.new(:protocol_error)
end
@state = :closed # MUST NOT send RST_STREAM
end
-
- def local_error
- StreamError
- end
end
end
diff --git a/lib/plum/stream_helper.rb b/lib/plum/stream_utils.rb
index 5dd2de9..0bcceec 100644
--- a/lib/plum/stream_helper.rb
+++ b/lib/plum/stream_utils.rb
@@ -1,7 +1,7 @@
using Plum::BinaryString
module Plum
- module StreamHelper
+ module StreamUtils
# Responds to HTTP request.
#
# @param headers [Hash<String, String>] The response headers.
@@ -15,20 +15,14 @@ module Plum
end
end
- # Reserves a stream to server push. Sends PUSH_STREAM and create new stream.
+ # Reserves a stream to server push. Sends PUSH_PROMISE and create new stream.
#
# @param headers [Hash<String, String>] The *request* headers. It must contain all of them: ':authority', ':method', ':scheme' and ':path'.
# @return [Stream] The stream to send push response.
def promise(headers)
stream = @connection.reserve_stream(weight: self.weight + 1, parent: self)
- payload = "".force_encoding(Encoding::BINARY)
- payload.push_uint32((0 << 31 | stream.id))
- payload.push(@connection.hpack_encoder.encode(headers))
-
- original = Frame.new(type: :push_promise,
- flags: [:end_headers],
- stream_id: id,
- payload: payload)
+ encoded = @connection.hpack_encoder.encode(headers)
+ original = Frame.push_promise(id, stream.id, encoded, :end_headers)
original.split_headers(@connection.remote_settings[:max_frame_size]).each do |frame|
send frame
end
@@ -39,10 +33,7 @@ module Plum
def send_headers(headers, end_stream:)
max = @connection.remote_settings[:max_frame_size]
encoded = @connection.hpack_encoder.encode(headers)
- original_frame = Frame.new(type: :headers,
- flags: [:end_headers, end_stream ? :end_stream : nil].compact,
- stream_id: id,
- payload: encoded)
+ original_frame = Frame.headers(id, encoded, :end_headers, (end_stream && :end_stream || nil))
original_frame.split_headers(max).each do |frame|
send frame
end
@@ -53,16 +44,10 @@ module Plum
max = @connection.remote_settings[:max_frame_size]
if data.is_a?(IO)
while !data.eof? && fragment = data.readpartial(max)
- send Frame.new(type: :data,
- stream_id: id,
- flags: (end_stream && data.eof? && [:end_stream]),
- payload: fragment)
+ send Frame.data(id, fragment, (end_stream && data.eof? && :end_stream))
end
else
- original = Frame.new(type: :data,
- stream_id: id,
- flags: (end_stream && [:end_stream]),
- payload: data.to_s)
+ original = Frame.data(id, data, (end_stream && :end_stream))
original.split_data(max).each do |frame|
send frame
end
diff --git a/lib/plum/version.rb b/lib/plum/version.rb
index c36aa7b..6eff66b 100644
--- a/lib/plum/version.rb
+++ b/lib/plum/version.rb
@@ -1,3 +1,3 @@
module Plum
- VERSION = "0.0.0"
+ VERSION = "0.0.1"
end
diff --git a/plum.gemspec b/plum.gemspec
index 66deef7..2292b62 100644
--- a/plum.gemspec
+++ b/plum.gemspec
@@ -1,7 +1,6 @@
-# coding: utf-8
-lib = File.expand_path('../lib', __FILE__)
+lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
-require 'plum/version'
+require "plum/version"
Gem::Specification.new do |spec|
spec.name = "plum"
@@ -10,21 +9,13 @@ Gem::Specification.new do |spec|
spec.email = ["k@rhe.jp"]
spec.summary = %q{A minimal implementation of HTTP/2 server.}
- spec.description = %q{A minimal implementation of HTTP/2 server.}
+ spec.description = spec.summary
spec.homepage = "https://github.com/rhenium/plum"
spec.license = "MIT"
- # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
- # delete this section to allow pushing this gem to any host.
- if spec.respond_to?(:metadata)
- spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
- else
- raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
- end
-
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
- spec.bindir = "bin"
+ spec.files = `git ls-files -z`.split("\x00")
spec.executables = spec.files.grep(%r{^bin/[^.]}) { |f| File.basename(f) }
+ spec.test_files = spec.files.grep(%r{^test/})
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "~> 1.10"
diff --git a/test/plum/server_connection/test_handle_frame.rb b/test/plum/connection/test_handle_frame.rb
index 55f94c7..6aa7db7 100644
--- a/test/plum/server_connection/test_handle_frame.rb
+++ b/test/plum/connection/test_handle_frame.rb
@@ -12,9 +12,20 @@ class ServerConnectionHandleFrameTest < Minitest::Test
}
end
+ def test_server_handle_settings
+ open_server_connection {|con|
+ assert_no_error {
+ con << Frame.new(type: :settings, stream_id: 0, flags: [:ack], payload: "").assemble
+ }
+ assert_connection_error(:frame_size_error) {
+ con << Frame.new(type: :settings, stream_id: 0, flags: [:ack], payload: "\x00").assemble
+ }
+ }
+ end
+
def test_server_handle_settings_invalid
open_server_connection {|con|
- refute_raises {
+ assert_no_error {
con << Frame.new(type: :settings, stream_id: 0, payload: "\xff\x01\x00\x00\x10\x10").assemble
}
}
@@ -46,4 +57,14 @@ class ServerConnectionHandleFrameTest < Minitest::Test
refute_equal(:ping, last.type) if last
}
end
+
+ ## GOAWAY
+ def test_server_handle_goaway_reply
+ open_server_connection {|con|
+ assert_no_error {
+ con << Frame.goaway(1234, :stream_closed).assemble
+ }
+ assert_equal(:goaway, sent_frames.last.type)
+ }
+ end
end
diff --git a/test/plum/hpack/test_context.rb b/test/plum/hpack/test_context.rb
index 942d34c..3b662ec 100644
--- a/test/plum/hpack/test_context.rb
+++ b/test/plum/hpack/test_context.rb
@@ -34,6 +34,24 @@ class HPACKContextTest < Minitest::Test
}
end
+ def test_search_static
+ context = new_context
+ i1 = context.search(":method", "POST")
+ assert_equal(3, i1)
+ end
+
+ def test_search_dynamic
+ context = new_context
+ context.store("あああ", "abc")
+ context.store("あああ", "いい")
+ i1 = context.search("あああ", "abc")
+ assert_equal(63, i1)
+ i2 = context.search("あああ", "AAA")
+ assert_equal(nil, i2)
+ i3 = context.search("あああ", nil)
+ assert_equal(62, i3)
+ end
+
private
def new_context(limit = 1 << 31)
klass = Class.new {
diff --git a/test/plum/hpack/test_decoder.rb b/test/plum/hpack/test_decoder.rb
index d8f5d10..9acb821 100644
--- a/test/plum/hpack/test_decoder.rb
+++ b/test/plum/hpack/test_decoder.rb
@@ -25,23 +25,44 @@ class HPACKDecoderTest < Minitest::Test
assert_equal([0b00001111].pack("C*"), buf)
end
+ def test_hpack_read_integer_too_big
+ buf = [0b11011111, 0b10011010, 0b10001010, 0b10001111, 0b11111111, 0b00000011].pack("C*")
+ assert_raises(HPACKError) {
+ new_decoder.__send__(:read_integer!, buf, 5)
+ }
+ end
+
+ def test_hpack_read_integer_incomplete
+ buf = [0b11011111, 0b10011010].pack("C*")
+ assert_raises(HPACKError) {
+ new_decoder.__send__(:read_integer!, buf, 5)
+ }
+ end
+
# C.2.1
def test_hpack_decode_indexing
- encoded = "\x40\x0a\x63\x75\x73\x74\x6f\x6d\x2d\x6b\x65\x79\x0d\x63\x75\x73\x74\x6f\x6d\x2d\x68\x65\x61\x64\x65\x72"
+ encoded = "\x40\x0acustom-key\x0dcustom-header"
result = new_decoder.decode(encoded)
assert_equal([["custom-key", "custom-header"]], result)
end
+ def test_hpack_decode_indexing_imcomplete
+ encoded = "\x40\x0acus"
+ assert_raises(HTTPError) {
+ new_decoder.decode(encoded)
+ }
+ end
+
# C.2.2
def test_hpack_decode_without_indexing
- encoded = "\x04\x0c\x2f\x73\x61\x6d\x70\x6c\x65\x2f\x70\x61\x74\x68"
+ encoded = "\x04\x0c/sample/path"
result = new_decoder.decode(encoded)
assert_equal([[":path", "/sample/path"]], result)
end
# C.2.3
def test_hpack_decode_without_indexing2
- encoded = "\x10\x08\x70\x61\x73\x73\x77\x6f\x72\x64\x06\x73\x65\x63\x72\x65\x74"
+ encoded = "\x10\x08password\x06secret"
result = new_decoder.decode(encoded)
assert_equal([["password", "secret"]], result)
end
diff --git a/test/plum/hpack/test_huffman.rb b/test/plum/hpack/test_huffman.rb
index 5fbda63..27c3411 100644
--- a/test/plum/hpack/test_huffman.rb
+++ b/test/plum/hpack/test_huffman.rb
@@ -26,4 +26,11 @@ class HPACKHuffmanTest < Minitest::Test
Plum::HPACK::Huffman.decode(encoded)
}
end
+
+ def test_eos_in_encoded
+ encoded = "\xff\xff\xff\xff" # \xff\xff\xff\xfc + padding
+ assert_raises(Plum::HPACKError) {
+ Plum::HPACK::Huffman.decode(encoded)
+ }
+ end
end
diff --git a/test/plum/stream/test_handle_frame.rb b/test/plum/stream/test_handle_frame.rb
index a48fe01..dce496f 100644
--- a/test/plum/stream/test_handle_frame.rb
+++ b/test/plum/stream/test_handle_frame.rb
@@ -9,7 +9,7 @@ class StreamHandleFrameTest < Minitest::Test
open_new_stream(state: :open) {|stream|
data = nil
stream.on(:data) {|_data| data = _data }
- stream.process_frame(Frame.new(type: :data, stream_id: stream.id,
+ stream.receive_frame(Frame.new(type: :data, stream_id: stream.id,
flags: [], payload: payload))
assert_equal(payload, data)
}
@@ -20,7 +20,7 @@ class StreamHandleFrameTest < Minitest::Test
open_new_stream(state: :open) {|stream|
data = nil
stream.on(:data) {|_data| data = _data }
- stream.process_frame(Frame.new(type: :data, stream_id: stream.id,
+ stream.receive_frame(Frame.new(type: :data, stream_id: stream.id,
flags: [:padded], payload: "".push_uint8(6).push(payload).push("\x00"*6)))
assert_equal(payload, data)
}
@@ -30,7 +30,7 @@ class StreamHandleFrameTest < Minitest::Test
payload = "ABC" * 5
open_new_stream(state: :open) {|stream|
assert_connection_error(:protocol_error) {
- stream.process_frame(Frame.new(type: :data, stream_id: stream.id,
+ stream.receive_frame(Frame.new(type: :data, stream_id: stream.id,
flags: [:padded], payload: "".push_uint8(100).push(payload).push("\x00"*6)))
}
}
@@ -39,7 +39,7 @@ class StreamHandleFrameTest < Minitest::Test
def test_stream_handle_data_end_stream
payload = "ABC" * 5
open_new_stream(state: :open) {|stream|
- stream.process_frame(Frame.new(type: :data, stream_id: stream.id,
+ stream.receive_frame(Frame.new(type: :data, stream_id: stream.id,
flags: [:end_stream], payload: payload))
assert_equal(:half_closed_remote, stream.state)
}
@@ -48,11 +48,10 @@ class StreamHandleFrameTest < Minitest::Test
def test_stream_handle_data_invalid_state
payload = "ABC" * 5
open_new_stream(state: :half_closed_remote) {|stream|
- stream.process_frame(Frame.new(type: :data, stream_id: stream.id,
- flags: [:end_stream], payload: payload))
- last = sent_frames.last
- assert_equal(:rst_stream, last.type)
- assert_equal(StreamError.new(:stream_closed).http2_error_code, last.payload.uint32)
+ assert_stream_error(:stream_closed) {
+ stream.receive_frame(Frame.new(type: :data, stream_id: stream.id,
+ flags: [:end_stream], payload: payload))
+ }
}
end
@@ -63,7 +62,7 @@ class StreamHandleFrameTest < Minitest::Test
stream.on(:headers) {|_headers|
headers = _headers
}
- stream.process_frame(Frame.new(type: :headers,
+ stream.receive_frame(Frame.new(type: :headers,
stream_id: stream.id,
flags: [:end_headers],
payload: HPACK::Encoder.new(0).encode([[":path", "/"]])))
@@ -79,12 +78,12 @@ class StreamHandleFrameTest < Minitest::Test
stream.on(:headers) {|_headers|
headers = _headers
}
- stream.process_frame(Frame.new(type: :headers,
+ stream.receive_frame(Frame.new(type: :headers,
stream_id: stream.id,
flags: [:end_stream],
payload: payload[0..4]))
assert_equal(nil, headers) # wait CONTINUATION
- stream.process_frame(Frame.new(type: :continuation,
+ stream.receive_frame(Frame.new(type: :continuation,
stream_id: stream.id,
flags: [:end_headers],
payload: payload[5..-1]))
@@ -100,7 +99,7 @@ class StreamHandleFrameTest < Minitest::Test
stream.on(:headers) {|_headers|
headers = _headers
}
- stream.process_frame(Frame.new(type: :headers,
+ stream.receive_frame(Frame.new(type: :headers,
stream_id: stream.id,
flags: [:end_headers, :padded],
payload: "".push_uint8(payload.bytesize).push(payload).push("\x00"*payload.bytesize)))
@@ -112,7 +111,7 @@ class StreamHandleFrameTest < Minitest::Test
open_new_stream {|stream|
payload = HPACK::Encoder.new(0).encode([[":path", "/"]])
assert_connection_error(:protocol_error) {
- stream.process_frame(Frame.new(type: :headers,
+ stream.receive_frame(Frame.new(type: :headers,
stream_id: stream.id,
flags: [:end_headers, :padded],
payload: "".push_uint8(payload.bytesize+1).push(payload).push("\x00"*(payload.bytesize+1))))
@@ -124,7 +123,7 @@ class StreamHandleFrameTest < Minitest::Test
open_new_stream {|stream|
payload = "\x00\x01\x02"
assert_connection_error(:compression_error) {
- stream.process_frame(Frame.new(type: :headers,
+ stream.receive_frame(Frame.new(type: :headers,
stream_id: stream.id,
flags: [:end_headers],
payload: payload))
@@ -136,19 +135,18 @@ class StreamHandleFrameTest < Minitest::Test
_payload = HPACK::Encoder.new(0).encode([[":path", "/"]])
open_new_stream(state: :reserved_local) {|stream|
assert_connection_error(:protocol_error) {
- stream.process_frame(Frame.new(type: :headers, stream_id: stream.id, flags: [:end_headers, :end_stream], payload: _payload))
+ stream.receive_frame(Frame.new(type: :headers, stream_id: stream.id, flags: [:end_headers, :end_stream], payload: _payload))
}
}
open_new_stream(state: :closed) {|stream|
assert_connection_error(:stream_closed) {
- stream.process_frame(Frame.new(type: :headers, stream_id: stream.id, flags: [:end_headers, :end_stream], payload: _payload))
+ stream.receive_frame(Frame.new(type: :headers, stream_id: stream.id, flags: [:end_headers, :end_stream], payload: _payload))
}
}
open_new_stream(state: :half_closed_remote) {|stream|
- stream.process_frame(Frame.new(type: :headers, stream_id: stream.id, flags: [:end_headers, :end_stream], payload: _payload))
- last = sent_frames.last
- assert_equal(:rst_stream, last.type)
- assert_equal(StreamError.new(:stream_closed).http2_error_code, last.payload.uint32)
+ assert_stream_error(:stream_closed) {
+ stream.receive_frame(Frame.new(type: :headers, stream_id: stream.id, flags: [:end_headers, :end_stream], payload: _payload))
+ }
}
end
@@ -163,7 +161,7 @@ class StreamHandleFrameTest < Minitest::Test
payload = "".push_uint32((1 << 31) | parent.id)
.push_uint8(50)
.push(header_block)
- stream.process_frame(Frame.new(type: :headers,
+ stream.receive_frame(Frame.new(type: :headers,
stream_id: stream.id,
flags: [:end_headers, :priority],
payload: payload))
@@ -182,7 +180,7 @@ class StreamHandleFrameTest < Minitest::Test
payload = "".push_uint32((1 << 31) | parent.id)
.push_uint8(50)
- stream.process_frame(Frame.new(type: :priority,
+ stream.receive_frame(Frame.new(type: :priority,
stream_id: stream.id,
payload: payload))
assert_equal(true, stream.exclusive)
@@ -195,12 +193,12 @@ class StreamHandleFrameTest < Minitest::Test
open_server_connection {|con|
stream = open_new_stream(con)
payload = "".push_uint32((1 << 31) | stream.id).push_uint8(6)
- stream.process_frame(Frame.new(type: :priority,
+ stream.receive_frame(Frame.new(type: :priority,
stream_id: stream.id,
payload: payload))
last = sent_frames.last
assert_equal(:rst_stream, last.type)
- assert_equal(ERROR_CODES[:protocol_error], last.payload.uint32)
+ assert_equal(HTTPError::ERROR_CODES[:protocol_error], last.payload.uint32)
}
end
@@ -212,7 +210,7 @@ class StreamHandleFrameTest < Minitest::Test
stream2 = open_new_stream(con, parent: parent)
payload = "".push_uint32((1 << 31) | parent.id).push_uint8(6)
- stream0.process_frame(Frame.new(type: :priority,
+ stream0.receive_frame(Frame.new(type: :priority,
stream_id: stream0.id,
payload: payload))
assert_equal(parent, stream0.parent)
@@ -221,10 +219,20 @@ class StreamHandleFrameTest < Minitest::Test
}
end
+ def test_stream_handle_frame_size_error
+ open_new_stream {|stream|
+ assert_stream_error(:frame_size_error) {
+ stream.receive_frame(Frame.new(type: :priority,
+ stream_id: stream.id,
+ payload: "\x00"))
+ }
+ }
+ end
+
## RST_STREAM
def test_stream_handle_rst_stream
open_new_stream(state: :reserved_local) {|stream|
- stream.process_frame(Frame.new(type: :rst_stream,
+ stream.receive_frame(Frame.new(type: :rst_stream,
stream_id: stream.id,
payload: "\x00\x00\x00\x00"))
assert_equal(:closed, stream.state)
@@ -234,7 +242,7 @@ class StreamHandleFrameTest < Minitest::Test
def test_stream_handle_rst_stream_idle
open_new_stream(state: :idle) {|stream|
assert_connection_error(:protocol_error) {
- stream.process_frame(Frame.new(type: :rst_stream,
+ stream.receive_frame(Frame.new(type: :rst_stream,
stream_id: stream.id,
payload: "\x00\x00\x00\x00"))
}
@@ -244,7 +252,7 @@ class StreamHandleFrameTest < Minitest::Test
def test_stream_handle_rst_stream_frame_size
open_new_stream(state: :reserved_local) {|stream|
assert_connection_error(:frame_size_error) {
- stream.process_frame(Frame.new(type: :rst_stream,
+ stream.receive_frame(Frame.new(type: :rst_stream,
stream_id: stream.id,
payload: "\x00\x00\x00"))
}
diff --git a/test/plum/test_binary_string.rb b/test/plum/test_binary_string.rb
index 69c9c3a..403248b 100644
--- a/test/plum/test_binary_string.rb
+++ b/test/plum/test_binary_string.rb
@@ -48,4 +48,17 @@ class BinaryStringTest < Minitest::Test
assert_equal("\xf0".b, sushi.byteshift(1).b)
assert_equal("\x9f\x8d\xa3".b, sushi.b)
end
+
+ def test_each_byteslice_block
+ ret = []
+ string = "12345678"
+ string.each_byteslice(3) {|part| ret << part }
+ assert_equal(["123", "456", "78"], ret)
+ end
+
+ def test_each_byteslice_enume
+ string = "12345678"
+ ret = string.each_byteslice(3)
+ assert_equal(["123", "456", "78"], ret.to_a)
+ end
end
diff --git a/test/plum/test_server_connection.rb b/test/plum/test_connection.rb
index 5bfdf98..4c9668b 100644
--- a/test/plum/test_server_connection.rb
+++ b/test/plum/test_connection.rb
@@ -2,16 +2,38 @@ require "test_helper"
using Plum::BinaryString
-class ServerConnectionTest < Minitest::Test
- def test_server_must_raise_cframe_size_error_when_exeeeded_max_size
+class ConnectionTest < Minitest::Test
+ def test_server_must_raise_frame_size_error_when_exeeeded_max_size
_settings = "".push_uint16(Frame::SETTINGS_TYPE[:max_frame_size]).push_uint32(2**14)
- con = open_server_connection
- con.settings(max_frame_size: 2**14)
- refute_raises {
- con << Frame.new(type: :settings, stream_id: 0, payload: _settings*(2**14/6)).assemble
+ limit = 2 ** 14
+
+ new_con = -> (&blk) {
+ c = open_server_connection
+ c.settings(max_frame_size: limit)
+ blk.call c
+ }
+
+ new_con.call {|con|
+ assert_no_error {
+ con << Frame.new(type: :settings, stream_id: 0, payload: _settings * (limit / 6)).assemble
+ }
}
- assert_connection_error(:frame_size_error) {
- con << Frame.new(type: :settings, stream_id: 0, payload: _settings*((2**14)/6+1)).assemble
+ new_con.call {|con|
+ assert_connection_error(:frame_size_error) {
+ con << Frame.new(type: :settings, stream_id: 0, payload: _settings * (limit / 6 + 1)).assemble
+ }
+ }
+
+ new_con.call {|con|
+ assert_connection_error(:frame_size_error) {
+ con << Frame.new(type: :headers, stream_id: 3, payload: "\x00" * (limit + 1)).assemble
+ }
+ }
+ new_con.call {|con|
+ assert_stream_error(:frame_size_error) {
+ con << Frame.new(type: :headers, stream_id: 3, flags: [:end_headers], payload: "").assemble
+ con << Frame.new(type: :data, stream_id: 3, payload: "\x00" * (limit + 1)).assemble
+ }
}
end
@@ -26,8 +48,8 @@ class ServerConnectionTest < Minitest::Test
def test_server_ignore_unknown_frame_type
open_server_connection {|con|
- refute_raises {
- con << Frame.new(type_value: 0x0f, stream_id: 0).assemble
+ assert_no_error {
+ con << "\x00\x00\x00\x0f\x00\x00\x00\x00\x00" # type: 0x0f, no flags, no payload, stream 0
}
}
end
diff --git a/test/plum/test_connection_utils.rb b/test/plum/test_connection_utils.rb
new file mode 100644
index 0000000..faa8782
--- /dev/null
+++ b/test/plum/test_connection_utils.rb
@@ -0,0 +1,29 @@
+require "test_helper"
+
+using BinaryString
+
+class ServerConnectionUtilsTest < Minitest::Test
+ def test_server_ping
+ open_server_connection {|con|
+ con.ping("ABCABCAB")
+
+ last = sent_frames.last
+ assert_equal(:ping, last.type)
+ assert_equal([], last.flags)
+ assert_equal("ABCABCAB", last.payload)
+ }
+ end
+
+ def test_server_goaway
+ open_server_connection {|con|
+ con << Frame.headers(3, "", :end_stream, :end_headers).assemble
+ con.goaway(:stream_closed)
+
+ last = sent_frames.last
+ assert_equal(:goaway, last.type)
+ assert_equal([], last.flags)
+ assert_equal(3, last.payload.uint32)
+ assert_equal(HTTPError::ERROR_CODES[:stream_closed], last.payload.uint32(4))
+ }
+ end
+end
diff --git a/test/plum/test_flow_control.rb b/test/plum/test_flow_control.rb
index f5d9a1b..758472d 100644
--- a/test/plum/test_flow_control.rb
+++ b/test/plum/test_flow_control.rb
@@ -31,14 +31,21 @@ class FlowControlTest < Minitest::Test
def test_flow_control_window_update_zero
open_new_stream {|stream|
- con = stream.connection
- # stream error
- con << Frame.new(type: :window_update,
- stream_id: stream.id,
- payload: "".push_uint32(0)).assemble
- last = sent_frames.last
- assert_equal(:rst_stream, last.type)
- assert_equal(ERROR_CODES[:protocol_error], last.payload.uint32)
+ assert_stream_error(:protocol_error) {
+ stream.receive_frame Frame.new(type: :window_update,
+ stream_id: stream.id,
+ payload: "".push_uint32(0))
+ }
+ }
+ end
+
+ def test_flow_control_window_update_frame_size
+ open_new_stream {|stream|
+ assert_connection_error(:frame_size_error) {
+ stream.receive_frame Frame.new(type: :window_update,
+ stream_id: stream.id,
+ payload: "".push_uint16(0))
+ }
}
end
@@ -90,7 +97,7 @@ class FlowControlTest < Minitest::Test
}
end
- def test_flow_control_update_initial_window_size
+ def test_flow_control_update_send_initial_window_size
open_new_stream {|stream|
con = stream.connection
con << Frame.new(type: :settings,
@@ -116,4 +123,45 @@ class FlowControlTest < Minitest::Test
assert_equal(3, last.payload.uint32)
}
end
+
+ def test_flow_control_recv_window_exceeded
+ prepare = ->(&blk) {
+ open_new_stream {|stream|
+ con = stream.connection
+ con.settings(initial_window_size: 24)
+ blk.call(con, stream)
+ }
+ }
+
+ prepare.call {|con, stream|
+ con.window_update(500) # extend only connection
+ con << Frame.headers(stream.id, "", :end_headers).assemble
+ assert_stream_error(:flow_control_error) {
+ con << Frame.data(stream.id, "\x00" * 30, :end_stream).assemble
+ }
+ }
+
+ prepare.call {|con, stream|
+ stream.window_update(500) # extend only stream
+ con << Frame.headers(stream.id, "", :end_headers).assemble
+ assert_connection_error(:flow_control_error) {
+ con << Frame.data(stream.id, "\x00" * 30, :end_stream).assemble
+ }
+ }
+ end
+
+ def test_flow_control_update_recv_initial_window_size
+ open_new_stream {|stream|
+ con = stream.connection
+ con.settings(initial_window_size: 24)
+ stream.window_update(1)
+ con << Frame.headers(stream.id, "", :end_headers).assemble
+ con << Frame.data(stream.id, "\x00" * 20, :end_stream).assemble
+ assert_equal(4, con.recv_remaining_window)
+ assert_equal(5, stream.recv_remaining_window)
+ con.settings(initial_window_size: 60)
+ assert_equal(40, con.recv_remaining_window)
+ assert_equal(41, stream.recv_remaining_window)
+ }
+ end
end
diff --git a/test/plum/test_frame.rb b/test/plum/test_frame.rb
index ecb2b88..0cd9e7e 100644
--- a/test/plum/test_frame.rb
+++ b/test/plum/test_frame.rb
@@ -18,21 +18,22 @@ class FrameTest < Minitest::Test
def test_parse
# R 0x1, stream_id 0x4, body "abc"
- buffer = "\x00\x00\x03" << "\x00" << "\x20" << "\x80\x00\x00\x04" << "abc" << "next_frame_data"
+ buffer = "\x00\x00\x03" << "\x00" << "\x09" << "\x80\x00\x00\x04" << "abc" << "next_frame_data"
frame = Plum::Frame.parse!(buffer)
- assert_equal(frame.length, 3)
- assert_equal(frame.type_value, 0x00)
- assert_equal(frame.flags_value, 0x20)
- assert_equal(frame.stream_id, 0x04)
- assert_equal(frame.payload, "abc")
- assert_equal(buffer, "next_frame_data")
+ assert_equal(3, frame.length)
+ assert_equal(:data, frame.type)
+ assert_equal([:end_stream, :padded], frame.flags)
+ assert_equal(0x04, frame.stream_id)
+ assert_equal("abc", frame.payload)
+ assert_equal("next_frame_data", buffer)
+ assert_equal(true, frame.frozen?)
end
# Frame#assemble
def test_assemble
- frame = Plum::Frame.new(length: 4, type_value: 5, flags_value: 0x5f, stream_id: 0x678, payload: "payl")
- bin = "\x00\x00\x04" << "\x05" << "\x5f" << "\x00\x00\x06\x78" << "payl"
- assert_equal(frame.assemble, bin)
+ frame = Plum::Frame.new(type: :push_promise, flags: [:end_headers, :padded], stream_id: 0x678, payload: "payl")
+ bin = "\x00\x00\x04" << "\x05" << "\x0c" << "\x00\x00\x06\x78" << "payl"
+ assert_equal(bin, frame.assemble)
end
# Frame#generate
@@ -40,12 +41,12 @@ class FrameTest < Minitest::Test
frame = Plum::Frame.new(type: :data,
stream_id: 12345,
flags: [:end_stream, :padded],
- payload: "ぺいろーど")
- assert_equal(frame.payload, "ぺいろーど")
- assert_equal(frame.length, "ぺいろーど".bytesize)
- assert_equal(frame.type_value, 0x00) # DATA
- assert_equal(frame.flags_value, 0x09) # 0x01 | 0x08
- assert_equal(frame.stream_id, 12345)
+ payload: "ぺいろーど".encode(Encoding::UTF_8))
+ assert_equal("ぺいろーど", frame.payload)
+ assert_equal("ぺいろーど".bytesize, frame.length)
+ assert_equal(:data, frame.type) # DATA
+ assert_equal([:end_stream, :padded], frame.flags) # 0x01 | 0x08
+ assert_equal(12345, frame.stream_id)
end
def test_inspect
@@ -53,8 +54,6 @@ class FrameTest < Minitest::Test
stream_id: 12345,
flags: [:end_stream, :padded],
payload: "ぺいろーど")
- refute_raises {
- frame.inspect
- }
+ frame.inspect
end
end
diff --git a/test/plum/test_frame_factory.rb b/test/plum/test_frame_factory.rb
new file mode 100644
index 0000000..ff8e67f
--- /dev/null
+++ b/test/plum/test_frame_factory.rb
@@ -0,0 +1,56 @@
+require "test_helper"
+
+using Plum::BinaryString
+class FrameFactoryTest < Minitest::Test
+ def test_rst_stream
+ frame = Frame.rst_stream(123, :stream_closed)
+ assert_frame(frame,
+ type: :rst_stream,
+ stream_id: 123)
+ assert_equal(HTTPError::ERROR_CODES[:stream_closed], frame.payload.uint32)
+ end
+
+ def test_goaway
+ frame = Frame.goaway(0x55, :stream_closed, "debug")
+ assert_frame(frame,
+ type: :goaway,
+ stream_id: 0,
+ payload: "\x00\x00\x00\x55\x00\x00\x00\x05debug")
+ end
+
+ def test_settings
+ frame = Frame.settings(header_table_size: 0x1010)
+ assert_frame(frame,
+ type: :settings,
+ stream_id: 0,
+ flags: [],
+ payload: "\x00\x01\x00\x00\x10\x10")
+ end
+
+ def test_settings_ack
+ frame = Frame.settings(:ack)
+ assert_frame(frame,
+ type: :settings,
+ stream_id: 0,
+ flags: [:ack],
+ payload: "")
+ end
+
+ def test_ping
+ frame = Frame.ping("12345678")
+ assert_frame(frame,
+ type: :ping,
+ stream_id: 0,
+ flags: [],
+ payload: "12345678")
+ end
+
+ def test_ping_ack
+ frame = Frame.ping(:ack, "12345678")
+ assert_frame(frame,
+ type: :ping,
+ stream_id: 0,
+ flags: [:ack],
+ payload: "12345678")
+ end
+end
diff --git a/test/plum/test_frame_helper.rb b/test/plum/test_frame_utils.rb
index 3a463ab..4564e9a 100644
--- a/test/plum/test_frame_helper.rb
+++ b/test/plum/test_frame_utils.rb
@@ -1,6 +1,6 @@
require "test_helper"
-class FrameHelperTest < Minitest::Test
+class FrameUtilsTest < Minitest::Test
def test_frame_enough_short
frame = Frame.new(type: :data, stream_id: 1, payload: "123")
ret = frame.split_data(3)
@@ -29,10 +29,18 @@ class FrameHelperTest < Minitest::Test
ret = frame.split_headers(3)
assert_equal(3, ret.size)
assert_equal("123", ret[0].payload)
- assert_equal([:priority, :end_stream].sort, ret[0].flags.sort)
+ assert_equal([:end_stream, :priority], ret[0].flags)
assert_equal("456", ret[1].payload)
assert_equal([], ret[1].flags)
assert_equal("7", ret[2].payload)
assert_equal([:end_headers], ret[2].flags)
end
+
+ def test_frame_parse_settings
+ # :header_table_size => 0x1010, :enable_push => 0x00, :header_table_size => 0x1011 (overwrite)
+ frame = Frame.new(type: :settings, flags: [], stream_id: 0, payload: "\x00\x01\x00\x00\x10\x10\x00\x02\x00\x00\x00\x00\x00\x01\x00\x00\x10\x11")
+ ret = frame.parse_settings
+ assert_equal(0x1011, ret[:header_table_size])
+ assert_equal(0x0000, ret[:enable_push])
+ end
end
diff --git a/test/plum/server_connection/test_negotiation.rb b/test/plum/test_https_connection.rb
index fc9287e..ba86b1e 100644
--- a/test/plum/server_connection/test_negotiation.rb
+++ b/test/plum/test_https_connection.rb
@@ -2,35 +2,36 @@ require "test_helper"
using Plum::BinaryString
-class ServerConnectionNegotiationTest < Minitest::Test
+class HTTPSConnectionNegotiationTest < Minitest::Test
def test_server_must_raise_cprotocol_error_invalid_magic_short
- con = ServerConnection.new(nil)
+ con = HTTPSConnection.new(StringIO.new)
assert_connection_error(:protocol_error) {
con << "HELLO"
}
end
def test_server_must_raise_cprotocol_error_invalid_magic_long
- con = ServerConnection.new(nil)
+ con = HTTPSConnection.new(StringIO.new)
assert_connection_error(:protocol_error) {
con << ("HELLO" * 100) # over 24
}
end
def test_server_must_raise_cprotocol_error_non_settings_after_magic
- con = ServerConnection.new(nil)
- con << ServerConnection::CLIENT_CONNECTION_PREFACE
+ con = HTTPSConnection.new(StringIO.new)
+ con << Connection::CLIENT_CONNECTION_PREFACE
assert_connection_error(:protocol_error) {
con << Frame.new(type: :window_update, stream_id: 0, payload: "".push_uint32(1)).assemble
}
end
def test_server_accept_fragmented_magic
- io = StringIO.new
- magic = ServerConnection::CLIENT_CONNECTION_PREFACE
- con = ServerConnection.new(io)
- con << magic[0...5]
- con << magic[5..-1]
- con << Frame.new(type: :settings, stream_id: 0).assemble
+ magic = Connection::CLIENT_CONNECTION_PREFACE
+ con = HTTPSConnection.new(StringIO.new)
+ assert_no_error {
+ con << magic[0...5]
+ con << magic[5..-1]
+ con << Frame.new(type: :settings, stream_id: 0).assemble
+ }
end
end
diff --git a/test/plum/test_server_connection_helper.rb b/test/plum/test_server_connection_helper.rb
deleted file mode 100644
index 383679c..0000000
--- a/test/plum/test_server_connection_helper.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require "test_helper"
-
-using BinaryString
-
-class ServerConnectionHelperTest < Minitest::Test
- def test_server_ping
- open_server_connection {|con|
- con.ping("ABCABCAB")
-
- last = sent_frames.last
- assert_equal(:ping, last.type)
- assert_equal([], last.flags)
- assert_equal("ABCABCAB", last.payload)
- }
- end
-end
diff --git a/test/plum/test_stream.rb b/test/plum/test_stream.rb
index ebe881f..9df9a55 100644
--- a/test/plum/test_stream.rb
+++ b/test/plum/test_stream.rb
@@ -6,22 +6,22 @@ class StreamTest < Minitest::Test
def test_stream_illegal_frame_type
open_new_stream {|stream|
assert_connection_error(:protocol_error) {
- stream.process_frame(Frame.new(type: :goaway, stream_id: stream.id, payload: "\x00\x00\x00\x00"))
+ stream.receive_frame(Frame.new(type: :goaway, stream_id: stream.id, payload: "\x00\x00\x00\x00"))
}
}
end
def test_stream_unknown_frame_type
open_new_stream {|stream|
- refute_raises {
- stream.process_frame(Frame.new(type_value: 0x0f, stream_id: stream.id, payload: "\x00\x00\x00\x00"))
+ assert_no_error {
+ stream.receive_frame(Frame.new(type_value: 0x0f, stream_id: stream.id, payload: "\x00\x00\x00\x00"))
}
}
end
def test_stream_close
open_new_stream(state: :half_closed_local) {|stream|
- stream.close(StreamError.new(:frame_size_error).http2_error_code)
+ stream.close(:frame_size_error)
last = sent_frames.last
assert_equal(:rst_stream, last.type)
diff --git a/test/plum/test_stream_helper.rb b/test/plum/test_stream_utils.rb
index 1d9b017..c230bf2 100644
--- a/test/plum/test_stream_helper.rb
+++ b/test/plum/test_stream_utils.rb
@@ -2,7 +2,7 @@ require "test_helper"
using BinaryString
-class StreamHelperTest < Minitest::Test
+class StreamUtilsTest < Minitest::Test
def test_stream_promise
open_new_stream {|stream|
push_stream = stream.promise([])
diff --git a/test/test_helper.rb b/test/test_helper.rb
index fb5aa91..c5fae66 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -15,6 +15,7 @@ unless ENV["SKIP_COVERAGE"]
end
end
+require "timeout"
require "minitest"
require "minitest/unit"
require "minitest/autorun"
diff --git a/test/utils/assertions.rb b/test/utils/assertions.rb
index 7c4b094..71928ed 100644
--- a/test/utils/assertions.rb
+++ b/test/utils/assertions.rb
@@ -1,14 +1,4 @@
module CustomAssertions
- def assert_http_error(klass, type, &blk)
- begin
- blk.call
- rescue klass => e
- assert_equal(type, e.http2_error_type)
- else
- flunk "#{klass.name} type: #{type} expected but nothing was raised."
- end
- end
-
def assert_connection_error(type, &blk)
assert_http_error(Plum::ConnectionError, type, &blk)
end
@@ -17,18 +7,54 @@ module CustomAssertions
assert_http_error(Plum::StreamError, type, &blk)
end
- def refute_raises(&blk)
+ def assert_no_error(stream: nil, connection: nil, &blk)
+ Plum::ConnectionError.reset
+ Plum::StreamError.reset
begin
blk.call
- rescue
- a = $!
- else
- a = nil
+ rescue Plum::HTTPError
end
- assert(!a, "No exceptions expected but raised: #{a}:\n#{a && a.backtrace.join("\n")}")
+ assert_nil(Plum::StreamError.last, "No stream error expected but raised: #{Plum::StreamError.last}")
+ assert_nil(Plum::ConnectionError.last, "No connection error expected but raised: #{Plum::ConnectionError.last}")
+ end
+
+ def assert_frame(frame, **args)
+ args.each do |name, value|
+ assert_equal(value, frame.__send__(name))
+ end
+ end
+
+ private
+ def assert_http_error(klass, type, &blk)
+ klass.reset
+ begin
+ blk.call
+ rescue klass
+ end
+ last = klass.last
+ assert(last, "#{klass.name} type: #{type} expected but nothing was raised.")
+ assert_equal(type, last, "#{klass.name} type: #{type} expected but type: #{last} was raised.")
end
end
+Minitest::Test.__send__(:prepend, CustomAssertions)
+
+module LastErrorExtension
+ def initialize(type, message = nil)
+ super
+ self.class.last = type
+ end
-class Minitest::Test
- include CustomAssertions
+ module ClassMethods
+ attr_accessor :last
+ def reset
+ self.last = nil
+ end
+ end
+
+ def self.prepended(base)
+ base.extend(ClassMethods)
+ base.reset
+ end
end
+Plum::ConnectionError.__send__(:prepend, LastErrorExtension)
+Plum::StreamError.__send__(:prepend, LastErrorExtension)
diff --git a/test/utils/server.rb b/test/utils/server.rb
index 91c0b59..e3b8386 100644
--- a/test/utils/server.rb
+++ b/test/utils/server.rb
@@ -1,10 +1,10 @@
require "timeout"
module ServerUtils
- def open_server_connection
+ def open_server_connection(scheme = :https)
io = StringIO.new
- @_con = ServerConnection.new(io)
- @_con << ServerConnection::CLIENT_CONNECTION_PREFACE
+ @_con = (scheme == :https ? HTTPSConnection : HTTPConnection).new(io)
+ @_con << Connection::CLIENT_CONNECTION_PREFACE
@_con << Frame.new(type: :settings, stream_id: 0).assemble
if block_given?
yield @_con
@@ -14,7 +14,7 @@ module ServerUtils
end
def open_new_stream(arg1 = nil, **kwargs)
- if arg1.is_a?(ServerConnection)
+ if arg1.is_a?(Connection)
con = arg1
else
con = open_server_connection
@@ -31,7 +31,7 @@ module ServerUtils
end
def sent_frames(con = nil)
- resp = (con || @_con).socket.string.dup
+ resp = (con || @_con).io.string.dup
frames = []
while f = Frame.parse!(resp)
frames << f
@@ -39,52 +39,22 @@ module ServerUtils
frames
end
- def start_server(&blk)
- ctx = OpenSSL::SSL::SSLContext.new
- ctx.alpn_select_cb = -> protocols { "h2" }
- ctx.cert = OpenSSL::X509::Certificate.new File.read(File.expand_path("../../server.crt", __FILE__))
- ctx.key = OpenSSL::PKey::RSA.new File.read(File.expand_path("../../server.key", __FILE__))
- tcp_server = TCPServer.new("127.0.0.1", LISTEN_PORT)
- ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
-
- plum = Plum::ServerConnection.new(nil)
-
- server_thread = Thread.new {
- begin
- timeout(3) {
- sock = ssl_server.accept
- plum.instance_eval { @socket = sock }
- plum.start
- }
- rescue TimeoutError
- flunk "server timeout"
- ensure
- tcp_server.close
- end
- }
- client_thread = Thread.new {
- begin
- timeout(3) { blk.call(plum) }
- rescue TimeoutError
- flunk "client timeout"
- end
- }
- client_thread.join
- server_thread.join
+ def capture_frames(con = nil, &blk)
+ io = (con || @_con).io
+ pos = io.string.bytesize
+ blk.call
+ resp = io.string.byteslice(pos, io.string.bytesize - pos)
+ frames = []
+ while f = Frame.parse!(resp)
+ frames << f
+ end
+ frames
end
- # Connect to server and returns client socket
- def start_client(ctx = nil, &blk)
- ctx ||= OpenSSL::SSL::SSLContext.new.tap {|ctx|
- ctx.alpn_protocols = ["h2"]
- }
-
- sock = TCPSocket.new("127.0.0.1", LISTEN_PORT)
- ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
- ssl.connect
- blk.call(ssl)
- ensure
- ssl.close
+ def capture_frame(con = nil, &blk)
+ frames = capture_frames(con, &blk)
+ assert_equal(1, frames.size, "Supplied block sent no frames or more than 1 frame")
+ frames.first
end
end