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