From b5f9936dbe3c0a7e9e96974f61b40c1ffee7e608 Mon Sep 17 00:00:00 2001 From: rick olson Date: Fri, 18 Oct 2019 11:10:41 -0600 Subject: [PATCH 1/8] remove temp_proxy and improve proxy tests --- lib/faraday/connection.rb | 8 ++--- spec/faraday/connection_spec.rb | 61 ++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/lib/faraday/connection.rb b/lib/faraday/connection.rb index 70a7507b3..e3fb4b6a1 100644 --- a/lib/faraday/connection.rb +++ b/lib/faraday/connection.rb @@ -99,7 +99,6 @@ def initialize_proxy(url, options) else proxy_from_env(url) end - @temp_proxy = @proxy end # Sets the Hash of URI query unencoded key/value pairs. @@ -490,11 +489,8 @@ def run_request(method, url, body, headers) raise ArgumentError, "unknown http method: #{method}" end - # Resets temp_proxy - @temp_proxy = proxy_for_request(url) - request = build_request(method) do |req| - req.options = req.options.merge(proxy: @temp_proxy) + req.options.proxy = proxy_for_request(url) req.url(url) if url req.headers.update(headers) if headers req.body = body if body @@ -514,7 +510,7 @@ def build_request(method) Request.create(method) do |req| req.params = params.dup req.headers = headers.dup - req.options = options + req.options = options.dup yield(req) if block_given? end end diff --git a/spec/faraday/connection_spec.rb b/spec/faraday/connection_spec.rb index 3a6c1236e..12997f908 100644 --- a/spec/faraday/connection_spec.rb +++ b/spec/faraday/connection_spec.rb @@ -330,21 +330,21 @@ describe 'proxy support' do it 'accepts string' do - with_env 'http_proxy' => 'http://proxy.com:80' do + with_env 'http_proxy' => 'http://env-proxy.com:80' do conn.proxy = 'http://proxy.com' expect(conn.proxy.host).to eq('proxy.com') end end it 'accepts uri' do - with_env 'http_proxy' => 'http://proxy.com:80' do + with_env 'http_proxy' => 'http://env-proxy.com:80' do conn.proxy = URI.parse('http://proxy.com') expect(conn.proxy.host).to eq('proxy.com') end end it 'accepts hash with string uri' do - with_env 'http_proxy' => 'http://proxy.com:80' do + with_env 'http_proxy' => 'http://env-proxy.com:80' do conn.proxy = { uri: 'http://proxy.com', user: 'rick' } expect(conn.proxy.host).to eq('proxy.com') expect(conn.proxy.user).to eq('rick') @@ -352,7 +352,7 @@ end it 'accepts hash' do - with_env 'http_proxy' => 'http://proxy.com:80' do + with_env 'http_proxy' => 'http://env-proxy.com:80' do conn.proxy = { uri: URI.parse('http://proxy.com'), user: 'rick' } expect(conn.proxy.host).to eq('proxy.com') expect(conn.proxy.user).to eq('rick') @@ -360,8 +360,8 @@ end it 'accepts http env' do - with_env 'http_proxy' => 'http://proxy.com:80' do - expect(conn.proxy.host).to eq('proxy.com') + with_env 'http_proxy' => 'http://env-proxy.com:80' do + expect(conn.proxy.host).to eq('env-proxy.com') end end @@ -453,8 +453,40 @@ expect { conn.proxy = { uri: :bad_uri, user: 'rick' } }.to raise_error(ArgumentError) end - it 'gives priority to manually set proxy' do + it 'uses env http_proxy' do + with_env 'http_proxy' => 'http://proxy.com' do + conn = Faraday.new + expect(conn.instance_variable_get('@manual_proxy')).to be_falsey + expect(conn.proxy_for_request('http://google.co.uk').host).to eq('proxy.com') + end + end + + it 'uses processes no_proxy before http_proxy' do with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'google.co.uk' do + conn = Faraday.new + expect(conn.instance_variable_get('@manual_proxy')).to be_falsey + expect(conn.proxy_for_request('http://google.co.uk')).to be_nil + end + end + + it 'uses env https_proxy' do + with_env 'https_proxy' => 'https://proxy.com' do + conn = Faraday.new + expect(conn.instance_variable_get('@manual_proxy')).to be_falsey + expect(conn.proxy_for_request('https://google.co.uk').host).to eq('proxy.com') + end + end + + it 'uses processes no_proxy before https_proxy' do + with_env 'https_proxy' => 'https://proxy.com', 'no_proxy' => 'google.co.uk' do + conn = Faraday.new + expect(conn.instance_variable_get('@manual_proxy')).to be_falsey + expect(conn.proxy_for_request('https://google.co.uk')).to be_nil + end + end + + it 'gives priority to manually set proxy' do + with_env 'https_proxy' => 'https://proxy.com', 'no_proxy' => 'google.co.uk' do conn = Faraday.new conn.proxy = 'http://proxy2.com' @@ -477,8 +509,11 @@ it 'dynamically checks proxy' do with_env 'http_proxy' => 'http://proxy.com:80' do conn = Faraday.new - conn.get('http://example.com') - expect(conn.instance_variable_get('@temp_proxy').host).to eq('proxy.com') + expect(conn.proxy.uri.host).to eq('proxy.com') + + conn.get('http://example.com') do |req| + expect(req.options.proxy.uri.host).to eq('proxy.com') + end end conn.get('http://example.com') @@ -489,9 +524,11 @@ with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do conn = Faraday.new - expect(conn.instance_variable_get('@temp_proxy').host).to eq('proxy.com') - conn.get('http://example.com') - expect(conn.instance_variable_get('@temp_proxy')).to be_nil + expect(conn.proxy.uri.host).to eq('proxy.com') + + conn.get('http://example.com') do |req| + expect(req.options.proxy).to be_nil + end end end end From 9897d3f09e0ba923efb369386f4ecff387c8ce9f Mon Sep 17 00:00:00 2001 From: rick olson Date: Fri, 18 Oct 2019 13:10:14 -0600 Subject: [PATCH 2/8] initial ProxySelector implementations with stubbed Environment --- lib/faraday.rb | 1 + lib/faraday/proxy_selector.rb | 187 ++++++++++++++++++++++++++++ spec/faraday/proxy_selector_spec.rb | 102 +++++++++++++++ 3 files changed, 290 insertions(+) create mode 100644 lib/faraday/proxy_selector.rb create mode 100644 spec/faraday/proxy_selector_spec.rb diff --git a/lib/faraday.rb b/lib/faraday.rb index 6b7bf8363..4d42ae8f7 100644 --- a/lib/faraday.rb +++ b/lib/faraday.rb @@ -5,6 +5,7 @@ require 'forwardable' require 'faraday/middleware_registry' require 'faraday/dependency_loader' +require 'faraday/proxy_selector' # This is the main namespace for Faraday. # diff --git a/lib/faraday/proxy_selector.rb b/lib/faraday/proxy_selector.rb new file mode 100644 index 000000000..58bf140c7 --- /dev/null +++ b/lib/faraday/proxy_selector.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +# Faraday module. +module Faraday + # Builds a ProxySelector that uses the given env. If no env is provided, + # this defaults to ENV unless Faraday.ignore_env_proxy is enabled. + # + # @param env [Hash, nil] Hash of environment variables, which can be set with + # Symbol, String, or uppercased String keys. + # Ex: :http_proxy, 'http_proxy', or 'HTTP_PROXY' + # @option env [String] :http_proxy Used as the proxy for HTTP and HTTPS + # requests, unless overridden by :https_proxy or :no_proxy + # @option env [String] :https_proxy Used as the proxy for HTTPS requests, + # unless overridden by :no_proxy + # @option env [String] :no_proxy A String that contains comma-separated values + # specifying hosts that should be excluded from proxying. Each value + # is represented by an IP address prefix (1.2.3.4), an IP address + # prefix in CIDR notation (1.2.3.4/8), a domain name, or a special DNS + # label (*). An IP address prefix and domain name can also include a + # literal port number (1.2.3.4:80). + # A domain name matches that name and all subdomains. A domain name + # with a leading "." matches subdomains only. For example "foo.com" + # matches "foo.com" and "bar.foo.com"; ".y.com" matches "x.y.com" but + # not "y.com". A single asterisk (*) indicates that no proxying should + # be done. A best effort is made to parse the string and errors are + # ignored. + # + # @return [Faraday::ProxySelector::Environment, Faraday::ProxySelector::Nil] + def self.proxy_with_env(env = nil) + return ProxySelector::Nil.new if env.nil? && Faraday.ignore_env_proxy + + ProxySelector::Environment.new(env) + end + + # Builds a ProxySelector that returns the given uri. + # + # @param uri [URI] The proxy URI. + # @param user [String, nil] Optional user info for proxy. + # @param password [String, nil] Optional password info for proxy. + # + # @return [Faraday::ProxySelector::Single] + def self.proxy_to(uri, user: nil, password: nil) + ProxySelector::Single.new(uri, user: user, password: password) + end + + # Builds a ProxySelector that returns the given uri. + # + # @param url [String] The proxy raw url. + # @param user [String, nil] Optional user info for proxy. + # @param password [String, nil] Optional password info for proxy. + # + # @return [Faraday::ProxySelector::Single] + def self.proxy_to_url(url, user: nil, password: nil) + proxy_to(Utils.URI(url), user: user, password: password) + end + + # Proxy is a generic class that knows the Proxy for any given URL. You can + # initialize a Proxy selector instance with one of the class methods: + # + # # Pulls from ENV + # proxy = Faraday.proxy_with_env + # + # # Set your own vars + # proxy = Faraday.proxy_with_env(http_proxy: "http://proxy.example.com") + # + # # Set with string URL + # proxy = Faraday.proxy_to_url("http://proxy.example.com") + # + # # Set with URI + # uri = Faraday::Utils::URI("http://proxy.example.com") + # proxy = Faraday.proxy_to(uri) # shortcut + # + # Once you have an instance, you can get the proxy for a request url: + # + # proxy.proxy_for_url("http://example.com") + # # => Faraday::ProxyOptions instance or nil + # + # uri = Faraday::Utils::URI("http://example.com") + # proxy.proxy_for(uri) + # # => Faraday::ProxyOptions instance or nil + class ProxySelector + # Single is a ProxySelector implementation that always returns the given + # proxy uri. + class Single < ProxySelector + def initialize(uri, user: nil, password: nil) + @options = ProxyOptions.new(uri, user, password) + end + + # Gets the configured proxy, regardless of the uri. + # + # @param _ [URI] Unused. + # + # @return [Faraday::ProxyOptions] + def proxy_for(_) + @options + end + + # Gets the configured proxy, regardless of the url. + # + # @param _ [String] Unused. + # + # @return [Faraday::ProxyOptions] + def proxy_for_url(_) + @options + end + + # Checks if the given uri has a configured proxy. Returns true, because + # every request uri should use the configured proxy. + # + # @param _ [URI] Unused. + # + # @return true + def use_for?(_) + !@options.nil? + end + + # Checks if the given url has a configured proxy. Returns true, because + # every request url should use the configured proxy. + # + # @param _ [String] Unused. + # + # @return true + def use_for_url?(_) + !@options.nil? + end + end + # Environment is a ProxySelector implementation that picks a proxy based on + # how the given request url matches with the http_proxy, https_proxy, and + # no_proxy settings. + # + # Note: Logic for parsing proxy env vars heavily inspired by + # http://golang.org/x/net/http/httpproxy + class Environment < ProxySelector + def initialize(env = nil) + @env = env || ENV + # parse http_proxy, https_proxy, no_proxy + end + + # Gets the proxy for the given uri + # + # @param uri [URI] URI being requested. + # + # @return [Faraday::ProxyOptions, nil] + def proxy_for(uri) + # check for proxy based on uri scheme + # check no_proxy + raise NotImplementedError, "given: #{uri.inspect}" + end + + # Gets the proxy for the given url + # + # @param url [String] URL being requested. + # + # @return [Faraday::ProxyOptions, nil] + def proxy_for_url(url) + proxy_for(Utils.URI(url)) + end + + # Checks if the given uri has a configured proxy. + # + # @param uri [URI] URI being requested. + # + # @return [Bool] + def use_for?(uri) + # check for proxy based on uri scheme + # check no_proxy + raise NotImplementedError, "given: #{uri.inspect}" + end + + # Checks if the given url has a configured proxy. + # + # @param url [String] URL being requested. + # + # @return [Bool] + def use_for_url?(url) + use_for?(Utils.URI(url)) + end + end + + # Nil is a ProxySelector implementation that always returns no proxy. Used + # if no proxy is manually configured, and Faraday.ignore_env_proxy is + # enabled. + class Nil < Single + def initialize; end + end + end +end diff --git a/spec/faraday/proxy_selector_spec.rb b/spec/faraday/proxy_selector_spec.rb new file mode 100644 index 000000000..9d82a9755 --- /dev/null +++ b/spec/faraday/proxy_selector_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'ProxySelector::Nil' do |proxy_sel| + it 'is the right type' do + expect(proxy_sel).to be_a(Faraday::ProxySelector::Nil) + end + + it 'returns no proxy for http url' do + expect(proxy_sel.proxy_for_url(http_request_url)).to be_nil + expect(proxy_sel.proxy_for(http_request_uri)).to be_nil + end + + it 'returns no proxy for https url' do + expect(proxy_sel.proxy_for_url(https_request_url)).to be_nil + expect(proxy_sel.proxy_for(https_request_uri)).to be_nil + end + + it 'doesnt use proxy for http url' do + expect(proxy_sel.use_for_url?(http_request_url)).to eq(false) + expect(proxy_sel.use_for?(http_request_uri)).to eq(false) + end + + it 'doesnt use proxy for https url' do + expect(proxy_sel.use_for_url?(https_request_url)).to eq(false) + expect(proxy_sel.use_for?(https_request_uri)).to eq(false) + end +end + +RSpec.shared_examples 'ProxySelector::Single' do |proxy_sel, expected_user, expected_pass| + it 'is the right type' do + expect(proxy_sel).to be_a(Faraday::ProxySelector::Single) + end + + it 'fetches proxy for http url' do + proxy = proxy_sel.proxy_for(http_request_uri) + expect(proxy_sel.proxy_for_url(http_request_url)).to eq(proxy) + expect(proxy).not_to be_nil + expect(proxy.host).to eq('proxy.com') + expect(proxy.user).to eq(expected_user) + expect(proxy.password).to eq(expected_pass) + end + + it 'fetches proxy for https url' do + proxy = proxy_sel.proxy_for(https_request_uri) + expect(proxy_sel.proxy_for_url(https_request_url)).to eq(proxy) + expect(proxy).not_to be_nil + expect(proxy.host).to eq('proxy.com') + expect(proxy.user).to eq(expected_user) + expect(proxy.password).to eq(expected_pass) + end + + it 'uses proxy for http url' do + expect(proxy_sel.use_for_url?(http_request_url)).to eq(true) + expect(proxy_sel.use_for?(http_request_uri)).to eq(true) + end + + it 'uses proxy for https url' do + expect(proxy_sel.use_for_url?(https_request_url)).to eq(true) + expect(proxy_sel.use_for?(https_request_uri)).to eq(true) + end +end + +RSpec.describe Faraday::ProxySelector do + let(:http_request_url) { 'http://http.example.com' } + let(:https_request_url) { 'https://https.example.com' } + let(:http_request_uri) { Faraday::Utils.URI(http_request_url) } + let(:https_request_uri) { Faraday::Utils.URI(https_request_url) } + + before do + @ignore_env_proxy = Faraday.ignore_env_proxy + end + + after do + Faraday.ignore_env_proxy = @ignore_env_proxy + end + + context '#with_env with env disabled' do + Faraday.ignore_env_proxy = true + proxy = Faraday.proxy_with_env(nil) + include_examples 'ProxySelector::Nil', proxy + end + + context '#none' do + include_examples 'ProxySelector::Nil', Faraday::ProxySelector::Nil.new + end + + context '#to_url without user auth' do + proxy = Faraday.proxy_to_url('http://proxy.com') + include_examples 'ProxySelector::Single', proxy, nil, nil + end + + context '#to_url with user auth in proxy url' do + proxy = Faraday.proxy_to_url('http://u%3A1:p%3A2@proxy.com') + include_examples 'ProxySelector::Single', proxy, 'u:1', 'p:2' + end + + context '#to_url with explicit user auth' do + proxy = Faraday.proxy_to_url('http://proxy.com', + user: 'u:1', password: 'p:2') + include_examples 'ProxySelector::Single', proxy, 'u:1', 'p:2' + end +end From 1a02f07a435d7abf3c8ceefcba3079f327970aa1 Mon Sep 17 00:00:00 2001 From: rick olson Date: Fri, 18 Oct 2019 13:39:25 -0600 Subject: [PATCH 3/8] teach ProxySelector::Environment to parse http_proxy and https_proxy --- lib/faraday/proxy_selector.rb | 21 ++++++++++++++-- spec/faraday/proxy_selector_spec.rb | 39 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/lib/faraday/proxy_selector.rb b/lib/faraday/proxy_selector.rb index 58bf140c7..525671fca 100644 --- a/lib/faraday/proxy_selector.rb +++ b/lib/faraday/proxy_selector.rb @@ -131,9 +131,14 @@ def use_for_url?(_) # Note: Logic for parsing proxy env vars heavily inspired by # http://golang.org/x/net/http/httpproxy class Environment < ProxySelector + attr_reader :http_proxy + attr_reader :https_proxy + def initialize(env = nil) - @env = env || ENV - # parse http_proxy, https_proxy, no_proxy + env ||= ENV + @http_proxy = parse_proxy(env, HTTP_PROXY_KEYS) + @https_proxy = parse_proxy(env, HTTPS_PROXY_KEYS) + # parse no_proxy end # Gets the proxy for the given uri @@ -175,6 +180,18 @@ def use_for?(uri) def use_for_url?(url) use_for?(Utils.URI(url)) end + + private + + def parse_proxy(env, keys) + value = nil + keys.detect { |k| value = env[k] } + value ? ProxyOptions.from(value) : nil + end + + HTTP_PROXY_KEYS = [:http_proxy, 'http_proxy', 'HTTP_PROXY'].freeze + HTTPS_PROXY_KEYS = [:https_proxy, 'https_proxy', 'HTTPS_PROXY'].freeze + NO_PROXY_KEYS = [:no_proxy, 'no_proxy', 'NO_PROXY'].freeze end # Nil is a ProxySelector implementation that always returns no proxy. Used diff --git a/spec/faraday/proxy_selector_spec.rb b/spec/faraday/proxy_selector_spec.rb index 9d82a9755..f1f12ef83 100644 --- a/spec/faraday/proxy_selector_spec.rb +++ b/spec/faraday/proxy_selector_spec.rb @@ -74,6 +74,45 @@ Faraday.ignore_env_proxy = @ignore_env_proxy end + context '#with_env with explicit hash' do + let(:proxy_url) { '://example.com' } + + before { Faraday.ignore_env_proxy = true } + + http_keys = Faraday::ProxySelector::Environment::HTTP_PROXY_KEYS + https_keys = Faraday::ProxySelector::Environment::HTTPS_PROXY_KEYS + http_keys.each do |http| + it "parses #{http}" do + selector = Faraday.proxy_with_env( + http => 'http'+proxy_url + ) + expect(selector.http_proxy.scheme).to eq('http') + expect(selector.http_proxy.host).to eq('example.com') + end + + https_keys.each do |https| + it "parses #{https}" do + selector = Faraday.proxy_with_env( + https => 'https'+proxy_url + ) + expect(selector.https_proxy.scheme).to eq('https') + expect(selector.https_proxy.host).to eq('example.com') + end + + it "parses #{http} & #{https}" do + selector = Faraday.proxy_with_env( + http => 'http'+proxy_url, + https => 'https'+proxy_url + ) + expect(selector.http_proxy.scheme).to eq('http') + expect(selector.http_proxy.host).to eq('example.com') + expect(selector.https_proxy.scheme).to eq('https') + expect(selector.https_proxy.host).to eq('example.com') + end + end + end + end + context '#with_env with env disabled' do Faraday.ignore_env_proxy = true proxy = Faraday.proxy_with_env(nil) From 6eb0efdbcb6e71d3bdce0ea5d5b230f38241b69a Mon Sep 17 00:00:00 2001 From: rick olson Date: Fri, 18 Oct 2019 17:10:43 -0600 Subject: [PATCH 4/8] teach ProxySelector::Environment to parse no_proxy --- lib/faraday/proxy_selector.rb | 185 +++++++++++++++++++++++----- spec/faraday/proxy_selector_spec.rb | 76 +++++++++++- 2 files changed, 229 insertions(+), 32 deletions(-) diff --git a/lib/faraday/proxy_selector.rb b/lib/faraday/proxy_selector.rb index 525671fca..ff962b17b 100644 --- a/lib/faraday/proxy_selector.rb +++ b/lib/faraday/proxy_selector.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'ipaddr' + # Faraday module. module Faraday # Builds a ProxySelector that uses the given env. If no env is provided, @@ -13,17 +15,8 @@ module Faraday # @option env [String] :https_proxy Used as the proxy for HTTPS requests, # unless overridden by :no_proxy # @option env [String] :no_proxy A String that contains comma-separated values - # specifying hosts that should be excluded from proxying. Each value - # is represented by an IP address prefix (1.2.3.4), an IP address - # prefix in CIDR notation (1.2.3.4/8), a domain name, or a special DNS - # label (*). An IP address prefix and domain name can also include a - # literal port number (1.2.3.4:80). - # A domain name matches that name and all subdomains. A domain name - # with a leading "." matches subdomains only. For example "foo.com" - # matches "foo.com" and "bar.foo.com"; ".y.com" matches "x.y.com" but - # not "y.com". A single asterisk (*) indicates that no proxying should - # be done. A best effort is made to parse the string and errors are - # ignored. + # specifying hosts that should be excluded from proxying. See + # Faraday::ProxySelector::Environment for more info. # # @return [Faraday::ProxySelector::Environment, Faraday::ProxySelector::Nil] def self.proxy_with_env(env = nil) @@ -95,14 +88,13 @@ def proxy_for(_) @options end + # @!method proxy_for_url(_) # Gets the configured proxy, regardless of the url. # # @param _ [String] Unused. # # @return [Faraday::ProxyOptions] - def proxy_for_url(_) - @options - end + alias proxy_for_url proxy_for # Checks if the given uri has a configured proxy. Returns true, because # every request uri should use the configured proxy. @@ -114,31 +106,47 @@ def use_for?(_) !@options.nil? end + # @!method use_for_url?(_) # Checks if the given url has a configured proxy. Returns true, because # every request url should use the configured proxy. # # @param _ [String] Unused. # # @return true - def use_for_url?(_) - !@options.nil? - end + alias use_for_url? use_for? end + + # Nil is a ProxySelector implementation that always returns no proxy. Used + # if no proxy is manually configured, and Faraday.ignore_env_proxy is + # enabled. + class Nil < Single + def initialize; end + end + # Environment is a ProxySelector implementation that picks a proxy based on # how the given request url matches with the http_proxy, https_proxy, and # no_proxy settings. # + # The no_proxy is a string containing comma-separated values specifying + # HTTP request hosts that should be excluded from proxying. Hosts can be + # specified as host names or IP address. See the HostMatcher and IPMatcher + # classes for the implementation of those respective filters. A single + # asterisk (*) indicates that no proxying should be done. It's + # implementation is in AsteriskMatcher. + # # Note: Logic for parsing proxy env vars heavily inspired by # http://golang.org/x/net/http/httpproxy class Environment < ProxySelector attr_reader :http_proxy attr_reader :https_proxy + attr_reader :ip_matchers + attr_reader :host_matchers def initialize(env = nil) env ||= ENV @http_proxy = parse_proxy(env, HTTP_PROXY_KEYS) @https_proxy = parse_proxy(env, HTTPS_PROXY_KEYS) - # parse no_proxy + parse_no_proxy(env) end # Gets the proxy for the given uri @@ -161,18 +169,18 @@ def proxy_for_url(url) proxy_for(Utils.URI(url)) end - # Checks if the given uri has a configured proxy. + # Checks if the given uri is allowed by the no_proxy setting. # # @param uri [URI] URI being requested. # # @return [Bool] def use_for?(uri) - # check for proxy based on uri scheme - # check no_proxy - raise NotImplementedError, "given: #{uri.inspect}" + return false if uri.host == 'localhost' + + host_port_use_proxy?(uri.host, uri.port) end - # Checks if the given url has a configured proxy. + # Checks if the given url is allowed by the no_proxy setting. # # @param url [String] URL being requested. # @@ -183,22 +191,143 @@ def use_for_url?(url) private + def host_port_use_proxy?(host, port) + return false if @host_matchers.any? { |m| m.matches?(host, port) } + + # attempt to parse every host as an IP + ip = IPAddr.new(host) + !(ip.loopback? || @ip_matchers.any? { |m| m.matches?(ip, port) }) + rescue IPAddr::InvalidAddressError + true + end + def parse_proxy(env, keys) value = nil keys.detect { |k| value = env[k] } value ? ProxyOptions.from(value) : nil end + def parse_no_proxy(env) + @ip_matchers = [] + @host_matchers = [] + value = nil + NO_PROXY_KEYS.detect { |k| value = env[k] } + return unless value + + value.split(',').each do |entry| + entry.strip! + entry.downcase! + + if entry == '*' + @ip_matchers = @host_matchers = AsteriskMatcher + break + end + + parse_no_proxy_entry(entry) unless entry.empty? + end + @ip_matchers.freeze + @host_matchers.freeze + end + + def parse_no_proxy_entry(entry) + port, ip, host = parse_entry(entry) + @ip_matchers << IPMatcher.new(ip, port) if ip + @host_matchers << HostMatcher.new(host, port) if host + end + + def parse_entry(entry) + # This is an IP or IP range with no port + # Parser raises if IP has port suffix. + [nil, IPAddr.new(entry), nil] + rescue IPAddr::InvalidAddressError + host, port = if /\A(?.*):(?

