lib/httpx/plugins/h2c.rb in httpx-0.3.1 vs lib/httpx/plugins/h2c.rb in httpx-0.4.0
- old
+ new
@@ -1,72 +1,111 @@
# frozen_string_literal: true
module HTTPX
module Plugins
+ #
+ # This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2.
+ #
+ # https://tools.ietf.org/html/rfc7540#section-3.2
+ #
module H2C
def self.load_dependencies(*)
require "base64"
end
module InstanceMethods
- def request(*args, keep_open: @keep_open, **options)
- return super if @_h2c_probed
- begin
- requests = __build_reqs(*args, **options)
+ def request(*args, **options)
+ h2c_options = options.merge(fallback_protocol: "h2c")
- upgrade_request = requests.first
- return super unless valid_h2c_upgrade_request?(upgrade_request)
- upgrade_request.headers["upgrade"] = "h2c"
- upgrade_request.headers.add("connection", "upgrade")
- upgrade_request.headers.add("connection", "http2-settings")
- upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(@options.http2_settings)
- # TODO: validate!
- upgrade_response = __send_reqs(*upgrade_request, **options).first
+ requests = build_requests(*args, h2c_options)
- if upgrade_response.status == 101
- channel = find_channel(upgrade_request)
- parser = channel.upgrade_parser("h2")
- parser.extend(UpgradeExtensions)
- parser.upgrade(upgrade_request, upgrade_response, **options)
- data = upgrade_response.to_s
- parser << data
- response = upgrade_request.response
- if response.status == 200
- requests.delete(upgrade_request)
- return response if requests.empty?
- end
- responses = __send_reqs(*requests)
- else
- # proceed as usual
- responses = [upgrade_response] + __send_reqs(*requests[1..-1])
- end
- return responses.first if responses.size == 1
- responses
- ensure
- @_h2c_probed = true
- close unless keep_open
- end
+ upgrade_request = requests.first
+ return super unless valid_h2c_upgrade_request?(upgrade_request)
+
+ upgrade_request.headers.add("connection", "upgrade")
+ upgrade_request.headers.add("connection", "http2-settings")
+ upgrade_request.headers["upgrade"] = "h2c"
+ upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(upgrade_request.options.http2_settings)
+ wrap { send_requests(*upgrade_request, h2c_options).first }
+
+ responses = send_requests(*requests, h2c_options)
+
+ return responses.first if responses.size == 1
+
+ responses
end
private
+ def fetch_response(request, connections, options)
+ response = super
+ if response && valid_h2c_upgrade?(request, response, options)
+ log { "upgrading to h2c..." }
+ connection = find_connection(request, connections, options)
+ connections << connection unless connections.include?(connection)
+ connection.upgrade(request, response)
+ end
+ response
+ end
+
VALID_H2C_METHODS = %i[get options head].freeze
private_constant :VALID_H2C_METHODS
def valid_h2c_upgrade_request?(request)
VALID_H2C_METHODS.include?(request.verb) &&
request.scheme == "http"
end
+
+ def valid_h2c_upgrade?(request, response, options)
+ options.fallback_protocol == "h2c" &&
+ request.headers.get("connection").include?("upgrade") &&
+ request.headers.get("upgrade").include?("h2c") &&
+ response.status == 101
+ end
end
- module UpgradeExtensions
- def upgrade(request, _response, **)
+ class H2CParser < Connection::HTTP2
+ def upgrade(request, response)
@connection.send_connection_preface
# skip checks, it is assumed that this is the first
# request in the connection
stream = @connection.upgrade
handle_stream(stream, request)
@streams[request] = stream
+
+ # clean up data left behind in the buffer, if the server started
+ # sending frames
+ data = response.to_s
+ @connection << data
+ end
+ end
+
+ module ConnectionMethods
+ using URIExtensions
+
+ def match?(uri, options)
+ return super unless uri.scheme == "http" && @options.fallback_protocol == "h2c"
+
+ super && options.fallback_protocol == "h2c"
+ end
+
+ def coalescable?(connection)
+ return super unless @options.fallback_protocol == "h2c" && @origin.scheme == "http"
+
+ @origin == connection.origin && connection.options.fallback_protocol == "h2c"
+ end
+
+ def upgrade(request, response)
+ @parser.reset if @parser
+ @parser = H2CParser.new(@write_buffer, @options)
+ set_parser_callbacks(@parser)
+ @parser.upgrade(request, response)
+ end
+
+ def build_parser(*)
+ return super unless @origin.scheme == "http"
+
+ super("http/1.1")
end
end
module FrameBuilder
include HTTP2