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/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/lib/faraday/proxy_selector.rb b/lib/faraday/proxy_selector.rb new file mode 100644 index 000000000..710400ce0 --- /dev/null +++ b/lib/faraday/proxy_selector.rb @@ -0,0 +1,326 @@ +# frozen_string_literal: true + +require 'ipaddr' + +# 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. See + # Faraday::ProxySelector::Environment for more info. + # + # @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) + 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 + # 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("http://proxy.example.com") + # + # # Set with URI + # uri = Faraday::Utils::URI("http://proxy.example.com") + # proxy = Faraday.proxy_to(uri) + # + # Once you have an instance, you can get the proxy for a request url: + # + # 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) } + + VALID_PROXY_PREFIXES[0] + url + end + + # 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, String] Unused. + # + # @return [Faraday::ProxyOptions] + def proxy_for(_) + @options + end + + # Checks if the given uri has a configured proxy. Returns true, because + # every request uri should use the configured proxy. + # + # @param _ [URI, String] Unused. + # + # @return true + def use_for?(_) + !@options.nil? + 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 + + # 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. 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 + # 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 + + # 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 + + # Gets the proxy for the given uri + # + # @param uri [URI, String] URI being requested. + # + # @return [Faraday::ProxyOptions, nil] + def proxy_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 + + # Checks if the given uri is allowed by the no_proxy setting. + # + # @param uri [URI, String] URI being requested. + # + # @return [Bool] + def use_for?(uri) + curi = self.class.canonical_proxy_uri(uri) + return false if curi.host == 'localhost' + + host_port_use_proxy?(curi.host, curi.port) + end + + 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] } + return nil unless value && !value.empty? + + ProxyOptions.from(self.class.canonical_proxy_url(value)) + 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 + + 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 + # 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/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 diff --git a/spec/faraday/proxy_selector_spec.rb b/spec/faraday/proxy_selector_spec.rb new file mode 100644 index 000000000..4b2e66f2c --- /dev/null +++ b/spec/faraday/proxy_selector_spec.rb @@ -0,0 +1,275 @@ +# 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(http_request_uri)).to be_nil + end + + it 'returns no proxy for https url' do + 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?(http_request_uri)).to eq(false) + end + + it 'doesnt use proxy for https url' do + 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(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(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?(http_request_uri)).to eq(true) + end + + it 'uses proxy for https url' do + 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) } + + context '#proxy_with_env with explicit hash' do + let(:proxy_url) { '://example.com' } + + 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 '#proxy_with_env with env disabled' do + @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 + end + + 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) + 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 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 without user auth' do + proxy = Faraday.proxy_to('http://proxy.com') + include_examples 'ProxySelector::Single', proxy, nil, nil + end + + context '#proxy_to without scheme' do + proxy = Faraday.proxy_to('proxy.com') + include_examples 'ProxySelector::Single', proxy, nil, nil + end + + 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 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' 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'], + # 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], + [{ 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(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?' 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?("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?('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?("http://#{host}/test")).to eq(false) + end + end + end + end +end