lib/httpx/plugins/proxy.rb in httpx-0.3.1 vs lib/httpx/plugins/proxy.rb in httpx-0.4.0
- old
+ new
@@ -4,15 +4,23 @@
require "ipaddr"
require "forwardable"
module HTTPX
module Plugins
+ #
+ # This plugin adds support for proxies. It ships with support for:
+ #
+ # * HTTP proxies
+ # * HTTPS proxies
+ # * Socks4/4a proxies
+ # * Socks5 proxies
+ #
module Proxy
Error = Class.new(Error)
- class Parameters
- extend Registry
+ PROXY_ERRORS = [TimeoutError, IOError, SystemCallError, Error].freeze
+ class Parameters
attr_reader :uri, :username, :password
def initialize(uri:, username: nil, password: nil)
@uri = uri.is_a?(URI::Generic) ? uri : URI(uri)
@username = username || @uri.user
@@ -24,130 +32,169 @@
end
def token_authentication
Base64.strict_encode64("#{user}:#{password}")
end
+
+ def ==(other)
+ if other.is_a?(Parameters)
+ @uri == other.uri &&
+ @username == other.username &&
+ @password == other.password
+ else
+ super
+ end
+ end
end
+ class << self
+ def configure(klass, *)
+ klass.plugin(:"proxy/http")
+ klass.plugin(:"proxy/socks4")
+ klass.plugin(:"proxy/socks5")
+ end
+
+ def extra_options(options)
+ Class.new(options.class) do
+ def_option(:proxy) do |pr|
+ Hash[pr]
+ end
+ end.new(options)
+ end
+ end
+
module InstanceMethods
def with_proxy(*args)
branch(default_options.with_proxy(*args))
end
private
- def proxy_params(uri)
+ def proxy_uris(uri, options)
@_proxy_uris ||= begin
- uris = @options.proxy ? Array(@options.proxy[:uri]) : []
+ uris = options.proxy ? Array(options.proxy[:uri]) : []
if uris.empty?
uri = URI(uri).find_proxy
uris << uri if uri
end
uris
end
- @options.proxy.merge(uri: @_proxy_uris.shift) unless @_proxy_uris.empty?
+ options.proxy.merge(uri: @_proxy_uris.first) unless @_proxy_uris.empty?
end
- def find_channel(request, **options)
+ def find_connection(request, connections, options)
+ return super unless options.respond_to?(:proxy)
+
uri = URI(request.uri)
- proxy = proxy_params(uri)
- raise Error, "Failed to connect to proxy" unless proxy
- @connection.find_channel(proxy) || build_channel(proxy, options)
- end
+ next_proxy = proxy_uris(uri, options)
+ raise Error, "Failed to connect to proxy" unless next_proxy
- def build_channel(proxy, options)
- return super if proxy.is_a?(URI::Generic)
- channel = build_proxy_channel(proxy, **options)
- set_channel_callbacks(channel, options)
- channel
+ proxy_options = options.merge(proxy: Parameters.new(**next_proxy))
+ connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
+ unless connections.nil? || connections.include?(connection)
+ connections << connection
+ set_connection_callbacks(connection, options)
+ end
+ connection
end
- def build_proxy_channel(proxy, **options)
- parameters = Parameters.new(**proxy)
- uri = parameters.uri
- log { "proxy: #{uri}" }
- proxy_type = Parameters.registry(parameters.uri.scheme)
- channel = proxy_type.new("tcp", uri, parameters, @options.merge(options), &method(:on_response))
- @connection.__send__(:resolve_channel, channel)
- channel
+ def build_connection(uri, options)
+ proxy = options.proxy
+ return super unless proxy
+
+ connection = options.connection_class.new("tcp", uri, options)
+ pool.init_connection(connection, options)
+ connection
end
- def fetch_response(request)
+ def fetch_response(request, connections, options)
response = super
if response.is_a?(ErrorResponse) &&
# either it was a timeout error connecting, or it was a proxy error
- (((response.error.is_a?(TimeoutError) || response.error.is_a?(IOError)) && request.state == :idle) ||
- response.error.is_a?(Error)) &&
- !@_proxy_uris.empty?
+ PROXY_ERRORS.any? { |ex| response.error.is_a?(ex) } && !@_proxy_uris.empty?
+ @_proxy_uris.shift
log { "failed connecting to proxy, trying next..." }
- channel = find_channel(request)
- channel.send(request)
+ request.transition(:idle)
+ connection = find_connection(request, connections, options)
+ connections << connection unless connections.include?(connection)
+ connection.send(request)
return
end
response
end
end
- module OptionsMethods
- def self.included(klass)
+ module ConnectionMethods
+ using URIExtensions
+
+ def initialize(*)
super
- klass.def_option(:proxy) do |pr|
- Hash[pr]
- end
+ return unless @options.proxy
+
+ # redefining the connection origin as the proxy's URI,
+ # as this will be used as the tcp peer ip.
+ @origin = URI(@options.proxy.uri.origin)
end
- end
- def self.configure(klass, *)
- klass.plugin(:"proxy/http")
- klass.plugin(:"proxy/socks4")
- klass.plugin(:"proxy/socks5")
- end
- end
- register_plugin :proxy, Proxy
- end
+ def match?(uri, options)
+ return super unless @options.proxy
- class ProxyChannel < Channel
- def initialize(type, uri, parameters, options, &blk)
- super(type, uri, options, &blk)
- @parameters = parameters
- end
+ super && @options.proxy == options.proxy
+ end
- def match?(*)
- true
- end
+ # should not coalesce connections here, as the IP is the IP of the proxy
+ def coalescable?(*)
+ return super unless @options.proxy
- def send(request, **args)
- @pending << [request, args]
- end
+ false
+ end
- def connecting?
- super || @state == :connecting || @state == :connected
- end
+ def send(request)
+ return super unless @options.proxy
+ return super unless connecting?
- def to_io
- case @state
- when :idle
- transition(:connecting)
- when :connected
- transition(:open)
- end
- @io.to_io
- end
+ @pending << request
+ end
- def call
- super
- case @state
- when :connecting
- consume
+ def connecting?
+ return super unless @options.proxy
+
+ super || @state == :connecting || @state == :connected
+ end
+
+ def to_io
+ return super unless @options.proxy
+
+ case @state
+ when :idle
+ transition(:connecting)
+ when :connected
+ transition(:open)
+ end
+ @io.to_io
+ end
+
+ def call
+ super
+ return unless @options.proxy
+
+ case @state
+ when :connecting
+ consume
+ end
+ end
+
+ def reset
+ return super unless @options.proxy
+
+ @state = :open
+ transition(:closing)
+ transition(:closed)
+ emit(:close)
+ end
end
end
-
- def reset
- @state = :open
- transition(:closing)
- transition(:closed)
- emit(:close)
- end
+ register_plugin :proxy, Proxy
end
class ProxySSL < SSL
def initialize(tcp, request_uri, options)
@io = tcp.to_io