From de36239d9abcc93f0bd0fa56ed9d0af2234294dc Mon Sep 17 00:00:00 2001 From: Maruth Goyal Date: Mon, 6 Nov 2023 11:19:27 -0800 Subject: [PATCH 1/6] ensure wire always contains a full frame --- lib/protocol/http2/frame.rb | 7 ++++--- lib/protocol/http2/framer.rb | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/protocol/http2/frame.rb b/lib/protocol/http2/frame.rb index 690816e..3c05a9c 100644 --- a/lib/protocol/http2/frame.rb +++ b/lib/protocol/http2/frame.rb @@ -146,7 +146,7 @@ def self.parse_header(buffer) end def read_header(stream) - if buffer = stream.read(9) and buffer.bytesize == 9 + if buffer = stream.peek(9) and buffer.bytesize == 9 @length, @type, @flags, @stream_id = Frame.parse_header(buffer) # puts "read_header: #{@length} #{@type} #{@flags} #{@stream_id}" else @@ -155,8 +155,9 @@ def read_header(stream) end def read_payload(stream) - if payload = stream.read(@length) and payload.bytesize == @length - @payload = payload + length_with_header = 9 + @length + if full_frame = stream.read(length_with_header) and full_frame.bytesize == length_with_header + @payload = full_frame[9..] else raise EOFError, "Could not read frame payload!" end diff --git a/lib/protocol/http2/framer.rb b/lib/protocol/http2/framer.rb index 6c5bcc7..67e5667 100644 --- a/lib/protocol/http2/framer.rb +++ b/lib/protocol/http2/framer.rb @@ -95,7 +95,7 @@ def write_frame(frame) end def read_header - if buffer = @stream.read(9) + if buffer = @stream.peek(9) if buffer.bytesize == 9 return Frame.parse_header(buffer) end From 6cc619a6f051fbb51824594a9e4d4fd89d406777 Mon Sep 17 00:00:00 2001 From: Maruth Goyal Date: Tue, 23 Jan 2024 09:16:11 -0800 Subject: [PATCH 2/6] always wrap in Async::IO::Stream --- lib/protocol/http2/framer.rb | 3 ++- protocol-http2.gemspec | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/protocol/http2/framer.rb b/lib/protocol/http2/framer.rb index 67e5667..5ff1cd7 100644 --- a/lib/protocol/http2/framer.rb +++ b/lib/protocol/http2/framer.rb @@ -15,6 +15,7 @@ require_relative 'goaway_frame' require_relative 'window_update_frame' require_relative 'continuation_frame' +require 'async/io' module Protocol module HTTP2 @@ -37,7 +38,7 @@ module HTTP2 class Framer def initialize(stream, frames = FRAMES) - @stream = stream + @stream = Async::IO::Stream.new(stream) @frames = frames end diff --git a/protocol-http2.gemspec b/protocol-http2.gemspec index 18cbf7c..5f66fdc 100644 --- a/protocol-http2.gemspec +++ b/protocol-http2.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.7.6" + spec.add_dependency "async-io", "~> 1.37" spec.add_dependency "protocol-hpack", "~> 1.4" spec.add_dependency "protocol-http", "~> 0.18" end From e54262a7f051560410138858c19fcc166df4ede8 Mon Sep 17 00:00:00 2001 From: Maruth Goyal Date: Tue, 23 Jan 2024 10:52:21 -0800 Subject: [PATCH 3/6] fix import --- lib/protocol/http2/framer.rb | 174 +++++++++++++++++------------------ 1 file changed, 86 insertions(+), 88 deletions(-) diff --git a/lib/protocol/http2/framer.rb b/lib/protocol/http2/framer.rb index 5ff1cd7..1ff2d1e 100644 --- a/lib/protocol/http2/framer.rb +++ b/lib/protocol/http2/framer.rb @@ -16,94 +16,92 @@ require_relative 'window_update_frame' require_relative 'continuation_frame' require 'async/io' +require 'async/io/stream' module Protocol - module HTTP2 - # HTTP/2 frame type mapping as defined by the spec - FRAMES = [ - DataFrame, - HeadersFrame, - PriorityFrame, - ResetStreamFrame, - SettingsFrame, - PushPromiseFrame, - PingFrame, - GoawayFrame, - WindowUpdateFrame, - ContinuationFrame, - ].freeze - - # Default connection "fast-fail" preamble string as defined by the spec. - CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze - - class Framer - def initialize(stream, frames = FRAMES) - @stream = Async::IO::Stream.new(stream) - @frames = frames - end - - def close - @stream.close - end - - def closed? - @stream.closed? - end - - def write_connection_preface - @stream.write(CONNECTION_PREFACE) - end - - def read_connection_preface - string = @stream.read(CONNECTION_PREFACE.bytesize) - - unless string == CONNECTION_PREFACE - raise HandshakeError, "Invalid connection preface: #{string.inspect}" - end - - return string - end - - # @return [Frame] the frame that has been read from the underlying IO. - # @raise if the underlying IO fails for some reason. - def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE) - # Read the header: - length, type, flags, stream_id = read_header - - # Async.logger.debug(self) {"read_frame: length=#{length} type=#{type} flags=#{flags} stream_id=#{stream_id} -> klass=#{@frames[type].inspect}"} - - # Allocate the frame: - klass = @frames[type] || Frame - frame = klass.new(stream_id, flags, type, length) - - # Read the payload: - frame.read(@stream, maximum_frame_size) - - # Async.logger.debug(self, name: "read") {frame.inspect} - - return frame - end - - def write_frame(frame) - # Async.logger.debug(self, name: "write") {frame.inspect} - - frame.write(@stream) - - # Don't call @stream.flush here because it can cause significant contention if there is a semaphore around this method. - # @stream.flush - - return frame - end - - def read_header - if buffer = @stream.peek(9) - if buffer.bytesize == 9 - return Frame.parse_header(buffer) - end - end - - raise EOFError, "Could not read frame header!" - end - end - end + module HTTP2 + # HTTP/2 frame type mapping as defined by the spec + FRAMES = [ + DataFrame, + HeadersFrame, + PriorityFrame, + ResetStreamFrame, + SettingsFrame, + PushPromiseFrame, + PingFrame, + GoawayFrame, + WindowUpdateFrame, + ContinuationFrame + ].freeze + + # Default connection "fast-fail" preamble string as defined by the spec. + CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + + class Framer + def initialize(stream, frames = FRAMES) + @stream = Async::IO::Stream.new(stream) + puts 'HSDFILSJDFLIJSLDF' + @frames = frames + end + + def close + @stream.close + end + + def closed? + @stream.closed? + end + + def write_connection_preface + @stream.write(CONNECTION_PREFACE) + end + + def read_connection_preface + string = @stream.read(CONNECTION_PREFACE.bytesize) + + raise HandshakeError, "Invalid connection preface: #{string.inspect}" unless string == CONNECTION_PREFACE + + string + end + + # @return [Frame] the frame that has been read from the underlying IO. + # @raise if the underlying IO fails for some reason. + def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE) + # Read the header: + length, type, flags, stream_id = read_header + + # Async.logger.debug(self) {"read_frame: length=#{length} type=#{type} flags=#{flags} stream_id=#{stream_id} -> klass=#{@frames[type].inspect}"} + + # Allocate the frame: + klass = @frames[type] || Frame + frame = klass.new(stream_id, flags, type, length) + + # Read the payload: + frame.read(@stream, maximum_frame_size) + + # Async.logger.debug(self, name: "read") {frame.inspect} + + frame + end + + def write_frame(frame) + # Async.logger.debug(self, name: "write") {frame.inspect} + + frame.write(@stream) + + # Don't call @stream.flush here because it can cause significant contention if there is a semaphore around this method. + # @stream.flush + + frame + end + + def read_header + if (buffer = @stream.peek(9)) && (buffer.bytesize == 9) + return Frame.parse_header(buffer) + end + + raise EOFError, 'Could not read frame header!' + end + end + end end From 0d59a19c818d5d81023401e369c19bac0f6acb2d Mon Sep 17 00:00:00 2001 From: Maruth Goyal Date: Tue, 23 Jan 2024 11:39:04 -0800 Subject: [PATCH 4/6] fix init --- lib/protocol/http2/framer.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/protocol/http2/framer.rb b/lib/protocol/http2/framer.rb index 1ff2d1e..24e8e40 100644 --- a/lib/protocol/http2/framer.rb +++ b/lib/protocol/http2/framer.rb @@ -39,8 +39,12 @@ module HTTP2 class Framer def initialize(stream, frames = FRAMES) - @stream = Async::IO::Stream.new(stream) - puts 'HSDFILSJDFLIJSLDF' + @stream = case stream + when Async::IO::Stream + stream + else + Async::IO::Stream.new(stream) + end @frames = frames end @@ -100,7 +104,7 @@ def read_header return Frame.parse_header(buffer) end - raise EOFError, 'Could not read frame header!' + raise EOFError, "Could not read frame header! buffer: #{buffer.size}" end end end From cb6c8f3bfa499fd817f0a9283be5c159df21c59c Mon Sep 17 00:00:00 2001 From: Maruth Goyal Date: Tue, 23 Jan 2024 11:40:54 -0800 Subject: [PATCH 5/6] fix --- lib/protocol/http2/framer.rb | 187 ++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 91 deletions(-) diff --git a/lib/protocol/http2/framer.rb b/lib/protocol/http2/framer.rb index 24e8e40..85d0905 100644 --- a/lib/protocol/http2/framer.rb +++ b/lib/protocol/http2/framer.rb @@ -3,6 +3,9 @@ # Released under the MIT License. # Copyright, 2019-2023, by Samuel Williams. + +require "async-io" + require_relative 'error' require_relative 'data_frame' @@ -15,97 +18,99 @@ require_relative 'goaway_frame' require_relative 'window_update_frame' require_relative 'continuation_frame' -require 'async/io' -require 'async/io/stream' module Protocol - module HTTP2 - # HTTP/2 frame type mapping as defined by the spec - FRAMES = [ - DataFrame, - HeadersFrame, - PriorityFrame, - ResetStreamFrame, - SettingsFrame, - PushPromiseFrame, - PingFrame, - GoawayFrame, - WindowUpdateFrame, - ContinuationFrame - ].freeze - - # Default connection "fast-fail" preamble string as defined by the spec. - CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" - - class Framer - def initialize(stream, frames = FRAMES) - @stream = case stream - when Async::IO::Stream - stream - else - Async::IO::Stream.new(stream) - end - @frames = frames - end - - def close - @stream.close - end - - def closed? - @stream.closed? - end - - def write_connection_preface - @stream.write(CONNECTION_PREFACE) - end - - def read_connection_preface - string = @stream.read(CONNECTION_PREFACE.bytesize) - - raise HandshakeError, "Invalid connection preface: #{string.inspect}" unless string == CONNECTION_PREFACE - - string - end - - # @return [Frame] the frame that has been read from the underlying IO. - # @raise if the underlying IO fails for some reason. - def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE) - # Read the header: - length, type, flags, stream_id = read_header - - # Async.logger.debug(self) {"read_frame: length=#{length} type=#{type} flags=#{flags} stream_id=#{stream_id} -> klass=#{@frames[type].inspect}"} - - # Allocate the frame: - klass = @frames[type] || Frame - frame = klass.new(stream_id, flags, type, length) - - # Read the payload: - frame.read(@stream, maximum_frame_size) - - # Async.logger.debug(self, name: "read") {frame.inspect} - - frame - end - - def write_frame(frame) - # Async.logger.debug(self, name: "write") {frame.inspect} - - frame.write(@stream) - - # Don't call @stream.flush here because it can cause significant contention if there is a semaphore around this method. - # @stream.flush - - frame - end - - def read_header - if (buffer = @stream.peek(9)) && (buffer.bytesize == 9) - return Frame.parse_header(buffer) - end - - raise EOFError, "Could not read frame header! buffer: #{buffer.size}" - end - end - end + module HTTP2 + # HTTP/2 frame type mapping as defined by the spec + FRAMES = [ + DataFrame, + HeadersFrame, + PriorityFrame, + ResetStreamFrame, + SettingsFrame, + PushPromiseFrame, + PingFrame, + GoawayFrame, + WindowUpdateFrame, + ContinuationFrame, + ].freeze + + # Default connection "fast-fail" preamble string as defined by the spec. + CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze + + class Framer + def initialize(stream, frames = FRAMES) + @stream = case stream + when Async::IO::Stream + stream + else + Async::IO::Stream.new(stream) + end + @frames = frames + end + + def close + @stream.close + end + + def closed? + @stream.closed? + end + + def write_connection_preface + @stream.write(CONNECTION_PREFACE) + end + + def read_connection_preface + string = @stream.read(CONNECTION_PREFACE.bytesize) + + unless string == CONNECTION_PREFACE + raise HandshakeError, "Invalid connection preface: #{string.inspect}" + end + + return string + end + + # @return [Frame] the frame that has been read from the underlying IO. + # @raise if the underlying IO fails for some reason. + def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE) + # Read the header: + length, type, flags, stream_id = read_header + + # Async.logger.debug(self) {"read_frame: length=#{length} type=#{type} flags=#{flags} stream_id=#{stream_id} -> klass=#{@frames[type].inspect}"} + + # Allocate the frame: + klass = @frames[type] || Frame + frame = klass.new(stream_id, flags, type, length) + + # Read the payload: + frame.read(@stream, maximum_frame_size) + + # Async.logger.debug(self, name: "read") {frame.inspect} + + return frame + end + + def write_frame(frame) + # Async.logger.debug(self, name: "write") {frame.inspect} + + frame.write(@stream) + + # Don't call @stream.flush here because it can cause significant contention if there is a semaphore around this method. + # @stream.flush + + return frame + end + + def read_header + if buffer = @stream.peek(9) + if buffer.bytesize == 9 + return Frame.parse_header(buffer) + end + end + + raise EOFError, "Could not read frame header!" + end + end + end end From 7ab4bfe71c613537fda2047cec610ca6f2f126bd Mon Sep 17 00:00:00 2001 From: Maruth Goyal Date: Tue, 23 Jan 2024 11:42:41 -0800 Subject: [PATCH 6/6] fix require --- lib/protocol/http2/framer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/protocol/http2/framer.rb b/lib/protocol/http2/framer.rb index 85d0905..5c03e79 100644 --- a/lib/protocol/http2/framer.rb +++ b/lib/protocol/http2/framer.rb @@ -4,7 +4,7 @@ # Copyright, 2019-2023, by Samuel Williams. -require "async-io" +require "async/io/stream" require_relative 'error'