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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +65 -0
- data/Gemfile.lock +6 -3
- data/README.md +43 -3
- data/lib/billy.rb +5 -0
- data/lib/billy/cache.rb +21 -5
- data/lib/billy/config.rb +8 -1
- data/lib/billy/handlers/cache_handler.rb +51 -0
- data/lib/billy/handlers/handler.rb +45 -0
- data/lib/billy/handlers/proxy_handler.rb +112 -0
- data/lib/billy/handlers/request_handler.rb +59 -0
- data/lib/billy/handlers/stub_handler.rb +48 -0
- data/lib/billy/proxy.rb +11 -36
- data/lib/billy/proxy_connection.rb +18 -140
- data/lib/billy/version.rb +1 -1
- data/puffing-billy.gemspec +2 -1
- data/spec/features/examples/facebook_api_spec.rb +2 -3
- data/spec/features/examples/tumblr_api_spec.rb +1 -1
- data/spec/lib/billy/cache_spec.rb +18 -1
- data/spec/lib/billy/handlers/cache_handler_spec.rb +124 -0
- data/spec/lib/billy/handlers/handler_spec.rb +16 -0
- data/spec/lib/billy/handlers/proxy_handler_spec.rb +171 -0
- data/spec/lib/billy/handlers/request_handler_spec.rb +144 -0
- data/spec/lib/billy/handlers/stub_handler_spec.rb +71 -0
- data/spec/lib/billy/resource_utils_spec.rb +3 -3
- data/spec/lib/proxy_spec.rb +15 -8
- data/spec/spec_helper.rb +1 -1
- metadata +37 -6
@@ -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
|
-
|
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
|
36
|
-
|
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
|
-
|
73
|
-
|
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',
|
77
|
-
p.handler =
|
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
|
-
@
|
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
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
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
|
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 =
|
104
|
-
res.headers =
|
105
|
-
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
data/puffing-billy.gemspec
CHANGED
@@ -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'
|
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'
|
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.
|
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
|