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