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.
@@ -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