Skip to content

Commit 6aed65f

Browse files
committed
Support HTTPS connections through proxy
1 parent 9496fc6 commit 6aed65f

File tree

6 files changed

+109
-13
lines changed

6 files changed

+109
-13
lines changed

.rubocop.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Metrics/BlockNesting:
33

44
Metrics/ClassLength:
55
CountComments: false
6-
Max: 110
6+
Max: 120
77

88
Metrics/PerceivedComplexity:
99
Max: 8

lib/http/client.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ def make_request(req, options)
6464
@state = :dirty
6565

6666
@connection ||= HTTP::Connection.new(req, options)
67-
@connection.send_request(req)
68-
@connection.read_headers!
67+
68+
unless @connection.failed_proxy_connect?
69+
@connection.send_request(req)
70+
@connection.read_headers!
71+
end
6972

7073
res = Response.new(
7174
@connection.status_code,

lib/http/connection.rb

+35-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module HTTP
66
# A connection to the HTTP server
77
class Connection
88
extend Forwardable
9+
910
# Attempt to read this much data
1011
BUFFER_SIZE = 16_384
1112

@@ -18,16 +19,18 @@ class Connection
1819
# @param [HTTP::Request] req
1920
# @param [HTTP::Options] options
2021
def initialize(req, options)
21-
@persistent = options.persistent?
22-
@keep_alive_timeout = options[:keep_alive_timeout].to_f
23-
@pending_request = false
24-
@pending_response = false
22+
@persistent = options.persistent?
23+
@keep_alive_timeout = options[:keep_alive_timeout].to_f
24+
@pending_request = false
25+
@pending_response = false
26+
@failed_proxy_connect = false
2527

2628
@parser = Response::Parser.new
2729

2830
@socket = options[:timeout_class].new(options[:timeout_options])
2931
@socket.connect(options[:socket_class], req.socket_host, req.socket_port)
3032

33+
send_proxy_connect_request(req)
3134
start_tls(req, options)
3235
reset_timer
3336
end
@@ -41,6 +44,11 @@ def initialize(req, options)
4144
# @see (HTTP::Response::Parser#headers)
4245
def_delegator :@parser, :headers
4346

47+
# @return [Boolean] whenever proxy connect failed
48+
def failed_proxy_connect?
49+
@failed_proxy_connect
50+
end
51+
4452
# Send a request to the server
4553
#
4654
# @param [Request] Request to send to the server
@@ -129,7 +137,7 @@ def expired?
129137
# @param (see #initialize)
130138
# @return [void]
131139
def start_tls(req, options)
132-
return unless req.uri.https? && !req.using_proxy?
140+
return unless req.uri.https? && !failed_proxy_connect?
133141

134142
ssl_context = options[:ssl_context]
135143

@@ -141,6 +149,28 @@ def start_tls(req, options)
141149
@socket.start_tls(req.uri.host, options[:ssl_socket_class], ssl_context)
142150
end
143151

152+
# Open tunnel through proxy
153+
def send_proxy_connect_request(req)
154+
return unless req.uri.https? && req.using_proxy?
155+
156+
@pending_request = true
157+
158+
req.connect_using_proxy @socket
159+
160+
@pending_request = false
161+
@pending_response = true
162+
163+
read_headers!
164+
165+
if @parser.status_code == 200
166+
@parser.reset
167+
@pending_response = false
168+
return
169+
end
170+
171+
@failed_proxy_connect = true
172+
end
173+
144174
# Resets expiration of persistent connection.
145175
# @return [void]
146176
def reset_timer

lib/http/request.rb

+27-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def redirect(uri, verb = @verb)
9797

9898
# Stream the request to a socket
9999
def stream(socket)
100-
include_proxy_authorization_header if using_authenticated_proxy?
100+
include_proxy_authorization_header if using_authenticated_proxy? && !@uri.https?
101101
Request::Writer.new(socket, body, headers, request_header).stream
102102
end
103103

@@ -113,15 +113,39 @@ def using_authenticated_proxy?
113113

114114
# Compute and add the Proxy-Authorization header
115115
def include_proxy_authorization_header
116-
digest = Base64.encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}").chomp
117-
headers["Proxy-Authorization"] = "Basic #{digest}"
116+
headers["Proxy-Authorization"] = proxy_authorization_header
117+
end
118+
119+
def proxy_authorization_header
120+
digest = Base64.strict_encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
121+
"Basic #{digest}"
122+
end
123+
124+
# Setup tunnel through proxy for SSL request
125+
def connect_using_proxy(socket)
126+
Request::Writer.new(socket, nil, proxy_connect_headers, proxy_connect_header).connect_through_proxy
118127
end
119128

