Skip to content

Commit

Permalink
added proxy server implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Reinsch committed Sep 21, 2014
1 parent 1626ddb commit 6ae20cf
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 20 deletions.
35 changes: 34 additions & 1 deletion lib/capybara.rb
Expand Up @@ -22,7 +22,7 @@ class << self
attr_accessor :asset_host, :app_host, :run_server, :default_host, :always_include_port
attr_accessor :server_port, :exact, :match, :exact_options, :visible_text_only
attr_accessor :default_selector, :default_wait_time, :ignore_hidden_elements
attr_accessor :save_and_open_page_path, :automatic_reload, :raise_server_errors
attr_accessor :save_and_open_page_path, :automatic_reload, :raise_server_errors, :use_proxy_protocol
attr_writer :default_driver, :current_driver, :javascript_driver, :session_name, :server_host
attr_accessor :app

Expand All @@ -46,6 +46,7 @@ class << self
# [ignore_hidden_elements = Boolean] Whether to ignore hidden elements on the page (Default: true)
# [automatic_reload = Boolean] Whether to automatically reload elements as Capybara is waiting (Default: true)
# [save_and_open_page_path = String] Where to put pages saved through save_and_open_page (Default: Dir.pwd)
# [use_proxy_protocol = Boolean] Use the proxy protocol to send requests to capybara's proxy server instead of normal web server. Using the proxy server stubs all requests and allows you to use subdomains (Default: false)
#
# === DSL Options
#
Expand Down Expand Up @@ -132,6 +133,23 @@ def server(&block)
end
end

##
#
# Register a proc that Capybara will use to start a proxy server which will receive any requests from the browser.
# The proxy method is used in case the browser supports proxy servers.
#
# By default, Capybara will setup it's internal proxy.
#
# @yield [app, port] This block receives a rack app and port and should run a Rack handler
#
def proxy_server(&block)
if block_given?
@proxy_server = block
else
@proxy_server
end
end

