From bf6ef13038cda57a9da02019ad08be254b7ab6cf Mon Sep 17 00:00:00 2001 From: James Sumners Date: Tue, 22 Oct 2024 15:13:46 -0400 Subject: [PATCH] chore: Replaced static openssl cert usage with in-process cert --- THIRD_PARTY_NOTICES.md | 44 +------ lib/collector/http-agents.js | 5 +- package.json | 3 +- test/integration/grpc/reconnect.tap.js | 17 ++- .../infinite-tracing-connection.tap.js | 124 +++++++++--------- test/integration/keep-alive.tap.js | 118 ----------------- test/integration/keep-alive.test.js | 82 ++++++++++++ test/lib/agent_helper.js | 10 -- test/lib/fake-cert.js | 51 +++++-- test/lib/proxy-server.js | 90 +++++++++++++ test/lib/test-collector.js | 8 +- test/smoke/proxy-api-connection-port.tap.js | 73 ----------- test/smoke/proxy-api-connection-port.test.js | 67 ++++++++++ test/smoke/proxy-api-connection-ssl.tap.js | 68 ---------- test/smoke/proxy-api-connection-ssl.test.js | 64 +++++++++ test/versioned/restify/restify.tap.js | 57 ++++---- test/versioned/undici/requests.tap.js | 19 +-- third_party_manifest.json | 74 +++++------ 18 files changed, 501 insertions(+), 473 deletions(-) delete mode 100644 test/integration/keep-alive.tap.js create mode 100644 test/integration/keep-alive.test.js create mode 100644 test/lib/proxy-server.js delete mode 100644 test/smoke/proxy-api-connection-port.tap.js create mode 100644 test/smoke/proxy-api-connection-port.test.js delete mode 100644 test/smoke/proxy-api-connection-ssl.tap.js create mode 100644 test/smoke/proxy-api-connection-ssl.test.js diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 47b7c5bf65..e5a749f804 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -68,7 +68,6 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic * [lint-staged](#lint-staged) * [lockfile-lint](#lockfile-lint) * [nock](#nock) -* [proxy](#proxy) * [proxyquire](#proxyquire) * [rimraf](#rimraf) * [self-cert](#self-cert) @@ -93,7 +92,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic ### @grpc/grpc-js -This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.11.3](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.11.3)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.11.3/LICENSE): +This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.12.2](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.12.2)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.12.2/LICENSE): ``` Apache License @@ -1043,7 +1042,7 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ### winston-transport -This product includes source derived from [winston-transport](https://github.com/winstonjs/winston-transport) ([v4.7.1](https://github.com/winstonjs/winston-transport/tree/v4.7.1)), distributed under the [MIT License](https://github.com/winstonjs/winston-transport/blob/v4.7.1/LICENSE): +This product includes source derived from [winston-transport](https://github.com/winstonjs/winston-transport) ([v4.8.0](https://github.com/winstonjs/winston-transport/tree/v4.8.0)), distributed under the [MIT License](https://github.com/winstonjs/winston-transport/blob/v4.8.0/LICENSE): ``` The MIT License (MIT) @@ -1076,7 +1075,7 @@ SOFTWARE. ### @aws-sdk/client-s3 -This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.658.1](https://github.com/aws/aws-sdk-js-v3/tree/v3.658.1)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.658.1/LICENSE): +This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.676.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.676.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.676.0/LICENSE): ``` Apache License @@ -1285,7 +1284,7 @@ This product includes source derived from [@aws-sdk/client-s3](https://github.co ### @aws-sdk/s3-request-presigner -This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.658.1](https://github.com/aws/aws-sdk-js-v3/tree/v3.658.1)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.658.1/LICENSE): +This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.676.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.676.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.676.0/LICENSE): ``` Apache License @@ -2208,7 +2207,7 @@ THE SOFTWARE. ### @slack/bolt -This product includes source derived from [@slack/bolt](https://github.com/slackapi/bolt) ([v3.21.4](https://github.com/slackapi/bolt/tree/v3.21.4)), distributed under the [MIT License](https://github.com/slackapi/bolt/blob/v3.21.4/LICENSE): +This product includes source derived from [@slack/bolt](https://github.com/slackapi/bolt) ([v3.22.0](https://github.com/slackapi/bolt/tree/v3.22.0)), distributed under the [MIT License](https://github.com/slackapi/bolt/blob/v3.22.0/LICENSE): ``` The MIT License (MIT) @@ -3372,7 +3371,7 @@ THE SOFTWARE. ### express -This product includes source derived from [express](https://github.com/expressjs/express) ([v4.21.0](https://github.com/expressjs/express/tree/v4.21.0)), distributed under the [MIT License](https://github.com/expressjs/express/blob/v4.21.0/LICENSE): +This product includes source derived from [express](https://github.com/expressjs/express) ([v4.21.1](https://github.com/expressjs/express/tree/v4.21.1)), distributed under the [MIT License](https://github.com/expressjs/express/blob/v4.21.1/LICENSE): ``` (The MIT License) @@ -3508,7 +3507,7 @@ SOFTWARE. ### jsdoc -This product includes source derived from [jsdoc](https://github.com/jsdoc/jsdoc) ([v4.0.3](https://github.com/jsdoc/jsdoc/tree/v4.0.3)), distributed under the [Apache-2.0 License](https://github.com/jsdoc/jsdoc/blob/v4.0.3/LICENSE.md): +This product includes source derived from [jsdoc](https://github.com/jsdoc/jsdoc) ([v4.0.4](https://github.com/jsdoc/jsdoc/tree/v4.0.4)), distributed under the [Apache-2.0 License](https://github.com/jsdoc/jsdoc/blob/v4.0.4/LICENSE.md): ``` # License @@ -3947,35 +3946,6 @@ SOFTWARE. ``` -### proxy - -This product includes source derived from [proxy](https://github.com/TooTallNate/proxy-agents) ([v2.2.0](https://github.com/TooTallNate/proxy-agents/tree/v2.2.0)), distributed under the [MIT License](https://github.com/TooTallNate/proxy-agents/blob/v2.2.0/LICENSE): - -``` -(The MIT License) - -Copyright (c) 2013 Nathan Rajlich - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` - ### proxyquire This product includes source derived from [proxyquire](https://github.com/thlorenz/proxyquire) ([v1.8.0](https://github.com/thlorenz/proxyquire/tree/v1.8.0)), distributed under the [MIT License](https://github.com/thlorenz/proxyquire/blob/v1.8.0/LICENSE): diff --git a/lib/collector/http-agents.js b/lib/collector/http-agents.js index 42e31c3fe1..7d45f7ccad 100644 --- a/lib/collector/http-agents.js +++ b/lib/collector/http-agents.js @@ -53,11 +53,14 @@ exports.proxyAgent = function proxyAgent(config) { } const proxyUrl = proxyOptions(config) + // Tests may supply 127.0.0.1 as the host, but SNI requires a hostname. + const servername = config.host const proxyOpts = { secureEndpoint: config.ssl, auth: proxyUrl.auth, ca: config?.certificates?.length ? config.certificates : [], - keepAlive: true + keepAlive: true, + servername } logger.info(`using proxy: ${proxyUrl}`) diff --git a/package.json b/package.json index 4d09f3067b..9d00aa8752 100644 --- a/package.json +++ b/package.json @@ -195,7 +195,7 @@ "newrelic-naming-rules": "./bin/test-naming-rules.js" }, "dependencies": { - "@grpc/grpc-js": "^1.9.4", + "@grpc/grpc-js": "^1.12.2", "@grpc/proto-loader": "^0.7.5", "@newrelic/security-agent": "^2.0.0", "@tyriar/fibonacci-heap": "^2.0.7", @@ -253,7 +253,6 @@ "lint-staged": "^11.0.0", "lockfile-lint": "^4.9.6", "nock": "11.8.0", - "proxy": "^2.1.1", "proxyquire": "^1.8.0", "rimraf": "^2.6.3", "self-cert": "^2.0.0", diff --git a/test/integration/grpc/reconnect.tap.js b/test/integration/grpc/reconnect.tap.js index 6388750058..ddf7a2b56b 100644 --- a/test/integration/grpc/reconnect.tap.js +++ b/test/integration/grpc/reconnect.tap.js @@ -16,8 +16,14 @@ const MetricMapper = require('../../../lib/metrics/mapper') const MetricNormalizer = require('../../../lib/metrics/normalizer') const StreamingSpanEvent = require('../../../lib/spans/streaming-span-event') +const fakeCert = require('../../lib/fake-cert') const helper = require('../../lib/agent_helper') +// We generate the certificate once for the whole suite because it is a CPU +// intensive operation and would slow down tests if each test created its +// own certificate. +const cert = fakeCert({ commonName: 'localhost' }) + tap.test('test that connection class reconnects', async (t) => { // one assert for the initial connection // a second assert for the disconnect @@ -50,7 +56,7 @@ tap.test('test that connection class reconnects', async (t) => { // Currently test-only configuration const origEnv = process.env.NEWRELIC_GRPCCONNECTION_CA - process.env.NEWRELIC_GRPCCONNECTION_CA = sslOpts.ca + process.env.NEWRELIC_GRPCCONNECTION_CA = cert.certificate t.teardown(() => { process.env.NEWRELIC_GRPCCONNECTION_CA = origEnv }) @@ -133,7 +139,7 @@ tap.test('Should reconnect even when data sent back', async (t) => { // Currently test-only configuration const origEnv = process.env.NEWRELIC_GRPCCONNECTION_CA - process.env.NEWRELIC_GRPCCONNECTION_CA = sslOpts.ca + process.env.NEWRELIC_GRPCCONNECTION_CA = cert.certificate t.teardown(() => { process.env.NEWRELIC_GRPCCONNECTION_CA = origEnv }) @@ -186,13 +192,12 @@ tap.test('Should reconnect even when data sent back', async (t) => { }) async function setupSsl() { - const [key, certificate, ca] = await helper.withSSL() return { - ca, + ca: null, authPairs: [ { - private_key: key, - cert_chain: certificate + private_key: cert.privateKeyBuffer, + cert_chain: cert.certificateBuffer } ] } diff --git a/test/integration/infinite-tracing-connection.tap.js b/test/integration/infinite-tracing-connection.tap.js index 77eb348de9..8aedfa7d98 100644 --- a/test/integration/infinite-tracing-connection.tap.js +++ b/test/integration/infinite-tracing-connection.tap.js @@ -11,8 +11,14 @@ const path = require('path') const grpc = require('@grpc/grpc-js') const protoLoader = require('@grpc/proto-loader') +const fakeCert = require('../lib/fake-cert') const helper = require('../lib/agent_helper') +// We generate the certificate once for the whole suite because it is a CPU +// intensive operation and would slow down tests if each test created its +// own certificate. +const cert = fakeCert({ commonName: 'localhost' }) + const PROTO_PATH = path.join(__dirname, '../..', '/lib/grpc/endpoints/infinite-tracing/v1.proto') const TEST_DOMAIN = 'test-collector.newrelic.com' @@ -258,70 +264,63 @@ const infiniteTracingService = grpc.loadPackageDefinition(packageDefinition).com nock.disableNetConnect() startingEndpoints = setupConnectionEndpoints(INITIAL_RUN_ID, INITIAL_SESSION_ID) - helper - .withSSL() - .then(([key, certificate, ca]) => { - const sslOpts = { - ca, - authPairs: [{ private_key: key, cert_chain: certificate }] - } + const sslOpts = { + ca: cert.certificateBuffer, + authPairs: [{ private_key: cert.privateKeyBuffer, cert_chain: cert.certificateBuffer }] + } - const services = [ - { - serviceDefinition: infiniteTracingService.IngestService.service, - implementation: { recordSpan, recordSpanBatch } + const services = [ + { + serviceDefinition: infiniteTracingService.IngestService.service, + implementation: { recordSpan, recordSpanBatch } + } + ] + + server = createGrpcServer(sslOpts, services, (err, port) => { + t.error(err) + + agent = helper.loadMockedAgent({ + license_key: EXPECTED_LICENSE_KEY, + apdex_t: Number.MIN_VALUE, // force transaction traces + host: TEST_DOMAIN, + plugins: { + // turn off native metrics to avoid unwanted gc metrics + native_metrics: { enabled: false } + }, + distributed_tracing: { enabled: true }, + slow_sql: { enabled: true }, + transaction_tracer: { + record_sql: 'obfuscated', + explain_threshold: Number.MIN_VALUE // force SQL traces + }, + utilization: { + detect_aws: false + }, + infinite_tracing: { + ...config, + span_events: { + queue_size: 2 + }, + trace_observer: { + host: helper.SSL_HOST, + port } - ] - - server = createGrpcServer(sslOpts, services, (err, port) => { - t.error(err) - - agent = helper.loadMockedAgent({ - license_key: EXPECTED_LICENSE_KEY, - apdex_t: Number.MIN_VALUE, // force transaction traces - host: TEST_DOMAIN, - plugins: { - // turn off native metrics to avoid unwanted gc metrics - native_metrics: { enabled: false } - }, - distributed_tracing: { enabled: true }, - slow_sql: { enabled: true }, - transaction_tracer: { - record_sql: 'obfuscated', - explain_threshold: Number.MIN_VALUE // force SQL traces - }, - utilization: { - detect_aws: false - }, - infinite_tracing: { - ...config, - span_events: { - queue_size: 2 - }, - trace_observer: { - host: helper.SSL_HOST, - port - } - } - }) + } + }) - agent.config.no_immediate_harvest = true + agent.config.no_immediate_harvest = true - // Currently test-only configuration - const origEnv = process.env.NEWRELIC_GRPCCONNECTION_CA - process.env.NEWRELIC_GRPCCONNECTION_CA = ca - t.teardown(() => { - process.env.NEWRELIC_GRPCCONNECTION_CA = origEnv - }) - - if (callback) { - callback() - } - }) - }) - .catch((err) => { - t.error(err) + // Currently test-only configuration + const origEnv = process.env.NEWRELIC_GRPCCONNECTION_CA + process.env.NEWRELIC_GRPCCONNECTION_CA = cert.certificate + t.teardown(() => { + process.env.NEWRELIC_GRPCCONNECTION_CA = origEnv }) + + if (callback) { + callback() + } + }) } }) }) @@ -387,11 +386,10 @@ function createGrpcServer(sslOptions, services, callback) { server.addService(service.serviceDefinition, service.implementation) } - const { ca, authPairs } = sslOptions - const credentials = grpc.ServerCredentials.createSsl(ca, authPairs, false) + const { authPairs } = sslOptions + const credentials = grpc.ServerCredentials.createSsl(null, authPairs, false) - // Select a random port - server.bindAsync('localhost:0', credentials, (err, port) => { + server.bindAsync('127.0.0.1:0', credentials, (err, port) => { if (err) { callback(err) } diff --git a/test/integration/keep-alive.tap.js b/test/integration/keep-alive.tap.js deleted file mode 100644 index fe6342e785..0000000000 --- a/test/integration/keep-alive.tap.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const read = require('fs').readFileSync -const join = require('path').join -const https = require('https') -const RemoteMethod = require('../../lib/collector/remote-method') -const { SSL_HOST } = require('../lib/agent_helper') - -const MAX_PORT_ATTEMPTS = 5 - -tap.test('RemoteMethod makes two requests with one connection', (t) => { - t.ok(true, 'Setup Test') - - // create a basic https server using our standard test certs - const opts = { - key: read(join(__dirname, '../lib/test-key.key')), - cert: read(join(__dirname, '../lib/self-signed-test-certificate.crt')) - } - const server = https.createServer(opts, function (req, res) { - res.write('hello ssl') - res.end() - }) - server.keepAliveTimeout = 2000 - - // set a reasonable server timeout for cleanup - // of the server's keep-alive connections - server.setTimeout(5000, (socket) => { - socket.end() - server.close() - }) - - // close server when test ends - t.teardown(() => { - server.close() - }) - - let attempts = 0 - server.on('error', (e) => { - // server port not guranteed to be not in use - if (e.code === 'EADDRINUSE') { - if (attempts >= MAX_PORT_ATTEMPTS) { - // eslint-disable-next-line no-console - console.log('Exceeded max attempts (%s), bailing out.', MAX_PORT_ATTEMPTS) - throw new Error('Unable to get unused port') - } - - attempts++ - - // eslint-disable-next-line no-console - console.log('Address in use, retrying...') - setTimeout(() => { - server.close() - - // start the server using a random port - server.listen() - }, 1000) - } - }) - - // start the server using a random port - server.listen() - - // make requests once successfully running - server.on('listening', () => { - const port = server.address().port - - // once we start a server, use a RemoteMethod - // object to make a request - const method = createRemoteMethod(port) - method.invoke({}, [], function (err, res) { - t.ok(200 === res.status, 'First request success') - - // once first request is done, create a second request - const method2 = createRemoteMethod(port) - method2.invoke({}, [], function (err2, res2) { - t.ok(200 === res2.status, 'Second request success') - // end the test - t.end() - }) - }) - }) - - let connections = 0 - - // setup a connection listener for the server - // if we see more than one, keep alive isn't - // working. - server.on('connection', function () { - connections++ - if (2 === connections) { - t.fail('RemoteMethod made second connection despite keep-alive.') - } - }) -}) - -function createRemoteMethod(port) { - const config = { - ssl: true, - max_payload_size_in_bytes: 1000000, - feature_flag: {} - } - - const endpoint = { - host: SSL_HOST, - port: port - } - - config.certificates = [read(join(__dirname, '../lib/ca-certificate.crt'), 'utf8')] - - const agent = { config, metrics: { measureBytes() {} } } - return new RemoteMethod('fake', agent, endpoint) -} diff --git a/test/integration/keep-alive.test.js b/test/integration/keep-alive.test.js new file mode 100644 index 0000000000..d4bc1f6f86 --- /dev/null +++ b/test/integration/keep-alive.test.js @@ -0,0 +1,82 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const https = require('node:https') + +const tspl = require('@matteo.collina/tspl') +const fakeCert = require('../lib/fake-cert') +const promiseResolvers = require('../lib/promise-resolvers') +const RemoteMethod = require('../../lib/collector/remote-method') + +test('RemoteMethod makes two requests with one connection', async (t) => { + const plan = tspl(t, { plan: 3 }) + const { promise, resolve, reject } = promiseResolvers() + const cert = fakeCert() + const serverOpts = { key: cert.privateKey, cert: cert.certificate } + const server = https.createServer(serverOpts, (req, res) => { + res.write('hello ssl') + res.end() + }) + server.keepAliveTimeout = 2_000 + + // Set a reasonable server timeout for cleanup of the server's + // keep-alive connections. + server.setTimeout(5_000, (socket) => { + socket.end() + server.close() + }) + + t.after(() => server.close()) + + let connections = 0 + server.on('connection', () => { + // Track the connections made to the server. We expect only one to be + // made due to HTTP keep-alive being used. + connections += 1 + if (connections === 2) { + reject(Error('RemoteMethod made second connection despite keep-alive.')) + } + }) + + await new Promise((done) => { + server.listen(0, '127.0.0.1', done) + }) + + const port = server.address().port + const method = createRemoteMethod(port, cert) + method.invoke({}, [], (error, res) => { + plan.equal(res.status, 200, 'first request success') + + const method2 = createRemoteMethod(port, cert) + method2.invoke({}, [], (error2, res2) => { + plan.equal(res2.status, 200, 'second request success') + resolve() + }) + }) + + await promise + plan.equal(connections, 1, 'should not have established more than 1 connection') +}) + +function createRemoteMethod(port, cert) { + const config = { + ssl: true, + max_payload_size_in_bytes: 1_000_000, + feature_flag: {} + } + + const endpoint = { + host: '127.0.0.1', + port: port + } + + config.certificates = [cert.certificate] + + const agent = { config, metrics: { measureBytes() {} } } + return new RemoteMethod('fake', agent, endpoint) +} diff --git a/test/lib/agent_helper.js b/test/lib/agent_helper.js index 3d31f25d85..ca2b38c0fa 100644 --- a/test/lib/agent_helper.js +++ b/test/lib/agent_helper.js @@ -5,8 +5,6 @@ 'use strict' -const path = require('path') -const fs = require('fs').promises const Agent = require('../../lib/agent') const API = require('../../api') const zlib = require('zlib') @@ -24,10 +22,6 @@ const crypto = require('crypto') const util = require('util') const cp = require('child_process') -const KEYPATH = path.join(__dirname, 'test-key.key') -const CERTPATH = path.join(__dirname, 'self-signed-test-certificate.crt') -const CAPATH = path.join(__dirname, 'ca-certificate.crt') - let _agent = null let _agentApi = null const tasks = [] @@ -346,10 +340,6 @@ helper.flushRedisDb = (client, dbIndex) => { }) } -helper.withSSL = () => { - return Promise.all([fs.readFile(KEYPATH), fs.readFile(CERTPATH), fs.readFile(CAPATH)]) -} - helper.randomPort = (callback) => { const net = require('net') // Min port: 1024 (without root) diff --git a/test/lib/fake-cert.js b/test/lib/fake-cert.js index fc9fd8a48c..aa387d87f9 100644 --- a/test/lib/fake-cert.js +++ b/test/lib/fake-cert.js @@ -6,12 +6,45 @@ 'use strict' const selfCert = require('self-cert') -module.exports = selfCert({ - attrs: { - stateName: 'Georgia', - locality: 'Atlanta', - orgName: 'New Relic', - shortName: 'new_relic' - }, - expires: new Date('2099-12-31') -}) + +/** + * @typedef {object} FakeCert + * @property {string} privateKey PEM formatted private key. + * @property {string} publicKey PEM formatted public key. + * @property {string} certificate PEM formatted TLS certificate. + * @property {Buffer} privateKeyBuffer Same as privateKey. + * @property {Buffer} publicKeyBuffer Same as publicKey. + * @property {Buffer} certificateBuffer Same as certificate. + */ + +/** + * Generate a self-signed certificate. When `commonName` is not provided, the + * certificate will target the local system; it will use `os.hostname()` as the + * common name. It always adds all local interfaces's IP addresses as + * subject alternate names. + * + * @param {object} params + * @param {string|null} [params.commonName=null] The subject name for the + * certificate. This is useful when generating a certificate for remote hosts, + * e.g. when generating a proxy certificate for staging-collector.newrelic.com. + * + * @returns {FakeCert} + */ +module.exports = function fakeCert({ commonName = null } = {}) { + const cert = selfCert({ + attrs: { + commonName: commonName, + stateName: 'Georgia', + locality: 'Atlanta', + orgName: 'New Relic', + shortName: 'new_relic' + }, + expires: new Date('2099-12-31') + }) + + cert.privateKeyBuffer = Buffer.from(cert.privateKey, 'utf8') + cert.publicKeyBuffer = Buffer.from(cert.publicKey, 'utf8') + cert.certificateBuffer = Buffer.from(cert.certificate, 'utf8') + + return cert +} diff --git a/test/lib/proxy-server.js b/test/lib/proxy-server.js new file mode 100644 index 0000000000..8c34216307 --- /dev/null +++ b/test/lib/proxy-server.js @@ -0,0 +1,90 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = createProxyServer + +const https = require('node:https') +const net = require('node:net') +const assert = require('node:assert') + +const connectResponse = [ + 'HTTP/1.1 200 Connection Established', + 'Proxy-agent: Node.js-Proxy', + '\r\n' +].join('\r\n') + +/** + * An extension of core's `https.Server` with utilities specific to the proxy. + * + * @extends http.Server + * @typedef {object} ProxyServer + */ + +/** + * Creates an HTTPS proxying server that listens on a random port on 127.0.0.1. + * This is useful when testing agent connections to the collector via the proxy + * configuration. The passed in certificate details should have a common name + * that matches the upstream proxied host. + * + * @param {object} params + * @param {string} params.privateKey A PEM formatted TLS certificate private key. + * @param {string} params.certificate A PEM formatted TLS public certificate. + * + * @returns {Promise} + */ +async function createProxyServer({ privateKey, certificate } = {}) { + assert.equal(typeof privateKey === 'string', true) + assert.equal(typeof certificate === 'string', true) + + // This proxy server is pretty much a straight copy from the docs: + // https://nodejs.org/api/http.html#event-connect. + const server = https.createServer({ key: privateKey, cert: certificate }) + + /** + * Indicates is the proxy has serviced any connections. + * + * @type {boolean} + * @memberof ProxyServer + */ + server.proxyUsed = false + + await new Promise((done) => { + server.listen(0, '127.0.0.1', done) + }) + + const connections = [] + server.on('connect', (req, clientSocket, head) => { + const { port, hostname } = new URL(`http://${req.url}`) + const serverSocket = net.connect(port || 443, hostname, () => { + connections.push({ clientSocket, serverSocket }) + clientSocket.write(connectResponse) + serverSocket.write(head) + serverSocket.pipe(clientSocket) + clientSocket.pipe(serverSocket) + }) + + serverSocket.on('data', () => { + server.proxyUsed = true + }) + }) + + /** + * Terminates all connections to the proxy and stops the server. + * + * @memberof ProxyServer + */ + server.shutdown = () => { + for (const conn of connections) { + conn.clientSocket.destroy() + conn.serverSocket.destroy() + } + server.close() + server.closeAllConnections() + } + + return server +} diff --git a/test/lib/test-collector.js b/test/lib/test-collector.js index 20e385581f..89819267cf 100644 --- a/test/lib/test-collector.js +++ b/test/lib/test-collector.js @@ -16,15 +16,17 @@ const fakeCert = require('./fake-cert') class Collector { #handlers = new Map() + #cert #server #address #runId constructor({ runId = 42 } = {}) { + this.#cert = fakeCert() this.#runId = runId this.#server = https.createServer({ - key: fakeCert.privateKey, - cert: fakeCert.certificate + key: this.#cert.privateKey, + cert: this.#cert.certificate }) this.#server.on('request', (req, res) => { const qs = querystring.decode(req.url.slice(req.url.indexOf('?') + 1)) @@ -112,7 +114,7 @@ class Collector { * @returns {string} */ get cert() { - return fakeCert.certificate + return this.#cert.certificate } /** diff --git a/test/smoke/proxy-api-connection-port.tap.js b/test/smoke/proxy-api-connection-port.tap.js deleted file mode 100644 index 6a28fb7b21..0000000000 --- a/test/smoke/proxy-api-connection-port.tap.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const join = require('path').join -const https = require('https') -const { createProxy: proxySetup } = require('proxy') -const read = require('fs').readFileSync -const configurator = require('../../lib/config') -const Agent = require('../../lib/agent') -const CollectorAPI = require('../../lib/collector/api') -const { getTestSecret } = require('../helpers/secrets') -const { SSL_HOST, destroyProxyAgent } = require('../lib/agent_helper') - -let port = 0 -const SSL_CONFIG = { - key: read(join(__dirname, '../lib/test-key.key')), - cert: read(join(__dirname, '../lib/self-signed-test-certificate.crt')) -} -const license = getTestSecret('TEST_LICENSE') - -tap.test('setting proxy_port should use the proxy agent', (t) => { - const server = proxySetup(https.createServer(SSL_CONFIG)) - t.teardown(() => { - destroyProxyAgent() - server.close() - }) - - server.listen(0, () => { - port = server.address().port - const config = configurator.initialize({ - app_name: 'node.js Tests', - license_key: license, - host: 'staging-collector.newrelic.com', - port: 443, - proxy_host: SSL_HOST, - proxy_port: port, - ssl: true, - utilization: { - detect_aws: false, - detect_pcf: false, - detect_azure: false, - detect_gcp: false, - detect_docker: false - }, - logging: { - level: 'trace' - }, - certificates: [read(join(__dirname, '..', 'lib', 'ca-certificate.crt'), 'utf8')] - }) - const agent = new Agent(config) - const api = new CollectorAPI(agent) - - api.connect((error, response) => { - t.notOk(error, 'connected without error') - - const returned = response && response.payload - t.ok(returned, 'got boot configuration') - t.ok(returned.agent_run_id, 'got run ID') - t.ok(agent.config.run_id, 'run ID set in configuration') - - api.shutdown((error) => { - t.notOk(error, 'should have shut down without issue') - t.notOk(agent.config.run_id, 'run ID should have been cleared by shutdown') - t.end() - }) - }) - }) -}) diff --git a/test/smoke/proxy-api-connection-port.test.js b/test/smoke/proxy-api-connection-port.test.js new file mode 100644 index 0000000000..c5e5c7e1e4 --- /dev/null +++ b/test/smoke/proxy-api-connection-port.test.js @@ -0,0 +1,67 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const tspl = require('@matteo.collina/tspl') + +const { getTestSecret } = require('../helpers/secrets') +const fakeCert = require('../lib/fake-cert') +const createServer = require('../lib/proxy-server') +const configurator = require('../../lib/config') +const Agent = require('../../lib/agent') +const CollectorAPI = require('../../lib/collector/api') + +const license = getTestSecret('TEST_LICENSE') + +test('setting proxy_port should use the proxy agent', async (t) => { + const plan = tspl(t, { plan: 8 }) + + const cert = fakeCert({ commonName: 'staging-collector.newrelic.com' }) + const proxy = await createServer(cert) + const config = configurator.initialize({ + app_name: 'node.js Tests', + license_key: license, + host: 'staging-collector.newrelic.com', + port: 443, + proxy_host: '127.0.0.1', + proxy_port: proxy.address().port, + ssl: true, + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + logging: { level: 'trace' }, + certificates: [cert.certificate] + }) + const agent = new Agent(config) + const api = new CollectorAPI(agent) + + t.after(() => { + proxy.shutdown() + }) + + api.connect((error, response) => { + plan.ifError(error, 'error during connection') + + const returned = response?.payload + plan.ok(returned, 'got boot configuration') + plan.ok(returned.agent_run_id, 'got run ID') + plan.ok(agent.config.run_id, 'run ID set in configuration') + plan.equal(returned.agent_run_id, agent.config.run_id) + + api.shutdown((error) => { + plan.ifError(error, 'should have shutdown without issue') + plan.equal(agent.config.run_id, undefined, 'run ID should have been cleared by shutdown') + plan.equal(proxy.proxyUsed, true, 'proxy must be used') + }) + }) + + await plan.completed +}) diff --git a/test/smoke/proxy-api-connection-ssl.tap.js b/test/smoke/proxy-api-connection-ssl.tap.js deleted file mode 100644 index 5561c63666..0000000000 --- a/test/smoke/proxy-api-connection-ssl.tap.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const join = require('path').join -const https = require('https') -const { createProxy: proxySetup } = require('proxy') -const read = require('fs').readFileSync -const configurator = require('../../lib/config') -const Agent = require('../../lib/agent') -const CollectorAPI = require('../../lib/collector/api') -const { getTestSecret } = require('../helpers/secrets') -const { SSL_HOST, destroyProxyAgent } = require('../lib/agent_helper') - -let port = 0 -const SSL_CONFIG = { - key: read(join(__dirname, '../lib/test-key.key')), - cert: read(join(__dirname, '../lib/self-signed-test-certificate.crt')) -} -const license = getTestSecret('TEST_LICENSE') - -tap.test('support ssl to the proxy', (t) => { - const server = proxySetup(https.createServer(SSL_CONFIG)) - t.teardown(() => { - destroyProxyAgent() - server.close() - }) - - server.listen(0, () => { - port = server.address().port - const config = configurator.initialize({ - app_name: 'node.js Tests', - license_key: license, - host: 'staging-collector.newrelic.com', - proxy: `https://${SSL_HOST}:${port}`, - ssl: true, - utilization: { - detect_aws: false, - detect_pcf: false, - detect_azure: false, - detect_gcp: false, - detect_docker: false - }, - certificates: [read(join(__dirname, '..', 'lib', 'ca-certificate.crt'), 'utf8')] - }) - const agent = new Agent(config) - const api = new CollectorAPI(agent) - - api.connect((error, response) => { - t.notOk(error, 'connected without error') - - const returned = response && response.payload - t.ok(returned, 'got boot configuration') - t.ok(returned.agent_run_id, 'got run ID') - t.ok(agent.config.run_id, 'run ID set in configuration') - - api.shutdown((error) => { - t.notOk(error, 'should have shut down without issue') - t.notOk(agent.config.run_id, 'run ID should have been cleared by shutdown') - t.end() - }) - }) - }) -}) diff --git a/test/smoke/proxy-api-connection-ssl.test.js b/test/smoke/proxy-api-connection-ssl.test.js new file mode 100644 index 0000000000..0c023f2abc --- /dev/null +++ b/test/smoke/proxy-api-connection-ssl.test.js @@ -0,0 +1,64 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const tspl = require('@matteo.collina/tspl') + +const { getTestSecret } = require('../helpers/secrets') +const fakeCert = require('../lib/fake-cert') +const createServer = require('../lib/proxy-server') +const configurator = require('../../lib/config') +const Agent = require('../../lib/agent') +const CollectorAPI = require('../../lib/collector/api') + +const license = getTestSecret('TEST_LICENSE') + +test('the `proxy` config param enables proxying', async (t) => { + const plan = tspl(t, { plan: 8 }) + + const cert = fakeCert({ commonName: 'staging-collector.newrelic.com' }) + const proxy = await createServer(cert) + const config = configurator.initialize({ + app_name: 'node.js Tests', + license_key: license, + host: 'staging-collector.newrelic.com', + proxy: `https://${proxy.address().address}:${proxy.address().port}`, + ssl: true, + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + certificates: [cert.certificate] + }) + const agent = new Agent(config) + const api = new CollectorAPI(agent) + + t.after(() => { + proxy.shutdown() + }) + + api.connect((error, response) => { + plan.ifError(error, 'error during connection') + + const returned = response?.payload + plan.ok(returned, 'got boot configuration') + plan.ok(returned.agent_run_id, 'got run ID') + plan.ok(agent.config.run_id, 'run ID set in configuration') + plan.equal(returned.agent_run_id, agent.config.run_id) + + api.shutdown((error) => { + plan.ifError(error, 'should have shutdown without issue') + plan.equal(agent.config.run_id, undefined, 'run ID should have been cleared by shutdown') + plan.equal(proxy.proxyUsed, true, 'proxy must be used') + }) + }) + + await plan.completed +}) diff --git a/test/versioned/restify/restify.tap.js b/test/versioned/restify/restify.tap.js index 8088d0eb4d..0cddace9ff 100644 --- a/test/versioned/restify/restify.tap.js +++ b/test/versioned/restify/restify.tap.js @@ -7,6 +7,7 @@ const tap = require('tap') +const fakeCert = require('../../lib/fake-cert') const helper = require('../../lib/agent_helper') require('../../lib/metrics_helper') @@ -75,38 +76,32 @@ tap.test('Restify', (t) => { t.ok(isFramework, 'should indicate that restify is a framework') }) - helper - .withSSL() - .then(([key, certificate, ca]) => { - const server = restify.createServer({ key: key, certificate: certificate }) - t.teardown(() => server.close()) - - server.get('/hello/:name', function sayHello(req, res, next) { - t.ok(agent.getTransaction(), 'transaction should be available in handler') - res.send('hello ' + req.params.name) - next() - }) - - server.listen(0, function () { - const port = server.address().port - t.notOk(agent.getTransaction(), 'transaction should not leak into server') - - const url = `https://${helper.SSL_HOST}:${port}/hello/friend` - helper.makeGetRequest(url, { ca }, function (error, response, body) { - if (error) { - t.fail(error) - return t.end() - } - - t.notOk(agent.getTransaction(), 'transaction should not leak into external request') - t.equal(body, 'hello friend', 'should return expected data') - }) - }) - }) - .catch((error) => { - t.fail('unable to set up SSL: ' + error) - t.end() + const cert = fakeCert() + + const server = restify.createServer({ key: cert.privateKey, certificate: cert.certificate }) + t.teardown(() => server.close()) + + server.get('/hello/:name', function sayHello(req, res, next) { + t.ok(agent.getTransaction(), 'transaction should be available in handler') + res.send('hello ' + req.params.name) + next() + }) + + server.listen(0, function () { + const port = server.address().port + t.notOk(agent.getTransaction(), 'transaction should not leak into server') + + const url = `https://127.0.0.1:${port}/hello/friend` + helper.makeGetRequest(url, { ca: cert.certificate }, function (error, response, body) { + if (error) { + t.fail(error) + return t.end() + } + + t.notOk(agent.getTransaction(), 'transaction should not leak into external request') + t.equal(body, 'hello friend', 'should return expected data') }) + }) }) t.test('should generate middleware metrics', (t) => { diff --git a/test/versioned/undici/requests.tap.js b/test/versioned/undici/requests.tap.js index f2f37a23e9..ba38bd905e 100644 --- a/test/versioned/undici/requests.tap.js +++ b/test/versioned/undici/requests.tap.js @@ -14,6 +14,9 @@ const https = require('https') const { version: pkgVersion } = require('undici/package') const semver = require('semver') +const fakeCert = require('../../lib/fake-cert') +const cert = fakeCert({ commonName: 'localhost' }) + tap.test('Undici request tests', (t) => { t.autoend() @@ -97,11 +100,13 @@ tap.test('Undici request tests', (t) => { }) t.test('should add HTTPS port to segment name when provided', async (t) => { - const [key, cert, ca] = await helper.withSSL() - const httpsServer = https.createServer({ key, cert }, (req, res) => { - res.write('SSL response') - res.end() - }) + const httpsServer = https.createServer( + { key: cert.privateKey, cert: cert.certificate }, + (req, res) => { + res.write('SSL response') + res.end() + } + ) t.teardown(() => { httpsServer.close() @@ -113,9 +118,7 @@ tap.test('Undici request tests', (t) => { const { port } = httpsServer.address() const client = new undici.Client(`https://localhost:${port}`, { - tls: { - ca - } + tls: { ca: cert.certificate } }) t.teardown(() => { diff --git a/third_party_manifest.json b/third_party_manifest.json index ec680e4029..b399b94ce7 100644 --- a/third_party_manifest.json +++ b/third_party_manifest.json @@ -1,5 +1,5 @@ { - "lastUpdated": "Mon Sep 30 2024 10:50:22 GMT-0400 (Eastern Daylight Time)", + "lastUpdated": "Tue Oct 22 2024 15:22:40 GMT-0400 (Eastern Daylight Time)", "projectName": "New Relic Node Agent", "projectUrl": "https://github.com/newrelic/node-newrelic", "includeOptDeps": true, @@ -44,15 +44,15 @@ }, "includeDev": true, "dependencies": { - "@grpc/grpc-js@1.11.3": { + "@grpc/grpc-js@1.12.2": { "name": "@grpc/grpc-js", - "version": "1.11.3", - "range": "^1.9.4", + "version": "1.12.2", + "range": "^1.12.2", "licenses": "Apache-2.0", "repoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", - "versionedRepoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.11.3", + "versionedRepoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.12.2", "licenseFile": "node_modules/@grpc/grpc-js/LICENSE", - "licenseUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.11.3/LICENSE", + "licenseUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.12.2/LICENSE", "licenseTextSource": "file", "publisher": "Google Inc." }, @@ -211,43 +211,43 @@ "licenseTextSource": "file", "publisher": "GitHub Inc." }, - "winston-transport@4.7.1": { + "winston-transport@4.8.0": { "name": "winston-transport", - "version": "4.7.1", + "version": "4.8.0", "range": "^4.5.0", "licenses": "MIT", "repoUrl": "https://github.com/winstonjs/winston-transport", - "versionedRepoUrl": "https://github.com/winstonjs/winston-transport/tree/v4.7.1", + "versionedRepoUrl": "https://github.com/winstonjs/winston-transport/tree/v4.8.0", "licenseFile": "node_modules/winston-transport/LICENSE", - "licenseUrl": "https://github.com/winstonjs/winston-transport/blob/v4.7.1/LICENSE", + "licenseUrl": "https://github.com/winstonjs/winston-transport/blob/v4.8.0/LICENSE", "licenseTextSource": "file", "publisher": "Charlie Robbins", "email": "charlie.robbins@gmail.com" } }, "devDependencies": { - "@aws-sdk/client-s3@3.658.1": { + "@aws-sdk/client-s3@3.676.0": { "name": "@aws-sdk/client-s3", - "version": "3.658.1", + "version": "3.676.0", "range": "^3.556.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js-v3", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.658.1", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.676.0", "licenseFile": "node_modules/@aws-sdk/client-s3/LICENSE", - "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.658.1/LICENSE", + "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.676.0/LICENSE", "licenseTextSource": "file", "publisher": "AWS SDK for JavaScript Team", "url": "https://aws.amazon.com/javascript/" }, - "@aws-sdk/s3-request-presigner@3.658.1": { + "@aws-sdk/s3-request-presigner@3.676.0": { "name": "@aws-sdk/s3-request-presigner", - "version": "3.658.1", + "version": "3.676.0", "range": "^3.556.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js-v3", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.658.1", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.676.0", "licenseFile": "node_modules/@aws-sdk/s3-request-presigner/LICENSE", - "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.658.1/LICENSE", + "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.676.0/LICENSE", "licenseTextSource": "file", "publisher": "AWS SDK for JavaScript Team", "url": "https://aws.amazon.com/javascript/" @@ -327,15 +327,15 @@ "licenseUrl": "https://github.com/octokit/rest.js/blob/v18.12.0/LICENSE", "licenseTextSource": "file" }, - "@slack/bolt@3.21.4": { + "@slack/bolt@3.22.0": { "name": "@slack/bolt", - "version": "3.21.4", + "version": "3.22.0", "range": "^3.7.0", "licenses": "MIT", "repoUrl": "https://github.com/slackapi/bolt", - "versionedRepoUrl": "https://github.com/slackapi/bolt/tree/v3.21.4", + "versionedRepoUrl": "https://github.com/slackapi/bolt/tree/v3.22.0", "licenseFile": "node_modules/@slack/bolt/LICENSE", - "licenseUrl": "https://github.com/slackapi/bolt/blob/v3.21.4/LICENSE", + "licenseUrl": "https://github.com/slackapi/bolt/blob/v3.22.0/LICENSE", "licenseTextSource": "file", "publisher": "Slack Technologies, LLC" }, @@ -544,15 +544,15 @@ "publisher": "Nicholas C. Zakas", "email": "nicholas+npm@nczconsulting.com" }, - "express@4.21.0": { + "express@4.21.1": { "name": "express", - "version": "4.21.0", + "version": "4.21.1", "range": "*", "licenses": "MIT", "repoUrl": "https://github.com/expressjs/express", - "versionedRepoUrl": "https://github.com/expressjs/express/tree/v4.21.0", + "versionedRepoUrl": "https://github.com/expressjs/express/tree/v4.21.1", "licenseFile": "node_modules/express/LICENSE", - "licenseUrl": "https://github.com/expressjs/express/blob/v4.21.0/LICENSE", + "licenseUrl": "https://github.com/expressjs/express/blob/v4.21.1/LICENSE", "licenseTextSource": "file", "publisher": "TJ Holowaychuk", "email": "tj@vision-media.ca" @@ -609,15 +609,15 @@ "publisher": "Typicode", "email": "typicode@gmail.com" }, - "jsdoc@4.0.3": { + "jsdoc@4.0.4": { "name": "jsdoc", - "version": "4.0.3", + "version": "4.0.4", "range": "^4.0.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/jsdoc/jsdoc", - "versionedRepoUrl": "https://github.com/jsdoc/jsdoc/tree/v4.0.3", + "versionedRepoUrl": "https://github.com/jsdoc/jsdoc/tree/v4.0.4", "licenseFile": "node_modules/jsdoc/LICENSE.md", - "licenseUrl": "https://github.com/jsdoc/jsdoc/blob/v4.0.3/LICENSE.md", + "licenseUrl": "https://github.com/jsdoc/jsdoc/blob/v4.0.4/LICENSE.md", "licenseTextSource": "file", "publisher": "Michael Mathews", "email": "micmath@gmail.com" @@ -697,20 +697,6 @@ "publisher": "Pedro Teixeira", "email": "pedro.teixeira@gmail.com" }, - "proxy@2.2.0": { - "name": "proxy", - "version": "2.2.0", - "range": "^2.1.1", - "licenses": "MIT", - "repoUrl": "https://github.com/TooTallNate/proxy-agents", - "versionedRepoUrl": "https://github.com/TooTallNate/proxy-agents/tree/v2.2.0", - "licenseFile": "node_modules/proxy/LICENSE", - "licenseUrl": "https://github.com/TooTallNate/proxy-agents/blob/v2.2.0/LICENSE", - "licenseTextSource": "file", - "publisher": "Nathan Rajlich", - "email": "nathan@tootallnate.net", - "url": "http://n8.io/" - }, "proxyquire@1.8.0": { "name": "proxyquire", "version": "1.8.0",