1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
|
# frozen_string_literal: true
require "strscan"
module HTTPX
module AltSvc
# makes connections able to accept requests destined to primary service.
module ConnectionMixin
using URIExtensions
H2_ALTSVC_SCHEMES = %w[https h2].freeze
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) #: http_uri
uri.origin == other_uri.origin || begin
case uri.scheme
when "h2"
H2_ALTSVC_SCHEMES.include?(other_uri.scheme) &&
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] = [] }
module_function
def cached_altsvc(origin)
now = Utils.now
@altsvc_mutex.synchronize do
lookup(origin, now)
end
end
def cached_altsvc_set(origin, entry)
now = Utils.now
@altsvc_mutex.synchronize do
return if @altsvcs[origin].any? { |altsvc| altsvc["origin"] == entry["origin"] }
entry["TTL"] = Integer(entry["ma"]) + now if entry.key?("ma")
@altsvcs[origin] << entry
entry
end
end
def lookup(origin, ttl)
return [] unless @altsvcs.key?(origin)
@altsvcs[origin] = @altsvcs[origin].select do |entry|
!entry.key?("TTL") || entry["TTL"] > ttl
end
@altsvcs[origin].reject { |entry| entry["noop"] }
end
def emit(request, response)
return unless response.respond_to?(:headers)
# Alt-Svc
return unless response.headers.key?("alt-svc")
origin = request.origin
host = request.uri.host
altsvc = response.headers["alt-svc"]
# https://datatracker.ietf.org/doc/html/rfc7838#section-3
# A field value containing the special value "clear" indicates that the
# origin requests all alternatives for that origin to be invalidated
# (including those specified in the same response, in case of an
# invalid reply containing both "clear" and alternative services).
if altsvc == "clear"
@altsvc_mutex.synchronize do
@altsvcs[origin].clear
end
return
end
parse(altsvc) do |alt_origin, alt_params|
alt_origin.host ||= host
yield(alt_origin, origin, alt_params)
end
end
def parse(altsvc)
return enum_for(__method__, altsvc) unless block_given?
scanner = StringScanner.new(altsvc)
until scanner.eos?
alt_service = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
alt_params = []
loop do
alt_param = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
alt_params << alt_param.strip if alt_param
scanner.skip(/;/)
break if scanner.eos? || scanner.scan(/ *, */)
end
alt_params = Hash[alt_params.map { |field| field.split("=", 2) }]
alt_proto, alt_authority = alt_service.split("=", 2)
alt_origin = parse_altsvc_origin(alt_proto, alt_authority)
return unless alt_origin
yield(alt_origin, alt_params.merge("proto" => alt_proto))
end
end
def parse_altsvc_scheme(alt_proto)
case alt_proto
when "h2c"
"http"
when "h2"
"https"
end
end
def parse_altsvc_origin(alt_proto, alt_origin)
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}")
end
end
end
|