\d+)\z/i =~ entry + [h, p.to_i] + else + [entry, nil] + end + + # There is no host part, likely the entry is malformed; ignore. + return if host.empty? + + begin + # This is an IP or IP range with explicit port + return [port, IPAddr.new(host), nil] + rescue IPAddr::InvalidAddressError # rubocop:disable Lint/HandleExceptions + end + + # can't parse as IP, assume it's a domain + [port, nil, host] + end + HTTP_PROXY_KEYS = [:http_proxy, 'http_proxy', 'HTTP_PROXY'].freeze HTTPS_PROXY_KEYS = [:https_proxy, 'https_proxy', 'HTTPS_PROXY'].freeze NO_PROXY_KEYS = [:no_proxy, 'no_proxy', 'NO_PROXY'].freeze end - # Nil is a ProxySelector implementation that always returns no proxy. Used - # if no proxy is manually configured, and Faraday.ignore_env_proxy is - # enabled. - class Nil < Single - def initialize; end + # IPMatcher parses an IP related entry in the no_proxy env variable. + class IPMatcher + # @param ip [IPAddr] IP address prefix (1.2.3.4) or an IP address prefix + # in CIDR notation (1.2.3.4/8). + # @param port [Integer, nil] Determines whether a given ip and port + # combo must match a specific port, or if any port is valid. + def initialize(ip, port) + @ip = ip + @port = port + end + + # Determines if the given ip and port are matched by this IPMatcher. + # + # @param ip [IPAddr] IP address prefix (1.2.3.4) of the request URL. + # @param port [Integer] Port of the request URL. + # + # @return [Bool] + def matches?(ip, port) + return false unless @port.nil? || @port == port + + @ip.include?(ip) + end + end + + # HostMatcher parses a no_proxy entry with a domain and optional port. A + # host name matches that name and all subdomains. A host name with a + # leading "." matches subdomains only. For example, "foo.com" matches + # "foo.com" and "bar.foo.com"; ".y.com" matches "x.y.com" but not "y.com". + class HostMatcher + # @param host [String] The host name entry from no_proxy. + # @param port [Integer, nil] Determines whether a given host and port + # combo must match a specific port, or if any port is valid. + def initialize(host, port) + host = host[1..-1] if host[0..1] == '*.' + + @port = port + @host = if (@match_host = host[0] != '.') + ".#{host}" + else + host + end + end + + # Determines if the given host and port are matched by this HostMatcher. + # + # @param host [String] Host name of the request URL. + # @param port [Integer] Port of the request URL. + # + # @return [Bool] + def matches?(host, port) + return false unless @port.nil? || @port == port + return true if host.end_with?(@host) + + @match_host && host == @host[1..-1] + end + end + + # AsteriskMatcher replaces all ip and host matchers in a + # ProxySelector::Environment with one that indicates no proxying will be + # done. + class AsteriskMatcher + def self.any? + true + end end end end diff --git a/spec/faraday/proxy_selector_spec.rb b/spec/faraday/proxy_selector_spec.rb index f1f12ef83..5c24845c7 100644 --- a/spec/faraday/proxy_selector_spec.rb +++ b/spec/faraday/proxy_selector_spec.rb @@ -84,7 +84,7 @@ http_keys.each do |http| it "parses #{http}" do selector = Faraday.proxy_with_env( - http => 'http'+proxy_url + http => 'http' + proxy_url ) expect(selector.http_proxy.scheme).to eq('http') expect(selector.http_proxy.host).to eq('example.com') @@ -93,7 +93,7 @@ https_keys.each do |https| it "parses #{https}" do selector = Faraday.proxy_with_env( - https => 'https'+proxy_url + https => 'https' + proxy_url ) expect(selector.https_proxy.scheme).to eq('https') expect(selector.https_proxy.host).to eq('example.com') @@ -101,8 +101,8 @@ it "parses #{http} & #{https}" do selector = Faraday.proxy_with_env( - http => 'http'+proxy_url, - https => 'https'+proxy_url + http => 'http' + proxy_url, + https => 'https' + proxy_url ) expect(selector.http_proxy.scheme).to eq('http') expect(selector.http_proxy.host).to eq('example.com') @@ -139,3 +139,71 @@ include_examples 'ProxySelector::Single', proxy, 'u:1', 'p:2' end end + +# https://github.com/golang/net/blob/da9a3fd4c5820e74b24a6cb7fb438dc9b0dd377c/http/httpproxy/proxy_test.go#L261 +context Faraday::ProxySelector::Environment do + context '#use_for_url?' do + no_proxy_tests = [ + # Never proxy localhost: + ['localhost', false], + ['127.0.0.1', false], + ['127.0.0.2', false], + ['[::1]', false], + ['[::2]', true], # not a loopback address + + ['192.168.1.1', false], # matches exact IPv4 + ['192.168.1.2', true], # ports do not match + ['192.168.1.3', false], # matches exact IPv4:port + ['192.168.1.4', true], # no match + ['10.0.0.2', false], # matches IPv4/CIDR + ['[2001:db8::52:0:1]', false], # matches exact IPv6 + ['[2001:db8::52:0:2]', true], # no match + ['[2001:db8::52:0:2]:443', false], # matches explicit [IPv6]:port + ['[2001:db8::52:0:3]', false], # matches exact [IPv6]:port + ['[2002:db8:a::123]', false], # matches IPv6/CIDR + ['[fe80::424b:c8be:1643:a1b6]', true], # no match + + ['barbaz.net', true], # does not match as .barbaz.net + ['www.barbaz.net', false], # does match as .barbaz.net + ['foobar.com', false], # does match as foobar.com + ['www.foobar.com', false], # match because NO_PROXY includes 'foobar.com' + ['foofoobar.com', true], # not match as a part of foobar.com + ['baz.com', true], # not match as a part of barbaz.com + ['localhost.net', true], # not match as suffix of address + ['local.localhost', true], # not match as prefix as address + ['barbarbaz.net', true], # not match, wrong domain + ['wildcard.io', true], # does not match as *.wildcard.io + ['nested.wildcard.io', false], # match as *.wildcard.io + ['awildcard.io', true] # not a match because of '*' + ] + + context '(full no_proxy example)' do + proxy = Faraday.proxy_with_env( + no_proxy: 'foobar.com, .barbaz.net, *.wildcard.io, 192.168.1.1, 192.168.1.2:81, 192.168.1.3:80, 10.0.0.0/30, 2001:db8::52:0:1, [2001:db8::52:0:2]:443, [2001:db8::52:0:3]:80, 2002:db8:a::45/64' + ) + + no_proxy_tests.each do |(host, matches)| + it "#{matches ? :allows : :forbids} proxy for #{host}" do + expect(proxy.use_for_url?("http://#{host}/test")).to eq(matches) + end + end + end + + context '(invalid no_proxy)' do + it 'forbids proxy' do + proxy = Faraday.proxy_with_env(no_proxy: ':1') + expect(proxy.use_for_url?('http://example.com')).to eq(true) + end + end + + context '(no_proxy=*)' do + proxy = Faraday.proxy_with_env(no_proxy: 'baz.com, *') + + no_proxy_tests.each do |(host, _)| + it "forbids proxy for #{host}" do + expect(proxy.use_for_url?("http://#{host}/test")).to eq(false) + end + end + end + end +end From 46f7aeb4fb9008f8cb6c64fd686ebc141bf7e02a Mon Sep 17 00:00:00 2001 From: rick olson Date: Fri, 18 Oct 2019 18:13:33 -0600 Subject: [PATCH 5/8] ensure proxy urls only have valid schemes --- lib/faraday/proxy_selector.rb | 28 ++++++++++++++--- spec/faraday/proxy_selector_spec.rb | 49 +++++++++++++++++++---------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/lib/faraday/proxy_selector.rb b/lib/faraday/proxy_selector.rb index ff962b17b..99896687f 100644 --- a/lib/faraday/proxy_selector.rb +++ b/lib/faraday/proxy_selector.rb @@ -33,6 +33,11 @@ def self.proxy_with_env(env = nil) # # @return [Faraday::ProxySelector::Single] def self.proxy_to(uri, user: nil, password: nil) + unless ProxySelector::VALID_PROXY_SCHEMES.include?(uri.scheme) + raise ArgumentError, "invalid proxy url #{uri.to_s.inspect}. " \ + 'Must start with http://, https://, or socks://' + end + ProxySelector::Single.new(uri, user: user, password: password) end @@ -44,7 +49,8 @@ def self.proxy_to(uri, user: nil, password: nil) # # @return [Faraday::ProxySelector::Single] def self.proxy_to_url(url, user: nil, password: nil) - proxy_to(Utils.URI(url), user: user, password: password) + curl = ProxySelector.canonical_proxy_url(url) + proxy_to(Utils.URI(curl), user: user, password: password) end # Proxy is a generic class that knows the Proxy for any given URL. You can @@ -72,6 +78,13 @@ def self.proxy_to_url(url, user: nil, password: nil) # proxy.proxy_for(uri) # # => Faraday::ProxyOptions instance or nil class ProxySelector + def self.canonical_proxy_url(url) + url_dc = url.to_s.downcase + return url if VALID_PROXY_PREFIXES.any? { |s| url_dc.start_with?(s) } + + VALID_PROXY_PREFIXES[0] + url + end + # Single is a ProxySelector implementation that always returns the given # proxy uri. class Single < ProxySelector @@ -125,7 +138,8 @@ def initialize; end # Environment is a ProxySelector implementation that picks a proxy based on # how the given request url matches with the http_proxy, https_proxy, and - # no_proxy settings. + # no_proxy settings. Invalid URL values in http_proxy or https_proxy will + # be ignored if reading from ENV. # # The no_proxy is a string containing comma-separated values specifying # HTTP request hosts that should be excluded from proxying. Hosts can be @@ -143,8 +157,7 @@ class Environment < ProxySelector attr_reader :host_matchers def initialize(env = nil) - env ||= ENV - @http_proxy = parse_proxy(env, HTTP_PROXY_KEYS) + @http_proxy = parse_proxy(env ||= ENV, HTTP_PROXY_KEYS) @https_proxy = parse_proxy(env, HTTPS_PROXY_KEYS) parse_no_proxy(env) end @@ -204,7 +217,9 @@ def host_port_use_proxy?(host, port) def parse_proxy(env, keys) value = nil keys.detect { |k| value = env[k] } - value ? ProxyOptions.from(value) : nil + return nil unless value && !value.empty? + + ProxyOptions.from(self.class.canonical_proxy_url(value)) end def parse_no_proxy(env) @@ -264,6 +279,9 @@ def parse_entry(entry) NO_PROXY_KEYS = [:no_proxy, 'no_proxy', 'NO_PROXY'].freeze end + VALID_PROXY_SCHEMES = Set.new(%w[http https socks5]).freeze + VALID_PROXY_PREFIXES = %w[http:// https:// socks5://].freeze + # IPMatcher parses an IP related entry in the no_proxy env variable. class IPMatcher # @param ip [IPAddr] IP address prefix (1.2.3.4) or an IP address prefix diff --git a/spec/faraday/proxy_selector_spec.rb b/spec/faraday/proxy_selector_spec.rb index 5c24845c7..ee5ff0462 100644 --- a/spec/faraday/proxy_selector_spec.rb +++ b/spec/faraday/proxy_selector_spec.rb @@ -66,19 +66,9 @@ let(:http_request_uri) { Faraday::Utils.URI(http_request_url) } let(:https_request_uri) { Faraday::Utils.URI(https_request_url) } - before do - @ignore_env_proxy = Faraday.ignore_env_proxy - end - - after do - Faraday.ignore_env_proxy = @ignore_env_proxy - end - - context '#with_env with explicit hash' do + context '#proxy_with_env with explicit hash' do let(:proxy_url) { '://example.com' } - before { Faraday.ignore_env_proxy = true } - http_keys = Faraday::ProxySelector::Environment::HTTP_PROXY_KEYS https_keys = Faraday::ProxySelector::Environment::HTTPS_PROXY_KEYS http_keys.each do |http| @@ -113,27 +103,52 @@ end end - context '#with_env with env disabled' do + context '#proxy_with_env with env disabled' do + after { Faraday.ignore_env_proxy = @ignore_env_proxy } + + @ignore_env_proxy = Faraday.ignore_env_proxy Faraday.ignore_env_proxy = true proxy = Faraday.proxy_with_env(nil) include_examples 'ProxySelector::Nil', proxy end - context '#none' do - include_examples 'ProxySelector::Nil', Faraday::ProxySelector::Nil.new + context '#proxy_to_url with invalid scheme' do + proxy_sel = Faraday.proxy_to_url('file://proxy.com') + + it 'is the right type' do + expect(proxy_sel).to be_a(Faraday::ProxySelector::Single) + end + + it 'has invalid proxy' do + proxy = proxy_sel.proxy_for(http_request_uri) + expect(proxy).not_to be_nil + expect(proxy.host).to eq('file') + end + end + + context '#proxy_to with invalid scheme' do + it 'raises' do + expect { Faraday.proxy_to(Faraday::Utils.URI('file://proxy.com')) } + .to raise_error(ArgumentError) + end end - context '#to_url without user auth' do + context '#proxy_to_url without user auth' do proxy = Faraday.proxy_to_url('http://proxy.com') include_examples 'ProxySelector::Single', proxy, nil, nil end - context '#to_url with user auth in proxy url' do + context '#proxy_to_url without scheme' do + proxy = Faraday.proxy_to_url('proxy.com') + include_examples 'ProxySelector::Single', proxy, nil, nil + end + + context '#proxy_to_url with user auth in proxy url' do proxy = Faraday.proxy_to_url('http://u%3A1:p%3A2@proxy.com') include_examples 'ProxySelector::Single', proxy, 'u:1', 'p:2' end - context '#to_url with explicit user auth' do + context '#proxy_to_url with explicit user auth' do proxy = Faraday.proxy_to_url('http://proxy.com', user: 'u:1', password: 'p:2') include_examples 'ProxySelector::Single', proxy, 'u:1', 'p:2' From 614b65c4eb17959ffd560ea0f2a6ea976deead4d Mon Sep 17 00:00:00 2001 From: rick olson Date: Fri, 18 Oct 2019 18:52:40 -0600 Subject: [PATCH 6/8] implement Environment#proxy_for --- lib/faraday/proxy_selector.rb | 6 ++-- spec/faraday/proxy_selector_spec.rb | 51 +++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/lib/faraday/proxy_selector.rb b/lib/faraday/proxy_selector.rb index 99896687f..c11d06315 100644 --- a/lib/faraday/proxy_selector.rb +++ b/lib/faraday/proxy_selector.rb @@ -168,9 +168,9 @@ def initialize(env = nil) # # @return [Faraday::ProxyOptions, nil] def proxy_for(uri) - # check for proxy based on uri scheme - # check no_proxy - raise NotImplementedError, "given: #{uri.inspect}" + proxy = (uri.scheme == 'https' && @https_proxy) || @http_proxy + proxy = nil if proxy && !use_for?(uri) + proxy end # Gets the proxy for the given url diff --git a/spec/faraday/proxy_selector_spec.rb b/spec/faraday/proxy_selector_spec.rb index ee5ff0462..52c817461 100644 --- a/spec/faraday/proxy_selector_spec.rb +++ b/spec/faraday/proxy_selector_spec.rb @@ -104,9 +104,8 @@ end context '#proxy_with_env with env disabled' do - after { Faraday.ignore_env_proxy = @ignore_env_proxy } - @ignore_env_proxy = Faraday.ignore_env_proxy + after { Faraday.ignore_env_proxy = @ignore_env_proxy } Faraday.ignore_env_proxy = true proxy = Faraday.proxy_with_env(nil) include_examples 'ProxySelector::Nil', proxy @@ -155,8 +154,54 @@ end end -# https://github.com/golang/net/blob/da9a3fd4c5820e74b24a6cb7fb438dc9b0dd377c/http/httpproxy/proxy_test.go#L261 context Faraday::ProxySelector::Environment do + # https://github.com/golang/net/blob/da9a3fd4c5820e74b24a6cb7fb438dc9b0dd377c/http/httpproxy/proxy_test.go#L22 + context '#proxy_for_url' do + # [ env, req_url, expected_proxy ] + [ + [{ http_proxy: '127.0.0.1:8080' }, nil, + 'http://127.0.0.1:8080'], + [{ http_proxy: 'cache.corp.example.com:1234' }, nil, + 'http://cache.corp.example.com:1234'], + [{ http_proxy: 'cache.corp.example.com' }, nil, + 'http://cache.corp.example.com'], + [{ http_proxy: 'https://cache.corp.example.com' }, nil, + 'https://cache.corp.example.com'], + [{ http_proxy: 'http://127.0.0.1:8080' }, nil, + 'http://127.0.0.1:8080'], + [{ http_proxy: 'https://127.0.0.1:8080' }, nil, + 'https://127.0.0.1:8080'], + [{ http_proxy: 'socks5://127.0.0.1' }, nil, + 'socks5://127.0.0.1'], + # Don't use secure for http + [{ http_proxy: 'http.proxy.tld', https_proxy: 'secure.proxy.tld' }, + 'http://insecure.tld/', 'http://http.proxy.tld'], + # Use secure for https. + [{ http_proxy: 'http.proxy.tld', https_proxy: 'secure.proxy.tld' }, + 'https://secure.tld/', 'http://secure.proxy.tld'], + [{ http_proxy: 'http.proxy.tld', https_proxy: 'https://secure.proxy.tld' }, + 'https://secure.tld/', 'https://secure.proxy.tld'], + [{ no_proxy: 'example.com', http_proxy: 'proxy' }, nil, nil], + [{ no_proxy: '.example.com', http_proxy: 'proxy' }, nil, + 'http://proxy'], + [{ no_proxy: 'ample.com', http_proxy: 'proxy' }, nil, 'http://proxy'], + [{ no_proxy: 'example.com', http_proxy: 'proxy' }, + 'http://foo.example.com/', nil], + [{ no_proxy: '.foo.com', http_proxy: 'proxy' }, nil, 'http://proxy'] + ].each do |(env, req_url, expected_proxy)| + it "expects#{" req url #{req_url} to have" if req_url} proxy #{expected_proxy.inspect} for #{env.inspect}" do + selector = Faraday.proxy_with_env(env) + proxy = selector.proxy_for_url(req_url || 'http://example.com') + if expected_proxy + expect(proxy.uri.to_s).to eq(expected_proxy) + else + expect(proxy).to be_nil + end + end + end + end + + # https://github.com/golang/net/blob/da9a3fd4c5820e74b24a6cb7fb438dc9b0dd377c/http/httpproxy/proxy_test.go#L261 context '#use_for_url?' do no_proxy_tests = [ # Never proxy localhost: From 5e90a06ed3840d539efdbcd10d98ce863d2de191 Mon Sep 17 00:00:00 2001 From: rick olson Date: Fri, 18 Oct 2019 19:17:19 -0600 Subject: [PATCH 7/8] implement fix for cgi environments https://github.com/golang/go/issues/16405 --- lib/faraday/proxy_selector.rb | 11 ++++++++++- spec/faraday/proxy_selector_spec.rb | 12 ++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/faraday/proxy_selector.rb b/lib/faraday/proxy_selector.rb index c11d06315..29fc1c5e5 100644 --- a/lib/faraday/proxy_selector.rb +++ b/lib/faraday/proxy_selector.rb @@ -157,7 +157,16 @@ class Environment < ProxySelector attr_reader :host_matchers def initialize(env = nil) - @http_proxy = parse_proxy(env ||= ENV, HTTP_PROXY_KEYS) + env ||= ENV + + # don't parse HTTP_PROXY if this looks like a CGI request + is_cgi = env['REQUEST_METHOD'] + @http_proxy = if is_cgi.nil? || is_cgi.empty? + parse_proxy(env, HTTP_PROXY_KEYS) + else + parse_proxy(env, HTTP_PROXY_KEYS[0...-1]) + end + @https_proxy = parse_proxy(env, HTTPS_PROXY_KEYS) parse_no_proxy(env) end diff --git a/spec/faraday/proxy_selector_spec.rb b/spec/faraday/proxy_selector_spec.rb index 52c817461..a2081c378 100644 --- a/spec/faraday/proxy_selector_spec.rb +++ b/spec/faraday/proxy_selector_spec.rb @@ -179,6 +179,18 @@ # Use secure for https. [{ http_proxy: 'http.proxy.tld', https_proxy: 'secure.proxy.tld' }, 'https://secure.tld/', 'http://secure.proxy.tld'], + # don't use HTTP_PROXY in a CGI environment, where HTTP_PROXY can be + # attacker-controlled. + [{ 'HTTP_PROXY' => 'http://10.1.2.3:8080', 'REQUEST_METHOD' => '1' }, nil, + nil], + # :http_proxy and "http_proxy" still work + [{ http_proxy: 'http://10.1.2.3:8080', 'REQUEST_METHOD' => '1' }, nil, + 'http://10.1.2.3:8080'], + [{ 'http_proxy' => 'http://10.1.2.3:8080', 'REQUEST_METHOD' => '1' }, nil, + 'http://10.1.2.3:8080'], + # HTTPS proxy is still used even in CGI environment. + [{ https_proxy: 'https://secure.proxy.tld', 'REQUEST_METHOD' => '1' }, nil, + nil], [{ http_proxy: 'http.proxy.tld', https_proxy: 'https://secure.proxy.tld' }, 'https://secure.tld/', 'https://secure.proxy.tld'], [{ no_proxy: 'example.com', http_proxy: 'proxy' }, nil, nil], From 4a17694fef7c8b6f2666f11c4564d1bfd838ae86 Mon Sep 17 00:00:00 2001 From: rick olson Date: Fri, 18 Oct 2019 19:37:35 -0600 Subject: [PATCH 8/8] remove redundant methods by checking for #scheme to see if it's a uri --- lib/faraday/proxy_selector.rb | 94 +++++++++-------------------- spec/faraday/proxy_selector_spec.rb | 46 ++++++-------- 2 files changed, 50 insertions(+), 90 deletions(-) diff --git a/lib/faraday/proxy_selector.rb b/lib/faraday/proxy_selector.rb index 29fc1c5e5..710400ce0 100644 --- a/lib/faraday/proxy_selector.rb +++ b/lib/faraday/proxy_selector.rb @@ -33,24 +33,8 @@ def self.proxy_with_env(env = nil) # # @return [Faraday::ProxySelector::Single] def self.proxy_to(uri, user: nil, password: nil) - unless ProxySelector::VALID_PROXY_SCHEMES.include?(uri.scheme) - raise ArgumentError, "invalid proxy url #{uri.to_s.inspect}. " \ - 'Must start with http://, https://, or socks://' - end - - ProxySelector::Single.new(uri, user: user, password: password) - end - - # Builds a ProxySelector that returns the given uri. - # - # @param url [String] The proxy raw url. - # @param user [String, nil] Optional user info for proxy. - # @param password [String, nil] Optional password info for proxy. - # - # @return [Faraday::ProxySelector::Single] - def self.proxy_to_url(url, user: nil, password: nil) - curl = ProxySelector.canonical_proxy_url(url) - proxy_to(Utils.URI(curl), user: user, password: password) + curi = ProxySelector.canonical_proxy_uri(uri) + ProxySelector::Single.new(curi, user: user, password: password) end # Proxy is a generic class that knows the Proxy for any given URL. You can @@ -63,21 +47,36 @@ def self.proxy_to_url(url, user: nil, password: nil) # proxy = Faraday.proxy_with_env(http_proxy: "http://proxy.example.com") # # # Set with string URL - # proxy = Faraday.proxy_to_url("http://proxy.example.com") + # proxy = Faraday.proxy_to("http://proxy.example.com") # # # Set with URI # uri = Faraday::Utils::URI("http://proxy.example.com") - # proxy = Faraday.proxy_to(uri) # shortcut + # proxy = Faraday.proxy_to(uri) # # Once you have an instance, you can get the proxy for a request url: # - # proxy.proxy_for_url("http://example.com") + # proxy.proxy_for("http://example.com") # # => Faraday::ProxyOptions instance or nil + # proxy.use_for?("http://example.com") + # # => true or false # # uri = Faraday::Utils::URI("http://example.com") # proxy.proxy_for(uri) # # => Faraday::ProxyOptions instance or nil + # proxy.use_for?(uri) + # # => true or false class ProxySelector + def self.canonical_proxy_uri(url) + if url.respond_to?(:scheme) + return url if VALID_PROXY_SCHEMES.include?(url.scheme) + + raise ArgumentError, "invalid proxy url #{url.to_s.inspect}. " \ + 'Must start with http://, https://, or socks://' + end + + Utils.URI(canonical_proxy_url(url)) + end + def self.canonical_proxy_url(url) url_dc = url.to_s.downcase return url if VALID_PROXY_PREFIXES.any? { |s| url_dc.start_with?(s) } @@ -94,39 +93,22 @@ def initialize(uri, user: nil, password: nil) # Gets the configured proxy, regardless of the uri. # - # @param _ [URI] Unused. + # @param _ [URI, String] Unused. # # @return [Faraday::ProxyOptions] def proxy_for(_) @options end - # @!method proxy_for_url(_) - # Gets the configured proxy, regardless of the url. - # - # @param _ [String] Unused. - # - # @return [Faraday::ProxyOptions] - alias proxy_for_url proxy_for - # Checks if the given uri has a configured proxy. Returns true, because # every request uri should use the configured proxy. # - # @param _ [URI] Unused. + # @param _ [URI, String] Unused. # # @return true def use_for?(_) !@options.nil? end - - # @!method use_for_url?(_) - # Checks if the given url has a configured proxy. Returns true, because - # every request url should use the configured proxy. - # - # @param _ [String] Unused. - # - # @return true - alias use_for_url? use_for? end # Nil is a ProxySelector implementation that always returns no proxy. Used @@ -173,42 +155,26 @@ def initialize(env = nil) # Gets the proxy for the given uri # - # @param uri [URI] URI being requested. + # @param uri [URI, String] URI being requested. # # @return [Faraday::ProxyOptions, nil] def proxy_for(uri) - proxy = (uri.scheme == 'https' && @https_proxy) || @http_proxy - proxy = nil if proxy && !use_for?(uri) + curi = self.class.canonical_proxy_uri(uri) + proxy = (curi.scheme == 'https' && @https_proxy) || @http_proxy + proxy = nil if proxy && !use_for?(curi) proxy end - # Gets the proxy for the given url - # - # @param url [String] URL being requested. - # - # @return [Faraday::ProxyOptions, nil] - def proxy_for_url(url) - proxy_for(Utils.URI(url)) - end - # Checks if the given uri is allowed by the no_proxy setting. # - # @param uri [URI] URI being requested. + # @param uri [URI, String] URI being requested. # # @return [Bool] def use_for?(uri) - return false if uri.host == 'localhost' - - host_port_use_proxy?(uri.host, uri.port) - end + curi = self.class.canonical_proxy_uri(uri) + return false if curi.host == 'localhost' - # Checks if the given url is allowed by the no_proxy setting. - # - # @param url [String] URL being requested. - # - # @return [Bool] - def use_for_url?(url) - use_for?(Utils.URI(url)) + host_port_use_proxy?(curi.host, curi.port) end private diff --git a/spec/faraday/proxy_selector_spec.rb b/spec/faraday/proxy_selector_spec.rb index a2081c378..4b2e66f2c 100644 --- a/spec/faraday/proxy_selector_spec.rb +++ b/spec/faraday/proxy_selector_spec.rb @@ -6,22 +6,18 @@ end it 'returns no proxy for http url' do - expect(proxy_sel.proxy_for_url(http_request_url)).to be_nil expect(proxy_sel.proxy_for(http_request_uri)).to be_nil end it 'returns no proxy for https url' do - expect(proxy_sel.proxy_for_url(https_request_url)).to be_nil expect(proxy_sel.proxy_for(https_request_uri)).to be_nil end it 'doesnt use proxy for http url' do - expect(proxy_sel.use_for_url?(http_request_url)).to eq(false) expect(proxy_sel.use_for?(http_request_uri)).to eq(false) end it 'doesnt use proxy for https url' do - expect(proxy_sel.use_for_url?(https_request_url)).to eq(false) expect(proxy_sel.use_for?(https_request_uri)).to eq(false) end end @@ -33,7 +29,7 @@ it 'fetches proxy for http url' do proxy = proxy_sel.proxy_for(http_request_uri) - expect(proxy_sel.proxy_for_url(http_request_url)).to eq(proxy) + expect(proxy_sel.proxy_for(http_request_url)).to eq(proxy) expect(proxy).not_to be_nil expect(proxy.host).to eq('proxy.com') expect(proxy.user).to eq(expected_user) @@ -42,7 +38,7 @@ it 'fetches proxy for https url' do proxy = proxy_sel.proxy_for(https_request_uri) - expect(proxy_sel.proxy_for_url(https_request_url)).to eq(proxy) + expect(proxy_sel.proxy_for(https_request_url)).to eq(proxy) expect(proxy).not_to be_nil expect(proxy.host).to eq('proxy.com') expect(proxy.user).to eq(expected_user) @@ -50,12 +46,10 @@ end it 'uses proxy for http url' do - expect(proxy_sel.use_for_url?(http_request_url)).to eq(true) expect(proxy_sel.use_for?(http_request_uri)).to eq(true) end it 'uses proxy for https url' do - expect(proxy_sel.use_for_url?(https_request_url)).to eq(true) expect(proxy_sel.use_for?(https_request_uri)).to eq(true) end end @@ -111,8 +105,8 @@ include_examples 'ProxySelector::Nil', proxy end - context '#proxy_to_url with invalid scheme' do - proxy_sel = Faraday.proxy_to_url('file://proxy.com') + context '#proxy_to with invalid url scheme' do + proxy_sel = Faraday.proxy_to('file://proxy.com') it 'is the right type' do expect(proxy_sel).to be_a(Faraday::ProxySelector::Single) @@ -125,38 +119,38 @@ end end - context '#proxy_to with invalid scheme' do + context '#proxy_to with invalid uri scheme' do it 'raises' do expect { Faraday.proxy_to(Faraday::Utils.URI('file://proxy.com')) } .to raise_error(ArgumentError) end end - context '#proxy_to_url without user auth' do - proxy = Faraday.proxy_to_url('http://proxy.com') + context '#proxy_to without user auth' do + proxy = Faraday.proxy_to('http://proxy.com') include_examples 'ProxySelector::Single', proxy, nil, nil end - context '#proxy_to_url without scheme' do - proxy = Faraday.proxy_to_url('proxy.com') + context '#proxy_to without scheme' do + proxy = Faraday.proxy_to('proxy.com') include_examples 'ProxySelector::Single', proxy, nil, nil end - context '#proxy_to_url with user auth in proxy url' do - proxy = Faraday.proxy_to_url('http://u%3A1:p%3A2@proxy.com') + context '#proxy_to with user auth in proxy url' do + proxy = Faraday.proxy_to('http://u%3A1:p%3A2@proxy.com') include_examples 'ProxySelector::Single', proxy, 'u:1', 'p:2' end - context '#proxy_to_url with explicit user auth' do - proxy = Faraday.proxy_to_url('http://proxy.com', - user: 'u:1', password: 'p:2') + context '#proxy_to with explicit user auth' do + proxy = Faraday.proxy_to('http://proxy.com', + user: 'u:1', password: 'p:2') include_examples 'ProxySelector::Single', proxy, 'u:1', 'p:2' end end context Faraday::ProxySelector::Environment do # https://github.com/golang/net/blob/da9a3fd4c5820e74b24a6cb7fb438dc9b0dd377c/http/httpproxy/proxy_test.go#L22 - context '#proxy_for_url' do + context '#proxy_for' do # [ env, req_url, expected_proxy ] [ [{ http_proxy: '127.0.0.1:8080' }, nil, @@ -203,7 +197,7 @@ ].each do |(env, req_url, expected_proxy)| it "expects#{" req url #{req_url} to have" if req_url} proxy #{expected_proxy.inspect} for #{env.inspect}" do selector = Faraday.proxy_with_env(env) - proxy = selector.proxy_for_url(req_url || 'http://example.com') + proxy = selector.proxy_for(req_url || 'http://example.com') if expected_proxy expect(proxy.uri.to_s).to eq(expected_proxy) else @@ -214,7 +208,7 @@ end # https://github.com/golang/net/blob/da9a3fd4c5820e74b24a6cb7fb438dc9b0dd377c/http/httpproxy/proxy_test.go#L261 - context '#use_for_url?' do + context '#use_for?' do no_proxy_tests = [ # Never proxy localhost: ['localhost', false], @@ -256,7 +250,7 @@ no_proxy_tests.each do |(host, matches)| it "#{matches ? :allows : :forbids} proxy for #{host}" do - expect(proxy.use_for_url?("http://#{host}/test")).to eq(matches) + expect(proxy.use_for?("http://#{host}/test")).to eq(matches) end end end @@ -264,7 +258,7 @@ context '(invalid no_proxy)' do it 'forbids proxy' do proxy = Faraday.proxy_with_env(no_proxy: ':1') - expect(proxy.use_for_url?('http://example.com')).to eq(true) + expect(proxy.use_for?('http://example.com')).to eq(true) end end @@ -273,7 +267,7 @@ no_proxy_tests.each do |(host, _)| it "forbids proxy for #{host}" do - expect(proxy.use_for_url?("http://#{host}/test")).to eq(false) + expect(proxy.use_for?("http://#{host}/test")).to eq(false) end end end