File: proxy.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 (351 lines) | stat: -rw-r--r-- 9,376 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# frozen_string_literal: true

module HTTPX
  class ProxyError < ConnectionError; end

  module Plugins
    #
    # This plugin adds support for proxies. It ships with support for:
    #
    # * HTTP proxies
    # * HTTPS proxies
    # * Socks4/4a proxies
    # * Socks5 proxies
    #
    # https://gitlab.com/os85/httpx/wikis/Proxy
    #
    module Proxy
      class ProxyConnectionError < ProxyError; end

      PROXY_ERRORS = [TimeoutError, IOError, SystemCallError, Error].freeze

      class << self
        def configure(klass)
          klass.plugin(:"proxy/http")
          klass.plugin(:"proxy/socks4")
          klass.plugin(:"proxy/socks5")
        end

        def extra_options(options)
          options.merge(supported_proxy_protocols: [])
        end

        def subplugins
          {
            retries: ProxyRetries,
          }
        end
      end

      class Parameters
        attr_reader :uri, :username, :password, :scheme, :no_proxy

        def initialize(uri: nil, scheme: nil, username: nil, password: nil, no_proxy: nil, **extra)
          @no_proxy = Array(no_proxy) if no_proxy
          @uris = Array(uri)
          uri = @uris.first

          @username = username
          @password = password

          @ns = 0

          if uri
            @uri = uri.is_a?(URI::Generic) ? uri : URI(uri)
            @username ||= @uri.user
            @password ||= @uri.password
          end

          @scheme = scheme

          return unless @uri && @username && @password

          @authenticator = nil
          @scheme ||= infer_default_auth_scheme(@uri)

          return unless @scheme

          @authenticator = load_authenticator(@scheme, @username, @password, **extra)
        end

        def shift
          # TODO: this operation must be synchronized
          @ns += 1
          @uri = @uris[@ns]

          return unless @uri

          @uri = URI(@uri) unless @uri.is_a?(URI::Generic)

          scheme = infer_default_auth_scheme(@uri)

          return unless scheme != @scheme

          @scheme = scheme
          @username = username || @uri.user
          @password = password || @uri.password
          @authenticator = load_authenticator(scheme, @username, @password)
        end

        def can_authenticate?(*args)
          return false unless @authenticator

          @authenticator.can_authenticate?(*args)
        end

        def authenticate(*args)
          return unless @authenticator

          @authenticator.authenticate(*args)
        end

        def ==(other)
          case other
          when Parameters
            @uri == other.uri &&
              @username == other.username &&
              @password == other.password &&
              @scheme == other.scheme
          when URI::Generic, String
            proxy_uri = @uri.dup
            proxy_uri.user = @username
            proxy_uri.password = @password
            other_uri = other.is_a?(URI::Generic) ? other : URI.parse(other)
            proxy_uri == other_uri
          else
            super
          end
        end

        private

        def infer_default_auth_scheme(uri)
          case uri.scheme
          when "socks5"
            uri.scheme
          when "http", "https"
            "basic"
          end
        end

        def load_authenticator(scheme, username, password, **extra)
          auth_scheme = scheme.to_s.capitalize

          require_relative "auth/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)

          Authentication.const_get(auth_scheme).new(username, password, **extra)
        end
      end

      # adds support for the following options:
      #
      # :proxy :: proxy options defining *:uri*, *:username*, *:password* or
      #           *:scheme* (i.e. <tt>{ uri: "http://proxy" }</tt>)
      module OptionsMethods
        private

        def option_proxy(value)
          value.is_a?(Parameters) ? value : Parameters.new(**Hash[value])
        end

        def option_supported_proxy_protocols(value)
          raise TypeError, ":supported_proxy_protocols must be an Array" unless value.is_a?(Array)

          value.map(&:to_s)
        end
      end

      module InstanceMethods
        def find_connection(request_uri, selector, options)
          return super unless options.respond_to?(:proxy)

          if (next_proxy = request_uri.find_proxy)
            return super(request_uri, selector, options.merge(proxy: Parameters.new(uri: next_proxy)))
          end

          proxy = options.proxy

          return super unless proxy

          next_proxy = proxy.uri

          raise ProxyError, "Failed to connect to proxy" unless next_proxy

          raise ProxyError,
                "#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)

          if (no_proxy = proxy.no_proxy)
            no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)

            # TODO: setting proxy to nil leaks the connection object in the pool
            return super(request_uri, selector, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(request_uri.host, next_proxy.host,
                                                                                                          next_proxy.port, no_proxy)
          end

          super(request_uri, selector, options.merge(proxy: proxy))
        end

        private

        def fetch_response(request, selector, options)
          response = request.response # in case it goes wrong later

          begin
            response = super

            if response.is_a?(ErrorResponse) && proxy_error?(request, response, options)
              options.proxy.shift

              # return last error response if no more proxies to try
              return response if options.proxy.uri.nil?

              log { "failed connecting to proxy, trying next..." }
              request.transition(:idle)
              send_request(request, selector, options)
              return
            end
            response
          rescue ProxyError
            # may happen if coupled with retries, and there are no more proxies to try, in which case
            # it'll end up here
            response
          end
        end

        def proxy_error?(_request, response, options)
          return false unless options.proxy

          error = response.error
          case error
          when NativeResolveError
            proxy_uri = URI(options.proxy.uri)

            unresolved_host = error.host

            # failed resolving proxy domain
            unresolved_host == proxy_uri.host
          when ResolveError
            proxy_uri = URI(options.proxy.uri)

            error.message.end_with?(proxy_uri.to_s)
          when ProxyConnectionError
            # timeout errors connecting to proxy
            true
          else
            false
          end
        end
      end

      module ConnectionMethods
        using URIExtensions

        def initialize(*)
          super
          return unless @options.proxy

          # redefining the connection origin as the proxy's URI,
          # as this will be used as the tcp peer ip.
          @proxy_uri = URI(@options.proxy.uri)
        end

        def peer
          @proxy_uri || super
        end

        def connecting?
          return super unless @options.proxy

          super || @state == :connecting || @state == :connected
        end

        def call
          super

          return unless @options.proxy

          case @state
          when :connecting
            consume
          end
        rescue *PROXY_ERRORS => e
          if connecting?
            error = ProxyConnectionError.new(e.message)
            error.set_backtrace(e.backtrace)
            raise error
          end

          raise e
        end

        def reset
          return super unless @options.proxy

          @state = :open

          super
          # emit(:close)
        end

        private

        def initialize_type(uri, options)
          return super unless options.proxy

          "tcp"
        end

        def connect
          return super unless @options.proxy

          case @state
          when :idle
            transition(:connecting)
          when :connected
            transition(:open)
          end
        end

        def handle_transition(nextstate)
          return super unless @options.proxy

          case nextstate
          when :closing
            # this is a hack so that we can use the super method
            # and it'll think that the current state is open
            @state = :open if @state == :connecting
          end
          super
        end

        def purge_after_closed
          super
          @io = @io.proxy_io if @io.respond_to?(:proxy_io)
        end
      end

      module ProxyRetries
        module InstanceMethods
          private

          def retryable_error?(ex, *)
            super || ex.is_a?(ProxyConnectionError)
          end
        end
      end
    end
    register_plugin :proxy, Proxy
  end

  class ProxySSL < SSL
    attr_reader :proxy_io

    def initialize(tcp, request_uri, options)
      @proxy_io = tcp
      @io = tcp.to_io
      super(request_uri, tcp.addresses, options)
      @hostname = request_uri.host
      @state = :connected
    end
  end
end