File: altsvc.rb

package info (click to toggle)
ruby-httpx 1.7.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,816 kB
  • sloc: ruby: 12,209; makefile: 4
file content (165 lines) | stat: -rw-r--r-- 4,612 bytes parent folder | download
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