120129
# Compute HTTP request header for direct or proxy request
121130
def request_header
122131
"#{verb.to_s.upcase} #{uri.normalize} HTTP/#{version}"
123132
end
124133

134+
# Compute HTTP request header SSL proxy connection
135+
def proxy_connect_header
136+
"CONNECT #{@uri.host}:#{@uri.port} HTTP/#{version}"
137+
end
138+
139+
# Headers to send with proxy connect request
140+
def proxy_connect_headers
141+
connect_headers = HTTP::Headers.coerce(
142+
"Host" => headers["Host"],
143+
"User-Agent" => headers["User-Agent"]
144+
)
145+
connect_headers["Proxy-Authorization"] = proxy_authorization_header if using_authenticated_proxy?
146+
connect_headers
147+
end
148+
125149
# Host for tcp socket
126150
def socket_host
127151
using_proxy? ? proxy[:proxy_address] : host

lib/http/request/writer.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ def stream
2929
send_request_body
3030
end
3131

32+
# Send headers needed to connect through proxy
33+
def connect_through_proxy
34+
add_headers
35+
@socket << join_headers
36+
end
37+
3238
# Adds the headers to the header array for the given request body we are working
3339
# with
3440
def add_body_type_headers
@@ -50,9 +56,8 @@ def join_headers
5056
def send_request_header
5157
add_headers
5258
add_body_type_headers
53-
header = join_headers
5459

55-
@socket << header
60+
@socket << join_headers
5661
end
5762

5863
def send_request_body

spec/lib/http_spec.rb

+34
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
RSpec.describe HTTP do
77
run_server(:dummy) { DummyServer.new }
8+
run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
9+
10+
let(:ssl_client) do
11+
HTTP::Client.new :ssl_context => SSLHelper.client_context
12+
end
813

914
context "getting resources" do
1015
it "is easy" do
@@ -63,6 +68,18 @@
6368
response = HTTP.via(proxy.addr, proxy.port, "username", "password").get dummy.endpoint
6469
expect(response.to_s).to match(/<!doctype html>/)
6570
end
71+
72+
context "ssl" do
73+
it "responds with the endpoint's body" do
74+
response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint
75+
expect(response.to_s).to match(/<!doctype html>/)
76+
end
77+
78+
it "ignores credentials" do
79+
response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint
80+
expect(response.to_s).to match(/<!doctype html>/)
81+
end
82+
end
6683
end
6784

6885
context "proxy with authentication" do
@@ -87,6 +104,23 @@
87104
response = HTTP.via(proxy.addr, proxy.port).get dummy.endpoint
88105
expect(response.status).to eq(407)
89106
end
107+
108+
context "ssl" do
109+
it "responds with the endpoint's body" do
110+
response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint
111+
expect(response.to_s).to match(/<!doctype html>/)
112+
end
113+
114+
it "responds with 407 when wrong credentials given" do
115+
response = ssl_client.via(proxy.addr, proxy.port, "user", "pass").get dummy_ssl.endpoint
116+
expect(response.status).to eq(407)
117+
end
118+
119+
it "responds with 407 if no credentials given" do
120+
response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint
121+
expect(response.status).to eq(407)
122+
end
123+
end
90124
end
91125
end
92126

0 commit comments

Comments
 (0)