From 862ce859ad650274fcb02ce3593becba415ee798 Mon Sep 17 00:00:00 2001 From: nikola Date: Mon, 8 Apr 2013 01:32:02 +0200 Subject: [PATCH 1/2] adds gem framework --- CHANGELOG.md | 0 Gemfile | 11 ++++ Gemfile.lock | 46 ++++++++++++++ LICENSE | 20 +++++++ README.md | 22 +++---- Rakefile | 2 + bin/vcr_proxy | 22 +++++++ lib/vcr_proxy.rb | 9 +++ lib/vcr_proxy/vcr_proxy.rb | 103 ++++++++++++++++++++++++++++++++ lib/vcr_proxy/version.rb | 3 + spec/spec_helper.rb | 1 + spec/vcr_proxy_spec.rb | 7 +++ vcr_proxy.gemspec | 30 ++++++++++ vcr_proxy.rb | 119 ------------------------------------- 14 files changed, 265 insertions(+), 130 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE create mode 100644 Rakefile create mode 100644 bin/vcr_proxy create mode 100644 lib/vcr_proxy.rb create mode 100644 lib/vcr_proxy/vcr_proxy.rb create mode 100644 lib/vcr_proxy/version.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/vcr_proxy_spec.rb create mode 100644 vcr_proxy.gemspec delete mode 100644 vcr_proxy.rb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..44cfb18 --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +source :rubygems + +gem 'vcr' + +group :development, :test do + gem 'guard', '>= 1.6.2' + gem 'pry', '>= 0.9.10' + gem 'pry-doc', '>= 0.4.4' + gem 'rake', '>= 10.0.4' + gem 'rspec', '>= 2.13' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..2dfaadd --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,46 @@ +GEM + remote: http://rubygems.org/ + specs: + coderay (1.0.9) + diff-lcs (1.2.2) + formatador (0.2.4) + guard (1.7.0) + formatador (>= 0.2.4) + listen (>= 0.6.0) + lumberjack (>= 1.0.2) + pry (>= 0.9.10) + thor (>= 0.14.6) + listen (0.7.3) + lumberjack (1.0.3) + method_source (0.8.1) + pry (0.9.12) + coderay (~> 1.0.5) + method_source (~> 0.8) + slop (~> 3.4) + pry-doc (0.4.5) + pry (>= 0.9) + yard (>= 0.8) + rake (10.0.4) + rspec (2.13.0) + rspec-core (~> 2.13.0) + rspec-expectations (~> 2.13.0) + rspec-mocks (~> 2.13.0) + rspec-core (2.13.1) + rspec-expectations (2.13.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.13.0) + slop (3.4.4) + thor (0.18.1) + vcr (2.4.0) + yard (0.8.5.2) + +PLATFORMS + ruby + +DEPENDENCIES + guard (>= 1.6.2) + pry (>= 0.9.10) + pry-doc (>= 0.4.4) + rake (>= 10.0.4) + rspec (>= 2.13) + vcr diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a54610a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2013 Nikola Chochkov + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 9859957..c14b188 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ # VCRProxy -A first try for a very basic implementation of a VCR proxy. VCR is an awesome tool to record and -replay HTTP interactions for your test suite (or other use cases) in Ruby. That means, when you -call an external site, VCR records the request at the first time, and replays at later requests. -The problem is, that VCR is pure Ruby, that means, it can hook into webmock or fakeweb, but not +A first try for a very basic implementation of a VCR proxy. VCR is an awesome tool to record and +replay HTTP interactions for your test suite (or other use cases) in Ruby. That means, when you +call an external site, VCR records the request at the first time, and replays at later requests. +The problem is, that VCR is pure Ruby, that means, it can hook into webmock or fakeweb, but not into requests made by external applications. -Such an external application could be an automated browser for your testsuite (e.g. selenium, phantomjs...). -When you try to do ajax calls from the frontend, that calls aren't recorded, because VCR can't know about them. -But: If you can configure your browser (or any other application) to use a proxy on a specific port, it would +Such an external application could be an automated browser for your testsuite (e.g. selenium, phantomjs...). +When you try to do ajax calls from the frontend, that calls aren't recorded, because VCR can't know about them. +But: If you can configure your browser (or any other application) to use a proxy on a specific port, it would be possible for VCR to record the request, when the proxy is written in Ruby. -And so I did: VCRProxy is a proxy server based on WEBrick (which is included in ruby by default), that hooks +And so I did: VCRProxy is a proxy server based on WEBrick (which is included in ruby by default), that hooks into the calls, records them with VCR and let VCR replay the records the second time. -If you have SSL calls (and that is the tricky part), a second MITM proxy is started. You just have to ensure -that you ignore SSL errors/warnings in your application, because WEBrick generates a self-signed SSL certificate +If you have SSL calls (and that is the tricky part), a second MITM proxy is started. You just have to ensure +that you ignore SSL errors/warnings in your application, because WEBrick generates a self-signed SSL certificate on the fly, and this certificate isn't signed. # Usage @@ -40,7 +40,7 @@ Test the server with curl --proxy localhost:9999 http://blekko.com/ws/?q=rails+/json ``` -For now VCR records the calls into `cassettes/records.yml`, this should be configurable in the future. If you start +For now VCR records the calls into `cassettes/records.yml`, this should be configurable in the future. If you start the command a second time, VCR replays the interaction. If you want to mock out HTTPS calls, try this diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2480d0e --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +require "bundler" +Bundler::GemHelper.install_tasks diff --git a/bin/vcr_proxy b/bin/vcr_proxy new file mode 100644 index 0000000..ce2791e --- /dev/null +++ b/bin/vcr_proxy @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +require 'vcr_proxy' + +# enable modifications to unparsed_uri +class WEBrick::HTTPRequest + def unparsed_uri=(str) + @unparsed_uri = str + end +end + +VCR.configure do |c| + c.hook_into :webmock + c.cassette_library_dir = 'cassettes' + c.default_cassette_options = { :record => :new_episodes } + c.ignore_localhost = true + c.ignore_hosts "127.0.0.1" +end + +server = VCRProxy.new(:Port => 9999) +trap("INT") { server.shutdown } +server.start diff --git a/lib/vcr_proxy.rb b/lib/vcr_proxy.rb new file mode 100644 index 0000000..641a9ab --- /dev/null +++ b/lib/vcr_proxy.rb @@ -0,0 +1,9 @@ +# stdlib modules +require 'net/http' +require 'webrick' +require 'webrick/https' +require 'webrick/httpproxy' + +require 'vcr' + +require 'vcr_proxy/vcr_proxy' diff --git a/lib/vcr_proxy/vcr_proxy.rb b/lib/vcr_proxy/vcr_proxy.rb new file mode 100644 index 0000000..104430d --- /dev/null +++ b/lib/vcr_proxy/vcr_proxy.rb @@ -0,0 +1,103 @@ +module VCRProxy + class << self + def start(opts = {}) + VCRProxy::Server.new(opts) + end + end + + class Server < WEBrick::HTTPProxyServer + def initialize(opts = {}) + super + @mitm_port = opts[:MITMPort] || 12322 + end + + # starts the MITM server + def start_ssl_mitm(host, port) + # WORKAROUND for "adress is already in use", just increase + # the port number and kill the old webrick + @mitm_port += 1 + @mitm_server.stop if @mitm_server + @mitm_thread.kill if @mitm_thread + + @mitm_server = WEBrick::HTTPServer.new({ + :Port => @mitm_port, + :SSLEnable => true, + :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE, + :SSLCertName => [ ["C", "US"], ["O", host], ["CN", host] ] + }) + + @mitm_server.mount_proc('/') do |req, res| + method, url, version = req.request_line.split(" ") + + remote_request = case method.upcase + when 'GET' + Net::HTTP::Get.new(req.unparsed_uri) + when 'POST' + Net::HTTP::Post.new(req.unparsed_uri) + when 'PUT' + Net::HTTP::Put.new(req.unparsed_uri) + when 'DELETE' + Net::HTTP::Delete.new(req.unparsed_uri) + when 'HEAD' + Net::HTTP::Head.new(req.unparsed_uri) + when 'OPTIONS' + Net::HTTP::Options.new(req.unparsed_uri) + else + puts "HTTP method '#{method}' not supported!" + end + + remote_request.body = req.body + remote_request.body = req.body + remote_request.initialize_http_header(transform_header(req.header)) + + uri = req.request_uri + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + + remote_response = http.request(remote_request) + + remote_response.code + res.body = remote_response.body + res.status = remote_response.code + + remote_response.header.each do |k| + res.header[k] = remote_response.header[k] + end + end + + @mitm_thread = Thread.new { @mitm_server.start } + end + + # transforms the webrick header format into the ruby net http format + # webrick: {"agent"=>["blabla"]} + # net http: {"agent"=>"blabla"} + def transform_header(header) + h = {} + header.each do |key, value| + if Array === value + h[key] = value.first + else + h[key] = value + end + end + h + end + + # the proxy tries to just forward SSL connections with a "CONNECT" + # catch that forwarding, and call ssl_mitm + def do_CONNECT(req, res) + host, port = req.unparsed_uri.split(":") + port = 443 unless port + start_ssl_mitm(host, port) + req.unparsed_uri = "127.0.0.1:#{@mitm_port}" + super req, res + end + + def service(req, res) + VCR.use_cassette("records") do + super(req, res) + end + end + end +end diff --git a/lib/vcr_proxy/version.rb b/lib/vcr_proxy/version.rb new file mode 100644 index 0000000..0493b8e --- /dev/null +++ b/lib/vcr_proxy/version.rb @@ -0,0 +1,3 @@ +module VCRProxy + VERSION = '0.0.1' +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..86307bf --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'vcr_proxy' diff --git a/spec/vcr_proxy_spec.rb b/spec/vcr_proxy_spec.rb new file mode 100644 index 0000000..02c5bbe --- /dev/null +++ b/spec/vcr_proxy_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe VCRProxy do + it "should work" do + VCRProxy.start({}).should be_kind_of?(VCRProxy) + end +end diff --git a/vcr_proxy.gemspec b/vcr_proxy.gemspec new file mode 100644 index 0000000..ca68e34 --- /dev/null +++ b/vcr_proxy.gemspec @@ -0,0 +1,30 @@ +require File.expand_path("../lib/vcr_proxy/version", __FILE__) + +Gem::Specification.new do |s| + s.name = "vcr_proxy" + s.version = VCRProxy::VERSION + s.platform = Gem::Platform::RUBY + s.authors = [""] + s.email = [""] + s.homepage = "http://github.com/23tux/vcr_proxy" + s.summary = "" + s.description = "" + + s.required_rubygems_version = ">= 1.3.6" + + s.add_development_dependency 'guard', '>= 1.6.2' + s.add_development_dependency 'pry', '>= 0.9.10' + s.add_development_dependency 'pry-doc', '>= 0.4.4' + s.add_development_dependency 'rake', '>= 10.0.4' + s.add_development_dependency 'rspec', '>= 2.13' + + s.require_paths = ['lib'] + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {spec}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + + # If you have C extensions, uncomment these: + # s.extensions = "ext/extconf.rb" + # s.require_paths << ['ext'] +end diff --git a/vcr_proxy.rb b/vcr_proxy.rb deleted file mode 100644 index 79429f8..0000000 --- a/vcr_proxy.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'net/http' -require 'webrick' -require 'webrick/https' -require 'webrick/httpproxy' -require 'vcr' -require 'debugger' - -# enable modifications to unparsed_uri -class WEBrick::HTTPRequest - def unparsed_uri=(str) - @unparsed_uri = str - end -end - -class VCRProxy < WEBrick::HTTPProxyServer - def initialize(options) - super(options) - @mitm_port = options[:MITMPort] || 12322 - end - - # starts the MITM server - def start_ssl_mitm(host, port) - # WORKAROUND for "adress is already in use", just increase - # the port number and kill the old webrick - @mitm_port += 1 - @mitm_server.stop if @mitm_server - @mitm_thread.kill if @mitm_thread - - @mitm_server = WEBrick::HTTPServer.new(:Port => @mitm_port, - :SSLEnable => true, - :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE, - :SSLCertName => [["C", "US"], ["O", host], ["CN", host] ]) - - @mitm_server.mount_proc('/') do |req,res| - method, url, version = req.request_line.split(" ") - - remote_request = case method.upcase - when 'GET' - Net::HTTP::Get.new(req.unparsed_uri) - when 'POST' - Net::HTTP::Post.new(req.unparsed_uri) - when 'PUT' - Net::HTTP::Put.new(req.unparsed_uri) - when 'DELETE' - Net::HTTP::Delete.new(req.unparsed_uri) - when 'HEAD' - Net::HTTP::Head.new(req.unparsed_uri) - when 'OPTIONS' - Net::HTTP::Options.new(req.unparsed_uri) - else - puts "HTTP method '#{method}' not supported!" - end - - remote_request.body = req.body - remote_request.body = req.body - remote_request.initialize_http_header(transform_header(req.header)) - - uri = req.request_uri - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - - remote_response = http.request(remote_request) - - remote_response.code - res.body = remote_response.body - res.status = remote_response.code - - remote_response.header.each do |k| - res.header[k] = remote_response.header[k] - end - end - - @mitm_thread = Thread.new { @mitm_server.start } - end - - # transforms the webrick header format into the ruby net http format - # webrick: {"agent"=>["blabla"]} - # net http: {"agent"=>"blabla"} - def transform_header header - h = {} - header.each do |key, value| - if value.class == Array - h[key] = value.first - else - h[key] = value - end - end - h - end - - # the proxy tries to just forward SSL connections with a "CONNECT" - # catch that forwarding, and call ssl_mitm - def do_CONNECT(req, res) - host, port = req.unparsed_uri.split(":") - port = 443 unless port - start_ssl_mitm(host, port) - req.unparsed_uri = "127.0.0.1:#{@mitm_port}" - super req, res - end - - def service(req, res) - VCR.use_cassette("records") do - super(req, res) - end - end -end - -VCR.configure do |c| - c.hook_into :webmock - c.cassette_library_dir = 'cassettes' - c.default_cassette_options = { :record => :new_episodes } - c.ignore_localhost = true - c.ignore_hosts "127.0.0.1" -end - -server = VCRProxy.new(:Port => 9999) -trap("INT"){ server.shutdown } -server.start From 292dc8cce7a551fd105d151745a0d3abbff6da52 Mon Sep 17 00:00:00 2001 From: nikola Date: Thu, 11 Apr 2013 15:00:23 +0200 Subject: [PATCH 2/2] Extends testing and gem framework; adds rails/rspec integration --- Gemfile | 13 ++- Gemfile.lock | 10 ++ Guardfile | 11 ++ README.md | 50 +++++++--- bin/vcr_proxy | 30 ++++-- lib/vcr_proxy.rb | 116 +++++++++++++++++++++- lib/vcr_proxy/constants.rb | 13 +++ lib/vcr_proxy/dependency.rb | 38 +++++++ lib/vcr_proxy/driver.rb | 16 +++ lib/vcr_proxy/railtie.rb | 7 ++ lib/vcr_proxy/rspec.rb | 11 ++ lib/vcr_proxy/{vcr_proxy.rb => server.rb} | 51 ++++++---- lib/vcr_proxy/tasks/vcr_proxy.rake | 17 ++++ spec/vcr_proxy_spec.rb | 10 +- vcr_proxy.gemspec | 3 + 15 files changed, 346 insertions(+), 50 deletions(-) create mode 100644 Guardfile mode change 100644 => 100755 bin/vcr_proxy create mode 100644 lib/vcr_proxy/constants.rb create mode 100644 lib/vcr_proxy/dependency.rb create mode 100644 lib/vcr_proxy/driver.rb create mode 100644 lib/vcr_proxy/railtie.rb create mode 100644 lib/vcr_proxy/rspec.rb rename lib/vcr_proxy/{vcr_proxy.rb => server.rb} (67%) create mode 100644 lib/vcr_proxy/tasks/vcr_proxy.rake diff --git a/Gemfile b/Gemfile index 44cfb18..be5b77a 100644 --- a/Gemfile +++ b/Gemfile @@ -3,9 +3,12 @@ source :rubygems gem 'vcr' group :development, :test do - gem 'guard', '>= 1.6.2' - gem 'pry', '>= 0.9.10' - gem 'pry-doc', '>= 0.4.4' - gem 'rake', '>= 10.0.4' - gem 'rspec', '>= 2.13' + gem 'guard', '>= 1.6.2' + gem 'guard-bundler' + gem 'guard-rspec' + gem 'rb-fsevent' + gem 'pry', '>= 0.9.10' + gem 'pry-doc', '>= 0.4.4' + gem 'rake', '>= 10.0.4' + gem 'rspec', '>= 2.13' end diff --git a/Gemfile.lock b/Gemfile.lock index 2dfaadd..be4e15f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,6 +10,12 @@ GEM lumberjack (>= 1.0.2) pry (>= 0.9.10) thor (>= 0.14.6) + guard-bundler (1.0.0) + bundler (~> 1.0) + guard (~> 1.1) + guard-rspec (2.5.0) + guard (>= 1.1) + rspec (~> 2.11) listen (0.7.3) lumberjack (1.0.3) method_source (0.8.1) @@ -21,6 +27,7 @@ GEM pry (>= 0.9) yard (>= 0.8) rake (10.0.4) + rb-fsevent (0.9.3) rspec (2.13.0) rspec-core (~> 2.13.0) rspec-expectations (~> 2.13.0) @@ -39,8 +46,11 @@ PLATFORMS DEPENDENCIES guard (>= 1.6.2) + guard-bundler + guard-rspec pry (>= 0.9.10) pry-doc (>= 0.4.4) rake (>= 10.0.4) + rb-fsevent rspec (>= 2.13) vcr diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..5e7fa46 --- /dev/null +++ b/Guardfile @@ -0,0 +1,11 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +guard :bundler do + watch('Gemfile') +end + +guard :rspec, :cli => '--drb --format progress --color' do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/.+\.rb$}) do |m| "spec/#{m[1]}_spec.rb" end +end diff --git a/README.md b/README.md index c14b188..09c88c5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # VCRProxy -A first try for a very basic implementation of a VCR proxy. VCR is an awesome tool to record and +A basic implementation of a VCR proxy. VCR is an awesome tool to record and replay HTTP interactions for your test suite (or other use cases) in Ruby. That means, when you call an external site, VCR records the request at the first time, and replays at later requests. The problem is, that VCR is pure Ruby, that means, it can hook into webmock or fakeweb, but not @@ -18,23 +18,46 @@ If you have SSL calls (and that is the tricky part), a second MITM proxy is star that you ignore SSL errors/warnings in your application, because WEBrick generates a self-signed SSL certificate on the fly, and this certificate isn't signed. -# Usage +# Installation -Clone the repo +Install the gem either through your `Gemfile`: ``` -git clone git://github.com/23tux/vcr_proxy.git +gem 'vcr_proxy' ``` -Have a look into the `vcr_proxy.rb` and look at the bottom to make sure, port `9999` is available at your machine. +or by: -Start the server with +``` +gem install vcr_proxy +``` + +# Rspec usage + +TODO: `Rspec` integration + +# Configuration +TODO: Allow custom VCR configuration through VCRProxy so that people could use +only VCRPRoxy ( without explicitly citing the vcr gem itself ). However we +should respect any existing VCR configuration. + +# Manual usage + +## Starting the server + +If you're on a Rails app, enjoy the rake tasks: + +``` +bundle exec rake vcr_proxy:start VCR_PROXY_PORT=9999 +``` + +or if you're not, see the: ``` -ruby vcr_proxy.rb +vcr_proxy --help ``` -Test the server with +and then a quick test: ``` curl --proxy localhost:9999 http://blekko.com/ws/?q=rails+/json @@ -43,7 +66,7 @@ curl --proxy localhost:9999 http://blekko.com/ws/?q=rails+/json For now VCR records the calls into `cassettes/records.yml`, this should be configurable in the future. If you start the command a second time, VCR replays the interaction. -If you want to mock out HTTPS calls, try this +If you want to mock out HTTPS calls, try this: ``` curl --proxy localhost:9999 --insecure https://blekko.com/ws/?q=rails+/json @@ -51,7 +74,10 @@ curl --proxy localhost:9999 --insecure https://blekko.com/ws/?q=rails+/json The `--insecure` option tells curl to ignore SSL warnings. -# Further Development +# Contributing + +Check back the current GitHub issues list, then: -There is a lot more to improve this thing. The issue sites tracks some already known issues from my site. -This thing should also be converted into a gem, and should provide some nice helper to hook into RSpec... +* clone +* create in a feature branch with specs +* create a Pull Request diff --git a/bin/vcr_proxy b/bin/vcr_proxy old mode 100644 new mode 100755 index ce2791e..9610d60 --- a/bin/vcr_proxy +++ b/bin/vcr_proxy @@ -1,22 +1,34 @@ #!/usr/bin/env ruby +require 'optparse' require 'vcr_proxy' +hash = {} + +OptionParser.new do |opts| + opts.banner = "Usage: vcr_proxy [options]" + + opts.on('-c', '--cassettes CASSETTES', 'location of cassettes folder') do |r| + hash[:cassettes] = r + end + + opts.on('-p', '--port PORT', 'VCRProxy server port') do |r| + hash[:port] = r.to_i + end +end.parse! + # enable modifications to unparsed_uri +# FIXME - lets not open HTTPRequest class and do sth else instead class WEBrick::HTTPRequest def unparsed_uri=(str) @unparsed_uri = str end end -VCR.configure do |c| - c.hook_into :webmock - c.cassette_library_dir = 'cassettes' - c.default_cassette_options = { :record => :new_episodes } - c.ignore_localhost = true - c.ignore_hosts "127.0.0.1" -end +# FIXME - we don't want to have custom VCR configuration because most apps will +# already have that. Or we should at least +VCRProxy.configure(hash) + +server = VCRProxy.start({ :Port => hash[:port] }) -server = VCRProxy.new(:Port => 9999) trap("INT") { server.shutdown } -server.start diff --git a/lib/vcr_proxy.rb b/lib/vcr_proxy.rb index 641a9ab..d805258 100644 --- a/lib/vcr_proxy.rb +++ b/lib/vcr_proxy.rb @@ -6,4 +6,118 @@ require 'vcr' -require 'vcr_proxy/vcr_proxy' +require 'vcr_proxy/server' +require 'vcr_proxy/constants' +require 'vcr_proxy/dependency' +require 'vcr_proxy/driver' + +if VCRProxy::Dependency.rails3? + require 'vcr_proxy/railtie' +end + +require 'capybara/poltergeist' + +VCRProxy.register_poltergeist_driver + +if VCRProxy::Dependency.rspec2? + require 'vcr_proxy/rspec' +end + +module VCRProxy + include Constants + + class << self + def prepare(opts = {}) + VCR.configure do |c| + c.default_cassette_options = { :record => :new_episodes } + end + Server.new(opts) + end + + def log + @logger ||= begin + if Dependency.rails? + Rails.logger + else + Logger.new(STDOUT) + end + end + end + + def port + ENV['VCR_PROXY_PORT'] || VCRProxy::DEFAULT_PORT + end + + def host + ENV['VCR_PROXY_HOST'] || VCRProxy::DEFAULT_HOST + end + + # start http proxy server and write the pid under tmp/pids + def start_with_pid + stop_with_pid + + Process.fork do + path = get_pid_root.join(VCRProxy::PID_FILE_PATH) + path.mkdir unless path.directory? + + path = path.join(VCRProxy::PID_FILE_NAME) + + opts = { + :Port => VCRProxy.port, + :RequestTimeout => 300, + :ProxyTimeout => true, + } + + server = VCRProxy.prepare(opts) + + log.info "VCRProxy starting on #{VCRProxy.port}, pid #{Process.pid}" + + trap('INT') { server.shutdown } + + path.open('w') do |file| + file.puts Process.pid + end + + server.start + end + end + + # send INT to VCRProxy pid if file found under tmp/pids/ + def stop_with_pid + path = get_pid_path + + if path.exist? + `kill -s INT #{path.read.chomp}` + path.delete + else + log.info "Looked in tmp/pids/; no VCRProxy pids found; nothing happened" + end + end + + # FIXME how do we implement configuraion + def configure(opts = {}) + VCR.configure do |c| + c.hook_into :webmock + c.cassette_library_dir = opts[:cassettes] ||= DEFAULT_CASSETTES + c.default_cassette_options = { :record => :new_episodes } + c.ignore_localhost = true + c.ignore_hosts "127.0.0.1" + end + end + + private + + def get_pid_path + get_pid_root.join(VCRProxy::PID_FILE_PATH).join(VCRProxy::PID_FILE_NAME) + end + + def get_pid_root + if Dependency.rails? + Rails.root + else + require 'pathname' + path Pathname.new '/' + end + end + end +end diff --git a/lib/vcr_proxy/constants.rb b/lib/vcr_proxy/constants.rb new file mode 100644 index 0000000..b3ff5b9 --- /dev/null +++ b/lib/vcr_proxy/constants.rb @@ -0,0 +1,13 @@ +module VCRProxy + module Constants + DEFAULT_PORT = 9994 + DEFAULT_HOST = 'localhost' + + # this setting will not be used if overwritten by VCR config + DEFAULT_CASSETTES = '/tmp/cassettes' + + # the location of the http proxy pid + PID_FILE_PATH = 'tmp/pids' + PID_FILE_NAME = 'vcr_proxy_server.pid' + end +end diff --git a/lib/vcr_proxy/dependency.rb b/lib/vcr_proxy/dependency.rb new file mode 100644 index 0000000..cc3b1f3 --- /dev/null +++ b/lib/vcr_proxy/dependency.rb @@ -0,0 +1,38 @@ +module VCRProxy + module Dependency + class << self + + def rails3? + safe_check_gem('rails', '>= 3.0') && running_rails3? + end + + def rails? + running_rails? + end + + def rspec2? + !! safe_check_gem('rspec', '>= 2.0') + end + + private + + def running_rails3? + running_rails? && Rails.version.to_i == 3 + end + + def running_rails? + !! defined?(Rails) + end + + def safe_check_gem(gem_name, version_string) + if Gem::Specification.respond_to?(:find_by_name) + Gem::Specification.find_by_name(gem_name, version_string) + elsif Gem.respond_to?(:available?) + Gem.available?(gem_name, version_string) + end + rescue Gem::LoadError + false + end + end + end +end diff --git a/lib/vcr_proxy/driver.rb b/lib/vcr_proxy/driver.rb new file mode 100644 index 0000000..f2091d6 --- /dev/null +++ b/lib/vcr_proxy/driver.rb @@ -0,0 +1,16 @@ +module VCRProxy + class << self + def register_poltergeist_driver + ::Capybara.register_driver :poltergeist_billy do |app| + options = { + phantomjs_options: [ + '--ignore-ssl-errors=yes', + "--proxy=#{VCRProxy.host}:#{VCRProxy.port}", + ] + } + + ::Capybara::Poltergeist::Driver.new(app, options) + end + end + end +end diff --git a/lib/vcr_proxy/railtie.rb b/lib/vcr_proxy/railtie.rb new file mode 100644 index 0000000..ab63444 --- /dev/null +++ b/lib/vcr_proxy/railtie.rb @@ -0,0 +1,7 @@ +module VCRProxy + class Railtie < Rails::Railtie + rake_tasks do + load 'vcr_proxy/tasks/vcr_proxy.rake' + end + end +end diff --git a/lib/vcr_proxy/rspec.rb b/lib/vcr_proxy/rspec.rb new file mode 100644 index 0000000..48c76e1 --- /dev/null +++ b/lib/vcr_proxy/rspec.rb @@ -0,0 +1,11 @@ +require 'rspec' + +RSpec.configure do |config| + config.before(:suite) do + VCRProxy.start_with_pid + end + + config.after(:suite) do + VCRProxy.stop_with_pid + end +end diff --git a/lib/vcr_proxy/vcr_proxy.rb b/lib/vcr_proxy/server.rb similarity index 67% rename from lib/vcr_proxy/vcr_proxy.rb rename to lib/vcr_proxy/server.rb index 104430d..ed5aff6 100644 --- a/lib/vcr_proxy/vcr_proxy.rb +++ b/lib/vcr_proxy/server.rb @@ -1,17 +1,16 @@ module VCRProxy - class << self - def start(opts = {}) - VCRProxy::Server.new(opts) - end - end - - class Server < WEBrick::HTTPProxyServer + class Server < ::WEBrick::HTTPProxyServer def initialize(opts = {}) super @mitm_port = opts[:MITMPort] || 12322 end - # starts the MITM server + # Starts the MITM server + # + # @param host [String] + # @param port [Integer] + # + # @return Thread object def start_ssl_mitm(host, port) # WORKAROUND for "adress is already in use", just increase # the port number and kill the old webrick @@ -19,7 +18,7 @@ def start_ssl_mitm(host, port) @mitm_server.stop if @mitm_server @mitm_thread.kill if @mitm_thread - @mitm_server = WEBrick::HTTPServer.new({ + @mitm_server = ::WEBrick::HTTPServer.new({ :Port => @mitm_port, :SSLEnable => true, :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE, @@ -31,17 +30,17 @@ def start_ssl_mitm(host, port) remote_request = case method.upcase when 'GET' - Net::HTTP::Get.new(req.unparsed_uri) + ::Net::HTTP::Get.new(req.unparsed_uri) when 'POST' - Net::HTTP::Post.new(req.unparsed_uri) + ::Net::HTTP::Post.new(req.unparsed_uri) when 'PUT' - Net::HTTP::Put.new(req.unparsed_uri) + ::Net::HTTP::Put.new(req.unparsed_uri) when 'DELETE' - Net::HTTP::Delete.new(req.unparsed_uri) + ::Net::HTTP::Delete.new(req.unparsed_uri) when 'HEAD' - Net::HTTP::Head.new(req.unparsed_uri) + ::Net::HTTP::Head.new(req.unparsed_uri) when 'OPTIONS' - Net::HTTP::Options.new(req.unparsed_uri) + ::Net::HTTP::Options.new(req.unparsed_uri) else puts "HTTP method '#{method}' not supported!" end @@ -51,14 +50,14 @@ def start_ssl_mitm(host, port) remote_request.initialize_http_header(transform_header(req.header)) uri = req.request_uri - http = Net::HTTP.new(uri.host, uri.port) + http = ::Net::HTTP.new(uri.host, uri.port) http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http.verify_mode = ::OpenSSL::SSL::VERIFY_NONE remote_response = http.request(remote_request) remote_response.code - res.body = remote_response.body + res.body = remote_response.body res.status = remote_response.code remote_response.header.each do |k| @@ -66,16 +65,24 @@ def start_ssl_mitm(host, port) end end - @mitm_thread = Thread.new { @mitm_server.start } + @mitm_thread = ::Thread.new { @mitm_server.start } end # transforms the webrick header format into the ruby net http format # webrick: {"agent"=>["blabla"]} # net http: {"agent"=>"blabla"} def transform_header(header) + # header.inject({}) do |memo, pair| + # if Array === value + # h[key] = value.first + # else + # h[key] = value + # end + # end + h = {} header.each do |key, value| - if Array === value + if ::Array === value h[key] = value.first else h[key] = value @@ -95,9 +102,9 @@ def do_CONNECT(req, res) end def service(req, res) - VCR.use_cassette("records") do + ::VCR.use_cassette("vcr_proxy_records", :record => :new_episodes) do super(req, res) end end end -end + end diff --git a/lib/vcr_proxy/tasks/vcr_proxy.rake b/lib/vcr_proxy/tasks/vcr_proxy.rake new file mode 100644 index 0000000..8467600 --- /dev/null +++ b/lib/vcr_proxy/tasks/vcr_proxy.rake @@ -0,0 +1,17 @@ +namespace :vcr_proxy do + namespace :server do + desc 'Start the VCR Proxy server. Default VCR_PROXY_PORT=9999.' + task :start do + require 'vcr_proxy' + + VCRProxy.start_with_pid + end + + desc 'Stop the VCR Proxy server if tmp/pids/ pid files are found' + task :start do + require 'vcr_proxy' + + VCRProxy.stop_with_pid + end + end +end diff --git a/spec/vcr_proxy_spec.rb b/spec/vcr_proxy_spec.rb index 02c5bbe..d0df5f7 100644 --- a/spec/vcr_proxy_spec.rb +++ b/spec/vcr_proxy_spec.rb @@ -1,7 +1,15 @@ require 'spec_helper' describe VCRProxy do + before :each do + @server = VCRProxy.start(:Port => 9999) + end + + after :each do + @server.shutdown + end + it "should work" do - VCRProxy.start({}).should be_kind_of?(VCRProxy) + @server.should be_kind_of(VCRProxy::Server) end end diff --git a/vcr_proxy.gemspec b/vcr_proxy.gemspec index ca68e34..2b6c727 100644 --- a/vcr_proxy.gemspec +++ b/vcr_proxy.gemspec @@ -10,6 +10,9 @@ Gem::Specification.new do |s| s.summary = "" s.description = "" + s.add_runtime_dependency 'capybara' + s.add_runtime_dependency 'poltergeist' + s.required_rubygems_version = ">= 1.3.6" s.add_development_dependency 'guard', '>= 1.6.2'