puffing-billy 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 77894c8ea71e9f2b91cb3aae48c6bc1311ab45ba
4
- data.tar.gz: 2af9cc1a8fafdfafa15ac9bf1d088e2cb648ae59
3
+ metadata.gz: 30ec3de1e00ab91373eb88106e301a8fe9963701
4
+ data.tar.gz: 7300cdd6f73ae4e5a5a6e6a1dd6108cb9074d272
5
5
  SHA512:
6
- metadata.gz: 00faa0d3c37550847f947b5440770e628978593476771c27f26d31516b8ff176540340a00cba098b2fca5ebcd86bc53b29a94d53af57c4b92547be3a3b07f603
7
- data.tar.gz: 642297885633e8f29efab30ad78faef48d173dd4b4a902cb92cbef118c616b349a01671d27f5b3b38af8b8ad4b341f251d0d23fdff9e1b7b3096b118efd246a6
6
+ metadata.gz: 1e00330461d05f7fa3e3f01cad76e35e152f2ffc44c7b3633d0b780c60e714429185b7200897eaef6c0d96e7067a78cb1cd63e680013601f9f873f3e0f58eeff
7
+ data.tar.gz: 6d1099b167707b5c41f38444f81ff39c6d6a1753e8072fdbbca85c2e2a605c391591ebf095bff3b6173e4a7060304b0c30c52e1a92723f0c502bb7a4a78bf036
data/.gitignore CHANGED
@@ -2,3 +2,4 @@ node_modules/
2
2
  log/test.log
3
3
  .idea/
4
4
  .ruby-version
