diff --git a/lib/httpx/altsvc.rb b/lib/httpx/altsvc.rb index efbae2b2..ebb8d395 100644 --- a/lib/httpx/altsvc.rb +++ b/lib/httpx/altsvc.rb @@ -4,6 +4,56 @@ require "strscan" module HTTPX module AltSvc + # makes connections able to accept requests destined to primary service. + module ConnectionMixin + def send(request) + request.headers["alt-used"] = @origin.authority if @parser && !@write_buffer.full? && match_altsvcs?(request.uri) + + super + end + + def match?(uri, options) + return false if !used? && (@state == :closing || @state == :closed) + + match_altsvcs?(uri) && match_altsvc_options?(uri, options) + end + + private + + # checks if this is connection is an alternative service of + # +uri+ + def match_altsvcs?(uri) + @origins.any? { |origin| altsvc_match?(uri, origin) } || + AltSvc.cached_altsvc(@origin).any? do |altsvc| + origin = altsvc["origin"] + altsvc_match?(origin, uri.origin) + end + end + + def match_altsvc_options?(uri, options) + return @options == options unless @options.ssl.all? do |k, v| + v == (k == :hostname ? uri.host : options.ssl[k]) + end + + @options.options_equals?(options, Options::REQUEST_BODY_IVARS + %i[@ssl]) + end + + def altsvc_match?(uri, other_uri) + other_uri = URI(other_uri) + + uri.origin == other_uri.origin || begin + case uri.scheme + when "h2" + (other_uri.scheme == "https" || other_uri.scheme == "h2") && + uri.host == other_uri.host && + uri.port == other_uri.port + else + false + end + end + end + end + @altsvc_mutex = Thread::Mutex.new @altsvcs = Hash.new { |h, k| h[k] = [] } @@ -99,7 +149,10 @@ module HTTPX end def parse_altsvc_origin(alt_proto, alt_origin) - alt_scheme = parse_altsvc_scheme(alt_proto) or return + alt_scheme = parse_altsvc_scheme(alt_proto) + + return unless alt_scheme + alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"") URI.parse("#{alt_scheme}://#{alt_origin}") diff --git a/lib/httpx/connection.rb b/lib/httpx/connection.rb index 7d8b457d..4bf4781f 100644 --- a/lib/httpx/connection.rb +++ b/lib/httpx/connection.rb @@ -95,15 +95,13 @@ module HTTPX return false if exhausted? ( - ( - @origins.include?(uri.origin) && - # if there is more than one origin to match, it means that this connection - # was the result of coalescing. To prevent blind trust in the case where the - # origin came from an ORIGIN frame, we're going to verify the hostname with the - # SSL certificate - (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host))) - ) && @options == options - ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options)) + @origins.include?(uri.origin) && + # if there is more than one origin to match, it means that this connection + # was the result of coalescing. To prevent blind trust in the case where the + # origin came from an ORIGIN frame, we're going to verify the hostname with the + # SSL certificate + (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host))) + ) && @options == options end def expired? @@ -167,24 +165,6 @@ module HTTPX end end - # checks if this is connection is an alternative service of - # +uri+ - def match_altsvcs?(uri) - @origins.any? { |origin| uri.altsvc_match?(origin) } || - AltSvc.cached_altsvc(@origin).any? do |altsvc| - origin = altsvc["origin"] - origin.altsvc_match?(uri.origin) - end - end - - def match_altsvc_options?(uri, options) - return @options == options unless @options.ssl[:hostname] == uri.host - - dup_options = @options.merge(ssl: { hostname: nil }) - dup_options.ssl.delete(:hostname) - dup_options == options - end - def connecting? @state == :idle end @@ -252,8 +232,6 @@ module HTTPX def send(request) if @parser && !@write_buffer.full? - request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri) - if @response_received_at && @keep_alive_timeout && Utils.elapsed_time(@response_received_at) > @keep_alive_timeout # when pushing a request into an existing connection, we have to check whether there diff --git a/lib/httpx/extensions.rb b/lib/httpx/extensions.rb index 5be2d34c..4441276c 100644 --- a/lib/httpx/extensions.rb +++ b/lib/httpx/extensions.rb @@ -54,21 +54,6 @@ module HTTPX def origin "#{scheme}://#{authority}" end unless URI::HTTP.method_defined?(:origin) - - def altsvc_match?(uri) - uri = URI.parse(uri) - - origin == uri.origin || begin - case scheme - when "h2" - (uri.scheme == "https" || uri.scheme == "h2") && - host == uri.host && - (port || default_port) == (uri.port || uri.default_port) - else - false - end - end - end end end end diff --git a/lib/httpx/session.rb b/lib/httpx/session.rb index 4433cab5..6099b992 100644 --- a/lib/httpx/session.rb +++ b/lib/httpx/session.rb @@ -203,9 +203,12 @@ module HTTPX alt_options = options.merge(ssl: options.ssl.merge(hostname: URI(origin).host)) connection = pool.find_connection(alt_origin, alt_options) || build_connection(alt_origin, alt_options) + # advertised altsvc is the same origin being used, ignore return if connection == existing_connection + connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin) + set_connection_callbacks(connection, connections, alt_options) log(level: 1) { "#{origin} alt-svc: #{alt_origin}" } diff --git a/sig/altsvc.rbs b/sig/altsvc.rbs new file mode 100644 index 00000000..535417f8 --- /dev/null +++ b/sig/altsvc.rbs @@ -0,0 +1,33 @@ +module HTTPX + module AltSvc + module ConnectionMixin + + def send: (Request request) -> void + + def match?: (URI::Generic uri, Options options) -> bool + + private + + def match_altsvcs?: (URI::Generic uri) -> bool + + def match_altsvc_options?: (URI::Generic uri, Options options) -> bool + end + + type altsvc_params = Hash[String, untyped] + + def self?.cached_altsvc: (String origin) -> Array[altsvc_params] + + def self?.cached_altsvc_set: (String origin, altsvc_params) -> void + + def self?.lookup: (String origin, Integer | Float ttl) -> Array[altsvc_params] + + def self?.emit: (Request request, response response) { (http_uri alt_origin, String origin, altsvc_params alt_params) -> void } -> void + + def self?.parse: (String altsvc) { (http_uri alt_origin, altsvc_params alt_params) -> void } -> void + | (String altsvc) -> Enumerable[[http_uri, altsvc_params]] + + def self?.parse_altsvc_scheme: (String alt_proto) -> String? + + def self.parse_altsvc_origin: (string alt_proto, String alt_origin) -> http_uri? + end +end \ No newline at end of file diff --git a/sig/connection.rbs b/sig/connection.rbs index 3a4a2be9..9dc69f2b 100644 --- a/sig/connection.rbs +++ b/sig/connection.rbs @@ -44,25 +44,23 @@ module HTTPX def addresses: () -> Array[ipaddr]? - def addresses=: (Array[ipaddr]) -> void + def addresses=: (Array[ipaddr] addresses) -> void + + def send: (Request request) -> void def match?: (URI::Generic uri, Options options) -> bool def expired?: () -> boolish - def mergeable?: (Connection) -> bool + def mergeable?: (Connection connection) -> bool - def coalescable?: (Connection) -> bool + def coalescable?: (Connection connection) -> bool def create_idle: (?Hash[Symbol, untyped] options) -> Connection - def merge: (Connection) -> void + def merge: (Connection connection) -> void - def purge_pending: () { (Request) -> void } -> void - - def match_altsvcs?: (URI::Generic uri) -> bool - - def match_altsvc_options?: (URI::Generic uri, Options options) -> bool + def purge_pending: () { (Request request) -> void } -> void def connecting?: () -> bool @@ -78,8 +76,6 @@ module HTTPX def reset: () -> void - def send: (Request) -> void - def timeout: () -> Numeric? def idling: () -> void @@ -112,15 +108,15 @@ module HTTPX def set_parser_callbacks: (HTTP1 | HTTP2 parser) -> void - def transition: (Symbol) -> void + def transition: (Symbol nextstate) -> void - def handle_transition: (Symbol) -> void + def handle_transition: (Symbol nextstate) -> void def build_socket: (?Array[ipaddr]? addrs) -> (TCP | SSL | UNIX) - def on_error: (HTTPX::TimeoutError | Error | StandardError) -> void + def on_error: (HTTPX::TimeoutError | Error | StandardError error) -> void - def handle_error: (StandardError) -> void + def handle_error: (StandardError error) -> void def purge_after_closed: () -> void diff --git a/sig/session.rbs b/sig/session.rbs index 698e6595..4ff98b49 100644 --- a/sig/session.rbs +++ b/sig/session.rbs @@ -33,7 +33,7 @@ module HTTPX def set_connection_callbacks: (Connection connection, Array[Connection] connections, Options options) -> void - def build_altsvc_connection: (Connection existing_connection, Array[Connection] connections, URI::Generic alt_origin, String origin, Hash[String, String] alt_params, Options options) -> Connection? + def build_altsvc_connection: (Connection existing_connection, Array[Connection] connections, URI::Generic alt_origin, String origin, Hash[String, String] alt_params, Options options) -> (Connection & AltSvc::ConnectionMixin)? def build_requests: (verb, uri, options) -> Array[Request] | (Array[[verb, uri, options]], options) -> Array[Request] diff --git a/test/altsvc_test.rb b/test/altsvc_test.rb index d41f88a6..e2750592 100644 --- a/test/altsvc_test.rb +++ b/test/altsvc_test.rb @@ -65,7 +65,7 @@ class AltSvcTest < Minitest::Test req = Request.new("GET", "http://www.example-clear-cache.com/") res = Response.new(req, 200, "2.0", { "alt-svc" => "clear" }) - AltSvc.emit(req, res) + AltSvc.emit(req, res) {} entries = AltSvc.cached_altsvc("http://www.example-clear-cache.com") assert entries.empty? diff --git a/test/support/requests/altsvc.rb b/test/support/requests/altsvc.rb index a409222f..b5fb6cfd 100644 --- a/test/support/requests/altsvc.rb +++ b/test/support/requests/altsvc.rb @@ -6,7 +6,7 @@ module Requests altsvc_host = ENV["HTTPBIN_ALTSVC_HOST"] altsvc_origin = origin(altsvc_host) - HTTPX.wrap do |http| + HTTPX.plugin(SessionWithPool).wrap do |http| altsvc_uri = build_uri("/get", altsvc_origin) response = http.get(altsvc_uri) verify_status(response, 200)