##
#
# Wraps the given string, which should contain an HTML document or fragment
Expand Down Expand Up @@ -173,6 +191,19 @@ def run_default_server(app, port)
Rack::Handler::WEBrick.run(app, :Host => server_host, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
end

##
#
# Runs Capybara's default proxy server for the given application and port
# under most circumstances you should not have to call this method
# manually.
#
# @param [Rack Application] app The rack application to run
# @param [Fixnum] port The port to run the application on
#
def run_default_proxy_server(app, port)
Capybara::ProxyServer.run(app, :Host => server_host, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
end

##
#
# @return [Symbol] The name of the driver to use by default
Expand Down Expand Up @@ -322,6 +353,7 @@ module Selenium; end
require 'capybara/dsl'
require 'capybara/window'
require 'capybara/server'
require 'capybara/proxy_server'
require 'capybara/selector'
require 'capybara/result'
require 'capybara/version'
Expand Down Expand Up @@ -357,6 +389,7 @@ module Selenium; end
config.always_include_port = false
config.run_server = true
config.server {|app, port| Capybara.run_default_server(app, port)}
config.proxy_server {|app, port| Capybara.run_default_proxy_server(app, port)}
config.default_selector = :css
config.default_wait_time = 2
config.ignore_hidden_elements = true
Expand Down
4 changes: 4 additions & 0 deletions lib/capybara/driver/base.rb
Expand Up @@ -133,4 +133,8 @@ def reset!
def needs_server?
false
end

def supports_proxy_protocol?
false
end
end
114 changes: 114 additions & 0 deletions lib/capybara/proxy_server.rb
@@ -0,0 +1,114 @@
require 'webrick'
require 'webrick/httpproxy'
require 'webrick/https'
require 'rack/test'

module Capybara
class ProxyServer < WEBrick::HTTPProxyServer

class SSLHandler < WEBrick::HTTPServer
def initialize(app, config)
super(config.merge(:Port => 443, :DoNotListen => true))
@rack_handler = Rack::Handler::WEBrick.new(self, app)
end

def service(req, res)
if (host_handler = Capybara::ProxyServer.host_mapping[req.host])
Rack::Handler::WEBrick.new(self, host_handler).service(req, res)
else
@rack_handler.service(req, res)
end
rescue => err
$stderr.puts err
$stderr.puts err.backtrace.join("\n")
end
end

DEFAULT_404_HANDLER = Proc.new {|env| [404, {}, []] }

class << self
attr_accessor :host_mapping

##
#
# Configure Capybara ProxyServer to suit your needs.
#
# Capybara::ProxyServer.configure do |config|
# config.route "i.kissmetrics.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER
# end
#
def configure
yield self
end

##
#
# Route a request to a specific host to a separate Rack application.
# This allows you to mock external services without actually doing external requests.
#
# You can use the Capybara::ProxyServer::DEFAULT_404_HANDLER to simply return a 404 error code, or create your own rack applications.
#
def route(host_map)
@host_mapping ||= {}
@host_mapping.merge!(host_map)
end

def run(app, config)
server = new(app, config.merge(:OutputBufferSize => 5))
server.start
server
end
end

def initialize(app, config)
super(config, WEBrick::Config::HTTP)
@rack_handler = Rack::Handler::WEBrick.new(self, app)
@ssl_context = generate_ssl_context
@ssl_server = SSLHandler.new(app, config)
end

def service(req, res)
if req.request_method == "CONNECT"
host, port = req.unparsed_uri.split(":", 2)
if port == '443'
ua = Thread.current[:WEBrickSocket]
res.status = WEBrick::HTTPStatus::RC_OK
res.send_response(ua)
req.parse(NullReader) rescue nil
ssl = OpenSSL::SSL::SSLSocket.new(ua, @ssl_context)
ssl.sync_close = true
ssl.accept
@ssl_server.run(ssl)
else
res.status = 501
end
elsif (host_handler = Capybara::ProxyServer.host_mapping[req.host])
Rack::Handler::WEBrick.new(self, host_handler).service(req, res)
else
@rack_handler.service(req, res)
end
rescue => err
$stderr.puts err
$stderr.puts err.backtrace.join("\n")
end

private

def generate_ssl_context
OpenSSL::SSL::SSLContext.new('SSLv23_server').tap do |ssl_context|
self_signed_cert, self_signed_cert_key =
WEBrick::Utils.create_self_signed_cert(512, [ [ "CN", "localhost" ] ], "Generated by Ruby/OpenSSL")
ssl_context.cert = self_signed_cert
ssl_context.key = self_signed_cert_key
end
end
end
end

Capybara::ProxyServer.configure do |config|
config.route "www.google-analytics.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER,
"ssl.google-analytics.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER,
"fonts.googleapis.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER,
"connect.facebook.net" => Capybara::ProxyServer::DEFAULT_404_HANDLER,
"platform.twitter.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER
end
8 changes: 7 additions & 1 deletion lib/capybara/selenium/driver.rb
Expand Up @@ -33,7 +33,6 @@ def initialize(app, options={})
raise e
end
end

@app = app
@browser = nil
@exit_status = nil
Expand Down Expand Up @@ -75,6 +74,13 @@ def find_css(selector)

def wait?; true; end
def needs_server?; true; end
def supports_proxy_protocol?; options[:browser] == :firefox; end

def setup_proxy_host(host, port)
proxy_server = "#{host}:#{port}"
options[:profile] ||= Selenium::WebDriver::Firefox::Profile.new
options[:profile].proxy = Selenium::WebDriver::Proxy.new(:http => proxy_server, :ssl => proxy_server)
end

def execute_script(script)
browser.execute_script script
Expand Down
11 changes: 9 additions & 2 deletions lib/capybara/server.rb
Expand Up @@ -33,8 +33,9 @@ def ports

attr_reader :app, :port, :host

def initialize(app, port=Capybara.server_port, host=Capybara.server_host)
def initialize(app, driver, port=Capybara.server_port, host=Capybara.server_host)
@app = app
@driver = driver
@middleware = Middleware.new(@app)
@server_thread = nil # suppress warnings
@host, @port = host, port
Expand Down Expand Up @@ -65,9 +66,15 @@ def responsive?
def boot
unless responsive?
Capybara::Server.ports[@app.object_id] = @port
if Capybara.use_proxy_protocol && @driver.supports_proxy_protocol?
server_proc = Capybara.proxy_server
@driver.setup_proxy_host(@host, @port)
else
server_proc = Capybara.server
end

@server_thread = Thread.new do
Capybara.server.call(@middleware, @port)
server_proc.call(@middleware, @port)
end

Timeout.timeout(60) { @server_thread.join(0.1) until responsive? }
Expand Down
2 changes: 1 addition & 1 deletion lib/capybara/session.rb
Expand Up @@ -63,7 +63,7 @@ def initialize(mode, app=nil)
@mode = mode
@app = app
if Capybara.run_server and @app and driver.needs_server?
@server = Capybara::Server.new(@app).boot
@server = Capybara::Server.new(@app, driver).boot
else
@server = nil
end
Expand Down
2 changes: 1 addition & 1 deletion lib/capybara/spec/session/current_url_spec.rb
@@ -1,6 +1,6 @@
Capybara::SpecHelper.spec '#current_url, #current_path, #current_host' do
before :all do
@servers = 2.times.map { Capybara::Server.new(TestApp.clone).boot }
@servers = 2.times.map { Capybara::Server.new(TestApp.clone, nil).boot }
# sanity check
expect(@servers[0].port).not_to eq(@servers[1].port)
expect(@servers.map { |s| s.port }).not_to include 80
Expand Down
28 changes: 14 additions & 14 deletions spec/server_spec.rb
Expand Up @@ -4,7 +4,7 @@

it "should spool up a rack server" do
@app = proc { |env| [200, {}, ["Hello Server!"]]}
@server = Capybara::Server.new(@app).boot
@server = Capybara::Server.new(@app, nil).boot

@res = Net::HTTP.start(@server.host, @server.port) { |http| http.get('/') }

Expand All @@ -13,7 +13,7 @@

it "should do nothing when no server given" do
expect do
@server = Capybara::Server.new(nil).boot
@server = Capybara::Server.new(nil, nil).boot
end.not_to raise_error
end

Expand All @@ -22,12 +22,12 @@
app = proc { |env| [200, {}, ['Hello Server!']] }

Capybara.server_host = '127.0.0.1'
server = Capybara::Server.new(app).boot
server = Capybara::Server.new(app, nil).boot
res = Net::HTTP.get(URI("http://127.0.0.1:#{server.port}"))
expect(res).to eq('Hello Server!')

Capybara.server_host = '0.0.0.0'
server = Capybara::Server.new(app).boot
server = Capybara::Server.new(app, nil).boot
res = Net::HTTP.get(URI("http://127.0.0.1:#{server.port}"))
expect(res).to eq('Hello Server!')
ensure
Expand All @@ -39,7 +39,7 @@
Capybara.server_port = 22789

@app = proc { |env| [200, {}, ["Hello Server!"]]}
@server = Capybara::Server.new(@app).boot
@server = Capybara::Server.new(@app, nil).boot

@res = Net::HTTP.start(@server.host, 22789) { |http| http.get('/') }
expect(@res.body).to include('Hello Server')
Expand All @@ -49,7 +49,7 @@

it "should use given port" do
@app = proc { |env| [200, {}, ["Hello Server!"]]}
@server = Capybara::Server.new(@app, 22790).boot
@server = Capybara::Server.new(@app, nil, 22790).boot

@res = Net::HTTP.start(@server.host, 22790) { |http| http.get('/') }
expect(@res.body).to include('Hello Server')
Expand All @@ -61,8 +61,8 @@
@app1 = proc { |env| [200, {}, ["Hello Server!"]]}
@app2 = proc { |env| [200, {}, ["Hello Second Server!"]]}

@server1 = Capybara::Server.new(@app1).boot
@server2 = Capybara::Server.new(@app2).boot
@server1 = Capybara::Server.new(@app1, nil).boot
@server2 = Capybara::Server.new(@app2, nil).boot

@res1 = Net::HTTP.start(@server1.host, @server1.port) { |http| http.get('/') }
expect(@res1.body).to include('Hello Server')
Expand All @@ -75,10 +75,10 @@
@app1 = proc { |env| [200, {}, ["Hello Server!"]]}
@app2 = proc { |env| [200, {}, ["Hello Second Server!"]]}

@server1a = Capybara::Server.new(@app1).boot
@server1b = Capybara::Server.new(@app1).boot
@server2a = Capybara::Server.new(@app2).boot
@server2b = Capybara::Server.new(@app2).boot
@server1a = Capybara::Server.new(@app1, nil).boot
@server1b = Capybara::Server.new(@app1, nil).boot
@server2a = Capybara::Server.new(@app2, nil).boot
@server2b = Capybara::Server.new(@app2, nil).boot

@res1 = Net::HTTP.start(@server1b.host, @server1b.port) { |http| http.get('/') }
expect(@res1.body).to include('Hello Server')
Expand All @@ -98,7 +98,7 @@
end

expect do
Capybara::Server.new(proc {|e|}).boot
Capybara::Server.new(proc {|e|}, nil).boot
end.to raise_error(RuntimeError, 'kaboom')
ensure
# TODO refactor out the defaults so it's reliant on unset state instead of
Expand All @@ -109,7 +109,7 @@

it "is not #responsive? when Net::HTTP raises a SystemCallError" do
app = lambda { [200, {}, ['Hello, world']] }
server = Capybara::Server.new(app)
server = Capybara::Server.new(app, nil)
expect(Net::HTTP).to receive(:start).and_raise(SystemCallError.allocate)
expect(server.responsive?).to eq false
end
Expand Down

0 comments on commit 6ae20cf

Please sign in to comment.