diff options
Diffstat (limited to 'lib/plum')
-rw-r--r-- | lib/plum/connection.rb | 26 | ||||
-rw-r--r-- | lib/plum/http_connection.rb | 88 | ||||
-rw-r--r-- | lib/plum/https_connection.rb | 15 |
3 files changed, 92 insertions, 37 deletions
diff --git a/lib/plum/connection.rb b/lib/plum/connection.rb index 32a7d2e..2c2536a 100644 --- a/lib/plum/connection.rb +++ b/lib/plum/connection.rb @@ -65,10 +65,6 @@ module Plum receive_frame(frame) end end - rescue ConnectionError => e - callback(:connection_error, e) - goaway(e.http2_error_type) - close end alias << receive @@ -87,8 +83,20 @@ module Plum @io.write(frame.assemble) end + def negotiate! + 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 + def new_stream(stream_id, **args) - if @streams.size > 0 && @streams.keys.last >= stream_id + if @streams.size > 0 && @streams.keys.max >= stream_id raise Plum::ConnectionError.new(:protocol_error) end @@ -140,6 +148,10 @@ module Plum end stream.receive_frame(frame) end + rescue ConnectionError => e + callback(:connection_error, e) + goaway(e.http2_error_type) + close end def receive_control_frame(frame) @@ -164,7 +176,7 @@ module Plum end end - def receive_settings(frame) + def receive_settings(frame, send_ack: true) if frame.flags.include?(:ack) raise ConnectionError.new(:frame_size_error) if frame.length != 0 return @@ -178,7 +190,7 @@ module Plum callback(:remote_settings, @remote_settings, old_remote_settings) - send_immediately Frame.settings(:ack) + send_immediately Frame.settings(:ack) if send_ack end def apply_remote_settings(old_remote_settings) diff --git a/lib/plum/http_connection.rb b/lib/plum/http_connection.rb index bde8b7d..e5e63f6 100644 --- a/lib/plum/http_connection.rb +++ b/lib/plum/http_connection.rb @@ -1,33 +1,91 @@ +using Plum::BinaryString + module Plum class HTTPConnection < Connection def initialize(io, local_settings = {}) + require "http/parser" super + @_http_parser = setup_parser + @_upgrade_retry = false # After sent 426 (Upgrade Required) end private - def negotiate! - if @buffer.bytesize >= 4 - if CLIENT_CONNECTION_PREFACE.start_with?(@buffer) - negotiate_with_knowledge + def setup_parser + headers = nil + body = "" + parser = HTTP::Parser.new + parser.on_message_begin = proc { } + parser.on_headers_complete = proc {|_headers| headers = _headers } + parser.on_body = proc {|chunk| body << chunk } + parser.on_message_complete = proc do |env| + # Upgrade from HTTP/1.1 + heads = headers.map {|n, v| [n.downcase, v] }.to_h + connection = heads["connection"] || "" + upgrade = heads["upgrade"] || "" + settings = heads["http2-settings"] + + if (connection.split(", ").sort == ["Upgrade", "HTTP2-Settings"].sort && + upgrade.split(", ").include?("h2c") && + settings != nil) + respond_switching_protocol + self.on(:negotiated) { + _frame = Frame.new(type: :settings, stream_id: 0, payload: Base64.urlsafe_decode64(settings)) + receive_settings(_frame, send_ack: false) # HTTP2-Settings + process_first_request(parser, heads, body) + } else - negotiate_with_upgrade + respond_not_supported + close end end - # next + + parser end - def negotiate_with_knowledge - if @buffer.bytesize >= 24 - if @buffer.byteshift(24) == CLIENT_CONNECTION_PREFACE - @state = :waiting_settings - settings(@local_settings) - end + def negotiate! + begin + super + rescue ConnectionError + # Upgrade from HTTP/1.1 + offset = @_http_parser << @buffer + @buffer.byteshift(offset) end - # next end - def negotiate_with_upgrade - raise NotImplementedError, "Parsing HTTP/1.1 is hard..." + def respond_switching_protocol + resp = "" + resp << "HTTP/1.1 101 Switching Protocols\r\n" + resp << "Connection: Upgrade\r\n" + resp << "Upgrade: h2c\r\n" + resp << "\r\n" + + io.write(resp) + end + + def respond_not_supported + data = "Use modern web browser with HTTP/2 support." + + resp = "" + resp << "HTTP/1.1 505 HTTP Version Not Supported\r\n" + resp << "Content-Type: text/plain\r\n" + resp << "Content-Length: #{data.bytesize}\r\n" + resp << "\r\n" + resp << data + + io.write(resp) + end + + def process_first_request(parser, heads, dat) + stream = new_stream(1) + heads = heads.merge({ ":method" => parser.http_method, + ":path" => parser.request_url, + ":authority" => heads["host"] }) + .reject {|n, v| ["connection", "http2-settings", "upgrade", "host"].include?(n) } + encoder = HPACK::Encoder.new(0, indexing: false) # don't pollute connection's HPACK context + headers = Frame.headers(1, encoder.encode(heads), :end_headers) # stream ID is 1 + headers.split_headers(local_settings[:max_frame_size]).each {|hfrag| stream.receive_frame(hfrag) } + data = Frame.data(1, dat, :end_stream) + data.split_data(local_settings[:max_frame_size]).each {|dfrag| stream.receive_frame(dfrag) } end end end diff --git a/lib/plum/https_connection.rb b/lib/plum/https_connection.rb index d05da40..e96a785 100644 --- a/lib/plum/https_connection.rb +++ b/lib/plum/https_connection.rb @@ -5,20 +5,5 @@ module Plum 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 |