puffing-billy 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ module Billy
2
+ class RequestHandler
3
+ extend Forwardable
4
+ include Handler
5
+
6
+ def_delegators :stub_handler, :stub
7
+
8
+ def handlers
9
+ @handlers ||= { :stubs => StubHandler.new,
10
+ :cache => CacheHandler.new,
11
+ :proxy => ProxyHandler.new }
12
+ end
13
+
14
+ def handle_request(method, url, headers, body)
15
+ # Process the handlers by order of importance
16
+ [:stubs, :cache, :proxy].each do |key|
17
+ if (response = handlers[key].handle_request(method, url, headers, body))
18
+ return response
19
+ end
20
+ end
21
+
22
+ body_msg = method == 'post' ? " with body '#{body}'" : ''
23
+ { :error => "Connection to #{url}#{body_msg} not cached and new http connections are disabled" }
24
+ end
25
+
26
+ def handles_request?(method, url, headers, body)
27
+ [:stubs, :cache, :proxy].each do |key|
28
+ return true if handlers[key].handles_request?(method, url, headers, body)
29
+ end
30
+
31
+ false
32
+ end
33
+
34
+ def reset
35
+ handlers.each_value do |handler|
36
+ handler.reset
37
+ end
38
+ end
39
+
40
+ def reset_stubs
41
+ handlers[:stubs].reset
42
+ end
43
+
44
+ def reset_cache
45
+ handlers[:cache].reset
46
+ end
47
+
48
+ def restore_cache
49
+ warn "[DEPRECATION] `restore_cache` is deprecated as cache files are dynamically checked. Use `reset_cache` if you just want to clear the cache."
50
+ reset_cache
51
+ end
52
+
53
+ private
54
+
55
+ def stub_handler
56
+ handlers[:stubs]
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,48 @@
1
+ require 'billy/handlers/handler'
2
+
3
+ module Billy
4
+ class StubHandler
5
+ include Handler
6
+
7
+ def handle_request(method, url, headers, body)
8
+ if handles_request?(method, url, headers, body)
9
+ if (stub = find_stub(method, url))
10
+ query_string = URI.parse(url).query || ""
11
+ params = CGI.parse(query_string)
12
+ stub.call(params, headers, body).tap do |response|
13
+ Billy.log(:info, "puffing-billy: STUB #{method} for '#{url}'")
14
+ return { status: response[0], headers: response[1], content: response[2] }
15
+ end
16
+ end
17
+ end
18
+
19
+ nil
20
+ end
21
+
22
+ def handles_request?(method, url, headers, body)
23
+ !find_stub(method, url).nil?
24
+ end
25
+
26
+ def reset
27
+ self.stubs = []
28
+ end
29
+
30
+ def stub(url, options = {})
31
+ new_stub = ProxyRequestStub.new(url, options)
32
+ stubs.unshift new_stub
33
+ new_stub
34
+ end
35
+
36
+ private
37
+
38
+ attr_writer :stubs
39
+
40
+ def stubs
41
+ @stubs ||= []
42
+ end
43
+
44
+ def find_stub(method, url)
45
+ stubs.find { |stub| stub.matches?(method, url) }
46
+ end
47
+ end
48
+ end
data/lib/billy/proxy.rb CHANGED
@@ -4,11 +4,14 @@ require 'eventmachine'
4
4
 
5
5
  module Billy
6
6
  class Proxy
7
- attr_reader :cache
7
+ extend Forwardable
8
+ attr_reader :request_handler
9
+
10
+ def_delegators :request_handler, :stub, :reset, :reset_cache, :restore_cache, :handle_request
8
11
 
9
12
  def initialize
13
+ @request_handler = Billy::RequestHandler.new
10
14
  reset
11
- @cache = Billy::Cache.new
12
15
  end
13
16
 
14
17
  def start(threaded = true)
@@ -32,49 +35,21 @@ module Billy
32
35
  Socket.unpack_sockaddr_in(EM.get_sockname(@signature)).first
33
36
  end
34
37
 
