Skip to content

Commit

Permalink
Support for defining HTTP proxy via the $HTTP_PROXY (etc) env-var (#307)
Browse files Browse the repository at this point in the history
* Add support for reading HTTP proxy information from *_PROXY env-vars
* Support clearing the *_PROXY env-vars during test runs
* Tests for when using *_PROXY env-vars
* Deal gracefully with invalid URLs in the $*_PROXY env-vars

- My spec_helper.rb before/after rules aren't clearing the *_PROXY
  env-vars correctly, so explicitly clear them before each example group.
- Because the the scheme of the test URL wasn't being updated, it was
  only using the $HTTP_PROXY / $http_proxy vars
- It seems the test server (Mongrel) isn't set-up to handle HTTPS, so
  have disabled those tests, for now.
  • Loading branch information
mexisme authored and igrigorik committed Jan 9, 2017
1 parent 1404ff1 commit 6061430
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 79 deletions.
28 changes: 26 additions & 2 deletions lib/em-http/http_connection_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ def initialize(uri, options)
@inactivity_timeout = options[:inactivity_timeout] ||= 10 # default connection inactivity (post-setup) timeout

@tls = options[:tls] || options[:ssl] || {}
@proxy = options[:proxy]

if bind = options[:bind]
@bind = bind[:host] || '0.0.0.0'
Expand All @@ -22,7 +21,9 @@ def initialize(uri, options)
uri.port ||= (@https ? 443 : 80)
@tls[:sni_hostname] = uri.host

if proxy = options[:proxy]
@proxy = options[:proxy] || proxy_from_env

if proxy
@host = proxy[:host]
@port = proxy[:port]
else
Expand All @@ -42,4 +43,27 @@ def connect_proxy?
def socks_proxy?
@proxy && (@proxy[:type] == :socks5)
end

def proxy_from_env
# TODO: Add support for $http_no_proxy or $no_proxy ?
proxy_str = if @https
ENV['HTTPS_PROXY'] || ENV['https_proxy']
else
ENV['HTTP_PROXY'] || ENV['http_proxy']

# Fall-back to $ALL_PROXY if none of the above env-vars have values
end || ENV['ALL_PROXY']

# Addressable::URI::parse will return `nil` if given `nil` and an empty URL for an empty string
# so, let's short-circuit that:
return if !proxy_str || proxy_str.empty?

proxy_env_uri = Addressable::URI::parse(proxy_str)
{ :host => proxy_env_uri.host, :port => proxy_env_uri.port, :type => :http }

rescue Addressable::URI::InvalidURIError
# An invalid env-var shouldn't crash the config step, IMHO.
# We should somehow log / warn about this, perhaps...
return
end
end
313 changes: 237 additions & 76 deletions spec/http_proxy_spec.rb
Original file line number Diff line number Diff line change
@@ -1,107 +1,268 @@
require 'helper'

shared_examples "*_PROXY var (through proxy)" do
it "should use HTTP proxy" do
EventMachine.run {
http = EventMachine::HttpRequest.new("#{proxy_test_scheme}://127.0.0.1:8090/?q=test").get

http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.response_header.should_not include("X_PROXY_AUTH")
http.response.should match('test')
EventMachine.stop
}
}
end
end

shared_examples "*_PROXY var (testing var)" do
subject { HttpConnectionOptions.new("#{proxy_test_scheme}://example.com", {}) }
it { expect(subject.proxy_from_env).to eq({ :host => "127.0.0.1", :port => 8083, :type => :http }) }
it { expect(subject.host).to eq "127.0.0.1" }
it { expect(subject.port).to be 8083 }
it do
case proxy_test_scheme.to_sym
when :http
expect(subject.http_proxy?).to be_truthy
when :https
expect(subject.connect_proxy?).to be_truthy
end
end
end

describe EventMachine::HttpRequest do

context "connections via" do
let(:proxy) { {:proxy => { :host => '127.0.0.1', :port => 8083 }} }
let(:authenticated_proxy) { {:proxy => { :host => '127.0.0.1', :port => 8083, :authorization => ["user", "name"] } } }

it "should use HTTP proxy" do
EventMachine.run {
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/?q=test', proxy).get

http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.response_header.should_not include("X_PROXY_AUTH")
http.response.should match('test')
EventMachine.stop
context "without *_PROXY env" do
let(:proxy) { {:proxy => { :host => '127.0.0.1', :port => 8083 }} }
let(:authenticated_proxy) { {:proxy => { :host => '127.0.0.1', :port => 8083, :authorization => ["user", "name"] } } }

it "should use HTTP proxy" do
EventMachine.run {
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/?q=test', proxy).get

http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.response_header.should_not include("X_PROXY_AUTH")
http.response.should match('test')
EventMachine.stop
}
}
}
end
end

it "should use HTTP proxy with authentication" do
EventMachine.run {
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/proxyauth?q=test', authenticated_proxy).get
it "should use HTTP proxy with authentication" do
EventMachine.run {
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/proxyauth?q=test', authenticated_proxy).get

http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.response_header['X_PROXY_AUTH'].should == "Proxy-Authorization: Basic dXNlcjpuYW1l"
http.response.should match('test')
EventMachine.stop
http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.response_header['X_PROXY_AUTH'].should == "Proxy-Authorization: Basic dXNlcjpuYW1l"
http.response.should match('test')
EventMachine.stop
}
}
}
end
end

