puffing-billy 0.10.1 → 0.11.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 +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
|