35
- def call(method, url, headers, body)
36
- stub = find_stub(method, url)
37
- unless stub.nil?
38
- query_string = URI.parse(url).query || ""
39
- params = CGI.parse(query_string)
40
- stub.call(params, headers, body)
41
- end
42
- end
43
-
44
- def stub(url, options = {})
45
- ret = ProxyRequestStub.new(url, options)
46
- @stubs.unshift ret
47
- ret
48
- end
49
-
50
- def reset
51
- @stubs = []
52
- end
53
-
54
- def reset_cache
55
- @cache.reset
56
- end
57
-
58
- def restore_cache
59
- warn "[DEPRECATION] `restore_cache` is deprecated as cache files are dynamincally checked. Use `reset_cache` if you just want to clear the cache."
60
- @cache.reset
38
+ def cache
39
+ Billy::Cache.instance
61
40
  end
62
41
 
63
42
  protected
64
43
 
65
- def find_stub(method, url)
66
- @stubs.find {|stub| stub.matches?(method, url) }
67
- end
68
-
69
44
  def main_loop
70
45
  EM.run do
71
46
  EM.error_handler do |e|
72
- puts e.class.name, e
73
- puts e.backtrace.join("\n")
47
+ Billy.log :error, "#{e.class} (#{e.message}):"
48
+ Billy.log :error, e.backtrace.join("\n")
74
49
  end
75
50
 
76
- @signature = EM.start_server('127.0.0.1', 0, ProxyConnection) do |p|
77
- p.handler = self
51
+ @signature = EM.start_server('127.0.0.1', Billy.config.proxy_port, ProxyConnection) do |p|
52
+ p.handler = request_handler
78
53
  p.cache = @cache
79
54
  end
80
55
 
@@ -3,6 +3,7 @@ require 'eventmachine'
3
3
  require 'http/parser'
4
4
  require 'em-http'
5
5
  require 'evma_httpserver'
6
+ require 'em-synchrony'
6
7
 
7
8
  module Billy
8
9
  class ProxyConnection < EventMachine::Connection
@@ -11,23 +12,10 @@ module Billy
11
12
 
12
13
  def post_init
13
14
  @parser = Http::Parser.new(self)
14
- @header_data = ""
15
15
  end
16
16
 
17
17
  def receive_data(data)
18
- @header_data << data if @headers.nil?
19
- begin
20
- @parser << data
21
- rescue HTTP::Parser::Error
22
- if @parser.http_method == 'CONNECT'
23
- # work-around for CONNECT requests until https://github.com/tmm1/http_parser.rb/pull/15 gets merged
24
- if @header_data.end_with?("\r\n\r\n")
25
- restart_with_ssl(@header_data.split("\r\n").first.split(/\s+/)[1])
26
- end
27
- else
28
- close_connection
29
- end
30
- end
18
+ @parser << data
31
19
  end
32
20
 
33
21
  def on_message_begin
@@ -48,7 +36,8 @@ module Billy
48
36
  restart_with_ssl(@parser.request_url)
49
37
  else
50
38
  if @ssl
51
- @url = "https://#{@ssl}#{@parser.request_url}"
39
+ uri = URI.parse(@parser.request_url)
40
+ @url = "https://#{@ssl}#{[uri.path,uri.query].compact.join('?')}"
52
41
  else
53
42
  @url = @parser.request_url
54
43
  end
@@ -69,137 +58,26 @@ module Billy
69
58
  end
70
59
 
71
60
  def handle_request
72
- if handler && handler.respond_to?(:call)
73
- result = handler.call(@parser.http_method, @url, @headers, @body)
74
- end
75
-
76
- if result
77
- Billy.log(:info, "puffing-billy: STUB #{@parser.http_method} for '#{@url}'")
78
- stub_request(result)
79
- elsif cache.cached?(@parser.http_method.downcase, @url, @body)
80
- Billy.log(:info, "puffing-billy: CACHE #{@parser.http_method} for '#{@url}'")
81
- respond_from_cache
82
- elsif !disabled_request?
83
- Billy.log(:info, "puffing-billy: PROXY #{@parser.http_method} for '#{@url}'")
84
- proxy_request
85
- else
86
- close_connection
87
- body_msg = @parser.http_method == 'post' ? " with body '#{@body}'" : ''
88
- raise "puffing-billy: Connection to #{@url}#{body_msg} not cached and new http connections are disabled"
61
+ EM.synchrony do
62
+ handler.handle_request(@parser.http_method, @url, @headers, @body).tap do |response|
63
+ if response.has_key?(:error)
64
+ close_connection
65
+ raise "puffing-billy: #{response[:error]}"
66
+ else
67
+ send_response(response)
68
+ end
69
+ end
89
70
  end
