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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e495bcc795342695eb34d2fe60ae1d43b44e99e
|
4
|
+
data.tar.gz: 1765e2fc100394d5244f369ca130351cabacb37a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 177544dfed171ecbc43f81e8151d800d38c7f199bd627aea62935da083994fce5cafaf5e936afc1596fdc04047d1b475b3cb0b14f4ebc7514ee41f1bbaa399da
|
7
|
+
data.tar.gz: 370c320802c05f34e53660f006e5d59b66e5a0ffdc5c880be5d04c803fe653d9d365b8daaed9f16f2454806302cb1cb9d35b204914bbbb8c19843d0fa119048d
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
language: ruby
|
2
|
+
cache: bundler
|
2
3
|
before_install:
|
3
4
|
- gem install bundler
|
5
|
+
- export PHANTOMJS_VERSION='2.1.1'
|
6
|
+
- export PHANTOMJS_URL='https://github.com/Medium/phantomjs'
|
7
|
+
- export PHANTOMJS_URL+="/releases/download/v${PHANTOMJS_VERSION}"
|
8
|
+
- export PHANTOMJS_URL+="/phantomjs-${PHANTOMJS_VERSION}-linux-x86_64.tar.bz2"
|
9
|
+
- >
|
10
|
+
wget -q ${PHANTOMJS_URL} &&
|
11
|
+
tar xfv phantomjs-${PHANTOMJS_VERSION}-linux-x86_64.tar.bz2 \
|
12
|
+
--wildcards */bin/phantomjs --strip-components=2
|
13
|
+
- export PATH="`pwd`:${PATH}"
|
14
|
+
before_script:
|
4
15
|
- phantomjs --version
|
16
|
+
- bundle --version
|
5
17
|
rvm:
|
6
18
|
- 1.9.3
|
7
19
|
- 2.0.0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
v0.11.0, 2017-11-09
|
2
|
+
-------------------
|
3
|
+
* Improved semantic versioning of dependencies [#197](https://github.com/oesmith/puffing-billy/pull/197)
|
4
|
+
* Implemented a dynamic generation of SSL request certificates [#198](https://github.com/oesmith/puffing-billy/pull/198)
|
5
|
+
* Added Billy.config.allow_params whitelist feature [#200](https://github.com/oesmith/puffing-billy/pull/200)
|
6
|
+
|
1
7
|
v0.10.1, 2017-10-12
|
2
8
|
-------------------
|
3
9
|
* Fix selenium webdriver deprecation warning [#194](https://github.com/oesmith/puffing-billy/pull/194)
|
data/Dockerfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
FROM ruby:1.9.3
|
2
|
+
|
3
|
+
RUN apt-get update -y
|
4
|
+
RUN apt-get install -y qt5-default libqt5webkit5-dev gstreamer1.0-plugins-base gstreamer1.0-tools gstreamer1.0-x
|
5
|
+
RUN gem install bundler
|
6
|
+
RUN \
|
7
|
+
export PHANTOMJS_VERSION='2.1.1' && \
|
8
|
+
export PHANTOMJS_URL='https://github.com/Medium/phantomjs/releases/download/v2.1.1/phantomjs-2.1.1-linux-x86_64.tar.bz2' && \
|
9
|
+
wget -q ${PHANTOMJS_URL} && \
|
10
|
+
tar xfv phantomjs-${PHANTOMJS_VERSION}-linux-x86_64.tar.bz2 \
|
11
|
+
-C /usr/bin --wildcards */bin/phantomjs --strip-components=2
|
12
|
+
RUN mkdir -p /app
|
13
|
+
COPY . /app
|
14
|
+
RUN cd /app && bundle install
|
data/README.md
CHANGED
@@ -253,6 +253,7 @@ Billy.configure do |c|
|
|
253
253
|
c.non_successful_error_level = :warn
|
254
254
|
c.non_whitelisted_requests_disabled = false
|
255
255
|
c.cache_path = 'spec/req_cache/'
|
256
|
+
c.certs_path = 'spec/req_certs/'
|
256
257
|
c.proxy_host = 'example.com' # defaults to localhost
|
257
258
|
c.proxy_port = 12345 # defaults to random
|
258
259
|
c.proxied_request_host = nil
|
@@ -273,6 +274,10 @@ caching. You should mostly use this for analytics and various social buttons as
|
|
273
274
|
they use cache avoidance techniques, but return practically the same response
|
274
275
|
that most often does not affect your test results.
|
275
276
|
|
277
|
+
`c.allow_params` is used to allow parameters of certain requests when caching. This is best used when a site
|
278
|
+
has a large number of analytics and social buttons. `c.allow_params` is the opposite of `c.ignore_params`,
|
279
|
+
a whitelist to a blacklist. In order to toggle between using one or the other, use `c.use_ignore_params`.
|
280
|
+
|
276
281
|
`c.strip_query_params` is used to strip query parameters when you stub some requests
|
277
282
|
with query parameters. Default value is true. For example, `proxy.stub('http://myapi.com/user/?country=FOO')`
|
278
283
|
is considered the same as: `proxy.stub('http://myapi.com/user/?anything=FOO')` and
|
@@ -291,7 +296,7 @@ using `c.dynamic_jsonp`. This is helpful when JSONP APIs use cache-busting
|
|
291
296
|
parameters. For example, if you want `http://example.com/foo?callback=bar&id=1&cache_bust=12345` and `http://example.com/foo?callback=baz&id=1&cache_bust=98765` to be cache hits for each other, you would set `c.dynamic_jsonp_keys = ['callback', 'cache_bust']` to ignore both params. Note
|
292
297
|
that in this example the `id` param would still be considered important.
|
293
298
|
|
294
|
-
`c.dynamic_jsonp_callback_name` is used to configure the name of the JSONP callback
|
299
|
+
`c.dynamic_jsonp_callback_name` is used to configure the name of the JSONP callback
|
295
300
|
parameter. The default is `callback`.
|
296
301
|
|
297
302
|
`c.path_blacklist = []` is used to always cache specific paths on any hostnames,
|
@@ -323,6 +328,13 @@ allowed, all others will throw an error with the URL attempted to be accessed.
|
|
323
328
|
This is useful for debugging issues in isolated environments (ie.
|
324
329
|
continuous integration).
|
325
330
|
|
331
|
+
`c.cache_path` can be used to locate the cache directory to a different place
|
332
|
+
other than `system temp directory/puffing-billy`.
|
333
|
+
|
334
|
+
`c.certs_path` can be used to locate the directory for dynamically generated
|
335
|
+
SSL certificates to a different place other than `system temp
|
336
|
+
directory/puffing-billy/certs`.
|
337
|
+
|
326
338
|
`c.proxy_host` and `c.proxy_port` are used for the Billy proxy itself which runs locally.
|
327
339
|
|
328
340
|
`c.proxied_request_host` and `c.proxied_request_port` are used if an internal proxy
|
@@ -332,6 +344,9 @@ server is required to access the internet. Most common in larger companies.
|
|
332
344
|
|
333
345
|
`c.after_cache_handles_request` is used to configure a callback that can operate on the response after it has been retrieved from the cache but before it is returned. The callback receives the request and response as arguments, with a request object like: `{ method: method, url: url, headers: headers, body: body }`. An example usage would be manipulating the Access-Control-Allow-Origin header so that your test server doesn't always have to run on the same port in order to accept cached responses to CORS requests:
|
334
346
|
|
347
|
+
`c.use_ignore_params` is used to choose whether to use the ignore_params blacklist or the allow_params whitelist. Set to `true` to use `c.ignore_params`,
|
348
|
+
`false` to use `c.allow_params`
|
349
|
+
|
335
350
|
```
|
336
351
|
Billy.configure do |c|
|
337
352
|
...
|
@@ -500,6 +515,54 @@ end
|
|
500
515
|
|
501
516
|
Note that this approach may cause unexpected behavior if your backend sends the Referer HTTP header (which is unlikely).
|
502
517
|
|
518
|
+
## SSL usage
|
519
|
+
|
520
|
+
Unfortunately we cannot setup the runtime certificate authority on your browser
|
521
|
+
at time of configuring the Capybara driver. So you need to take care of this
|
522
|
+
step yourself as a prepartion. A good point would be directly after configuring
|
523
|
+
this gem.
|
524
|
+
|
525
|
+
### Google Chrome Headless example
|
526
|
+
|
527
|
+
Google Chrome/Chromium is capable to run as a test browser with the new
|
528
|
+
headless mode which is not able to handle the deprecated
|
529
|
+
`--ignore-certificate-errors` flag. But the headless mode is capable of
|
530
|
+
handling the user PKI certificate store. So you just need to import the
|
531
|
+
runtime Puffing Billy certificate authority on your system store, or generate a
|
532
|
+
new store for your current session. The following examples demonstrates the
|
533
|
+
former variant:
|
534
|
+
|
535
|
+
```ruby
|
536
|
+
# Overwrite the local home directory for chrome. We use this
|
537
|
+
# to setup a custom SSL certificate store.
|
538
|
+
ENV['HOME'] = "#{Dir.tmpdir}/chrome-home-#{Time.now.to_i}"
|
539
|
+
|
540
|
+
# Clear and recreate the Chrome home directory.
|
541
|
+
FileUtils.rm_rf(ENV['HOME'])
|
542
|
+
FileUtils.mkdir_p(ENV['HOME'])
|
543
|
+
|
544
|
+
# Setup a new pki certificate database for Chrome
|
545
|
+
system <<~SCRIPT
|
546
|
+
cd "#{ENV['HOME']}"
|
547
|
+
curl -s -k -o "cacert-root.crt" "http://www.cacert.org/certs/root.crt"
|
548
|
+
curl -s -k -o "cacert-class3.crt" "http://www.cacert.org/certs/class3.crt"
|
549
|
+
echo > .password
|
550
|
+
mkdir -p .pki/nssdb
|
551
|
+
CERT_DIR=sql:$HOME/.pki/nssdb
|
552
|
+
certutil -N -d .pki/nssdb -f .password
|
553
|
+
certutil -d ${CERT_DIR} -A -t TC \
|
554
|
+
-n "CAcert.org" -i cacert-root.crt
|
555
|
+
certutil -d ${CERT_DIR} -A -t TC \
|
556
|
+
-n "CAcert.org Class 3" -i cacert-class3.crt
|
557
|
+
certutil -d sql:$HOME/.pki/nssdb -A \
|
558
|
+
-n puffing-billy -t "CT,C,C" -i #{Billy.certificate_authority.cert_file}
|
559
|
+
SCRIPT
|
560
|
+
```
|
561
|
+
|
562
|
+
Mind the reset of the `HOME` environment variable. Fortunately Chrome takes
|
563
|
+
care of the users home, so we can setup a new temporary directory for the test
|
564
|
+
run, without messing with potential user configurations.
|
565
|
+
|
503
566
|
## Resources
|
504
567
|
|
505
568
|
* [Bring Ruby VCR to Javascript testing with Capybara and puffing-billy](http://architects.dzone.com/articles/bring-ruby-vcr-javascript)
|
data/lib/billy.rb
CHANGED
@@ -7,6 +7,10 @@ require 'billy/handlers/proxy_handler'
|
|
7
7
|
require 'billy/handlers/cache_handler'
|
8
8
|
require 'billy/proxy_request_stub'
|
9
9
|
require 'billy/cache'
|
10
|
+
require 'billy/ssl/certificate_helpers'
|
11
|
+
require 'billy/ssl/authority'
|
12
|
+
require 'billy/ssl/certificate'
|
13
|
+
require 'billy/ssl/certificate_chain'
|
10
14
|
require 'billy/proxy'
|
11
15
|
require 'billy/proxy_connection'
|
12
16
|
require 'billy/railtie' if defined?(Rails)
|
@@ -19,4 +23,8 @@ module Billy
|
|
19
23
|
proxy
|
20
24
|
)
|
21
25
|
end
|
26
|
+
|
27
|
+
def self.certificate_authority
|
28
|
+
@certificate_authority ||= Billy::Authority.new
|
29
|
+
end
|
22
30
|
end
|
data/lib/billy/cache.rb
CHANGED
@@ -69,7 +69,11 @@ module Billy
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def key(method, orig_url, body, log_key = false)
|
72
|
-
|
72
|
+
if Billy.config.use_ignore_params
|
73
|
+
ignore_params = Billy.config.ignore_params.include?(format_url(orig_url, true))
|
74
|
+
else
|
75
|
+
ignore_params = !Billy.config.allow_params.include?(format_url(orig_url, true))
|
76
|
+
end
|
73
77
|
merge_cached_response_key = _merge_cached_response_key(orig_url)
|
74
78
|
url = Addressable::URI.parse(format_url(orig_url, ignore_params))
|
75
79
|
key = if merge_cached_response_key
|
data/lib/billy/config.rb
CHANGED
@@ -6,12 +6,12 @@ module Billy
|
|
6
6
|
DEFAULT_WHITELIST = ['127.0.0.1', 'localhost']
|
7
7
|
RANDOM_AVAILABLE_PORT = 0 # https://github.com/eventmachine/eventmachine/wiki/FAQ#wiki-can-i-start-a-server-on-a-random-available-port
|
8
8
|
|
9
|
-
attr_accessor :logger, :cache, :cache_request_headers, :whitelist, :path_blacklist, :ignore_params,
|
9
|
+
attr_accessor :logger, :cache, :cache_request_headers, :whitelist, :path_blacklist, :ignore_params, :allow_params,
|
10
10
|
:persist_cache, :ignore_cache_port, :non_successful_cache_disabled, :non_successful_error_level,
|
11
|
-
:non_whitelisted_requests_disabled, :cache_path, :proxy_host, :proxy_port, :proxied_request_inactivity_timeout,
|
11
|
+
:non_whitelisted_requests_disabled, :cache_path, :certs_path, :proxy_host, :proxy_port, :proxied_request_inactivity_timeout,
|
12
12
|
:proxied_request_connect_timeout, :dynamic_jsonp, :dynamic_jsonp_keys, :dynamic_jsonp_callback_name, :merge_cached_responses_whitelist,
|
13
13
|
:strip_query_params, :proxied_request_host, :proxied_request_port, :cache_request_body_methods, :after_cache_handles_request,
|
14
|
-
:cache_simulates_network_delays, :cache_simulates_network_delay_time, :record_stub_requests
|
14
|
+
:cache_simulates_network_delays, :cache_simulates_network_delay_time, :record_stub_requests, :use_ignore_params
|
15
15
|
|
16
16
|
def initialize
|
17
17
|
@logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
@@ -25,6 +25,7 @@ module Billy
|
|
25
25
|
@path_blacklist = []
|
26
26
|
@merge_cached_responses_whitelist = []
|
27
27
|
@ignore_params = []
|
28
|
+
@allow_params = []
|
28
29
|
@persist_cache = false
|
29
30
|
@dynamic_jsonp = false
|
30
31
|
@dynamic_jsonp_keys = ['callback']
|
@@ -34,6 +35,7 @@ module Billy
|
|
34
35
|
@non_successful_error_level = :warn
|
35
36
|
@non_whitelisted_requests_disabled = false
|
36
37
|
@cache_path = File.join(Dir.tmpdir, 'puffing-billy')
|
38
|
+
@certs_path = File.join(Dir.tmpdir, 'puffing-billy', 'certs')
|
37
39
|
@proxy_host = 'localhost'
|
38
40
|
@proxy_port = RANDOM_AVAILABLE_PORT
|
39
41
|
@proxied_request_inactivity_timeout = 10 # defaults from https://github.com/igrigorik/em-http-request/wiki/Redirects-and-Timeouts
|
@@ -46,6 +48,7 @@ module Billy
|
|
46
48
|
@cache_simulates_network_delays = false
|
47
49
|
@cache_simulates_network_delay_time = 0.1
|
48
50
|
@record_stub_requests = false
|
51
|
+
@use_ignore_params = true
|
49
52
|
end
|
50
53
|
end
|
51
54
|
|
@@ -53,10 +53,7 @@ module Billy
|
|
53
53
|
@ssl = url
|
54
54
|
@parser = Http::Parser.new(self)
|
55
55
|
send_data("HTTP/1.0 200 Connection established\r\nProxy-agent: Puffing-Billy/0.0.0\r\n\r\n")
|
56
|
-
start_tls(
|
57
|
-
private_key_file: File.expand_path('../mitm.key', __FILE__),
|
58
|
-
cert_chain_file: File.expand_path('../mitm.crt', __FILE__)
|
59
|
-
)
|
56
|
+
start_tls(certificate_chain(url))
|
60
57
|
end
|
61
58
|
|
62
59
|
def handle_request
|
@@ -93,6 +90,15 @@ module Billy
|
|
93
90
|
res.content = response[:content]
|
94
91
|
res.send_response
|
95
92
|
end
|
96
|
-
|
93
|
+
|
94
|
+
def certificate_chain(url)
|
95
|
+
domain = url.split(':').first
|
96
|
+
ca = Billy.certificate_authority.cert
|
97
|
+
cert = Billy::Certificate.new(domain)
|
98
|
+
chain = Billy::CertificateChain.new(domain, cert.cert, ca)
|
99
|
+
|
100
|
+
{ private_key_file: cert.key_file,
|
101
|
+
cert_chain_file: chain.file }
|
102
|
+
end
|
97
103
|
end
|
98
104
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'openssl'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module Billy
|
8
|
+
# This class is dedicated to the generation of a brand new certificate
|
9
|
+
# authority which can be picked up by a browser to verify and secure any
|
10
|
+
# communication with puffing billy. This authority certificate will be
|
11
|
+
# generated once on runtime and will sign each request certificate. So
|
12
|
+
# we do not have to deal with outdated certificates or stuff like that.
|
13
|
+
#
|
14
|
+
# The resulting certificate authority is at its bare minimum to keep
|
15
|
+
# things simple and snappy. We do not handle a certificate revoke list
|
16
|
+
# (CRL) nor any other special key handling, even if we enable these
|
17
|
+
# extensions. It's just a mimic of the mighty mitmproxy certificate
|
18
|
+
# authority file.
|
19
|
+
class Authority
|
20
|
+
include Billy::CertificateHelpers
|
21
|
+
|
22
|
+
attr_reader :key, :cert
|
23
|
+
|
24
|
+
# The authority generation does not require any arguments from outside
|
25
|
+
# of this class definition. We just generate the certificate and thats
|
26
|
+
# it.
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
#
|
30
|
+
# ca = Billy::Authority.new
|
31
|
+
# [ca.cert_file, ca.key_file]
|
32
|
+
def initialize
|
33
|
+
@key = OpenSSL::PKey::RSA.new(2048)
|
34
|
+
@cert = generate
|
35
|
+
end
|
36
|
+
|
37
|
+
# Write out the private key to file (PEM format) and give back the
|
38
|
+
# file path.
|
39
|
+
def key_file
|
40
|
+
write_file('ca.key', key.to_pem)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Write out the certifcate to file (PEM format) and give back the
|
44
|
+
# file path.
|
45
|
+
def cert_file
|
46
|
+
write_file('ca.crt', cert.to_pem)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Defines a static list of available extensions on the certificate.
|
52
|
+
def extensions
|
53
|
+
[
|
54
|
+
# ln_sn, value, critical
|
55
|
+
['basicConstraints', 'CA:TRUE', true],
|
56
|
+
['keyUsage', 'keyCertSign, cRLSign', true],
|
57
|
+
['subjectKeyIdentifier', 'hash', false],
|
58
|
+
['authorityKeyIdentifier', 'keyid:always', false]
|
59
|
+
]
|
60
|
+
end
|
61
|
+
|
62
|
+
# Give back the static subject name of the certificate.
|
63
|
+
def name
|
64
|
+
'/CN=Puffing Billy/O=Puffing Billy/'
|
65
|
+
end
|
66
|
+
|
67
|
+
# Generate a fresh new certificate for the configured domain.
|
68
|
+
def generate
|
69
|
+
cert = OpenSSL::X509::Certificate.new
|
70
|
+
configure(cert)
|
71
|
+
add_extensions(cert)
|
72
|
+
cert.sign(key, OpenSSL::Digest::SHA256.new)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Setup all relevant properties of the given certificate to produce
|
76
|
+
# a valid and useable certificate.
|
77
|
+
def configure(cert)
|
78
|
+
cert.version = 2
|
79
|
+
cert.serial = serial
|
80
|
+
cert.subject = OpenSSL::X509::Name.parse(name)
|
81
|
+
cert.issuer = cert.subject
|
82
|
+
cert.public_key = key.public_key
|
83
|
+
cert.not_before = days_ago(2)
|
84
|
+
cert.not_after = days_from_now(2)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Add all extensions (defined by the +extensions+ method) to the given
|
88
|
+
# certificate.
|
89
|
+
def add_extensions(cert)
|
90
|
+
factory = OpenSSL::X509::ExtensionFactory.new
|
91
|
+
factory.subject_certificate = cert
|
92
|
+
factory.issuer_certificate = cert
|
93
|
+
extensions.each do |ln_sn, value, critical|
|
94
|
+
cert.add_extension(factory.create_extension(ln_sn, value, critical))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'openssl'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module Billy
|
8
|
+
# This class is dedicated to the generation of a request certifcate for a
|
9
|
+
# given domain name. We have to generate for each handled connection a new
|
10
|
+
# request certifcate, due to the fact that each request has probably a
|
11
|
+
# different domain name which will be proxied. So we can't know of future
|
12
|
+
# domain name we could include in the list of subject alternative names
|
13
|
+
# which is required by modern browsers. (Chrome 58+)
|
14
|
+
#
|
15
|
+
# We use our generated certifcate authority to sign any request certifcate,
|
16
|
+
# so a client can be prepared to trust us before a possible test scenario
|
17
|
+
# starts.
|
18
|
+
#
|
19
|
+
# This behaviour and functionality mimics the mighty mitmproxy and it will
|
20
|
+
# enable the usage of Chrome Headless at a time where no ssl issue ignoring
|
21
|
+
# works. And its even secure at testing level.
|
22
|
+
class Certificate
|
23
|
+
include Billy::CertificateHelpers
|
24
|
+
|
25
|
+
attr_reader :key, :cert, :domain
|
26
|
+
|
27
|
+
# To generate a new request certifcate just pass the domain in and you
|
28
|
+
# are ready to go.
|
29
|
+
#
|
30
|
+
# Example:
|
31
|
+
#
|
32
|
+
# cert = Billy::Certificate.new('localhost')
|
33
|
+
# [cert.cert_file, cert.key_file]
|
34
|
+
def initialize(domain)
|
35
|
+
@domain = domain
|
36
|
+
@key = OpenSSL::PKey::RSA.new(2048)
|
37
|
+
@cert = generate
|
38
|
+
end
|
39
|
+
|
40
|
+
# Write out the private key to file (PEM format) and give back the
|
41
|
+
# file path.
|
42
|
+
def key_file
|
43
|
+
write_file("request-#{domain}.key", key.to_pem)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Write out the certifcate to file (PEM format) and give back the
|
47
|
+
# file path.
|
48
|
+
def cert_file
|
49
|
+
write_file("request-#{domain}.crt", cert.to_pem)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Defines a static list of available extensions on the certificate.
|
55
|
+
def extensions
|
56
|
+
# ln_sn, value, critical
|
57
|
+
[['subjectAltName', "DNS:#{domain}", false]]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate a fresh new certificate for the configured domain.
|
61
|
+
def generate
|
62
|
+
cert = OpenSSL::X509::Certificate.new
|
63
|
+
configure(cert)
|
64
|
+
add_extensions(cert)
|
65
|
+
cert.sign(Billy.certificate_authority.key, OpenSSL::Digest::SHA256.new)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Generate a new certificate signing request (CSR) which will be picked
|
69
|
+
# up by the certificate subject and public key.
|
70
|
+
def signing_request
|
71
|
+
req = OpenSSL::X509::Request.new
|
72
|
+
req.public_key = key.public_key
|
73
|
+
req.subject = OpenSSL::X509::Name.new([['CN', domain]])
|
74
|
+
req.sign(key, OpenSSL::Digest::SHA256.new)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Setup all relevant properties of the given certificate to produce
|
78
|
+
# a valid and useable certificate.
|
79
|
+
def configure(cert)
|
80
|
+
req = signing_request
|
81
|
+
cert.issuer = Billy.certificate_authority.cert.subject
|
82
|
+
cert.not_before = days_ago(2)
|
83
|
+
cert.not_after = days_from_now(2)
|
84
|
+
cert.public_key = req.public_key
|
85
|
+
cert.serial = serial
|
86
|
+
cert.subject = req.subject
|
87
|
+
cert.version = 2
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add all extensions (defined by the +extensions+ method) to the given
|
91
|
+
# certificate.
|
92
|
+
def add_extensions(cert)
|
93
|
+
factory = OpenSSL::X509::ExtensionFactory.new
|
94
|
+
factory.issuer_certificate = Billy.certificate_authority.cert
|
95
|
+
factory.subject_certificate = cert
|
96
|
+
extensions.each do |ln_sn, value, critical|
|
97
|
+
cert.add_extension(factory.create_extension(ln_sn, value, critical))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|