puffing-billy 0.10.1 → 0.11.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 +12 -0
- data/CHANGELOG.md +6 -0
- data/Dockerfile +14 -0
- data/README.md +64 -1
- data/lib/billy.rb +8 -0
- data/lib/billy/cache.rb +5 -1
- data/lib/billy/config.rb +6 -3
- data/lib/billy/proxy_connection.rb +11 -5
- data/lib/billy/ssl/authority.rb +98 -0
- data/lib/billy/ssl/certificate.rb +101 -0
- data/lib/billy/ssl/certificate_chain.rb +41 -0
- data/lib/billy/ssl/certificate_helpers.rb +36 -0
- data/lib/billy/version.rb +1 -1
- data/puffing-billy.gemspec +4 -4
- data/spec/lib/billy/cache_spec.rb +51 -11
- data/spec/lib/billy/ssl/authority_spec.rb +84 -0
- data/spec/lib/billy/ssl/certificate_chain_spec.rb +39 -0
- data/spec/lib/billy/ssl/certificate_spec.rb +89 -0
- data/spec/lib/proxy_spec.rb +5 -1
- data/spec/spec_helper.rb +6 -0
- data/spec/support/test_server.rb +10 -4
- metadata +35 -13
- data/Gemfile.lock +0 -158
- data/lib/billy/mitm.crt +0 -22
- data/lib/billy/mitm.key +0 -27
- data/spec/fixtures/test-server.crt +0 -15
- data/spec/fixtures/test-server.key +0 -15
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Billy
|
7
|
+
# This class is dedicated to the generation of a certificate chain in the
|
8
|
+
# PEM format. Fortunately we just have to concatenate the given certificates
|
9
|
+
# in the given order and write them to temporary file which will last until
|
10
|
+
# the current process terminates.
|
11
|
+
#
|
12
|
+
# We do not have to generate a certificate chain to make puffing billy work
|
13
|
+
# on modern browser like Chrome 59+ or Firefox 55+, but its good to ship it
|
14
|
+
# anyways. This mimics the behaviour of the mighty mitmproxy.
|
15
|
+
class CertificateChain
|
16
|
+
include Billy::CertificateHelpers
|
17
|
+
|
18
|
+
attr_reader :certificates, :domain
|
19
|
+
|
20
|
+
# Just pass all certificates into the new instance. We use the variadic
|
21
|
+
# argument feature here to ease the usage and improve the readability.
|
22
|
+
#
|
23
|
+
# Example:
|
24
|
+
#
|
25
|
+
# certs_chain_file = Billy::CertificateChain.new('localhost',
|
26
|
+
# cert1,
|
27
|
+
# cert2, ..).file
|
28
|
+
def initialize(domain, *certs)
|
29
|
+
@domain = domain
|
30
|
+
@certificates = [certs].flatten
|
31
|
+
end
|
32
|
+
|
33
|
+
# Write out the certificates chain file and pass the path back. This will
|
34
|
+
# produce a temporary file which will be remove after the current process
|
35
|
+
# terminates.
|
36
|
+
def file
|
37
|
+
contents = certificates.map { |cert| cert.to_pem }.join
|
38
|
+
write_file("chain-#{domain}.pem", contents)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'openssl'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module Billy
|
8
|
+
# A set of common certificate helper methods.
|
9
|
+
module CertificateHelpers
|
10
|
+
|
11
|
+
# Give back the date from now plus given days.
|
12
|
+
def days_from_now(days)
|
13
|
+
Time.now + (days * 24 * 60 * 60)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Give back the date from now minus given days.
|
17
|
+
def days_ago(days)
|
18
|
+
Time.now - (days * 24 * 60 * 60)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Generate a random serial number for a certificate.
|
22
|
+
def serial
|
23
|
+
rand(1_000_000..100_000_000_000)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create/Overwrite a new file with the given name
|
27
|
+
# and ensure the location is safely created. Pass
|
28
|
+
# back the resulting path.
|
29
|
+
def write_file(name, contents)
|
30
|
+
path = File.join(Billy.config.certs_path, name)
|
31
|
+
FileUtils.mkdir_p(File.dirname(path))
|
32
|
+
File.write(path, contents)
|
33
|
+
path
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/billy/version.rb
CHANGED
data/puffing-billy.gemspec
CHANGED
@@ -27,12 +27,12 @@ Gem::Specification.new do |gem|
|
|
27
27
|
gem.add_development_dependency 'rb-inotify'
|
28
28
|
gem.add_development_dependency 'pry'
|
29
29
|
gem.add_development_dependency 'cucumber'
|
30
|
-
gem.add_development_dependency 'watir-webdriver'
|
30
|
+
gem.add_development_dependency 'watir-webdriver', '0.9.1'
|
31
31
|
# addressable 2.5.0 drops support for ruby 1.9.3
|
32
|
-
gem.add_runtime_dependency 'addressable', '~> 2.4.0'
|
33
|
-
gem.add_runtime_dependency 'eventmachine', '~> 1.0.4'
|
32
|
+
gem.add_runtime_dependency 'addressable', '~> 2.4', '>= 2.4.0'
|
33
|
+
gem.add_runtime_dependency 'eventmachine', '~> 1.0', '>= 1.0.4'
|
34
34
|
gem.add_runtime_dependency 'em-synchrony'
|
35
|
-
gem.add_runtime_dependency 'em-http-request', '~> 1.1.0'
|
35
|
+
gem.add_runtime_dependency 'em-http-request', '~> 1.1', '>= 1.1.0'
|
36
36
|
gem.add_runtime_dependency 'eventmachine_httpserver'
|
37
37
|
gem.add_runtime_dependency 'http_parser.rb', '~> 0.6.0'
|
38
38
|
gem.add_runtime_dependency 'multi_json'
|
@@ -1,18 +1,18 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Billy::Cache do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
let(:params_fragment_url) { "#{base_url}#{params}#{fragment}" }
|
4
|
+
let(:cache) { Billy::Cache.instance }
|
5
|
+
let(:params) { '?foo=bar' }
|
6
|
+
let(:callback) { '&callback=quux' }
|
7
|
+
let(:fragment) { '#baz' }
|
8
|
+
let(:base_url) { 'http://example.com' }
|
9
|
+
let(:pipe_url) { 'https://fonts.googleapis.com:443/css?family=Cabin+Sketch:400,700|Love+Ya+Like+A+Sister' }
|
10
|
+
let(:fragment_url) { "#{base_url}/#{fragment}" }
|
11
|
+
let(:params_url) { "#{base_url}#{params}" }
|
12
|
+
let(:params_url_with_callback) { "#{base_url}#{params}#{callback}" }
|
13
|
+
let(:params_fragment_url) { "#{base_url}#{params}#{fragment}" }
|
15
14
|
|
15
|
+
describe 'format_url' do
|
16
16
|
context 'with ignore_params set to false' do
|
17
17
|
it 'is a no-op if there are no params' do
|
18
18
|
expect(cache.format_url(base_url)).to eq base_url
|
@@ -115,4 +115,44 @@ describe Billy::Cache do
|
|
115
115
|
end
|
116
116
|
end
|
117
117
|
end
|
118
|
+
|
119
|
+
describe 'key' do
|
120
|
+
context 'with use_ignore_params set to false' do
|
121
|
+
before do
|
122
|
+
allow(Billy.config).to receive(:use_ignore_params) { false }
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should use the same cache key if the base url IS NOT whitelisted in allow_params" do
|
126
|
+
key1 = cache.key('put', params_url, 'body')
|
127
|
+
key2 = cache.key('put', params_url, 'body')
|
128
|
+
expect(key1).to eq key2
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should have the same cache key if the base IS whitelisted in allow_params" do
|
132
|
+
allow(Billy.config).to receive(:allow_params) { [base_url] }
|
133
|
+
key1 = cache.key('put', params_url, 'body')
|
134
|
+
key2 = cache.key('put', params_url, 'body')
|
135
|
+
expect(key1).to eq key2
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should have different cache keys if the base url is added in between two requests" do
|
139
|
+
key1 = cache.key('put', params_url, 'body')
|
140
|
+
allow(Billy.config).to receive(:allow_params) { [base_url] }
|
141
|
+
key2 = cache.key('put', params_url, 'body')
|
142
|
+
expect(key1).not_to eq key2
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should not use ignore_params when whitelisted" do
|
146
|
+
allow(Billy.config).to receive(:allow_params) { [base_url] }
|
147
|
+
expect(cache).to receive(:format_url).once.with(params_url, true).and_call_original
|
148
|
+
expect(cache).to receive(:format_url).once.with(params_url, false).and_call_original
|
149
|
+
key1 = cache.key('put', params_url, 'body')
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should use ignore_params when not whitelisted" do
|
153
|
+
expect(cache).to receive(:format_url).twice.with(params_url, true).and_call_original
|
154
|
+
cache.key('put', params_url, 'body')
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
118
158
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Billy::Authority do
|
4
|
+
let(:auth1) { Billy::Authority.new }
|
5
|
+
let(:auth2) { Billy::Authority.new }
|
6
|
+
|
7
|
+
context('#key') do
|
8
|
+
it 'generates a new key each time' do
|
9
|
+
expect(auth1.key).not_to be(auth2.key)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'generates 2048 bit keys' do
|
13
|
+
expect(auth1.key.n.num_bytes * 8).to be(2048)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context('#cert') do
|
18
|
+
it 'generates a new certificate each time' do
|
19
|
+
expect(auth1.cert).not_to be(auth2.cert)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'generates unique serials' do
|
23
|
+
expect(auth1.cert.serial).not_to be(auth2.cert.serial)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'configures a start date some days ago' do
|
27
|
+
expect(auth1.cert.not_before).to \
|
28
|
+
be_between((Date.today - 3).to_time, Date.today.to_time)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'configures an end date in some days' do
|
32
|
+
expect(auth1.cert.not_after).to \
|
33
|
+
be_between(Date.today.to_time, (Date.today + 3).to_time)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'configures the subject' do
|
37
|
+
expect(auth1.cert.subject.to_s).to \
|
38
|
+
be_eql('/CN=Puffing Billy/O=Puffing Billy')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'configures the certificate authority constrain' do
|
42
|
+
expect(auth1.cert.extensions.first.to_s).to \
|
43
|
+
be_eql('basicConstraints = critical, CA:TRUE')
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'configures SSLv3' do
|
47
|
+
# Zero-index version numbers. Yay.
|
48
|
+
expect(auth1.cert.version).to be(2)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context('#key_file') do
|
53
|
+
it 'pass back the path' do
|
54
|
+
expect(auth1.key_file).to match(/ca.key$/)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'creates a temporary file' do
|
58
|
+
expect(File.exist?(auth1.key_file)).to be(true)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'creates a PEM formatted certificate' do
|
62
|
+
expect(File.read(auth1.key_file)).to match(/^[A-Za-z0-9\-\+\/\=]+$/)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'writes out a private key' do
|
66
|
+
key = OpenSSL::PKey::RSA.new(File.read(auth1.key_file))
|
67
|
+
expect(key.private?).to be(true)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context('#cert_file') do
|
72
|
+
it 'pass back the path' do
|
73
|
+
expect(auth1.cert_file).to match(/ca.crt$/)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'creates a temporary file' do
|
77
|
+
expect(File.exist?(auth1.cert_file)).to be(true)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'creates a PEM formatted certificate' do
|
81
|
+
expect(File.read(auth1.cert_file)).to match(/^[A-Za-z0-9\-\+\/\=]+$/)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Billy::CertificateChain do
|
4
|
+
let(:cert1) { Billy::Certificate.new('localhost') }
|
5
|
+
let(:cert2) { Billy::Certificate.new('localhost.localdomain') }
|
6
|
+
let(:chain) do
|
7
|
+
Billy::CertificateChain.new('localhost', cert1.cert, cert2.cert)
|
8
|
+
end
|
9
|
+
|
10
|
+
context('#initialize') do
|
11
|
+
it 'holds all certificates in order' do
|
12
|
+
expect(chain.certificates).to be_eql([cert1.cert, cert2.cert])
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'holds the domain' do
|
16
|
+
expect(chain.domain).to be_eql('localhost')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context('#file') do
|
21
|
+
it 'pass back the path' do
|
22
|
+
expect(chain.file).to match(/chain-localhost.pem/)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'writes out all certificates' do
|
26
|
+
chain.certificates.each do |cert|
|
27
|
+
expect(File.read(chain.file)).to include(cert.to_pem)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'creates a temporary file' do
|
32
|
+
expect(File.exist?(chain.file)).to be(true)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'creates a PEM formatted certificate chain' do
|
36
|
+
expect(File.read(chain.file)).to match(/^[A-Za-z0-9\-\+\/\=]+$/)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Billy::Certificate do
|
4
|
+
let(:cert1) { Billy::Certificate.new('localhost') }
|
5
|
+
let(:cert2) { Billy::Certificate.new('localhost.localdomain') }
|
6
|
+
|
7
|
+
context('#domain') do
|
8
|
+
it 'holds the domain' do
|
9
|
+
expect(Billy::Certificate.new('test.tld').domain).to be_eql('test.tld')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context('#key') do
|
14
|
+
it 'generates a new key each time' do
|
15
|
+
expect(cert1.key).not_to be(cert2.key)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'generates 2048 bit keys' do
|
19
|
+
expect(cert1.key.n.num_bytes * 8).to be(2048)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context('#cert') do
|
24
|
+
it 'generates a new certificate each time' do
|
25
|
+
expect(cert1.cert).not_to be(cert2.cert)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'generates unique serials' do
|
29
|
+
expect(cert1.cert.serial).not_to be(cert2.cert.serial)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'configures a start date some days ago' do
|
33
|
+
expect(cert1.cert.not_before).to \
|
34
|
+
be_between((Date.today - 3).to_time, Date.today.to_time)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'configures an end date in some days' do
|
38
|
+
expect(cert1.cert.not_after).to \
|
39
|
+
be_between(Date.today.to_time, (Date.today + 3).to_time)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'configures the correct subject' do
|
43
|
+
expect(cert1.cert.subject.to_s).to be_eql('/CN=localhost')
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'configures the subject alternative names' do
|
47
|
+
expect(cert1.cert.extensions.first.to_s).to \
|
48
|
+
be_eql('subjectAltName = DNS:localhost')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'configures SSLv3' do
|
52
|
+
# Zero-index version numbers. Yay.
|
53
|
+
expect(cert1.cert.version).to be(2)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context('#key_file') do
|
58
|
+
it 'pass back the path' do
|
59
|
+
expect(cert1.key_file).to match(/request-localhost.key$/)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'creates a temporary file' do
|
63
|
+
expect(File.exist?(cert1.key_file)).to be(true)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'creates a PEM formatted certificate' do
|
67
|
+
expect(File.read(cert1.key_file)).to match(/^[A-Za-z0-9\-\+\/\=]+$/)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'writes out a private key' do
|
71
|
+
key = OpenSSL::PKey::RSA.new(File.read(cert1.key_file))
|
72
|
+
expect(key.private?).to be(true)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context('#cert_file') do
|
77
|
+
it 'pass back the path' do
|
78
|
+
expect(cert1.cert_file).to match(/request-localhost.crt$/)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'creates a temporary file' do
|
82
|
+
expect(File.exist?(cert1.cert_file)).to be(true)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'creates a PEM formatted certificate' do
|
86
|
+
expect(File.read(cert1.cert_file)).to match(/^[A-Za-z0-9\-\+\/\=]+$/)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/lib/proxy_spec.rb
CHANGED
@@ -297,9 +297,13 @@ describe Billy::Proxy do
|
|
297
297
|
proxy: { uri: proxy.url },
|
298
298
|
request: { timeout: 1.0 }
|
299
299
|
}
|
300
|
+
faraday_ssl_options = faraday_options.merge(ssl: {
|
301
|
+
verify: true,
|
302
|
+
ca_file: Billy.certificate_authority.cert_file
|
303
|
+
})
|
300
304
|
|
301
305
|
@http = Faraday.new @http_url, faraday_options
|
302
|
-
@https = Faraday.new @https_url,
|
306
|
+
@https = Faraday.new @https_url, faraday_ssl_options
|
303
307
|
@http_error = Faraday.new @error_url, faraday_options
|
304
308
|
end
|
305
309
|
|
data/spec/spec_helper.rb
CHANGED
@@ -5,6 +5,7 @@ require 'billy/capybara/rspec'
|
|
5
5
|
require 'billy/watir/rspec'
|
6
6
|
require 'rack'
|
7
7
|
require 'logger'
|
8
|
+
require 'fileutils'
|
8
9
|
|
9
10
|
browser = Billy::Browsers::Watir.new :phantomjs
|
10
11
|
Capybara.app = Rack::Directory.new(File.expand_path('../../examples', __FILE__))
|
@@ -20,6 +21,11 @@ RSpec.configure do |config|
|
|
20
21
|
config.filter_run :focus
|
21
22
|
config.order = 'random'
|
22
23
|
|
24
|
+
config.before :suite do
|
25
|
+
FileUtils.rm_rf(Billy.config.certs_path)
|
26
|
+
FileUtils.rm_rf(Billy.config.cache_path)
|
27
|
+
end
|
28
|
+
|
23
29
|
config.before :all do
|
24
30
|
start_test_servers
|
25
31
|
@browser = browser
|
data/spec/support/test_server.rb
CHANGED
@@ -57,14 +57,20 @@ module Billy
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
+
def certificate_chain(domain)
|
61
|
+
ca = Billy.certificate_authority.cert
|
62
|
+
cert = Billy::Certificate.new(domain)
|
63
|
+
chain = Billy::CertificateChain.new(domain, cert.cert, ca)
|
64
|
+
{ private_key_file: cert.key_file,
|
65
|
+
cert_chain_file: chain.file }
|
66
|
+
|
67
|
+
end
|
68
|
+
|
60
69
|
def start_server(echo, ssl = false)
|
61
70
|
http_server = Thin::Server.new '127.0.0.1', 0, echo
|
62
71
|
if ssl
|
63
72
|
http_server.ssl = true
|
64
|
-
http_server.ssl_options =
|
65
|
-
private_key_file: File.expand_path('../../fixtures/test-server.key', __FILE__),
|
66
|
-
cert_chain_file: File.expand_path('../../fixtures/test-server.crt', __FILE__)
|
67
|
-
}
|
73
|
+
http_server.ssl_options = certificate_chain('localhost')
|
68
74
|
end
|
69
75
|
http_server.start
|
70
76
|
http_server
|