Refactor proxy support into a separate class that represents the proxy connection as a real object.

This commit is contained in:
James Coglan
2014-11-06 23:37:13 +00:00
parent 286109496e
commit 0a200bbdef
5 changed files with 168 additions and 97 deletions
+23 -7
View File
@@ -197,17 +197,33 @@ sent back by the server:
The client driver supports connections via HTTP proxies using the `CONNECT`
method. Instead of sending the WebSocket handshake immediately, it will send a
`CONNECT` request, wait for a `200` response, and then proceed as normal. To use
this feature, set the `:proxy` option to the HTTP origin of the proxy, including
any authorization information.
`CONNECT` request, wait for a `200` response, and then proceed as normal.
To use this feature, call `proxy = driver.proxy(url)` where `url` is the origin
of the proxy, including a username and password if required. This produces an
object that manages the process of connecting via the proxy. You should call
`proxy.start` to begin the connection process, and pass data you receive via the
socket to `proxy.parse(data)`. You should use these methods _instead_ of
`driver.start` and `driver.parse(data)`.
In the event that proxy connection fails, `proxy` will emit an `:error`. You can
inspect the proxy's response via `proxy.status` and `proxy.headers`.
```rb
driver = WebSocket::Driver.client(socket, :proxy => 'http://username:password@proxy.example.com')
proxy.on :error do |error|
puts error.message
puts proxy.status
puts proxy.headers.inspect
end
```
It is up to you to set up a TCP connection to the proxy yourself. If you prefer,
you can perform the `CONNECT` logic yourself and then hand off the TCP
connection to the client driver to continue the handshake process.
You can pass additional options to the proxy to control it. Before calling
`proxy.start` you can set custom headers using `proxy.set_header`:
```rb
proxy.set_header('User-Agent', 'ruby')
proxy.start
```
### Driver API
+1
View File
@@ -51,6 +51,7 @@ module WebSocket
autoload :EventEmitter, root + '/event_emitter'
autoload :Headers, root + '/headers'
autoload :Hybi, root + '/hybi'
autoload :Proxy, root + '/proxy'
autoload :Server, root + '/server'
include EventEmitter
+10 -50
View File
@@ -2,8 +2,6 @@ module WebSocket
class Driver
class Client < Hybi
PORTS = {'ws' => 80, 'wss' => 443}
def self.generate_key
Base64.encode64((1..16).map { rand(255).chr } * '').strip
end
@@ -13,8 +11,7 @@ module WebSocket
def initialize(socket, options = {})
super
@proxy = options[:proxy]
@ready_state = @proxy ? -2 : -1
@ready_state = -1
@key = Client.generate_key
@accept = Hybi.generate_accept(@key)
@http = HTTP::Response.new
@@ -24,54 +21,29 @@ module WebSocket
'hybi-13'
end
def proxy(origin, options = {})
Proxy.new(self, origin, options)
end
def start
return false unless @ready_state < 0
if @ready_state == -2
@socket.write(Driver.encode(proxy_connect_request, :binary))
@ready_state = -1
elsif @ready_state == -1
@socket.write(Driver.encode(handshake_request, :binary))
@ready_state = 0
end
return false unless @ready_state == -1
@socket.write(Driver.encode(handshake_request, :binary))
@ready_state = 0
true
end
def parse(buffer)
return super if @ready_state > 0
@http.parse(buffer)
return fail_handshake('Invalid HTTP response') if @http.error?
return unless @http.complete?
if @ready_state == -1
validate_proxy_response
else
validate_handshake
end
validate_handshake if @http.complete?
parse(@http.body) if @ready_state == 1
end
private
def proxy_connect_request
proxy = URI.parse(@proxy)
uri = URI.parse(@socket.url)
port = uri.port || PORTS[uri.scheme]
headers = [ "CONNECT #{uri.host}:#{port} HTTP/1.1",
"Host: #{uri.host}",
"Connection: keep-alive",
"Proxy-Connection: keep-alive"
]
if proxy.user
auth = Base64.encode64([proxy.user, proxy.password] * ':').gsub(/\n/, '')
headers << "Proxy-Authorization: Basic #{auth}"
end
(headers + ['', '']).join("\r\n")
end
def handshake_request
uri = URI.parse(@socket.url)
host = uri.host + (uri.port ? ":#{uri.port}" : '')
@@ -98,18 +70,6 @@ module WebSocket
(headers + [@headers.to_s, '']).join("\r\n")
end
def validate_proxy_response
if @http.code == 200
@http = HTTP::Response.new
start
else
message = "Can't establish a connection to the server at #{@socket.url}"
emit(:error, ProtocolError.new(message))
@ready_state = 3
emit(:close, CloseEvent.new(1006, ''))
end
end
def fail_handshake(message)
message = "Error during WebSocket handshake: #{message}"
emit(:error, ProtocolError.new(message))
+81
View File
@@ -0,0 +1,81 @@
module WebSocket
class Driver
class Proxy
include EventEmitter
PORTS = {'ws' => 80, 'wss' => 443}
attr_reader :status, :headers
def initialize(client, origin, options)
super()
@client = client
@headers = Headers.new
@http = HTTP::Response.new
@socket = client.instance_variable_get(:@socket)
@origin = URI.parse(@socket.url)
@url = URI.parse(origin)
@options = options
@state = 0
end
def set_header(name, value)
return false unless @state == 0
@headers[name] = value
true
end
def start
return false unless @state == 0
@state = 1
host = @origin.host + (@origin.port ? ":#{@origin.port}" : '')
port = @origin.port || PORTS[@origin.scheme]
headers = [ "CONNECT #{@origin.host}:#{port} HTTP/1.1",
"Host: #{host}",
"Connection: keep-alive",
"Proxy-Connection: keep-alive"
]
if @url.user
auth = Base64.encode64([@url.user, @url.password] * ':').gsub(/\n/, '')
headers << "Proxy-Authorization: Basic #{auth}"
end
@socket.write((headers + [@headers.to_s, '']).join("\r\n"))
true
end
def parse(buffer)
return @delegate.parse(buffer) if @delegate
@http.parse(buffer)
return unless @http.complete?
@status = @http.code
@headers = Headers.new(@http.headers)
if @status != 200
message = "Can't establish a connection to the server at #{@socket.url}"
emit(:error, ProtocolError.new(message))
@ready_state = 3
return emit(:close, CloseEvent.new(1006, ''))
end
@socket.start_tls if @origin.scheme == 'wss'
configure_delegate(@client)
@client.start
end
private
def configure_delegate(delegate)
@delegate = delegate
end
end
end
end
+53 -40
View File
@@ -132,35 +132,6 @@ describe WebSocket::Driver::Client do
end
end
describe "using a proxy" do
let(:options) { {:proxy => "http://proxy.example.com"} }
it "sends a CONNECT request to the proxy" do
expect(socket).to receive(:write).with(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"\r\n")
driver.start
end
end
describe "using an authenticated proxy" do
let(:options) { {:proxy => "http://user:pass@proxy.example.com"} }
it "sends a CONNECT request to the proxy" do
expect(socket).to receive(:write).with(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" +
"\r\n")
driver.start
end
end
it "changes the state to :connecting" do
driver.start
expect(driver.state).to eq :connecting
@@ -168,32 +139,74 @@ describe WebSocket::Driver::Client do
end
end
describe "in the :connecting state" do
before { driver.start }
describe "using a proxy" do
it "sends a CONNECT request" do
proxy = driver.proxy("http://proxy.example.com")
expect(socket).to receive(:write).with(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"\r\n")
proxy.start
end
describe "using a proxy" do
let(:options) { {:proxy => "http://proxy.example.com"} }
it "sends an authenticated CONNECT request" do
proxy = driver.proxy("http://user:pass@proxy.example.com")
expect(socket).to receive(:write).with(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" +
"\r\n")
proxy.start
end
it "writes the handshake request when the proxy connects" do
it "sends a CONNECT request with custom headers" do
proxy = driver.proxy("http://proxy.example.com")
proxy.set_header("User-Agent", "Chrome")
expect(socket).to receive(:write).with(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"User-Agent: Chrome\r\n" +
"\r\n")
proxy.start
end
describe "receiving a response" do
let(:proxy) { driver.proxy("http://proxy.example.com") }
before do
proxy.on(:error) { |e| @error = e }
proxy.on(:close) { |e| @close = [e.code, e.reason] }
end
it "emits a WebSocket handshake when the proxy connects" do
expect(socket).to receive(:write).with(
"GET /socket HTTP/1.1\r\n" +
"GET /socket HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"\r\n")
driver.parse("HTTP/1.1 200 OK\r\n\r\n")
proxy.parse("HTTP/1.1 200 OK\r\n\r\n")
end
it "emits an error if the proxy does not connect" do
expect(socket).to_not receive(:write)
driver.parse("HTTP/1.1 403 Forbidden\r\n\r\n")
it "emits an 'error' event if the proxy does not connect" do
proxy.parse("HTTP/1.1 403 Forbidden\r\n\r\n")
expect(@open).to eq false
expect(@error.message).to eq "Can't establish a connection to the server at ws://www.example.com/socket"
expect(@close).to eq [1006, ""]
expect(driver.state).to eq :closed
end
end
end
describe "in the :connecting state" do
before { driver.start }
describe "with a valid response" do
before { driver.parse(response) }