diff options
author | Kazuki Yamaguchi <k@rhe.jp> | 2015-08-13 22:25:20 +0900 |
---|---|---|
committer | Kazuki Yamaguchi <k@rhe.jp> | 2015-08-13 22:25:20 +0900 |
commit | eb5facc1549cf044bf474b52a6a78dc55ea5fa0a (patch) | |
tree | 7014c7cb540bd9ae931ffe3aff41ae84bca9aae4 | |
parent | 799da829bcc62bc92c1cb3088591bdb9a92d1799 (diff) | |
parent | 6b0787d8ceb85f4eb6feeba091529e71c82dd07d (diff) | |
download | plum-eb5facc1549cf044bf474b52a6a78dc55ea5fa0a.tar.gz |
Merge branch 'master' of github.com:rhenium/plum
47 files changed, 1207 insertions, 824 deletions
@@ -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 @@ -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 |