Repository: mcollina/autocannon
Pull request #303 (2020-11-18 21:50:49)

Adding support for passing in custom tlsOptions
**Why**
Currently there is no way to pass custom client certificates through autocannon if the server being benchmarked requires client certificate validation

**How**
- Adding support in the js API to pass in tlsOptions through to the httpClient.
- Updated the test tls server to echo the client certificate email as a way to verify things are working correctly


Patch:
diff --git a/README.md b/README.md
index 557ecc6..bc360c6 100644
--- a/README.md
+++ b/README.md
@@ -265,6 +265,7 @@ Start autocannon against the given target.
     * `servername`: A `String` identifying the server name for the SNI (Server Name Indication) TLS extension. _OPTIONAL_ default: Defaults to the hostname of the URL when it is not an IP address.
     * `excludeErrorStats`: A `Boolean` which allows you to disable tracking non 2xx code responses in latency and bytes per second calculations. _OPTIONAL_ default: `false`.
     * `expectBody`: A `String` representing the expected response body. Each request whose response body is not equal to `expectBody`is counted in `mismatches`. If enabled, mismatches count towards bailout. _OPTIONAL_
+    * `tlsOptions`: An `Object` that is passed into `tls.connect` call ([Full list of options](https://nodejs.org/api/tls.html#tls_tls_connect_port_host_options_callback)). Note: this only applies if your url is secure.
 * `cb`: The callback which is called on completion of a benchmark. Takes the following params. _OPTIONAL_.
     * `err`: If there was an error encountered with the run.
     * `results`: The results of the run.
diff --git a/lib/httpClient.js b/lib/httpClient.js
index 0e7ef80..2005d4c 100644
--- a/lib/httpClient.js
+++ b/lib/httpClient.js
@@ -21,6 +21,7 @@ function Client (opts) {
   const pipelining = this.opts.pipelining
   this.opts.port = this.opts.port || 80
   this.opts.expectBody = this.opts.expectBody || null
+  this.opts.tlsOptions = this.opts.tlsOptions || {}
   this.timeout = (this.opts.timeout || 10) * 1000
   this.ipc = !!this.opts.socketPath
   this.secure = this.opts.protocol === 'https:'
@@ -131,9 +132,9 @@ Client.prototype._connect = function () {
     }
 
     if (this.ipc) {
-      this.conn = tls.connect(this.opts.socketPath, { rejectUnauthorized: false })
+      this.conn = tls.connect(this.opts.socketPath, { ...this.opts.tlsOptions, rejectUnauthorized: false })
     } else {
-      this.conn = tls.connect(this.opts.port, this.opts.hostname, { rejectUnauthorized: false, servername })
+      this.conn = tls.connect(this.opts.port, this.opts.hostname, { ...this.opts.tlsOptions, rejectUnauthorized: false, servername })
     }
   } else {
     if (this.ipc) {
diff --git a/lib/run.js b/lib/run.js
index 2dd90fb..0aa0f8e 100644
--- a/lib/run.js
+++ b/lib/run.js
@@ -284,6 +284,10 @@ function _run (opts, cb, tracker) {
         url.rate = distributeNums(opts.overallRate, i)
       }
 
+      if (opts.tlsOptions) {
+        url.tlsOptions = opts.tlsOptions
+      }
+
       const client = new Client(url)
       client.on('response', onResponse)
       client.on('connError', onError)


Test patch:
diff --git a/test/helper.js b/test/helper.js
index 6a8bbd4..c691350 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -107,7 +107,7 @@ function startHttpsServer (opts = {}) {
   return server
 }
 
-// this server will echo the SNI Server Name in a HTTP header
+// this server will echo the SNI Server Name and emailAddress from the client certificate in a HTTP header
 function startTlsServer () {
   const key = fs.readFileSync(path.join(__dirname, '/key.pem'))
   const cert = fs.readFileSync(path.join(__dirname, '/cert.pem'))
@@ -116,7 +116,9 @@ function startTlsServer () {
   const options = {
     key,
     cert,
-    passphrase
+    passphrase,
+    requestCert: true,
+    rejectUnauthorized: false
   }
 
   const server = tls.createServer(options, handle)
@@ -125,9 +127,16 @@ function startTlsServer () {
 
   function handle (socket) {
     const servername = socket.servername || ''
+    const certificate = socket.getPeerCertificate()
+    const email = (certificate && certificate.subject && certificate.subject.emailAddress) || ''
     socket.on('data', function (data) {
       // Assume this is a http get request and send back the servername in an otherwise empty reponse.
-      socket.write('HTTP/1.1 200 OK\nX-servername: ' + servername + '\nContent-Length: 0\n\n')
+      socket.write('HTTP/1.1 200 OK\n')
+      socket.write('X-servername: ' + servername + '\n')
+      if (email) {
+        socket.write('X-email: ' + email + '\n')
+      }
+      socket.write('Content-Length: 0\n\n')
       socket.setEncoding('utf8')
       socket.pipe(socket)
     })
diff --git a/test/httpClient.test.js b/test/httpClient.test.js
index 60873f6..14e4bc8 100644
--- a/test/httpClient.test.js
+++ b/test/httpClient.test.js
@@ -12,6 +12,7 @@ const httpsServer = helper.startHttpsServer()
 const tlsServer = helper.startTlsServer()
 const trailerServer = helper.startTrailerServer()
 const bl = require('bl')
+const fs = require('fs')
 
 const makeResponseFromBody = ({ server, method, body, headers = {} }) => {
   const sentHeaders = {
@@ -170,6 +171,28 @@ test('client ignores falsy SNI servername', (t) => {
   })
 })
 
+test('client passes through tlsOptions to connect', (t) => {
+  t.plan(4)
+
+  var opts = tlsServer.address()
+  opts.protocol = 'https:'
+  opts.tlsOptions = {
+    key: fs.readFileSync(path.join(__dirname, '/key.pem')),
+    cert: fs.readFileSync(path.join(__dirname, '/cert.pem')),
+    passphrase: 'test'
+  }
+  const client = new Client(opts)
+  let count = 0
+
+  client.on('headers', (response) => {
+    t.equal(response.statusCode, 200, 'status code matches')
+    t.deepEqual(response.headers, ['X-servername', '', 'X-email', 'tes@test.com', 'Content-Length', '0'])
+    if (count++ > 0) {
+      client.destroy()
+    }
+  })
+})
+
 test('http client automatically reconnects', (t) => {
   t.plan(4)