Repository: mcollina/autocannon
Pull request #128 (2018-04-18 09:05:48)

Added optional server name for the SNI TLS extension
Server Name Indication (SNI) is an extension to TLS by which a client indicates which hostname it is attempting to connect to at the start of the handshaking process. This allows a server to present multiple certificates on the same IP address.

https://en.wikipedia.org/wiki/Server_Name_Indication


Patch:
diff --git a/README.md b/README.md
index 6662d8f..c9f3a45 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,8 @@ Available options:
         Print the output as newline delimited json. This will cause the progress bar and results not to be rendered. default: false.
   -f/--forever
         Run the benchmark forever. Efficiently restarts the benchmark on completion. default: false.
+  -s/--servername
+        Server name for the SNI (Server Name Indication) TLS extension.
   -v/--version
         Print the version number.
   -h/--help
@@ -139,6 +141,7 @@ Start autocannon against the given target.
     * `requests`: An `Array` of `Object`s which represents the sequence of requests to make while benchmarking. Can be used in conjunction with the `body`, `headers` and `method` params above. The `Object`s in this array can have `body`, `headers`, `method`, or `path` attributes, which overwrite those that are passed in this `opts` object. Therefore, the ones in this (`opts`) object take precedence and should be viewed as defaults. Check the samples folder for an example of how this might be used. _OPTIONAL_.
     * `idReplacement`: A `Boolean` which enables the replacement of `[<id>]` tags within the request body with a randomly generated ID, allowing for unique fields to be sent with requests. Check out [an example of programmatic usage](./samples/using-id-replacement.js) can be found in the samples. _OPTIONAL_ default: `false`
     * `forever`: A `Boolean` which allows you to setup an instance of autocannon that restarts indefinitely after emiting results with the `done` event. Useful for efficiently restarting your instance. To stop running forever, you must cause a `SIGINT` or call the `.stop()` function on your instance. _OPTIONAL_ default: `false`
+    * `servername`: A `String` identifying the server name for the SNI (Server Name Indication) TLS extension. _OPTIONAL_ default: `undefined`.
 * `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/autocannon.js b/autocannon.js
index da5062a..4c7e6d6 100755
--- a/autocannon.js
+++ b/autocannon.js
@@ -29,6 +29,7 @@ function parseArguments (argvs) {
       method: 'm',
       headers: ['H', 'header'],
       body: 'b',
+      servername: 's',
       bailout: 'B',
       input: 'i',
       maxConnectionRequests: 'M',
diff --git a/help.txt b/help.txt
index 7a44874..0797a45 100644
--- a/help.txt
+++ b/help.txt
@@ -50,6 +50,8 @@ Available options:
         Print the output as newline delimited json. This will cause the progress bar and results not to be rendered. default: false.
   -f/--forever
         Run the benchmark forever. Efficiently restarts the benchmark on completion. default: false.
+  -s/--servername
+        Server name for the SNI (Server Name Indication) TLS extension.
   -v/--version
         Print the version number.
   -h/--help
diff --git a/lib/defaultOptions.js b/lib/defaultOptions.js
index 4c1d40e..39ebed8 100644
--- a/lib/defaultOptions.js
+++ b/lib/defaultOptions.js
@@ -14,7 +14,8 @@ const defaultOptions = {
   reconnectRate: 0,
   forever: false,
   idReplacement: false,
-  requests: [{}]
+  requests: [{}],
+  servername: undefined
 }
 
 module.exports = defaultOptions
diff --git a/lib/httpClient.js b/lib/httpClient.js
index c17bc1f..2579375 100644
--- a/lib/httpClient.js
+++ b/lib/httpClient.js
@@ -106,7 +106,7 @@ Client.prototype._connect = function () {
     if (this.ipc) {
       this.conn = tls.connect(this.opts.socketPath, { rejectUnauthorized: false })
     } else {
-      this.conn = tls.connect(this.opts.port, this.opts.hostname, { rejectUnauthorized: false })
+      this.conn = tls.connect(this.opts.port, this.opts.hostname, { rejectUnauthorized: false, servername: this.opts.servername })
     }
   } else {
     if (this.ipc) {
diff --git a/lib/run.js b/lib/run.js
index ea40f05..9a09c11 100644
--- a/lib/run.js
+++ b/lib/run.js
@@ -91,6 +91,7 @@ function run (opts, cb) {
   url.rate = opts.connectionRate || opts.overallRate
   url.idReplacement = opts.idReplacement
   url.socketPath = opts.socketPath
+  url.servername = opts.servername
 
   let clients = []
   initialiseClients(clients)


Test patch:
diff --git a/test/helper.js b/test/helper.js
index c944b84..59a8c80 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -2,6 +2,7 @@
 
 const http = require('http')
 const https = require('https')
+const tls = require('tls')
 const fs = require('fs')
 const path = require('path')
 
@@ -74,7 +75,47 @@ function startHttpsServer () {
   return server
 }
 
+// this server will echo the SNI Server Name in a HTTP header
+function startTlsServer () {
+  const key = fs.readFileSync(path.join(__dirname, '/key.pem'))
+  const cert = fs.readFileSync(path.join(__dirname, '/cert.pem'))
+  const passphrase = 'test'
+  var servername = ''
+
+  const options = {
+    key,
+    cert,
+    passphrase,
+    SNICallback: function (name, cb) {
+      servername = name
+      cb(null, tls.createSecureContext({
+        key,
+        cert,
+        passphrase
+      }))
+    }
+  }
+
+  const server = tls.createServer(options, handle)
+
+  server.listen(0)
+
+  function handle (socket) {
+    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.setEncoding('utf8')
+      socket.pipe(socket)
+    })
+  }
+
+  server.unref()
+
+  return server
+}
+
 module.exports.startServer = startServer
 module.exports.startTimeoutServer = startTimeoutServer
 module.exports.startHttpsServer = startHttpsServer
 module.exports.startTrailerServer = startTrailerServer
+module.exports.startTlsServer = startTlsServer
diff --git a/test/httpClient.test.js b/test/httpClient.test.js
index 3886978..ba9dd71 100644
--- a/test/httpClient.test.js
+++ b/test/httpClient.test.js
@@ -6,6 +6,7 @@ const helper = require('./helper')
 const server = helper.startServer()
 const timeoutServer = helper.startTimeoutServer()
 const httpsServer = helper.startHttpsServer()
+const tlsServer = helper.startTlsServer()
 const trailerServer = helper.startTrailerServer()
 const bl = require('bl')
 
@@ -41,6 +42,41 @@ test('client calls a https server twice', (t) => {
   })
 })
 
+test('client calls a tls server without SNI servername twice', (t) => {
+  t.plan(4)
+
+  var opts = tlsServer.address()
+  opts.protocol = 'https:'
+  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', '', 'Content-Length', '0'])
+    if (count++ > 0) {
+      client.destroy()
+    }
+  })
+})
+
+test('client calls a tls server with SNI servername twice', (t) => {
+  t.plan(4)
+
+  var opts = tlsServer.address()
+  opts.protocol = 'https:'
+  opts.servername = 'example.com'
+  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', opts.servername, 'Content-Length', '0'])
+    if (count++ > 0) {
+      client.destroy()
+    }
+  })
+})
+
 test('http client automatically reconnects', (t) => {
   t.plan(4)