Repository: mcollina/autocannon (JavaScript)

Issue:
Unable to load PFX certificate with tlsOptions + workers
Example config -

`const instance = autocannon({
url: 'https://localhost:8080',
connection: 10,
workers: 3,
tlsOptions: {
pfx: fs.readFileSync('path/of/p12/keystore')
passphrase: 'test'
}
}`

When I use this config. I get 'Error: Unable to load PFX certificate'

This config works when I don't use 'workers' option.


Discussion:
Good spot! There has to be some issues problem when sending the certificate over to the worker. 

Would you like to send a Pull Request to address this issue? Remember to add unit tests.
@mcollina I took some time to investigate this issue and the problem is not with sending the certificate to the worker, that works fine. Problem is with the certificate being, incorrectly, converted from a typed array `Uint8Array` to an object. This is done by the [clone](https://github.com/pvorb/clone) library used to clone the `opts` in the [httpClient.js](https://github.com/mcollina/autocannon/blob/master/lib/httpClient.js#L19) file.

The **clone** library has an open [issue](https://github.com/pvorb/clone/issues/69) about this, but there has been no activity on it, since it was opened in 2016.

What do you think, should we replace the library with something like [lodash.clonedeep](https://www.npmjs.com/package/lodash.clonedeep)?

I've tested with two approaches:
- using the `lodash.clonedeep` lib
- replacing `clone(opts)` with `Object.assign({}, opts)`

They both work - however, with `Object.assign` we would have to be careful as it's not deep cloning the object (as opposed to the approach to use a library).
Sorry for being late to add this but I found a workaround until the problem is fixed.

`tlsOptions: { cert: fs.readFileSync('path/of/cert/in/pem/format').toString('utf8'), key:  fs.readFileSync('path/of/key/in/pem/format').toString('utf8') } `

so P12 keystore can be converted to PEM format cert and key which can be supplied to tlsOptions option.
I think we can use the v8 serializer/deserializer combo: https://nodejs.org/dist/latest-v16.x/docs/api/v8.html#v8_v8_serialize_value.
@mcollina While the approach, you mentioned, will work in this use case and will solve the problem, unfortunately it will not work in other cases when there might be some options/configs that are functions. 

I've tried your proposed solution:
```js
const cloneDeep = obj => v8.deserialize(v8.serialize(obj))
```
but the tests are failing.

The reason that this approach (called "The structured clone algorithm") will not work (and the reason why tests fail) is that the Function objects can't be duplicated. See the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#things_that_dont_work_with_structured_clone) explaining this.

I think the best option would be to just use `lodash.clonedeep`. What do you think?
go with that!
@mcollina & @gpathak18 I've created a PR, please review when you have time. Thanks.



Patch:
diff --git a/lib/httpClient.js b/lib/httpClient.js
index 027d95fd..95dc77eb 100644
--- a/lib/httpClient.js
+++ b/lib/httpClient.js
@@ -8,7 +8,7 @@ const retimer = require('retimer')
 const HTTPParser = require('http-parser-js').HTTPParser
 const RequestIterator = require('./requestIterator')
 const noop = require('./noop')
-const clone = require('clone')
+const clone = require('lodash.clonedeep')
 const PipelinedRequestsQueue = require('./pipelinedRequestsQueue')
 
 function Client (opts) {
diff --git a/lib/requestIterator.js b/lib/requestIterator.js
index 1b1a5611..a8fa5761 100644
--- a/lib/requestIterator.js
+++ b/lib/requestIterator.js
@@ -3,7 +3,7 @@
 const hyperid = require('hyperid')(true)
 const inherits = require('util').inherits
 const requestBuilder = require('./httpRequestBuilder')
-const clone = require('clone')
+const clone = require('lodash.clonedeep')
 
 const toHeaderKeyValue = (headersArray) => {
   const headersKeyValue = {}
diff --git a/lib/subargAliases.js b/lib/subargAliases.js
index d7f0bf38..a67fd624 100644
--- a/lib/subargAliases.js
+++ b/lib/subargAliases.js
@@ -1,6 +1,6 @@
 'use strict'
 
-const clone = require('clone')
+const clone = require('lodash.clonedeep')
 
 const subArgAlias = {
   warmup: {
diff --git a/package.json b/package.json
index 748a8bf3..17e6eff9 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,6 @@
     "chalk": "^4.1.0",
     "char-spinner": "^1.0.1",
     "cli-table3": "^0.6.0",
-    "clone": "^2.1.2",
     "color-support": "^1.1.1",
     "cross-argv": "^1.0.0",
     "form-data": "^4.0.0",
@@ -60,6 +59,7 @@
     "hdr-histogram-percentiles-obj": "^3.0.0",
     "http-parser-js": "^0.5.2",
     "hyperid": "^2.0.3",
+    "lodash.clonedeep": "^4.5.0",
     "manage-path": "^2.0.0",
     "on-net-listen": "^1.1.1",
     "pretty-bytes": "^5.4.1",


Test patch:
diff --git a/test/keystore.pkcs12 b/test/keystore.pkcs12
new file mode 100644
index 00000000..45b47887
Binary files /dev/null and b/test/keystore.pkcs12 differ
diff --git a/test/workers.test.js b/test/workers.test.js
index a18fd77f..8f0671b5 100644
--- a/test/workers.test.js
+++ b/test/workers.test.js
@@ -1,14 +1,16 @@
 'use strict'
 
 const path = require('path')
+const fs = require('fs')
 const test = require('tap').test
 const http = require('http')
 const initJob = require('../lib/init')
 const { hasWorkerSupport } = require('../lib/util')
 const helper = require('./helper')
-const server = helper.startServer()
+const httpsServer = helper.startHttpsServer()
 
 test('returns error when no worker support was found', (t) => {
+  const server = helper.startServer()
   initJob({
     url: 'http://localhost:' + server.address().port,
     connections: 3,
@@ -27,6 +29,7 @@ test('returns error when no worker support was found', (t) => {
 })
 
 test('init with workers', { skip: !hasWorkerSupport }, (t) => {
+  const server = helper.startServer()
   initJob({
     url: 'http://localhost:' + server.address().port,
     connections: 3,
@@ -159,3 +162,20 @@ test('setupClient works with workers', { skip: !hasWorkerSupport }, (t) => {
     t.end()
   })
 })
+
+test('tlsOptions using pfx work as intended in workers', { skip: !hasWorkerSupport }, (t) => {
+  initJob({
+    url: 'https://localhost:' + httpsServer.address().port,
+    connections: 1,
+    amount: 1,
+    workers: 2,
+    tlsOptions: {
+      pfx: fs.readFileSync(path.join(__dirname, '/keystore.pkcs12')),
+      passphrase: 'test'
+    }
+  }, function (err, result) {
+    t.error(err)
+    t.ok(result, 'requests are ok')
+    t.end()
+  })
+})