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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 49c2a689f350d56b8719c2d63a9ee65a8b9e08a6
4
- data.tar.gz: c7f1d50a3a17009c4e394ed87a4c07cae64dc0de
3
+ metadata.gz: 9e495bcc795342695eb34d2fe60ae1d43b44e99e
4
+ data.tar.gz: 1765e2fc100394d5244f369ca130351cabacb37a
5
5
  SHA512:
6
- metadata.gz: 85873ec39d4108e7914ed711f777b9ac96a50250ebff960840132410d9dc221837982d4c90c086b4257681a073850340c96ffb08f1ba3d52d007271ff8b50511
7
- data.tar.gz: ba586d5487c7bef9c682fc834c06c91f2e93364cc450ed4c74ab868a2399ec61a907f5d44d8cf728e7fd66d83c0cf41920df0aae4ad05e9b973421632d25971e
6
+ metadata.gz: 177544dfed171ecbc43f81e8151d800d38c7f199bd627aea62935da083994fce5cafaf5e936afc1596fdc04047d1b475b3cb0b14f4ebc7514ee41f1bbaa399da
7
+ data.tar.gz: 370c320802c05f34e53660f006e5d59b66e5a0ffdc5c880be5d04c803fe653d9d365b8daaed9f16f2454806302cb1cb9d35b204914bbbb8c19843d0fa119048d
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  node_modules/
2
+ /Gemfile.lock
2
3
  log/test.log
3
4
  .idea/
4
5
  .ruby-version
@@ -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
@@ -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)
@@ -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)
@@ -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
@@ -69,7 +69,11 @@ module Billy
69
69
  end
70
70
 
71
71
  def key(method, orig_url, body, log_key = false)
72
- ignore_params = Billy.config.ignore_params.include?(format_url(orig_url, true))
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
@@ -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