90
71
  end
91
72
 
92
- def stub_request(result)
93
- response = EM::DelegatedHttpResponse.new(self)
94
- response.status = result[0]
95
- response.headers = result[1].merge('Connection' => 'close')
96
- response.content = result[2]
97
- response.send_response
98
- end
73
+ private
99
74
 
100
- def respond_from_cache
101
- cached_res = cache.fetch(@parser.http_method.downcase, @url, @body)
75
+ def send_response(response)
102
76
  res = EM::DelegatedHttpResponse.new(self)
103
- res.status = cached_res[:status]
104
- res.headers = cached_res[:headers]
105
- res.content = cached_res[:content]
77
+ res.status = response[:status]
78
+ res.headers = response[:headers]
79
+ res.content = response[:content]
106
80
  res.send_response
107
81
  end
108
-
109
- def proxy_request
110
- headers = Hash[@headers.map { |k,v| [k.downcase, v] }]
111
- headers.delete('accept-encoding')
112
-
113
- req = EventMachine::HttpRequest.new(@url)
114
- req_opts = {
115
- :redirects => 0,
116
- :keepalive => false,
117
- :head => headers,
118
- :ssl => { :verify => false }
119
- }
120
- req_opts[:body] = @body if @body
121
-
122
- req = req.send(@parser.http_method.downcase, req_opts)
123
-
124
- req.errback do
125
- Billy.log(:error, "puffing-billy: Request failed: #{@url}")
126
- close_connection
127
- end
128
-
129
- req.callback do
130
- res_status = req.response_header.status
131
- res_headers = req.response_header.raw
132
- res_headers = res_headers.merge('Connection' => 'close')
133
- res_headers.delete('Transfer-Encoding')
134
- res_content = req.response.force_encoding('BINARY')
135
-
136
- handle_response_code(res_status)
137
-
138
- if cacheable?(res_headers, res_status)
139
- cache.store(@parser.http_method.downcase, @url, headers, @body, res_headers, res_status, res_content)
140
- end
141
-
142
- res = EM::DelegatedHttpResponse.new(self)
143
- res.status = res_status
144
- res.headers = res_headers
145
- res.content = res_content
146
- res.send_response
147
- end
148
- end
149
-
150
- def disabled_request?
151
- url = URI(@url)
152
- # In isolated environments, you may want to stop the request from happening
153
- # or else you get "getaddrinfo: Name or service not known" errors
154
- if Billy.config.non_whitelisted_requests_disabled
155
- blacklisted_path?(url.path) || !whitelisted_url?(url)
156
- end
157
- end
158
-
159
- def handle_response_code(status)
160
- log_level = successful_status?(status) ? :info : Billy.config.non_successful_error_level
161
- log_message = "puffing-billy: Received response status code #{status} for #{@url}"
162
- Billy.log(log_level, log_message)
163
- if log_level == :error
164
- close_connection
165
- raise log_message
166
- end
167
- end
168
-
169
- def cacheable?(headers, status)
170
- if Billy.config.cache
171
- url = URI(@url)
172
- # Cache the responses if they aren't whitelisted host[:port]s but always cache blacklisted paths on any hosts
173
- cacheable_headers?(headers) && cacheable_status?(status) && (!whitelisted_url?(url) || blacklisted_path?(url.path))
174
- end
175
- end
176
-
177
- private
178
-
179
- def whitelisted_host?(host)
180
- Billy.config.whitelist.include?(host)
181
- end
182
-
183
- def whitelisted_url?(url)
184
- whitelisted_host?(url.host) || whitelisted_host?("#{url.host}:#{url.port}")
185
- end
186
-
187
- def blacklisted_path?(path)
188
- Billy.config.path_blacklist.index{|bl| path.include?(bl)}
189
- end
190
-
191
- def successful_status?(status)
192
- (200..299).include?(status) || status == 304
193
- end
194
-
195
- def cacheable_headers?(headers)
196
- #TODO: test headers for cacheability (ie. Cache-Control: no-cache)
197
- true
198
- end
199
-
200
- def cacheable_status?(status)
201
- Billy.config.non_successful_cache_disabled ? successful_status?(status) : true
202
- end
203
-
204
82
  end