5
+ *.swp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ script: bundle exec rspec spec
data/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ v0.3.0, 2014-12-29
2
+ ------------------
3
+
4
+ * Fixing a bug where proxy to SSL can duplicate https in the request_url (#36)
5
+ * Refactor proxy responses (#37)
6
+ * Update http_parser to 0.6.0 and remove CONNECT request hack (#38)
7
+ * Allow configurable Billy proxy port (#40)
8
+ * Refactor handlers (#41)
9
+ * Mark step definitions as ruby code (#52)
10
+ * Adds EventMachine timeout configuration (#57)
11
+ * Support dynamic jsonp with params (#58)
12
+ * Writing error messages to the logger rather than stdout (#69)
13
+ * Don't recommend changing javascript_driver config (#70)
14
+ * README link pointing at wrong target (#73)
15
+ * Adding example config to README for playing nicely with Webmock, VCR (#74)
16
+ * Make dynamic_jsonp regex less brittle (#81)
17
+
18
+ v0.2.3, 2014-02-07
19
+ ------------------
20
+
21
+ * Fixed facebook spec (#24)
22
+ * Check for existing persistent cache files on demand (#26)
23
+ * Lazy-loading proxy and other minor fixes (#28)
24
+ * Support service-oriented architectures, scope cache to scenarios, and sort JSON in POSTs to avoid duplicate cache files (#30)
25
+ * Set the minimum version of capybara-webkit to 1.0.0 (#31)
26
+ * Updated gems, cache request headers, handle non-successful responses, ability to stop new connections (#33)
27
+ * Add requires matching gem name (#34)
28
+ * Remove duplicated rspec devel dependency (#35)
29
+
30
+ v0.2.1, 2013-05-12
31
+ ------------------
32
+
33
+ * Add cucumber documentation to readme. (#12)
34
+ * Use multi_json (#13)
35
+ * Remove require from Gemfile example (#14)
36
+ * Add a README example of returning headers (#16)
37
+
38
+ v0.2.0, 2013-03-17
39
+ ------------------
40
+
41
+ * Update README with HTTPS quirk and trailing slash behaviour. (#3)
42
+ * Fixes to work with Capybara-Webkit (#6)
43
+ * VCR-like cache (#7)
44
+
45
+ v0.1.3, 2012-11-05
46
+ ------------------
47
+
48
+ * Implemented caching
49
+
50
+ v0.1.2, 2012-10-12
51
+ ------------------
52
+
53
+ * Slightly saner driver setup
54
+ * Updated README
55
+
56
+ v0.1.1, 2012-10-12
57
+ ------------------
58
+
59
+ * Content encoding fixes
60
+ * Updated README
61
+
62
+ v0.1.0, 2012-10-11
63
+ ------------------
64
+
65
+ * Initial release
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- puffing-billy (0.2.3)
4
+ puffing-billy (0.3.0)
5
5
  capybara
6
6
  em-http-request
7
+ em-synchrony
7
8
  eventmachine
8
9
  eventmachine_httpserver
9
- http_parser.rb
10
+ http_parser.rb (~> 0.6.0)
10
11
  multi_json
11
12
 
12
13
  GEM
@@ -29,7 +30,7 @@ GEM
29
30
  ffi (~> 1.0, >= 1.0.11)
30
31
  cliver (0.3.2)
31
32
  coderay (1.1.0)
32
- cookiejar (0.3.0)
33
+ cookiejar (0.3.1)
33
34
  cucumber (1.3.10)
34
35
  builder (>= 2.1.2)
35
36
  diff-lcs (>= 1.1.3)
@@ -46,6 +47,8 @@ GEM
46
47
  http_parser.rb (>= 0.6.0)
47
48
  em-socksify (0.3.0)
48
49
  eventmachine (>= 1.0.0.beta.4)
50
+ em-synchrony (1.0.3)
51
+ eventmachine (>= 1.0.0.beta.1)
49
52
  eventmachine (1.0.3)
50
53
  eventmachine_httpserver (0.2.1)
51
54
  faraday (0.9.0)
data/README.md CHANGED
@@ -120,10 +120,9 @@ Feature: Stubbing via billy
120
120
 
121
121
  And in steps:
122
122
 
123
- ```
123
+ ```ruby
124
124
  Before('@billy') do
125
125
  Capybara.current_driver = :poltergeist_billy
126
- Capybara.javascript_driver = :poltergeist_billy
127
126
  end
128
127
 
129
128
  And /^a stub for google$/ do
@@ -202,6 +201,18 @@ caching. You should mostly use this for analytics and various social buttons as
202
201
  they use cache avoidance techniques, but return practically the same response
203
202
  that most often does not affect your test results.
204
203
 
204
+ `c.dynamic_jsonp` is used to rewrite the body of JSONP responses based on the
205
+ callback parameter. For example, if a request to `http://example.com/foo?callback=bar`
206
+ returns `bar({"some": "json"});` and is recorded, then a later request to
207
+ `http://example.com/foo?callback=baz` will be a cache hit and respond with
208
+ `baz({"some": "json"});` This is useful because most JSONP implementations
209
+ base the callback name off of a timestamp or something else dynamic.
210
+
211
+ `c.dynamic_jsonp_keys` is used to configure which parameters to ignore when
212
+ using `c.dynamic_jsonp`. This is helpful when JSONP APIs use cache-busting
213
+ parameters. For example, if you want `http://example.com/foo?callback=bar&id=1&cache_bust=12345` and `http://example.com/foo?callback=baz&id=1&cache_bust=98765` to be cache hits for each other, you would set `c.dynamic_jsonp_keys = ['callback', 'cache_bust']` to ignore both params. Note
214
+ that in this example the `id` param would still be considered important.
215
+
205
216
  `c.path_blacklist = []` is used to always cache specific paths on any hostnames,
206
217
  including whitelisted ones. This is useful if your AUT has routes that get data
207
218
  from external services, such as `/api` where the ajax request is a local URL but
@@ -287,13 +298,42 @@ RSpec.configure do |config|
287
298
  end
288
299
  ```
289
300
 
301
+ ## Proxy timeouts
302
+
303
+ By default, the Puffing Billy proxy will use the EventMachine:HttpRequest timeouts of 5 seconds
304
+ for connect and 10 seconds for inactivity when talking to downstream servers.
305
+
306
+ These can be configured as follows:
307
+
308
+ ```ruby
309
+ Billy.configure do |c|
310
+ c.proxied_request_connect_timeout = 20
311
+ c.proxied_request_inactivity_timeout = 20
312
+ end
313
+ ```
314
+
290
315
  ## Customising the javascript driver
291
316
 
292
317
  If you use a customised Capybara driver, remember to set the proxy address
293
318
  and tell it to ignore SSL certificate warnings. See
294
- [lib/billy/rspec.rb](https://github.com/oesmith/puffing-billy/blob/master/lib/billy/rspec.rb)
319
+ [lib/billy.rb](https://github.com/oesmith/puffing-billy/blob/master/lib/billy.rb)
295
320
  to see how Billy's default drivers are configured.
296
321
 
322
+ ## Working with VCR and Webmock
323
+ If you use VCR and Webmock elsewhere in your specs, you will need to disable them
324
+ for your specs utilizing Puffing Billy. To do so, you can configure your `spec_helper.rb`
325
+ as shown below:
326
+
327
+ ```ruby
328
+ RSpec.configure do |config|
329
+ config.around(:each, type: :feature) do |example|
330
+ WebMock.allow_net_connect!
331
+ VCR.turned_off { example.run }
332
+ WebMock.disable_net_connect!
333
+ end
334
+ end
335
+ ```
336
+
297
337
  ## FAQ
298
338
 
299
339
  1. Why name it after a train?
data/lib/billy.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  require "billy/version"
2
2
  require "billy/config"
3
+ require "billy/handlers/handler"
4
+ require "billy/handlers/request_handler"
5
+ require "billy/handlers/stub_handler"
6
+ require "billy/handlers/proxy_handler"
7
+ require "billy/handlers/cache_handler"
3
8
  require "billy/proxy_request_stub"
4
9
  require "billy/cache"
5
10
  require "billy/proxy"
data/lib/billy/cache.rb CHANGED
@@ -2,9 +2,12 @@ require 'resolv'
2
2
  require 'uri'
3
3
  require 'yaml'
4
4
  require 'billy/json_utils'
5
+ require 'singleton'
5
6
 
6
7
  module Billy
7
8
  class Cache
9
+ include Singleton
10
+
8
11
  attr_reader :scope
9
12
 
10
13
  def initialize
@@ -30,7 +33,7 @@ module Billy
30
33
  begin
31
34
  @cache[key] = YAML.load(File.open(cache_file(key))) if persisted?(key)
32
35
  rescue ArgumentError => e
33
- puts "Could not parse YAML: #{e.message}"
36
+ Billy.log :error, "Could not parse YAML: #{e.message}"
34
37
  nil
35
38
  end
36
39
  end
@@ -83,14 +86,27 @@ module Billy
83
86
  key
84
87
  end
85
88
 
86
- def format_url(url, ignore_params=false)
89
+ def format_url(url, ignore_params=false, dynamic_jsonp=Billy.config.dynamic_jsonp)
87
90
  url = URI(url)
88
91
  port_to_include = Billy.config.ignore_cache_port ? '' : ":#{url.port}"
89
92
  formatted_url = url.scheme+'://'+url.host+port_to_include+url.path
90
- unless ignore_params
91
- formatted_url += '?'+url.query if url.query
92
- formatted_url += '#'+url.fragment if url.fragment
93
+
94
+ return formatted_url if ignore_params
95
+
96
+ if url.query
97
+ query_string = if dynamic_jsonp
98
+ query_hash = Rack::Utils.parse_query(url.query)
99
+ Billy.config.dynamic_jsonp_keys.each{|k| query_hash.delete(k) }
100
+ Rack::Utils.build_query(query_hash)
101
+ else
102
+ url.query
103
+ end
104
+
105
+ formatted_url += "?#{query_string}"
93
106
  end
107
+
108
+ formatted_url += '#'+url.fragment if url.fragment
109
+
94
110
  formatted_url
95
111
  end
96
112
 
data/lib/billy/config.rb CHANGED
@@ -4,10 +4,12 @@ require 'tmpdir'
4
4
  module Billy
5
5
  class Config
6
6
  DEFAULT_WHITELIST = ['127.0.0.1', 'localhost']
7
+ RANDOM_AVAILABLE_PORT = 0 # https://github.com/eventmachine/eventmachine/wiki/FAQ#wiki-can-i-start-a-server-on-a-random-available-port
7
8
 
8
9
  attr_accessor :logger, :cache, :cache_request_headers, :whitelist, :path_blacklist, :ignore_params,
9
10
  :persist_cache, :ignore_cache_port, :non_successful_cache_disabled, :non_successful_error_level,
10
- :non_whitelisted_requests_disabled, :cache_path
11
+ :non_whitelisted_requests_disabled, :cache_path, :proxy_port, :proxied_request_inactivity_timeout,
12
+ :proxied_request_connect_timeout, :dynamic_jsonp, :dynamic_jsonp_keys
11
13
 
12
14
  def initialize
13
15
  @logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
@@ -21,11 +23,16 @@ module Billy
21
23
  @path_blacklist = []
22
24
  @ignore_params = []
23
25
  @persist_cache = false
26
+ @dynamic_jsonp = false
27
+ @dynamic_jsonp_keys = ["callback"]
24
28
  @ignore_cache_port = true
25
29
  @non_successful_cache_disabled = false
26
30
  @non_successful_error_level = :warn
27
31
  @non_whitelisted_requests_disabled = false
28
32
  @cache_path = File.join(Dir.tmpdir, 'puffing-billy')
33
+ @proxy_port = RANDOM_AVAILABLE_PORT
34
+ @proxied_request_inactivity_timeout = 10 # defaults from https://github.com/igrigorik/em-http-request/wiki/Redirects-and-Timeouts
35
+ @proxied_request_connect_timeout = 5
29
36
  end
30
37
  end
31
38
 
@@ -0,0 +1,51 @@
1
+ require 'billy/handlers/handler'
2
+ require 'cgi'
3
+
4
+ module Billy
5
+ class CacheHandler
6
+ extend Forwardable
7
+ include Handler
8
+
9
+ def_delegators :cache, :reset, :cached?
10
+
11
+ def initialize
12
+ @cache = Billy::Cache.instance
13
+ end
14
+
15
+ def handle_request(method, url, headers, body)
16
+ method = method.downcase
17
+ if handles_request?(method, url, headers, body)
18
+ if (response = cache.fetch(method, url, body))
19
+ Billy.log(:info, "puffing-billy: CACHE #{method} for '#{url}'")
20
+
21
+ if Billy.config.dynamic_jsonp
22
+ replace_response_callback(response, url)
23
+ end
24
+
25
+ return response
26
+ end
27
+ end
28
+ nil
29
+ end
30
+
31
+ def handles_request?(method, url, headers, body)
32
+ cached?(method, url, body)
33
+ end
34
+
35
+ private
36
+
37
+ def replace_response_callback(response, url)
38
+ request_uri = URI::parse(url)
39
+ if request_uri.query
40
+ params = CGI::parse(request_uri.query)
41
+ if params['callback'].any? and response[:content].match(/\w+\(/)
42
+ response[:content].sub!(/\w+\(/, params['callback'].first + '(')
43
+ end
44
+ end
45
+ end
46
+
47
+ def cache
48
+ @cache
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ module Billy
2
+ module Handler
3
+ ##
4
+ #
5
+ # Handles an incoming HTTP request and returns a response.
6
+ #
7
+ # This method accepts HTTP request parameters and must return
8
+ # a response hash containing the keys :status, :headers,
9
+ # and :content, or nil if the request cannot be fulfilled.
10
+ #
11
+ # @param [String] http_method The HTTP method used, e.g. 'http' or 'https'
12
+ # @param [String] url The URL requested.
13
+ # @param [String] headers The headers of the HTTP request.
14
+ # @param [String] body The body of the HTTP request.
15
+ # @return [Hash] A hash with the keys [:status, :headers, :content]
16
+ # Returns {:error => "Some error message"} if a failure occurs.
17
+ # Returns nil if the request cannot be fulfilled.
18
+ def handle_request(http_method, url, headers, body)
19
+ { error: 'The handler has not overridden the handle_request method!' }
20
+ end
21
+
22
+ ##
23
+ #
24
+ # Checks if the Handler can respond to the given request.
25
+ #
26
+ # @param [String] http_method The HTTP method used, e.g. 'http' or 'https'
27
+ # @param [String] url The URL requested.
28
+ # @param [String] headers The headers of the HTTP request.
29
+ # @param [String] body The body of the HTTP request.
30
+ # @return [Boolean] True if the Handler can respond to the request, else false.
31
+ #
32
+ def handles_request?(http_method, url, headers, body)
33
+ false
34
+ end
35
+
36
+ ##
37
+ #
38
+ # Resets the Handler to the default/new state
39
+ #
40
+ # This allows the handler to be set back to its default state
41
+ # at the end of tests or whenever else necessary.
42
+ #
43
+ def reset; end
44
+ end
45
+ end
@@ -0,0 +1,112 @@
1
+ require 'billy/handlers/handler'
2
+ require 'eventmachine'
3
+ require 'em-synchrony/em-http'
4
+
5
+ module Billy
6
+ class ProxyHandler
7
+ include Handler
8
+
9
+ def handles_request?(method, url, headers, body)
10
+ !disabled_request?(url)
11
+ end
12
+
13
+ def handle_request(method, url, headers, body)
14
+ if handles_request?(method, url, headers, body)
15
+ req = EventMachine::HttpRequest.new(url, {
16
+ :inactivity_timeout => Billy.config.proxied_request_inactivity_timeout,
17
+ :connect_timeout => Billy.config.proxied_request_connect_timeout})
18
+
19
+ req = req.send(method.downcase, build_request_options(headers, body))
20
+
21
+ if req.error
22
+ return { :error => "Request to #{url} failed with error: #{req.error}" }
23
+ end
24
+
25
+ if req.response
26
+ response = process_response(req)
27
+
28
+ unless allowed_response_code?(response[:status])
29
+ if Billy.config.non_successful_error_level == :error
30
+ return { :error => "Request failed due to response status #{response[:status]} for '#{url}' which was not allowed." }
31
+ else
32
+ Billy.log(:warn, "puffing-billy: Received response status code #{response[:status]} for '#{url}'")
33
+ end
34
+ end
35
+
36
+ if cacheable?(url, response[:headers], response[:status])
37
+ Billy::Cache.instance.store(method.downcase, url, headers, body, response[:headers], response[:status], response[:content])
38
+ end
39
+
40
+ Billy.log(:info, "puffing-billy: PROXY #{method} succeeded for '#{url}'")
41
+ return response
42
+ end
43
+ end
44
+ nil
45
+ end
46
+
47
+ private
48
+
49
+ def build_request_options(headers, body)
50
+ headers = Hash[headers.map { |k,v| [k.downcase, v] }]
51
+ headers.delete('accept-encoding')
52
+
53
+ req_opts = {
54
+ :redirects => 0,
55
+ :keepalive => false,
56
+ :head => headers,
57
+ :ssl => { :verify => false }
58
+ }
59
+ req_opts[:body] = body if body
60
+ req_opts
61
+ end
62
+
63
+ def process_response(req)
64
+ response = {
65
+ :status => req.response_header.status,
66
+ :headers => req.response_header.raw,
67
+ :content => req.response.force_encoding('BINARY') }
68
+ response[:headers].merge!('Connection' => 'close')
69
+ response[:headers].delete('Transfer-Encoding')
70
+ response
71
+ end
72
+
73
+ def disabled_request?(url)
74
+ return false unless Billy.config.non_whitelisted_requests_disabled
75
+
76
+ uri = URI(url)
77
+ # In isolated environments, you may want to stop the request from happening
78
+ # or else you get "getaddrinfo: Name or service not known" errors
79
+ blacklisted_path?(uri.path) || !whitelisted_url?(uri)
80
+ end
81
+
82
+ def allowed_response_code?(status)
83
+ successful_status?(status)
84
+ end
85
+
86
+ def cacheable?(url, headers, status)
87
+ return false unless Billy.config.cache
88
+
89
+ url = URI(url)
90
+ # Cache the responses if they aren't whitelisted host[:port]s but always cache blacklisted paths on any hosts
91
+ cacheable_status?(status) && (!whitelisted_url?(url) || blacklisted_path?(url.path))
92
+ end
93
+
94
+ def whitelisted_url?(url)
95
+ !Billy.config.whitelist.index do |v|
96
+ v =~ /^#{url.host}(?::#{url.port})?$/
97
+ end.nil?
98
+ end
99
+
100
+ def blacklisted_path?(path)
101
+ !Billy.config.path_blacklist.index{|bl| path.include?(bl)}.nil?
102
+ end
103
+
104
+ def successful_status?(status)
105
+ (200..299).include?(status) || status == 304
106
+ end
107
+
108
+ def cacheable_status?(status)
109
+ Billy.config.non_successful_cache_disabled ? successful_status?(status) : true
110
+ end
111
+ end
112
+ end