it "should send absolute URIs to the proxy server" do
EventMachine.run {
it "should send absolute URIs to the proxy server" do
EventMachine.run {

http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/?q=test', proxy).get
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/?q=test', proxy).get

http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200

# The test proxy server gives the requested uri back in this header
http.response_header['X_THE_REQUESTED_URI'].should == 'http://127.0.0.1:8090/?q=test'
http.response_header['X_THE_REQUESTED_URI'].should_not == '/?q=test'
http.response.should match('test')
EventMachine.stop
# The test proxy server gives the requested uri back in this header
http.response_header['X_THE_REQUESTED_URI'].should == 'http://127.0.0.1:8090/?q=test'
http.response_header['X_THE_REQUESTED_URI'].should_not == '/?q=test'
http.response.should match('test')
EventMachine.stop
}
}
}
end
end

it "should strip basic auth from before the host in URI sent to proxy" do
EventMachine.run {
it "should strip basic auth from before the host in URI sent to proxy" do
EventMachine.run {

http = EventMachine::HttpRequest.new('http://user:[email protected]:8090/echo_authorization_header', proxy).get
http = EventMachine::HttpRequest.new('http://user:[email protected]:8090/echo_authorization_header', proxy).get

http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
# The test proxy server gives the requested uri back in this header
http.response_header['X_THE_REQUESTED_URI'].should == 'http://127.0.0.1:8090/echo_authorization_header'
# Ensure the basic auth was converted to a header correctly
http.response.should match('authorization:Basic dXNlcjpwYXNz')
EventMachine.stop
http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
# The test proxy server gives the requested uri back in this header
http.response_header['X_THE_REQUESTED_URI'].should == 'http://127.0.0.1:8090/echo_authorization_header'
# Ensure the basic auth was converted to a header correctly
http.response.should match('authorization:Basic dXNlcjpwYXNz')
EventMachine.stop
}
}
}
end
end

it "should include query parameters specified in the options" do
EventMachine.run {
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/', proxy).get :query => { 'q' => 'test' }
it "should include query parameters specified in the options" do
EventMachine.run {
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/', proxy).get :query => { 'q' => 'test' }

http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.response.should match('test')
EventMachine.stop
http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.response.should match('test')
EventMachine.stop
}
}
}
end
end

it "should use HTTP proxy while redirecting" do
EventMachine.run {
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect', proxy).get :redirects => 1
it "should use HTTP proxy while redirecting" do
EventMachine.run {
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/redirect', proxy).get :redirects => 1

http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200
http.errback { failed(http) }
http.callback {
http.response_header.status.should == 200

http.response_header['X_THE_REQUESTED_URI'].should == 'http://127.0.0.1:8090/gzip'
http.response_header['X_THE_REQUESTED_URI'].should_not == '/redirect'
http.response_header['X_THE_REQUESTED_URI'].should == 'http://127.0.0.1:8090/gzip'
http.response_header['X_THE_REQUESTED_URI'].should_not == '/redirect'

http.response_header["CONTENT_ENCODING"].should == "gzip"
http.response.should == "compressed"
http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/gzip'
http.redirects.should == 1
http.response_header["CONTENT_ENCODING"].should == "gzip"
http.response.should == "compressed"
http.last_effective_url.to_s.should == 'http://127.0.0.1:8090/gzip'
http.redirects.should == 1

EventMachine.stop
EventMachine.stop
}
}
}
end
end

context "when parsing *_PROXY var (through proxy)s" do
context "with $HTTP_PROXY env" do
let(:proxy_test_scheme) { :http }

before(:all) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['HTTP_PROXY'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (through proxy)"
end

context "with $http_proxy env" do
let(:proxy_test_scheme) { :http }

before(:all) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['http_proxy'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (through proxy)"
end

## TODO: Use a Mongrel HTTP server that can handle SSL:
context "with $HTTPS_PROXY env", skip: "Mongrel isn't configured to handle HTTPS, currently" do
let(:proxy_test_scheme) { :https }

before(:all) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['HTTPS_PROXY'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (through proxy)"
end

## TODO: Use a Mongrel HTTP server that can handle SSL:
context "with $https_proxy env", skip: "Mongrel isn't configured to handle HTTPS, currently" do
let(:proxy_test_scheme) { :https }

before(:all) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['https_proxy'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (through proxy)"
end

context "with $ALL_PROXY env" do
let(:proxy_test_scheme) { :http }

before(:all) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['ALL_PROXY'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (through proxy)"
end
end
end

context "when parsing *_PROXY vars" do
context "without a *_PROXY var" do
before(:all) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
end

subject { HttpConnectionOptions.new("http://example.com", {}) }
it { expect(subject.proxy_from_env).to be_nil }
it { expect(subject.host).to eq "example.com" }
it { expect(subject.port).to be 80 }
it { expect(subject.http_proxy?).to be_falsey }
it { expect(subject.connect_proxy?).to be_falsey }
end

context "with $HTTP_PROXY env" do
let(:proxy_test_scheme) { :http }

before(:each) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['HTTP_PROXY'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (testing var)"
end

context "with $http_proxy env" do
let(:proxy_test_scheme) { :http }

before(:each) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['http_proxy'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (testing var)"
end

context "with $HTTPS_PROXY env" do
let(:proxy_test_scheme) { :https }

before(:each) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['HTTPS_PROXY'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (testing var)"
end

context "with $https_proxy env" do
let(:proxy_test_scheme) { :https }

before(:each) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['https_proxy'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (testing var)"
end

context "with $ALL_PROXY env" do
let(:proxy_test_scheme) { :https }

before(:each) do
PROXY_ENV_VARS.each {|k| ENV.delete k }
ENV['ALL_PROXY'] = 'http://127.0.0.1:8083'
end

include_examples "*_PROXY var (testing var)"
end
end
end
Loading

0 comments on commit 6061430

Please sign in to comment.