205
83
  end
data/lib/billy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Billy
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -27,9 +27,10 @@ Gem::Specification.new do |gem|
27
27
  gem.add_development_dependency "pry"
28
28
  gem.add_development_dependency "cucumber"
29
29
  gem.add_runtime_dependency "eventmachine"
30
+ gem.add_runtime_dependency "em-synchrony"
30
31
  gem.add_runtime_dependency "em-http-request"
31
32
  gem.add_runtime_dependency "eventmachine_httpserver"
32
- gem.add_runtime_dependency "http_parser.rb"
33
+ gem.add_runtime_dependency "http_parser.rb", "~> 0.6.0"
33
34
  gem.add_runtime_dependency "multi_json"
34
35
  gem.add_runtime_dependency "capybara"
35
36
  end
@@ -7,15 +7,14 @@ describe 'Facebook API example', :type => :feature, :js => true do
7
7
  # mock a signed request from facebook. the JS api never verifies the
8
8
  # signature, so all it needs is the base64-encoded payload
9
9
  signed_request = "xxxxxxxxxx.#{Base64.encode64('{"user_id":"1234567"}')}"
10
- # redirect to the 'redirect_uri', with some extra crap in the query
11
- # string
10
+ # redirect to the 'redirect_uri', with some extra crap in the query string
12
11
  {:redirect_to => "#{params['redirect_uri'][0]}&access_token=foobar&expires_in=600&base_domain=localhost&https=1&signed_request=#{signed_request}"}
13
12
  })
14
13
 
15
14
  proxy.stub('https://graph.facebook.com:443/me').and_return(:jsonp => {:name => 'Tester 1'})
16
15
  end
17
16
 
18
- it 'should show me as logged-in', :js => true do
17
+ it 'should show me as logged-in' do
19
18
  visit '/facebook_api.html'
20
19
  click_on "Login"
21
20
  expect(page).to have_content "Hi, Tester 1"
@@ -19,7 +19,7 @@ describe 'Tumblr API example', :type => :feature, :js => true do
19
19
  })
20
20
  end
21
21
 
22
- it 'should show news stories', :js => true do
22
+ it 'should show news stories' do
23
23
  visit '/tumblr_api.html'
24
24
  expect(page).to have_link('News Item 1', :href => 'http://example.com/news/1')
25
25
  expect(page).to have_content('News item 1 content here')
@@ -2,12 +2,14 @@ require 'spec_helper'
2
2
 
3
3
  describe Billy::Cache do
4
4
  describe 'format_url' do
5
- let(:cache) { Billy::Cache.new }
5
+ let(:cache) { Billy::Cache.instance }
6
6
  let(:params) { '?foo=bar' }
7
+ let(:callback) { '&callback=quux' }
7
8
  let(:fragment) { '#baz' }
8
9
  let(:base_url) { 'http://example.com' }
9
10
  let(:fragment_url) { "#{base_url}/#{fragment}" }
10
11
  let(:params_url) { "#{base_url}#{params}" }
12
+ let(:params_url_with_callback) { "#{base_url}#{params}#{callback}" }
11
13
  let(:params_fragment_url) { "#{base_url}#{params}#{fragment}" }
12
14
 
13
15
  context 'with ignore_params set to false' do
@@ -20,6 +22,21 @@ describe Billy::Cache do
20
22
  it 'appends params and fragment if both are present' do
21
23
  expect(cache.format_url(params_fragment_url)).to eq params_fragment_url
22
24
  end
25
+ context "when dynamic_jsonp is true" do
26
+ it 'omits the callback param by default' do
27
+ expect(cache.format_url(params_url_with_callback, false, true)).to eq params_url
28
+ end
29
+
30
+ it 'omits the params listed in Billy.config.dynamic_jsonp_keys' do
31
+ allow(Billy.config).to receive(:dynamic_jsonp_keys) { ["foo"] }
32
+
33
+ expect(cache.format_url(params_url_with_callback, false, true)).to eq "#{base_url}?callback=quux"
34
+ end
35
+ end
36
+
37
+ it 'retains the callback param is dynamic_jsonp is false' do
38
+ expect(cache.format_url(params_url_with_callback)).to eq params_url_with_callback
39
+ end
23
40
  end
24
41
 
25
42
  context 'with ignore_params set to true' do