Skip to content

Commit e4eecc1

Browse files
authored
Merge pull request #72 from launchdarkly/eb/ch19217/proxy
fix proxy implementation so it actually works
2 parents c66703d + 06c3214 commit e4eecc1

File tree

6 files changed

+160
-56
lines changed

6 files changed

+160
-56
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Note that this gem will automatically switch to using the Rails logger it is det
8383

8484
HTTPS proxy
8585
------------
86-
The Ruby SDK uses Faraday to handle all of its network traffic. Faraday provides built-in support for the use of an HTTPS proxy. If the HTTPS_PROXY environment variable is present then the SDK will proxy all network requests through the URL provided.
86+
The Ruby SDK uses Faraday and Socketry to handle its network traffic. Both of these provide built-in support for the use of an HTTPS proxy. If the HTTPS_PROXY environment variable is present then the SDK will proxy all network requests through the URL provided.
8787

8888
How to set the HTTPS_PROXY environment variable on Mac/Linux systems:
8989
```

ldclient-rb.gemspec

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,13 @@ Gem::Specification.new do |spec|
2626
spec.add_development_dependency "codeclimate-test-reporter", "~> 0"
2727
spec.add_development_dependency "redis", "~> 3.3.5"
2828
spec.add_development_dependency "connection_pool", ">= 2.1.2"
29-
if RUBY_VERSION >= "2.0.0"
30-
spec.add_development_dependency "rake", "~> 10.0"
31-
spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
32-
else
33-
spec.add_development_dependency "rake", "12.1.0"
34-
# higher versions of rake fail to install in JRuby 1.7
35-
end
29+
spec.add_development_dependency "rake", "~> 10.0"
30+
spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
3631
spec.add_development_dependency "timecop", "~> 0.9.1"
3732

3833
spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
39-
if RUBY_VERSION >= "2.1.0"
40-
spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
41-
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
42-
else
43-
spec.add_runtime_dependency "faraday", [">= 0.9", "< 0.14.0"]
44-
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 2"]
45-
end
34+
spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
35+
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
4636
spec.add_runtime_dependency "semantic", "~> 1.6.0"
4737
spec.add_runtime_dependency "thread_safe", "~> 0.3"
4838
spec.add_runtime_dependency "net-http-persistent", "~> 2.9"

lib/sse_client/sse_client.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ def initialize(uri, options = {})
2424
@read_timeout = options[:read_timeout] || DEFAULT_READ_TIMEOUT
2525
@logger = options[:logger] || default_logger
2626

27-
proxy = ENV['HTTP_PROXY'] || ENV['http_proxy'] || options[:proxy]
28-
if proxy
29-
proxyUri = URI(proxy)
30-
if proxyUri.scheme == 'http' || proxyUri.scheme == 'https'
27+
if options[:proxy]
28+
@proxy = options[:proxy]
29+
else
30+
proxyUri = @uri.find_proxy
31+
if !proxyUri.nil? && (proxyUri.scheme == 'http' || proxyUri.scheme == 'https')
3132
@proxy = proxyUri
3233
end
3334
end
@@ -152,8 +153,7 @@ def dispatch_event(event)
152153
def build_headers
153154
h = {
154155
'Accept' => 'text/event-stream',
155-
'Cache-Control' => 'no-cache',
156-
'Host' => @uri.host
156+
'Cache-Control' => 'no-cache'
157157
}
158158
h['Last-Event-Id'] = @last_id if !@last_id.nil?
159159
h.merge(@headers)

lib/sse_client/streaming_http.rb

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,21 @@ module SSE
77
# The socket is created and managed by Socketry, which we use so that we can have a read timeout.
88
#
99
class StreamingHTTPConnection
10-
def initialize(uri, proxy, headers, connect_timeout, read_timeout)
11-
if proxy
12-
@socket = open_socket(proxy, connect_timeout)
13-
@socket.write(build_proxy_request(uri, proxy))
14-
else
15-
@socket = open_socket(uri, connect_timeout)
16-
end
10+
attr_reader :status, :headers
1711

12+
def initialize(uri, proxy, headers, connect_timeout, read_timeout)
13+
@socket = HTTPConnectionFactory.connect(uri, proxy, connect_timeout, read_timeout)
1814
@socket.write(build_request(uri, headers))
19-
2015
@reader = HTTPResponseReader.new(@socket, read_timeout)
16+
@status = @reader.status
17+
@headers = @reader.headers
2118
end
2219

2320
def close
2421
@socket.close if @socket
2522
@socket = nil
2623
end
27-
28-
def status
29-
@reader.status
30-
end
31-
32-
def headers
33-
@reader.headers
34-
end
35-
24+
3625
# Generator that returns one line of the response body at a time (delimited by \r, \n,
3726
# or \r\n) until the response is fully consumed or the socket is closed.
3827
def read_lines
@@ -46,25 +35,55 @@ def read_all
4635

4736
private
4837

49-
def open_socket(uri, connect_timeout)
50-
if uri.scheme == 'https'
51-
Socketry::SSL::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
52-
else
53-
Socketry::TCP::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
54-
end
55-
end
56-
5738
# Build an HTTP request line and headers.
5839
def build_request(uri, headers)
5940
ret = "GET #{uri.request_uri} HTTP/1.1\r\n"
41+
ret << "Host: #{uri.host}\r\n"
6042
headers.each { |k, v|
6143
ret << "#{k}: #{v}\r\n"
6244
}
6345
ret + "\r\n"
6446
end
47+
end
48+
49+
#
50+
# Used internally to send the HTTP request, including the proxy dialogue if necessary.
51+
#
52+
class HTTPConnectionFactory
53+
def self.connect(uri, proxy, connect_timeout, read_timeout)
54+
if !proxy
55+
return open_socket(uri, connect_timeout)
56+
end
57+
58+
socket = open_socket(proxy, connect_timeout)
59+
socket.write(build_proxy_request(uri, proxy))
60+
61+
# temporarily create a reader just for the proxy connect response
62+
proxy_reader = HTTPResponseReader.new(socket, read_timeout)
63+
if proxy_reader.status != 200
64+
raise ProxyError, "proxy connection refused, status #{proxy_reader.status}"
65+
end
66+
67+
# start using TLS at this point if appropriate
68+
if uri.scheme.downcase == 'https'
69+
wrap_socket_in_ssl_socket(socket)
70+
else
71+
socket
72+
end
73+
end
74+
75+
private
76+
77+
def self.open_socket(uri, connect_timeout)
78+
if uri.scheme.downcase == 'https'
79+
Socketry::SSL::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
80+
else
81+
Socketry::TCP::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
82+
end
83+
end
6584

6685
# Build a proxy connection header.
67-
def build_proxy_request(uri, proxy)
86+
def self.build_proxy_request(uri, proxy)
6887
ret = "CONNECT #{uri.host}:#{uri.port} HTTP/1.1\r\n"
6988
ret << "Host: #{uri.host}:#{uri.port}\r\n"
7089
if proxy.user || proxy.password
@@ -74,6 +93,19 @@ def build_proxy_request(uri, proxy)
7493
ret << "\r\n"
7594
ret
7695
end
96+
97+
def self.wrap_socket_in_ssl_socket(socket)
98+
io = IO.try_convert(socket)
99+
ssl_sock = OpenSSL::SSL::SSLSocket.new(io, OpenSSL::SSL::SSLContext.new)
100+
ssl_sock.connect
101+
Socketry::SSL::Socket.new.from_socket(ssl_sock)
102+
end
103+
end
104+
105+
class ProxyError < StandardError
106+
def initialize(message)
107+
super
108+
end
77109
end
78110

79111
#

spec/sse_client/sse_shared.rb

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
require "spec_helper"
22
require "webrick"
3+
require "webrick/httpproxy"
4+
require "webrick/https"
35

46
class StubHTTPServer
57
def initialize
68
@port = 50000
79
begin
8-
@server = WEBrick::HTTPServer.new(
9-
BindAddress: '127.0.0.1',
10-
Port: @port,
11-
AccessLog: [],
12-
Logger: NullLogger.new
13-
)
10+
@server = create_server(@port)
1411
rescue Errno::EADDRINUSE
1512
@port += 1
1613
retry
1714
end
1815
end
1916

17+
def create_server(port)
18+
WEBrick::HTTPServer.new(
19+
BindAddress: '127.0.0.1',
20+
Port: port,
21+
AccessLog: [],
22+
Logger: NullLogger.new
23+
)
24+
end
25+
2026
def start
2127
Thread.new { @server.start }
2228
end
@@ -34,14 +40,39 @@ def setup_response(uri_path, &action)
3440
end
3541
end
3642

43+
class StubProxyServer < StubHTTPServer
44+
attr_reader :request_count
45+
attr_accessor :connect_status
46+
47+
def initialize
48+
super
49+
@request_count = 0
50+
end
51+
52+
def create_server(port)
53+
WEBrick::HTTPProxyServer.new(
54+
BindAddress: '127.0.0.1',
55+
Port: port,
56+
AccessLog: [],
57+
Logger: NullLogger.new,
58+
ProxyContentHandler: proc do |req,res|
59+
if !@connect_status.nil?
60+
res.status = @connect_status
61+
end
62+
@request_count += 1
63+
end
64+
)
65+
end
66+
end
67+
3768
class NullLogger
3869
def method_missing(*)
3970
self
4071
end
4172
end
4273

43-
def with_server
44-
server = StubHTTPServer.new
74+
def with_server(server = nil)
75+
server = StubHTTPServer.new if server.nil?
4576
begin
4677
server.start
4778
yield server

spec/sse_client/streaming_http_spec.rb

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ def with_connection(cxn)
2929
with_connection(subject.new(server.base_uri.merge("/foo?bar"), nil, headers, 30, 30)) do
3030
received_req = requests.pop
3131
expect(received_req.unparsed_uri).to eq("/foo?bar")
32-
expect(received_req.header).to eq({ "accept" => ["text/plain"] })
32+
expect(received_req.header).to eq({
33+
"accept" => ["text/plain"],
34+
"host" => [server.base_uri.host]
35+
})
3336
end
3437
end
3538
end
@@ -98,6 +101,54 @@ def with_connection(cxn)
98101
expect { subject.new(server.base_uri, nil, {}, 30, 0.25) }.to raise_error(Socketry::TimeoutError)
99102
end
100103
end
104+
105+
it "connects to HTTP server through proxy" do
106+
body = "hi"
107+
with_server do |server|
108+
server.setup_response("/") do |req,res|
109+
res.body = body
110+
end
111+
with_server(StubProxyServer.new) do |proxy|
112+
with_connection(subject.new(server.base_uri, proxy.base_uri, {}, 30, 30)) do |cxn|
113+
read_body = cxn.read_all
114+
expect(read_body).to eq("hi")
115+
expect(proxy.request_count).to eq(1)
116+
end
117+
end
118+
end
119+
end
120+
121+
it "throws error if proxy responds with error status" do
122+
with_server do |server|
123+
server.setup_response("/") do |req,res|
124+
res.body = body
125+
end
126+
with_server(StubProxyServer.new) do |proxy|
127+
proxy.connect_status = 403
128+
expect { subject.new(server.base_uri, proxy.base_uri, {}, 30, 30) }.to raise_error(SSE::ProxyError)
129+
end
130+
end
131+
end
132+
133+
# The following 2 tests were originally written to connect to an embedded HTTPS server made with
134+
# WEBrick. Unfortunately, some unknown problem prevents WEBrick's self-signed certificate feature
135+
# from working in JRuby 9.1 (but not in any other Ruby version). Therefore these tests currently
136+
# hit an external URL.
137+
138+
it "connects to HTTPS server" do
139+
with_connection(subject.new(URI("https://app.launchdarkly.com"), nil, {}, 30, 30)) do |cxn|
140+
expect(cxn.status).to eq 200
141+
end
142+
end
143+
144+
it "connects to HTTPS server through proxy" do
145+
with_server(StubProxyServer.new) do |proxy|
146+
with_connection(subject.new(URI("https://app.launchdarkly.com"), proxy.base_uri, {}, 30, 30)) do |cxn|
147+
expect(cxn.status).to eq 200
148+
expect(proxy.request_count).to eq(1)
149+
end
150+
end
151+
end
101152
end
102153

103154
#

0 commit comments

Comments
 (0)