From 335d284a99cbe5dc0405cb4b1d64e7f0d77953f5 Mon Sep 17 00:00:00 2001 From: Stanislav Mishchyshyn Date: Tue, 25 Jun 2024 17:06:06 +0300 Subject: [PATCH] fix: improve reconnection logic increase tests coverage --- .github/workflows/easy-tunnel-ci.yaml | 14 +- .prettierrc | 6 + README.md | 2 +- bin/et.js | 21 +- cert.pem | 32 ++ easyTunnel.d.ts | 3 +- easyTunnel.spec.js | 562 +++++++++++++++++++++----- fixtures/tls/ca-crt.pem | 22 + fixtures/tls/ca-crt.srl | 1 + fixtures/tls/ca-key.pem | 28 ++ fixtures/tls/client-crt.pem | 26 ++ fixtures/tls/client-csr.pem | 27 ++ fixtures/tls/client-key.pem | 52 +++ fixtures/tls/server-crt.pem | 27 ++ fixtures/tls/server-csr.pem | 27 ++ fixtures/tls/server-key.pem | 52 +++ lib/Tunnel.js | 107 +++-- lib/TunnelCluster.js | 84 ++-- package-lock.json | 461 +++++++++++++++++++-- package.json | 10 +- server.js | 52 +++ tmp/ca.pem | 21 + tmp/clientcert.pem | 18 + tmp/clientprivate.pem | 15 + 24 files changed, 1474 insertions(+), 196 deletions(-) create mode 100644 .prettierrc create mode 100644 cert.pem create mode 100644 fixtures/tls/ca-crt.pem create mode 100644 fixtures/tls/ca-crt.srl create mode 100644 fixtures/tls/ca-key.pem create mode 100644 fixtures/tls/client-crt.pem create mode 100644 fixtures/tls/client-csr.pem create mode 100644 fixtures/tls/client-key.pem create mode 100644 fixtures/tls/server-crt.pem create mode 100644 fixtures/tls/server-csr.pem create mode 100644 fixtures/tls/server-key.pem create mode 100644 server.js create mode 100644 tmp/ca.pem create mode 100644 tmp/clientcert.pem create mode 100644 tmp/clientprivate.pem diff --git a/.github/workflows/easy-tunnel-ci.yaml b/.github/workflows/easy-tunnel-ci.yaml index 0a0ef39..6b333a5 100644 --- a/.github/workflows/easy-tunnel-ci.yaml +++ b/.github/workflows/easy-tunnel-ci.yaml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 @@ -21,4 +21,16 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm ci + - name: Checkout server + uses: actions/checkout@v3 + with: + repository: namecheap/mytunnel-server + ref: master + path: server + - name: Start server + run: | + npm ci + npm run dev & + npx -y wait-on tcp:8087 --log + working-directory: server - run: npm test diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7507b44 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "arrowParens": "avoid", + "singleQuote": true, + "tabWidth": 2, + "printWidth": 120 +} \ No newline at end of file diff --git a/README.md b/README.md index a974a5d..658deeb 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ const easyTunnel = require('@namecheap/easy-tunnel'); - `port` (number) [required] The local port number to expose through easy-tunnel. - `subdomain` (string) Request a specific subdomain on the proxy server. **Note** You may not actually receive this name depending on availability. -- `host` (string) URL for the upstream proxy server. Defaults to `https://localtunnel.me`. +- `host` (string) URL for the upstream proxy server. Defaults to `http://localhost:8087`. - `local_host` (string) Proxy to this hostname instead of `localhost`. This will also cause the `Host` header to be re-written to this value in proxied requests. - `local_https` (boolean) Enable tunneling to local HTTPS server. - `local_cert` (string) Path to certificate PEM file for local HTTPS server. diff --git a/bin/et.js b/bin/et.js index 3a5c420..f9e5c30 100644 --- a/bin/et.js +++ b/bin/et.js @@ -17,7 +17,7 @@ const { argv } = yargs .option('h', { alias: 'host', describe: 'Upstream server providing forwarding', - default: 'https://localtunnel.me', + default: 'http://localhost:8087', }) .option('s', { alias: 'subdomain', @@ -50,11 +50,20 @@ const { argv } = yargs describe: 'Print basic request info', }) .option('request-secure-tunnel', { - describe: 'Requests tunel server to create secure tunnel if it is available.', + alias: 'secure', + describe: 'Requests tunnel server to create secure tunnel if it is available.', }) - .option('local_max_reconnect_count', { + .option('local-max-retries', { describe: 'Max number of reconnection retries to local server if it goes offline.', - default: 90, + default: Infinity, + }) + .option('connect-timeout', { + describe: 'Connection timeout (ms)', + default: 10_000, + }) + .option('idle-timeout', { + describe: 'Idle socket timeout (ms)', + default: 15_000, }) .require('port') .boolean('local-https') @@ -82,7 +91,9 @@ if (typeof argv.port !== 'number') { local_ca: argv.localCa, allow_invalid_cert: argv.allowInvalidCert, request_secure_tunnel: argv.requestSecureTunnel, - local_max_reconnect_count: argv.local_max_reconnect_count + local_max_retries: argv.localMaxRetries, + connect_timeout: argv.connectTimeout, + idle_timeout: argv.idleTimeout }).catch(err => { throw err; }); diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..698edbf --- /dev/null +++ b/cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFmTCCA4GgAwIBAgIUN8ZRrUaNcWvSZUcrQODSzxj3tdUwDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAwwMY2EubG9jYWxob3N0 +MB4XDTI0MDYyNDE2NTM1MloXDTI1MDYyNDE2NTM1MlowXDELMAkGA1UEBhMCQVUx +EzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMg +UHR5IEx0ZDEVMBMGA1UEAwwMY2EubG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAqvgDXnVfqT30mFY2Wg7cpeVBMZmW6/JiM8Ncu2GQMAOc +PHgQYLPmuDuHY7gUMt/NB5DAfiL7TpGjnx6W0nS70rgYWjXF6FpMW245QIiB1fKQ +Swk/ssu4WjnbzRxkiPdHOZlsKLCl74i5HASSpVWKlR9w36a+p3WiCK7XiE56RdyU +5otbWalKSpmEu3+UOYEbEFxSy7RQJBAsLGmYs5tgLzTfXNaukwCwYU9H7h0d1Lgq +1y0m2O2DGVwBEpCX9e9sT0M5T3gKpaFT770jacv2I2jP3plSLPdNbij8TQ2jrR4N +wWU1D+KXSkE16ojPO9WI0Hy9tgEiD7ac6b36hcT1mgmBaVJ/aoBRVAqBADnMz4jt +Qb5rknUJnTQ6uoLXs+D4dblCRP7ZSZVeGqhLL7oEJ78VVrk8bD/IMYo2wVREEOsn +rVSG+iQ9FzAgqWcBVPLqnzvlvjVsUH6PlCfQfVRWiWUmDUEKEcr2IMSZfqUODKKG +MV3nc/Xt8d5YoRG2aHptWxgRLEDWb8jRpmeR9iSzb4KXaAQtohsmxJapgTc9Uoxl +1En5hFIQng/+aRQkLc6FrWte8Qx0jtOgpDQ3q15UM6nHaDk6zR93IXQDcHSBMV30 +YALl0PSzO5Pn/sh081+CUxzpOS2AiGVgTuxwRYM9ZJB9RZ9qRHFoJN2kfKaMsTsC +AwEAAaNTMFEwHQYDVR0OBBYEFILVdBynDOWHfudabk+lRs5ALuEHMB8GA1UdIwQY +MBaAFILVdBynDOWHfudabk+lRs5ALuEHMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAFpUr+/aeYFl/EU8i9ZQ7FbGLmRNJMU/WYBePBfLC5kdScCb +hdavps5lozaJ6ECjuBCGMDsDMu1Z+zV8Ps2UgMRYZcmucSg2DRXooY3WxrAiTgrd +VEyaLotfulG+Am70t4mrdbQfhEWX/fMTh1ruFq1sn0gk7mD1gvlaElxH200p8Yyf +qAE60JKkVikvuaff/eplIEbU3C2BGXU3+/+/qRtr+Cy5HC9SLuOLgSJidi2P192+ +Z3jpZEd7EMKMZ/k8knBtoZM8nZ7q+LqQmnEP+BFURG2Ht8i9mK+NHWfvUuZoyCaU +mhcG37+lGjRS8x94SgHFDKKsyeF7qI5RSuWSd4b/2/ZurW1Us9C03Evu2/dcHhku +eczKA9T0MM6ZrIPrzM7whNldLredJoai734Kzq1kkl6Z/lESy/dlXGBA+UgnGuP0 +LQSmJ8RQis8k01nm4T0vht+7GYJ1QVzEB1I4/WVhjll6ig+s+Dvx8tT01nmxepxR +TjbFYVQuS3G9TWkP/OJm87sHZmYfDdVjQQlpSYaSQ2qEthVtmzTVg6r4FBxGErGL +6z7bNmqnpnrmCebjl+xoRwyatTLTZz/+lmtszYg+WhzEsy61PaU4fzF8y0VfncJ9 +6dP0XP3CVgKjHIDM4Jsk6rDkzOc8yU3ESkcnCPCIJIQQtBk31EyKPRp4ALTK +-----END CERTIFICATE----- diff --git a/easyTunnel.d.ts b/easyTunnel.d.ts index 36e8dfb..2f98a6d 100644 --- a/easyTunnel.d.ts +++ b/easyTunnel.d.ts @@ -15,7 +15,8 @@ declare interface BootstrapOpts { local_ca?: string; allow_invalid_cert?: boolean; request_secure_tunnel?: boolean; - local_max_reconnect_count?: number; + local_max_retries?: number; + local_reconnect_delay?: number; } declare const localtunnel: (opts: BootstrapOpts) => Promise; diff --git a/easyTunnel.spec.js b/easyTunnel.spec.js index 5ec10b5..43019d6 100644 --- a/easyTunnel.spec.js +++ b/easyTunnel.spec.js @@ -1,134 +1,506 @@ -/* eslint-disable no-console */ - const crypto = require('crypto'); const http = require('http'); const https = require('https'); +const net = require('net'); +const tls = require('tls'); +const fs = require('fs'); +const { setTimeout } = require('timers/promises'); const url = require('url'); const supertest = require('supertest'); const chai = require('chai'); chai.use(require('chai-string')); const expect = chai.expect; const assert = require('assert'); +const nock = require('nock'); const easyTunnel = require('./easyTunnel'); +const tunnelPort = 4200; +const fakeHost = 'https://local.tunnel'; + let fakePort; let testServer; describe('localtunnel', () => { + before(done => { + testServer = http.createServer(); + testServer.on('request', (req, res) => { + res.write(req.headers.host); + res.end(); + }); + testServer.listen(() => { + const { port } = testServer.address(); + fakePort = port; + done(); + }); + }); - before(done => { - testServer = http.createServer(); - testServer.on('request', (req, res) => { - res.write(req.headers.host); - res.end(); - }); - testServer.listen(() => { - const { port } = testServer.address(); - fakePort = port; - done(); + beforeEach(() => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = 1; + }); + + after(() => { + testServer.close(); + nock.restore(); + }); + + it('query easyTunnel server w/ ident', async () => { + const tunnel = await easyTunnel({ port: fakePort, host: 'http://lvh.me:8087' }); + + try { + await supertest(tunnel.url) + .get('/') + .expect(200) + .expect(res => { + expect(tunnel.url).to.endsWith(res.text); }); - }); + } finally { + tunnel.close(); + } + }); - after(() => { - testServer.close(); - }); + it('request specific domain', async () => { + const subdomain = Math.random().toString(36).substr(2); + const tunnel = await easyTunnel({ port: fakePort, subdomain }); + tunnel.close(); - it('query easyTunnel server w/ ident', async () => { - const tunnel = await easyTunnel({ port: fakePort }); + expect(tunnel.url).to.startsWith(`http://${subdomain}.`); + }); - try { - await supertest(tunnel.url) - .get('/') - .expect(200) - .expect(res => { - expect(tunnel.url).to.endsWith(res.text); - }); - } finally { - tunnel.close(); - } - }); + describe('--local-host localhost', () => { + it('override Host header with local-host', async () => { + const tunnel = await easyTunnel({ port: fakePort, local_host: 'localhost', host: 'http://lvh.me:8087' }); - it('request specific domain', async () => { - const subdomain = Math.random() - .toString(36) - .substr(2); - const tunnel = await easyTunnel({ port: fakePort, subdomain }); + try { + await supertest(tunnel.url) + .get('/') + .expect(200) + .expect(res => { + expect(res.text).to.equal('localhost'); + }); + } finally { tunnel.close(); + } + }); + }); - expect(tunnel.url).to.startsWith(`https://${subdomain}.`); + describe('--local-host 127.0.0.1', () => { + it('override Host header with local-host', async () => { + const tunnel = await easyTunnel({ port: fakePort, local_host: '127.0.0.1', host: 'http://lvh.me:8087' }); + + try { + await supertest(tunnel.url) + .get('/') + .expect(200) + .expect(res => { + expect(res.text).to.equal('127.0.0.1'); + }); + } finally { + tunnel.close(); + } }); - describe('--local-host localhost', () => { - it('override Host header with local-host', async () => { - const tunnel = await easyTunnel({ port: fakePort, local_host: 'localhost' }); + it('send chunked request', async () => { + const tunnel = await easyTunnel({ port: fakePort, local_host: '127.0.0.1', host: 'http://lvh.me:8087' }); + + const parsed = url.parse(tunnel.url); + const opt = { + host: parsed.host, + port: 8087, + headers: { + host: parsed.hostname, + 'Transfer-Encoding': 'chunked', + }, + path: '/', + }; + + await new Promise((resolve, reject) => { + const req = http.request(opt, res => { + res.setEncoding('utf8'); + let body = ''; + res.on('data', chunk => { + body += chunk; + }); + + res.on('end', () => { try { - await supertest(tunnel.url) - .get('/') - .expect(200) - .expect(res => { - expect(res.text).to.equal('localhost'); - }); + assert.strictEqual(body, '127.0.0.1'); + } catch (e) { + reject(e); } finally { - tunnel.close(); + tunnel.close(); } + resolve(); + }); }); + + req.end(crypto.randomBytes(1024 * 8).toString('base64')); + }); }); + }); - describe('--local-host 127.0.0.1', () => { - it('override Host header with local-host', async () => { - const tunnel = await easyTunnel({ port: fakePort, local_host: '127.0.0.1' }); + it('should open n sockets', async () => { + const maxSockets = 8; + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: tunnelPort, + max_conn_count: maxSockets, + is_tunnel_secure: false, + ip: '127.0.0.1', + url: 'https://test.localhost', + }); - try { - await supertest(tunnel.url) - .get('/') - .expect(200) - .expect(res => { - expect(res.text).to.equal('127.0.0.1'); - }); - } finally { - tunnel.close(); - } + const remoteSocket = net.createServer(); + remoteSocket.listen(tunnelPort); + let connectedSockets = 0; + remoteSocket.on('connection', () => (connectedSockets += 1)); + const tunnel = await easyTunnel({ port: fakePort, host: fakeHost }); + await setTimeout(1000); + expect(connectedSockets).equals(maxSockets); + tunnel.close(); + remoteSocket.close(); + }); + + it('should connect to tls tunnel', async () => { + const maxSockets = 8; + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: tunnelPort, + max_conn_count: maxSockets, + is_tunnel_secure: true, + ip: '127.0.0.1', + url: 'https://localhost', + }); + + const remoteSocket = tls.createServer({ + cert: fs.readFileSync('./fixtures/tls/server-crt.pem'), + key: fs.readFileSync('./fixtures/tls/server-key.pem'), + }); + remoteSocket.listen(tunnelPort); + let connectedSockets = 0; + remoteSocket.on('secureConnection', () => (connectedSockets += 1)); + process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; + const tunnel = await easyTunnel({ port: fakePort, host: fakeHost }); + await setTimeout(1000); + expect(connectedSockets).equals(maxSockets); + tunnel.close(); + remoteSocket.close(); + }); + + it('should connect to local tls server', async () => { + const testTlsServer = https.createServer({ + cert: fs.readFileSync('./fixtures/tls/server-crt.pem'), + key: fs.readFileSync('./fixtures/tls/server-key.pem'), + }); + testTlsServer.on('request', (req, res) => { + res.write('TLS'); + res.end(); + }); + await new Promise((resolve, reject) => testTlsServer.listen(err => (err ? reject(err) : resolve()))); + const { port } = testTlsServer.address(); + const tunnel = await easyTunnel({ port, host: 'http://lvh.me:8087', local_https: true, allow_invalid_cert: true }); + try { + await supertest(tunnel.url) + .get('/') + .expect(200) + .expect(res => { + expect(res.text).to.equal('TLS'); }); + } finally { + tunnel.close(); + } + }); - it('send chunked request', async () => { - const tunnel = await easyTunnel({ port: fakePort, local_host: '127.0.0.1' }); - - const parsed = url.parse(tunnel.url); - const opt = { - host: parsed.host, - port: 443, - headers: { - host: parsed.hostname, - 'Transfer-Encoding': 'chunked', - }, - path: '/', - }; - - await new Promise((resolve, reject) => { - const req = https.request(opt, res => { - res.setEncoding('utf8'); - let body = ''; - - res.on('data', chunk => { - body += chunk; - }); - - res.on('end', () => { - try { - assert.strictEqual(body, '127.0.0.1'); - } catch (e) { - reject(e); - } finally { - tunnel.close(); - } - resolve(); - }); - }); - - req.end(crypto.randomBytes(1024 * 8).toString('base64')); - }); + it('should connect to local tls server (with client cert)', async () => { + const cert = fs.readFileSync('./fixtures/tls/server-crt.pem'); + const key = fs.readFileSync('./fixtures/tls/server-key.pem'); + const testTlsServer = https.createServer({ cert, key }); + testTlsServer.on('request', (req, res) => { + res.write('TLS'); + res.end(); + }); + await new Promise((resolve, reject) => testTlsServer.listen(err => (err ? reject(err) : resolve()))); + const { port } = testTlsServer.address(); + const tunnel = await easyTunnel({ + port, + host: 'http://lvh.me:8087', + local_https: true, + local_cert: './fixtures/tls/client-crt.pem', + local_key: './fixtures/tls/client-key.pem', + local_ca: './fixtures/tls/ca-crt.pem', + }); + try { + await supertest(tunnel.url) + .get('/') + .expect(200) + .expect(res => { + expect(res.text).to.equal('TLS'); }); + } finally { + tunnel.close(); + } + }); + + it('should request secure channel with flag', async () => { + const maxSockets = 1; + nock(fakeHost).get('/?new&secureTunnel').reply(200, { + id: 'test', + port: tunnelPort, + max_conn_count: maxSockets, + is_tunnel_secure: false, + ip: '127.0.0.1', + url: 'https://test.localhost', + }); + const remoteSocket = net.createServer(); + remoteSocket.listen(tunnelPort); + let connectedSockets = 0; + remoteSocket.on('connection', () => (connectedSockets += 1)); + const tunnel = await easyTunnel({ + port: fakePort, + host: fakeHost, + request_secure_tunnel: true, + }); + await setTimeout(1000); + assert.equal(connectedSockets, maxSockets); + tunnel.close(); + remoteSocket.close(); + }); + + it('handle --connect-timeout on initial request', async () => { + const tunnel = easyTunnel({ port: fakePort, host: 'http://8.8.8.8', connect_timeout: 2000 }); + await assert.rejects(tunnel, { message: 'timeout of 2000ms exceeded' }); + }); + + it('handle --connect-timeout on socket connect', async () => { + const maxSockets = 1; + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: tunnelPort, + max_conn_count: maxSockets, + is_tunnel_secure: false, + url: 'https://test.localhost', + }); + + const tunnel = easyTunnel({ port: fakePort, host: fakeHost, connect_timeout: 2000 }); + await assert.rejects(tunnel, { message: 'Tunnel timed out' }); + }); + + it('should reconnect on local socket close', done => { + const remoteSocket = net.createServer(); + remoteSocket.listen(() => { + const { port: remoteSocketPort } = remoteSocket.address(); + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: remoteSocketPort, + max_conn_count: 1, + is_tunnel_secure: false, + ip: '127.0.0.1', + url: 'https://test.localhost', + }); + + let localSocket; + testServer.on('connection', socket => { + localSocket = socket; + }); + + let tunnel; + remoteSocket.once('connection', async socket => { + socket.resume(); + remoteSocket.once('connection', () => { + tunnel.close(); + done(); + }); + await setTimeout(1000); + localSocket.end('bye\n'); + }); + + easyTunnel({ + port: fakePort, + host: fakeHost, + }) + .then(_tunnel => (tunnel = _tunnel)) + .catch(done); + }); + }); + + it('should reconnect on local socket destroy', done => { + const remoteSocket = net.createServer(); + remoteSocket.listen(() => { + const { port: remoteSocketPort } = remoteSocket.address(); + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: remoteSocketPort, + max_conn_count: 1, + is_tunnel_secure: false, + ip: '127.0.0.1', + url: 'https://test.localhost', + }); + + let localSocket; + testServer.on('connection', socket => { + localSocket = socket; + }); + + let tunnel; + remoteSocket.once('connection', async socket => { + socket.resume(); + remoteSocket.once('connection', () => { + tunnel.close(); + done(); + }); + await setTimeout(1000); + localSocket.destroy(); + }); + + easyTunnel({ + port: fakePort, + host: fakeHost, + connect_timeout: 1000, + }) + .then(_tunnel => (tunnel = _tunnel)) + .catch(done); + }); + }); + + it('should reconnect on local socket reset', done => { + const remoteSocket = net.createServer(); + remoteSocket.listen(() => { + const { port: remoteSocketPort } = remoteSocket.address(); + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: remoteSocketPort, + max_conn_count: 1, + is_tunnel_secure: false, + ip: '127.0.0.1', + url: 'https://test.localhost', + }); + + let localSocket; + testServer.on('connection', socket => { + localSocket = socket; + }); + + let tunnel; + remoteSocket.once('connection', async socket => { + socket.resume(); + remoteSocket.once('connection', () => { + tunnel.close(); + done(); + }); + await setTimeout(1000); + localSocket.resetAndDestroy(); + }); + + easyTunnel({ + port: fakePort, + host: fakeHost, + }) + .then(_tunnel => (tunnel = _tunnel)) + .catch(done); + }); + }); + + it('should reconnect on local socket reset', done => { + const remoteSocket = net.createServer(); + remoteSocket.listen(() => { + const { port: remoteSocketPort } = remoteSocket.address(); + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: remoteSocketPort, + max_conn_count: 1, + is_tunnel_secure: false, + ip: '127.0.0.1', + url: 'https://test.localhost', + }); + + let tunnel; + remoteSocket.once('connection', async socket => { + remoteSocket.once('connection', () => { + tunnel.close(); + done(); + }); + await setTimeout(1000); + socket.resetAndDestroy(); + }); + + easyTunnel({ + port: fakePort, + host: fakeHost, + }) + .then(_tunnel => (tunnel = _tunnel)) + .catch(done); + }); + }); + + it('should throw on ECONNREFUSED', done => { + const remoteSocket = net.createServer(); + remoteSocket.listen(() => { + const { port: remoteSocketPort } = remoteSocket.address(); + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: remoteSocketPort, + max_conn_count: 1, + is_tunnel_secure: false, + ip: '127.0.0.1', + url: 'https://test.localhost', + }); + + let tunnel; + remoteSocket.once('connection', async socket => { + await setTimeout(1000); + tunnel.once('error', error => { + expect(error.message).to.match(/connection refused:/); + done(); + }); + remoteSocket.close(); + socket.destroy(); + }); + + easyTunnel({ + port: fakePort, + host: fakeHost, + }) + .then(_tunnel => (tunnel = _tunnel)) + .catch(done); + }); + }); + + it('handle --idle-timeout', done => { + const remoteSocket = net.createServer(); + remoteSocket.listen(() => { + const { port: remoteSocketPort } = remoteSocket.address(); + nock(fakeHost).get('/?new').reply(200, { + id: 'test', + port: remoteSocketPort, + max_conn_count: 5, + is_tunnel_secure: false, + ip: '127.0.0.1', + url: 'https://test.localhost', + }); + + let tunnel, listenerSet; + remoteSocket.on('connection', async socket => { + socket.resume(); + await setTimeout(1000); + if (!listenerSet) { + tunnel.once('error', error => { + expect(error.message).match(/connection refused/); + tunnel.close(); + done(); + }); + listenerSet = true; + } + remoteSocket.close(); + }); + + easyTunnel({ + port: fakePort, + host: fakeHost, + idle_timeout: 2000, + }) + .then(_tunnel => (tunnel = _tunnel)) + .catch(done); }); + }); }); diff --git a/fixtures/tls/ca-crt.pem b/fixtures/tls/ca-crt.pem new file mode 100644 index 0000000..2e84cc5 --- /dev/null +++ b/fixtures/tls/ca-crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmzCCAoOgAwIBAgIUTQQvTpyg5x8ToON9JTQcz/X/eXcwDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAwwMY2EubG9jYWxob3N0 +MCAXDTI0MDYyNDE2NTgzMVoYDzIwNTExMTA5MTY1ODMxWjBcMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRUwEwYDVQQDDAxjYS5sb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCppV1uvIGTXUjz8AKwVLci4fylTsM8xQtvTpYzWvVB +8KZi7QBe4LVUQKZkTJR/7pWNdpqKCgqdgVoQnxI2VsjY7nas5vGF/GvCx6sr/ixx +mztBkgh9i2F/T5uiBoyplWWA/442oPgEn01xCKSX6WFv+rAhUCqFdmDQ2r8dOsbs +19xx0RgdZde4v+pxk+WQi/zzaayQBg9aiATUcIx3af6jMo8NoGyWxL4WOUBmwhwz +WXSBg49voXADhD+WDh1FVvNygimn535aJO8NnimoBeF7n6ObdUQwQbkmAmFImUL4 +fFXTL48XvMLxmya9MYnJ6dWaEUwsmVmBf1oh5nYocnZBAgMBAAGjUzBRMB0GA1Ud +DgQWBBSuYWDSDmQGJEWeBI/96qhIzOWB+jAfBgNVHSMEGDAWgBSuYWDSDmQGJEWe +BI/96qhIzOWB+jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCR +giOheU1zJ6d/V6wvGzK+xIhJUlRO/ksRQBf8v5HHha6XNt3isfgLgJff1RAXWz5c +dKijjETdmt8hfQP0/GcvafhBn3mhHCAX/PxUWD02p48IXXtCE2Iz74QTv3p81YJT +EoSH/JhGK3UwVdaaD1ZQNC7YU+x1aItJLPFBCR18eK+emlX026fvj584msNbWKxl +yimvd1UmFF79N1a8kygo1BC0kcFoLp1IHmXI1WW50D4xHKxxxppNOwy17JmRnVLH +JaQ4N9pg9Mqrdqo00Fc9QHuqzcbLrVFWUtxtve8tu97GLXPDqQJtr2ZSKOJ4yVoT +8C30Zu94T4ri10NsUE5Z +-----END CERTIFICATE----- diff --git a/fixtures/tls/ca-crt.srl b/fixtures/tls/ca-crt.srl new file mode 100644 index 0000000..f3c65c2 --- /dev/null +++ b/fixtures/tls/ca-crt.srl @@ -0,0 +1 @@ +424BBF98B048DE163B9AB775F120200158D49C2A diff --git a/fixtures/tls/ca-key.pem b/fixtures/tls/ca-key.pem new file mode 100644 index 0000000..aaa5bd2 --- /dev/null +++ b/fixtures/tls/ca-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCppV1uvIGTXUjz +8AKwVLci4fylTsM8xQtvTpYzWvVB8KZi7QBe4LVUQKZkTJR/7pWNdpqKCgqdgVoQ +nxI2VsjY7nas5vGF/GvCx6sr/ixxmztBkgh9i2F/T5uiBoyplWWA/442oPgEn01x +CKSX6WFv+rAhUCqFdmDQ2r8dOsbs19xx0RgdZde4v+pxk+WQi/zzaayQBg9aiATU +cIx3af6jMo8NoGyWxL4WOUBmwhwzWXSBg49voXADhD+WDh1FVvNygimn535aJO8N +nimoBeF7n6ObdUQwQbkmAmFImUL4fFXTL48XvMLxmya9MYnJ6dWaEUwsmVmBf1oh +5nYocnZBAgMBAAECggEAOVImv8PyBFkAzWvLfMkjGCZDt5dlYKMzuehT2AZj2GP3 +1HVAKs7CdjViA4Hcq11yKtpoXTwHWjDavcMB5Fpugt4QO1vNP/iKcMYGkFbRrZFF +9GHjfIgb6Wh8rcKIxMQ2B+BRrSO2qdkp0YeaFbpFshCCcr8jnvTgwbEn4jh5/oxk +OSiczYNybGux0oO7jewtT8y4ijKw4uPwAV4tXFEucKpe9Zpg5NADMK8BudugwoHO +67WeTyBIY5UrmgbGAfCdQ+u6xF2Kp/uR6sn9Tmamh1MBEnjB8CJh8FjzaizbovwT +qmZE6AigFGHIW5guHoLtRFIstM63QcfjbV6CMX1iAQKBgQDgW1pL6ntKY2q3mEY6 +3kY/aK13KV16h8ZAtJoQdhApgxn5xzlofZaLIo/cytj6ElvQ3cp6Mj7oCCjEfxfu +X2Zolku7ifSPEsCTdp2EXgszMCHOpGjnl+PXP6XOi7RyWuFmtG454rD3BAjZln/6 +VLTYGiSXADmfaKt8gQaB6g0F8wKBgQDBkp3B9GSiPJGE/yiCidv99Vj3ucQDjnJh +SdjnWidlYaiuD7pc5NK7FR21Q4ZrxNhxioOyCllhdpOJV302MkGRptZCpc6Btk19 +68RAYfrPDuVRkn7OzfVZi2o3simOlTE9GmLSFRMaBTnsQbFytb/zzCOh2wohfXY1 +zIFwW/8b+wKBgQDUv4vWpVmYZsHRq0IdnJ08j8S+VKliAdJLlXbq2SnmU32UUAju +Pvk8ot+M3YX3TOVoIIlaar8gRGx0OJi24Bw6XRsfkWgpK/0VWMtxs8QnHCNS0rDv +vCa//Ij8XZoVPnyzabGEjqSE+Hxz6LUe6qg6rD+6OOprcVP8UlWbaBhFZQKBgQCs +L3cU6Af8KFRpMW8fbvG5XVzePugVIcissbu3T4JrtAsPif4EU6b/szDR5FbV1iuS +E9xBquMGxytFqAVBCEM7BGTitAqVZ0xXDjlr67vmIyHpBZ625o2wMUsyb9B3fE5o +Q+hTZ1uucaeseGMX3oP4oUWw296PG8Li6B7bi3nJsQKBgDkeBsDi0Gcgy2TyiVMJ +G/TVNOhYQgOrSqwr3aWmpWOblzRxK7wIXlHQrXI+y1OFPlmiX/7JfxUGTrGHi6jq +LS5B4HIrh9/aHJj+I/xaSoqbS0I5PzJLsAi9H/fm11VL2DdwDOcVGc/IWaZPfu2J +Ui3Yp6XLNeZLag+VXl1mSCC/ +-----END PRIVATE KEY----- diff --git a/fixtures/tls/client-crt.pem b/fixtures/tls/client-crt.pem new file mode 100644 index 0000000..426336f --- /dev/null +++ b/fixtures/tls/client-crt.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEczCCA1ugAwIBAgIUQku/mLBI3hY7mrd18SAgAVjUnCowDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAwwMY2EubG9jYWxob3N0 +MCAXDTI0MDYyNDE2NTkyMFoYDzIwNTExMTA5MTY1OTIwWjBFMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqEteg1m/ +ueZkXLB+I+9WUagHdgDn+CCr1IsUeZCRK+sgksFRyTI5trJpRNnIyZDKSS/T/o4x +LQIT8q+gpA4IhSOiQ7q0PKKyPkawITueDA2HJuX3qbONI0uwtin34bT4BihdNsAf +oXjFjOareD6Lj3D0TdyLeHgAlJf5/WNwd1Ua+9/KnMpSD224rEGvhKP4CzReT6i7 +XiLAhZgEtQaYWCICa2HV+EVHu2m/BoORsYTaFIc9VlEUNrFJrhnKsPknrGg+cD/Y +kQ1AwAXtyjLP4hXxjFPgeaA8N/xEG7p2BzCs6Ip78EpKVLHAGcOvGDfV/jN5e4XA +8Ef4iSVR74PI9nViOIO6aXbw+DLcmf7AZolK1nbaTJ0bPyhNEZ8Kte8YIrvF6yKt +qnMo/TidaaoCUVrdA1m6wMnQjpjjHkGBTMiWkVcnaduubc3Jdh9VuNxb0ivHZ71J +anO5u/E8/LOFISa0k8BOggb4SLokEDZK++5kCwfD0S9ZSY0MwVs0gtWT/ksFjdO4 +EDuta0BbgpV3Cv+9y2nCzLkwL6rWlO3S8tZFHq+TwzvOWTPA801zLrhhQLhEDtqK +Kr8MTa3j+YiuZ/0+i+60m5dXb714dda6L3o/AAlrxSx838SOrWtYtUsGDFDni1xm +TlHTXDHZq3446MSkQATeC5vKzEK6FYi0nocCAwEAAaNCMEAwHQYDVR0OBBYEFG7f +AGtfPJB5dZm9OTLj88b3MfQbMB8GA1UdIwQYMBaAFK5hYNIOZAYkRZ4Ej/3qqEjM +5YH6MA0GCSqGSIb3DQEBCwUAA4IBAQAu8BEBvyl1oYeJls4sgNx+rXPpuHI1r3gJ +vSXv6fk40ogjjj7UO/Me5Io7OWGmIVNPj4yAZSLNG5P25ee5SWMN2Qvqgf7a2nXb +fm5Nsb91VlxPT3G5k50/FwRBe5RR7d6h7a7othqGfuLwyY7Xhbiu+XP5BCLHZD3r +Q8y/WPrabF/JajfTfZbZ1WN6XJWl8OWrCC16jNz9C8DQu72OpfmBYpdexfgrCiTb +sTHUBetY5f6ZoWUJW5sPm4TG5jpudrZe+wIQ8VjhC78LP52JVD9pZJjD+b5QIDS7 +WO5hTQD5af8f8rtIY1xAjz9tsi6YQymxBsCszfrsPmRSZ7Rbhg+B +-----END CERTIFICATE----- diff --git a/fixtures/tls/client-csr.pem b/fixtures/tls/client-csr.pem new file mode 100644 index 0000000..74d94a0 --- /dev/null +++ b/fixtures/tls/client-csr.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEijCCAnICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAKhLXoNZv7nmZFywfiPvVlGoB3YA5/ggq9SLFHmQ +kSvrIJLBUckyObayaUTZyMmQykkv0/6OMS0CE/KvoKQOCIUjokO6tDyisj5GsCE7 +ngwNhybl96mzjSNLsLYp9+G0+AYoXTbAH6F4xYzmq3g+i49w9E3ci3h4AJSX+f1j +cHdVGvvfypzKUg9tuKxBr4Sj+As0Xk+ou14iwIWYBLUGmFgiAmth1fhFR7tpvwaD +kbGE2hSHPVZRFDaxSa4ZyrD5J6xoPnA/2JENQMAF7coyz+IV8YxT4HmgPDf8RBu6 +dgcwrOiKe/BKSlSxwBnDrxg31f4zeXuFwPBH+IklUe+DyPZ1YjiDuml28Pgy3Jn+ +wGaJStZ22kydGz8oTRGfCrXvGCK7xesirapzKP04nWmqAlFa3QNZusDJ0I6Y4x5B +gUzIlpFXJ2nbrm3NyXYfVbjcW9Irx2e9SWpzubvxPPyzhSEmtJPAToIG+Ei6JBA2 +SvvuZAsHw9EvWUmNDMFbNILVk/5LBY3TuBA7rWtAW4KVdwr/vctpwsy5MC+q1pTt +0vLWRR6vk8M7zlkzwPNNcy64YUC4RA7aiiq/DE2t4/mIrmf9PovutJuXV2+9eHXW +ui96PwAJa8UsfN/Ejq1rWLVLBgxQ54tcZk5R01wx2at+OOjEpEAE3gubysxCuhWI +tJ6HAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAgEAWkd+hiTBN7Yz1uU+UisYnZVF +XqWgCHb78CAb3on9mHJijwJD8+la4VBoAZgcFeP34vLFrUmmf/H9ObXbMZtjW48a +OL4PTAUBaHXGy8OAk4zOjTJNZSUgms8YQJWfY78DsBJmf/zmpYOZX4NggczbQvQT +qvDQoz2TJStog8NJKXfJDHqrIG03FRxyQed+Akh55rlsapeNYQucrrLFf6m+Andx +xLqOXkqr8hz4jkyH5bNz9kRMFZ1CJhANQE5l/2RV8BZ02EUMYPKS71yZFYbd6PT2 +6m5LyA62Kor+6JHYMTFDaqHGcIkHVnCCzEjQn1nU/57EoLNHiRoi9mr7kbygp14o +b5b8zvj+oQBuvDwvYCxmrhKPg05iprPuiDt+n9ONtn5yfm2n+UC+TquePEAJO3DH +gsBe4SmNe7+G5xi8e11eLBZlLioZEeFcMhkphI0k3uxhPEySv4AmIRSR+H8aWAhg +67yaY9856edN4tM2sLHuuW6BLswdj33Ebylhk7DNgqTkfR27LzOUOZu3dX+/hGlP +oogJKe16FLOu2hEJjMmDblFNNBnI88gcX7vTSCmXjzdh4u5IYMpMKo+p0eYFjWmZ +S3F4qxohJ7AtSoH/b6MW8hEMI+2b5JnJ5E+IR+mu1GNGR3V7wx9G5aQ2CfJ18217 +FkqbAgbk8VD0t09467A= +-----END CERTIFICATE REQUEST----- diff --git a/fixtures/tls/client-key.pem b/fixtures/tls/client-key.pem new file mode 100644 index 0000000..d4bb298 --- /dev/null +++ b/fixtures/tls/client-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +Bq7vWNhyJVoyZuE8etGZ1mv/06Mj +-----END PRIVATE KEY----- diff --git a/fixtures/tls/server-crt.pem b/fixtures/tls/server-crt.pem new file mode 100644 index 0000000..767f7f5 --- /dev/null +++ b/fixtures/tls/server-crt.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEhzCCA2+gAwIBAgIUQku/mLBI3hY7mrd18SAgAVjUnCkwDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAwwMY2EubG9jYWxob3N0 +MCAXDTI0MDYyNDE2NTgzNVoYDzIwNTExMTA5MTY1ODM1WjBZMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQCKwN53gFREOwzrlxbznCy/aeVsPkMyCad58mO7QZYV9/dr +AvqaGFeqvPeJtt27QV0WV4SCnqoeyt9UmWh4ICgHWy5Kt6zZh25QR4/gglkNDyAs +Vnlf7z5F60bty5XxJDCZNVgK87SX50REb+xcXHNZlUEylNjiSpgfa8bd5GzotWbe +pxsIfUynua64VYqmySTwtomsElVtJG1EgmtVdvP+3jqlZYUMQ00+VxzKSA1VnsYa +DJ1FaMSyjya4LAvawg7FG8bX/JVN7bRKXITcFauZVMMP8iJZzgxWqxI38A2W0m0N +dJR/qhqZULOERWf9BLcgc2CGtz/Lg51oPVgwutrMboqVz4uUlhu3Qn/qsVGRR7ag +qAReD4QeDPhajpcm1xHMCw3m2268UIZlB+IFLyX8fLl+njsUuRMYXhxy/hMbJuum +AE25FjT6nrdHqOogOQjBrRk1rCXq2wL6gTJNGtGktsyWox8Ev65TPD/M/fFLIHy8 +QGkgv4UDzyCCOCqN3zGNXgg0Ec/CoGbHH8K007fjyv7s5BH9UkKX5JCEFO+DUu0s +gEh1MIQYosrM0UIoOx+HGVJsgBLY1DIhC6av2p3rtnOJ7Ne8u+RXxxXAEoDm3ruR +AMbNu4VQmzx4h3vvStDjwxAy4Tj/01RbDasUWbZbYM1xKxxO+aR5cYme4UgnHQID +AQABo0IwQDAdBgNVHQ4EFgQUfyh3c+vZniS9RuO7BJ7vsOsLqu0wHwYDVR0jBBgw +FoAUrmFg0g5kBiRFngSP/eqoSMzlgfowDQYJKoZIhvcNAQELBQADggEBAG5zAJ2o +YtgUB+PQBl3A4+735yJeirKBxnbu1HffjiPuV1KXpvf04DKoJ1Py2mcYwF+Jd5jH +ck0LErLOkMfxBDk6+NsazdfeiX6eF49sMKk7xiKgK50iz9vNgfswn9dcUsS+t1cH +72ruJdsK+8txbQ4N0itfkE9QBSjweHBW/yXiZcx8fSbVfK79f6a+nQgYoi9oOCvk +yaIrlDMU3Q4RjRwyINLGKsnp2EFREtXqCLi0McgzPNNbwJU2kQfPps0uLyMwGliP +LDLUdiJOeeVxvDh0UqN0qARY0D4vg+S42BvcpJD9RRk3uT0QsFrVBY0CUyqdD93y +HzeFx50aG21vNvc= +-----END CERTIFICATE----- diff --git a/fixtures/tls/server-csr.pem b/fixtures/tls/server-csr.pem new file mode 100644 index 0000000..1f65b4a --- /dev/null +++ b/fixtures/tls/server-csr.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEnjCCAoYCAQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9j +YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAisDed4BURDsM +65cW85wsv2nlbD5DMgmnefJju0GWFff3awL6mhhXqrz3ibbdu0FdFleEgp6qHsrf +VJloeCAoB1suSres2YduUEeP4IJZDQ8gLFZ5X+8+RetG7cuV8SQwmTVYCvO0l+dE +RG/sXFxzWZVBMpTY4kqYH2vG3eRs6LVm3qcbCH1Mp7muuFWKpskk8LaJrBJVbSRt +RIJrVXbz/t46pWWFDENNPlccykgNVZ7GGgydRWjEso8muCwL2sIOxRvG1/yVTe20 +SlyE3BWrmVTDD/IiWc4MVqsSN/ANltJtDXSUf6oamVCzhEVn/QS3IHNghrc/y4Od +aD1YMLrazG6Klc+LlJYbt0J/6rFRkUe2oKgEXg+EHgz4Wo6XJtcRzAsN5ttuvFCG +ZQfiBS8l/Hy5fp47FLkTGF4ccv4TGybrpgBNuRY0+p63R6jqIDkIwa0ZNawl6tsC ++oEyTRrRpLbMlqMfBL+uUzw/zP3xSyB8vEBpIL+FA88ggjgqjd8xjV4INBHPwqBm +xx/CtNO348r+7OQR/VJCl+SQhBTvg1LtLIBIdTCEGKLKzNFCKDsfhxlSbIAS2NQy +IQumr9qd67ZziezXvLvkV8cVwBKA5t67kQDGzbuFUJs8eId770rQ48MQMuE4/9NU +Ww2rFFm2W2DNcSscTvmkeXGJnuFIJx0CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IC +AQBcbMoBEeEQhVlTDM9exlQw6ziz0bws0S7YDhjcR52d9lHgx4H/+dHx++Ao3YuU +psZWBt9oCxYw7vgzuh2nBL6vygLL63+co2Dgc1dtO0HTepAB62cGbOGCvcM4gn8j +a5rKGIg3fogH2Dba9M03bxcp8Bg3bsL5gIr2Eun6p3orDiD3TPTJlNxh7+8QyzjH +hjUdtI3QvBaknosu6ZfpphQMn6bEZPYPz9wLPEkbmLJpbpkShQOIUPnQKJXcyxf4 +vWJAlrigwS1BSL8ST21VWnE0JaJHPsnJy/hrRcxpplErtQVLO7am3SWNnwC8qZ37 +vJwpGSv1jRO0IPTSqo0X8AjbuE6xZZRyWau2yy/OIHjspwkqtrUHs9+QxgBMAOFs +oMwyLWXL2749xgx/QV9qCUB4l+ti/cU8xJeVg3KjWgUC9AkFLz/hSODczQr5inth +xZEuWoprVThGoXGpxUIle4xO17ijDuyWN55/Yw7zMhx0NZ7/F0SRj1dzAkCacjyY +PPeD19BFuO1AGwjJePRlxWMCCuaDsahyjF/IBfo+4AeUqvsTo2OKN0XvKmnLObEH +5C8/oHzhzWap/FrqfP33EnAlQ+1rbvfwRMrHu6DTXiD55Ea3gB+AiIjRIVmEcKpA +g5CFF9n6KlZSGcurKlIL6hoQnG4nkZllU5x42WlNDZFs0g== +-----END CERTIFICATE REQUEST----- diff --git a/fixtures/tls/server-key.pem b/fixtures/tls/server-key.pem new file mode 100644 index 0000000..eeca6a8 --- /dev/null +++ b/fixtures/tls/server-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +/wYgCBkU4ziOL8359iL0tyapYWDn5g== +-----END PRIVATE KEY----- diff --git a/lib/Tunnel.js b/lib/Tunnel.js index 2855636..802b9f5 100644 --- a/lib/Tunnel.js +++ b/lib/Tunnel.js @@ -3,7 +3,7 @@ const { parse } = require('url'); const { EventEmitter } = require('events'); const axios = require('axios'); -const debug = require('debug')('mytunnel:client'); +const debug = require('debug')('mytunnel:Tunnel'); const TunnelCluster = require('./TunnelCluster'); @@ -13,15 +13,29 @@ module.exports = class Tunnel extends EventEmitter { this.opts = opts; this.closed = false; if (!this.opts.host) { - this.opts.host = 'https://localtunnel.me'; + this.opts.host = 'http://localhost:8087'; } + this.destroyTimer = null; } _getInfo(body) { /* eslint-disable camelcase */ const { id, ip, port, url, cached_url, max_conn_count, is_tunnel_secure } = body; - const { host, port: local_port, local_host } = this.opts; - const { local_https, local_cert, local_key, local_ca, allow_invalid_cert, local_max_reconnect_count } = this.opts; + const { + host, + port: local_port, + local_host, + local_https, + local_cert, + local_key, + local_ca, + allow_invalid_cert, + local_max_retries, + local_reconnect_delay, + connect_timeout, + idle_timeout, + } = this.opts; + return { name: id, url, @@ -38,7 +52,10 @@ module.exports = class Tunnel extends EventEmitter { local_ca, allow_invalid_cert, is_tunnel_secure, - local_max_reconnect_count, + local_max_retries, + local_reconnect_delay, + connect_timeout: connect_timeout ?? 10_000, + idle_timeout: idle_timeout ?? 15_000, }; /* eslint-enable camelcase */ } @@ -51,6 +68,7 @@ module.exports = class Tunnel extends EventEmitter { const params = { responseType: 'json', + timeout: opt.connect_timeout, }; const baseUri = `${opt.host}/`; @@ -58,29 +76,26 @@ module.exports = class Tunnel extends EventEmitter { const assignedDomain = opt.subdomain; // where to quest let uri = baseUri + (assignedDomain || '?new'); - if(opt.request_secure_tunnel) { - uri += assignedDomain ? "?" : "&"; - uri += "secureTunnel" + if (opt.request_secure_tunnel) { + uri += assignedDomain ? '?' : '&'; + uri += 'secureTunnel'; } (function getUrl() { axios - .get(uri, params) - .then(res => { - const body = res.data; - debug('got tunnel information', res.data); - if (res.status !== 200) { - const err = new Error( - (body && body.message) || 'localtunnel server returned an error, please try again' - ); - return cb(err); - } - cb(null, getInfo(body)); - }) - .catch(err => { - debug(`tunnel server offline: ${err.message}, retry 1s`); - return setTimeout(getUrl, 1000); - }); + .get(uri, params) + .then(res => { + const body = res.data; + debug('got tunnel information', res.data); + if (res.status !== 200) { + const err = new Error((body && body.message) || 'localtunnel server returned an error, please try again'); + return cb(err); + } + cb(null, getInfo(body)); + }) + .catch(err => { + return cb(err); + }); })(); } @@ -104,11 +119,12 @@ module.exports = class Tunnel extends EventEmitter { let tunnelCount = 0; - let atLeastOneSocketOpen = false + let atLeastOneSocketOpen = false; // track open count this.tunnelCluster.on('open', tunnel => { tunnelCount++; debug('tunnel open [total: %d]', tunnelCount); + this.cancelDestroy(); const closeHandler = () => { tunnel.destroy(); @@ -130,18 +146,25 @@ module.exports = class Tunnel extends EventEmitter { } this.once('close', closeHandler); tunnel.once('close', () => { + debug('tunnel close'); this.removeListener('close', closeHandler); }); }); // when a tunnel dies, open a new one - this.tunnelCluster.on('dead', () => { - tunnelCount--; + this.tunnelCluster.on('dead', tunnelOptions => { + if (tunnelCount > 0) { + tunnelCount -= 1; + this.scheduleDestroy(info.connect_timeout); + } else { + debug('Emitted dead tunnels more than it should'); + } + debug('tunnel dead [total: %d]', tunnelCount); if (this.closed) { return; } - this.tunnelCluster.open(); + setTimeout(() => this.tunnelCluster.open(tunnelOptions), 1000); }); this.tunnelCluster.on('request', req => { @@ -150,8 +173,17 @@ module.exports = class Tunnel extends EventEmitter { // establish as many tunnels as allowed for (let count = 0; count < info.max_conn; ++count) { - this.tunnelCluster.open(); + const idleMonitoring = count === 0; + this.tunnelCluster.open({ idleMonitoring }); } + + this.scheduleDestroy(info.connect_timeout); + + this.on('error', error => { + if (!atLeastOneSocketOpen) { + cb(error); + } + }); } open(cb) { @@ -176,4 +208,21 @@ module.exports = class Tunnel extends EventEmitter { this.closed = true; this.emit('close'); } + + scheduleDestroy(ms) { + if (!this.destroyTimer) { + this.destroyTimer = setTimeout(() => this.destroy(), ms); + } + } + + cancelDestroy() { + if (this.destroyTimer !== null) { + clearTimeout(this.destroyTimer); + this.destroyTimer = null; + } + } + + destroy() { + this.emit('error', new Error('Tunnel timed out')); + } }; diff --git a/lib/TunnelCluster.js b/lib/TunnelCluster.js index 7667d45..7e277c1 100644 --- a/lib/TunnelCluster.js +++ b/lib/TunnelCluster.js @@ -1,22 +1,28 @@ const { EventEmitter } = require('events'); -const debug = require('debug')('mytunnel:client'); +const { randomUUID } = require('crypto'); +const Debug = require('debug'); const fs = require('fs'); const net = require('net'); const tls = require('tls'); +const pump = require('pump'); const HeaderHostTransformer = require('./HeaderHostTransformer'); +const getSocketId = () => randomUUID().slice(0, 7); + // manages groups of tunnels module.exports = class TunnelCluster extends EventEmitter { constructor(opts = {}) { super(opts); this.opts = opts; - this.localReconnectionRetryCount = 0; + this.localReconnectionRetryCount = 0; } - open() { + open({ idleMonitoring = false } = {}) { const opt = this.opts; const self = this; + const id = getSocketId(); + const debug = Debug(`mytunnel:TunnelCluster:${id}`); // Prefer IP if returned by the server const remoteHostOrIp = opt.remote_ip || opt.remote_host; @@ -26,25 +32,30 @@ module.exports = class TunnelCluster extends EventEmitter { const localProtocol = opt.local_https ? 'https' : 'http'; const allowInvalidCert = opt.allow_invalid_cert; const isTunnelSecure = opt.is_tunnel_secure; - const localReconnectionMaxRetryCount = opt.local_max_reconnect_count !== undefined ? opt.local_max_reconnect_count : 90; + const localReconnectionMaxRetryCount = + opt.local_max_retries !== undefined ? opt.local_max_retries : Infinity; + const localReconnectionDelay = opt.local_reconnect_delay !== undefined ? opt.local_reconnect_delay : 1000; debug( - 'establishing tunnel %s://%s:%s <> %s:%s', + 'establishing tunnel %s://%s:%s <> %s:%s [secure:%s]', localProtocol, localHost, localPort, remoteHostOrIp, - remotePort + remotePort, + isTunnelSecure ); const connectionOptions = { host: remoteHostOrIp, port: remotePort, + timeout: opt.connect_timeout, }; - + // connection to localtunnel server - debug(`Tunnel secure state: ${isTunnelSecure}`); - const remote = isTunnelSecure ? tls.connect({...connectionOptions, servername: remoteHostOrIp}) : net.connect(connectionOptions); + const remote = isTunnelSecure + ? tls.connect({ ...connectionOptions, servername: remoteHostOrIp }) + : net.connect(connectionOptions); remote.setKeepAlive(true); @@ -56,19 +67,22 @@ module.exports = class TunnelCluster extends EventEmitter { if (err.code === 'ECONNREFUSED') { this.emit( 'error', - new Error( - `connection refused: ${remoteHostOrIp}:${remotePort} (check your firewall settings)` - ) + new Error(`connection refused: ${remoteHostOrIp}:${remotePort} (check your firewall settings)`) ); } - - remote.end(); }); + // idle connection detection + if (idleMonitoring) { + remote.once('timeout', () => { + debug('remote timeout'); + remote.end(); + }); + } + const connLocal = () => { if (remote.destroyed) { debug('remote destroyed'); - this.emit('dead'); return; } @@ -93,38 +107,26 @@ module.exports = class TunnelCluster extends EventEmitter { ? tls.connect({ host: localHost, port: localPort, ...getLocalCertOpts() }) : net.connect({ host: localHost, port: localPort }); - const remoteClose = () => { - debug('remote close'); - this.emit('dead'); - local.end(); - }; - - remote.once('close', remoteClose); - - // TODO some languages have single threaded servers which makes opening up - // multiple local connections impossible. We need a smarter way to scale - // and adjust for such instances to avoid beating on the door of the server local.once('error', err => { - debug('local error %s', err.message); + debug('local error %O', err); local.end(); - - remote.removeListener('close', remoteClose); - remote.end(); - if (self.localReconnectionRetryCount < localReconnectionMaxRetryCount) { - self.localReconnectionRetryCount++; - debug(`Local server connection is lost, reconnnecting. Attempt ${self.localReconnectionRetryCount}/${localReconnectionMaxRetryCount}`); - + self.localReconnectionRetryCount += 1; + debug( + `Local server connection is lost, reconnecting. Attempt ${self.localReconnectionRetryCount}/${localReconnectionMaxRetryCount}` + ); + // retrying connection to local server - setTimeout(connLocal, 1000); - } + setTimeout(connLocal, localReconnectionDelay); + } else { + this.emit('error', new Error('Local server unreachable')); + } }); local.once('connect', () => { debug('connected locally'); remote.resume(); - - debug(`Local reconnection counter is reset at value ${self.localReconnectionRetryCount}`); + self.localReconnectionRetryCount = 0; let stream = remote; @@ -136,7 +138,10 @@ module.exports = class TunnelCluster extends EventEmitter { stream = remote.pipe(new HeaderHostTransformer({ host: opt.local_host })); } - stream.pipe(local).pipe(remote); + pump(stream, local, remote, err => { + debug('stream finished', err); + this.emit('dead'); + }); // when local closes, also get a new remote local.once('close', hadError => { @@ -157,6 +162,7 @@ module.exports = class TunnelCluster extends EventEmitter { // tunnel is considered open when remote connects remote.once('connect', () => { + remote.setTimeout(opt.idle_timeout); this.emit('open', remote); connLocal(); }); diff --git a/package-lock.json b/package-lock.json index d30f36d..64d1412 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,10 @@ "version": "1.2.0", "license": "MIT", "dependencies": { - "axios": "0.27.2", + "axios": "1.7.2", "debug": "4.3.1", "openurl": "1.1.1", + "pump": "^3.0.0", "yargs": "17.5.1" }, "bin": { @@ -21,19 +22,43 @@ "@types/node": "16.11.39", "chai": "^4.3.0", "chai-string": "^1.5.0", + "concurrently": "^8.2.2", "mocha": "~10.0.0", + "nock": "^13.5.4", + "selfsigned": "^2.4.1", "supertest": "^6.1.3" }, "engines": { "node": ">=12" } }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@types/node": { "version": "16.11.39", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.39.tgz", "integrity": "sha512-K0MsdV42vPwm9L6UwhIxMAOmcvH/1OoVkZyCgEtVu4Wx7sElGloy/W7kMBNe/oJ7V/jW9BVt1F6RahH6e7tPXw==", "dev": true }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -105,12 +130,13 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axios/node_modules/form-data": { @@ -307,12 +333,96 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", "dev": true }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -363,6 +473,14 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -427,9 +545,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -677,6 +795,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -692,6 +816,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -877,6 +1007,29 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -890,7 +1043,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -969,6 +1121,29 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.9.6", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", @@ -1016,6 +1191,12 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1024,6 +1205,15 @@ "node": ">=0.10.0" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1044,6 +1234,19 @@ } ] }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", @@ -1068,6 +1271,21 @@ "randombytes": "^2.1.0" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1176,6 +1394,21 @@ "node": ">=8.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -1216,8 +1449,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/y18n": { "version": "5.0.5", @@ -1320,12 +1552,30 @@ } }, "dependencies": { + "@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, "@types/node": { "version": "16.11.39", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.39.tgz", "integrity": "sha512-K0MsdV42vPwm9L6UwhIxMAOmcvH/1OoVkZyCgEtVu4Wx7sElGloy/W7kMBNe/oJ7V/jW9BVt1F6RahH6e7tPXw==", "dev": true }, + "@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -1379,12 +1629,13 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" }, "dependencies": { "form-data": { @@ -1542,12 +1793,72 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", "dev": true }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -1581,6 +1892,14 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1624,9 +1943,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "form-data": { "version": "3.0.1", @@ -1797,6 +2116,12 @@ "argparse": "^2.0.1" } }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1806,6 +2131,12 @@ "p-locate": "^5.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -1940,6 +2271,23 @@ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true }, + "nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1950,7 +2298,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "requires": { "wrappy": "1" } @@ -2002,6 +2349,26 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "qs": { "version": "6.9.6", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", @@ -2037,17 +2404,42 @@ "picomatch": "^2.2.1" } }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, + "selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "requires": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + } + }, "semver": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", @@ -2066,6 +2458,18 @@ "randombytes": "^2.1.0" } }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, + "spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2146,6 +2550,18 @@ "is-number": "^7.0.0" } }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -2177,8 +2593,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "y18n": { "version": "5.0.5", diff --git a/package.json b/package.json index fb875eb..4eb6109 100644 --- a/package.json +++ b/package.json @@ -15,19 +15,25 @@ "et": "bin/et.js" }, "scripts": { + "dev:client": "nodemon --inspect ./bin/et.js --port 3001 --host http://lvh.me:8087 --subdomain test --open --idle-timeout=6000000", + "dev:backend": "nodemon server.js", + "dev": "DEBUG=mytunnel:* concurrently --raw npm:dev:backend npm:dev:client", "test": "mocha --timeout 15000 --exit -- *.spec.js" }, "dependencies": { - "axios": "0.27.2", + "axios": "1.7.2", "debug": "4.3.1", "openurl": "1.1.1", + "pump": "3.0.0", "yargs": "17.5.1" }, "devDependencies": { "@types/node": "16.11.39", "chai": "^4.3.0", "chai-string": "^1.5.0", + "concurrently": "^8.2.2", "mocha": "~10.0.0", + "nock": "^13.5.4", "supertest": "^6.1.3" }, "publishConfig": { @@ -35,6 +41,6 @@ "registry": "https://registry.npmjs.org/" }, "engines": { - "node": ">=12" + "node": ">=16" } } diff --git a/server.js b/server.js new file mode 100644 index 0000000..f4bde46 --- /dev/null +++ b/server.js @@ -0,0 +1,52 @@ +const http = require('http'); +const Debug = require('debug'); +const { randomUUID } = require('crypto'); + +// Create an HTTP server +const server = http.createServer( + { connectionsCheckingInterval: 1000, keepAlive: false, requestTimeout: 10_000, headersTimeout: 10_000 }, + (req, res) => { + res.end('Hello, world!\n'); + } +); + +// Event listener for when a client connects to the server +let connections = 0; +server.on('connection', socket => { + const socketId = randomUUID().slice(0, 7); + const debug = Debug(`mytunnel:TestBackend:${socketId}`); + debug(`A new connection was made by a client, total`, ++connections); + socket.on('data', data => debug('client data', data)); + socket.on('error', err => debug('client error', err)); + socket.on('close', err => { + debug('client close (%s) [total: %s]', err, --connections); + }); + socket.on('end', () => debug('client end')); + socket.on('timeout', () => debug('client timeout')); + + // Simulate broken server + if (connections === 5) { + setTimeout(() => socket.resetAndDestroy(), 5000); + } + if (connections === 6) { + setTimeout(() => socket.destroy(), 6000); + } + if (connections === 7) { + setTimeout(() => socket.destroySoon(), 7000); + } + if (connections === 8) { + setTimeout(() => socket.end(), 8000); + } +}); +const debug = Debug('mytunnel:TestBackend'); +server.on('error', err => debug('server error', err)); +server.on('close', err => debug('server close', err)); +server.on('', err => debug('server close', err)); + +// Define the port to listen on +const port = 3001; + +// The server starts listening on the specified port +server.listen(port, () => { + console.info(`Server is listening on port ${port}`); +}); diff --git a/tmp/ca.pem b/tmp/ca.pem new file mode 100644 index 0000000..7616eae --- /dev/null +++ b/tmp/ca.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIJbKccrvS/UjHVMA0GCSqGSIb3DQEBCwUAMGkxFDASBgNV +BAMTC2V4YW1wbGUub3JnMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWEx +EzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRl +c3QwHhcNMjQwNjI0MTY1NTEwWhcNMjUwNjI0MTY1NTEwWjBpMRQwEgYDVQQDEwtl +eGFtcGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYD +VQQHEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuaErUfFUL4k71gD9i3mMVnd8 +YQ4lryvOilzYlqYDPpgSUM5MSFzmINBvCJhN62R2RE9gfjTCkS4VFZZLomdW2Ejr +4k7VJx93osqmOqImUiVEDtC3w6+yo7jkkJOEAzdxGODzgGezw6dC3o3lu42saIlz +hvOYBIKN3l0jSjaMfy4bdYLurpxIGGD8tfQ489wcRNgbNwLOJVLrj7Jh/rHuW471 +VIQPhKDJIQbFHnnq6pNzfMh7ouujFf6P8aP+g1NBsYQwdimimPpKxWJOE817OYTm +a7AXguSzRptPxkFD/Luk40o+6K0vt8+YfZ7DcEAQYeAYc3y7XPb2GZGkE+3AuQID +AQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCHPOre0wUS +/I8g20fLdTjowzMSdNKNGTj3l0sbidfHdRw71rOVNmiNFB2NlVh0zAZPTxFE47pg +wMD7OJSH7hTlvEHlPSV8tGAIWjvx8M3IFo3lfbsQxE0Tp/akOmr/UcPXqa6ZhG8B +5O04m775i9ZwSlmslnyHf/wjtTXPOLai9YXbGSgSjzpTS/mJ0XFEC/sfeOnKoWI/ +1nMcpFHPFJzfEMrzdIHHn8rrBozzscyVTxtn2qDEosaZ7OnOM0Pd7eftTMlxiU2I +KKAfGgN2e9NXGU7m0Kal9wPaBrI2K0z+ReLSuifEQwwiTbNxDAwxvZH14zKd4osL +fPKKz103VMnY +-----END CERTIFICATE----- diff --git a/tmp/clientcert.pem b/tmp/clientcert.pem new file mode 100644 index 0000000..f5a5ca5 --- /dev/null +++ b/tmp/clientcert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC1DCCAbygAwIBAgIJEeYcfeOYH4Z/MA0GCSqGSIb3DQEBBQUAMGkxFDASBgNV +BAMTC2V4YW1wbGUub3JnMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWEx +EzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRl +c3QwHhcNMjQwNjI0MTY1NTEwWhcNMjUwNjI0MTY1NTEwWjBuMRkwFwYDVQQDExBK +b2huIERvZSBqZG9lMTIzMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWEx +EzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRl +c3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANAsp9si+h4dIXHwubPDDhoA +xFdgy23YlgyuvdW6IE/JkHDteZnyFOwQxOU9WlUjtHFACWEF9c/BOIgIxeVSAl6Y +KZu3ILPQ3FW5ha0tBwCARL9f5QgbI6T244EQoCtoR279PMd2Q2OBKihyQyFjmO9T +J/u8WvIH+QKbvRz6Sy5/AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAE/F5lhctUmS +6Ypr9gd1GKMdvX6jhB/LXfk5inc/83Trg8jUeMn5FR64rj+FPGNK+vjp+s1GwRsU +ls1oC9x5HWMoyn3vykNvMubHByspu454jAETpc1ywzaAek1v1g9nWA0I5zFeneug +z+mSX/rN9AG7Jy0qbfSxsZSpgk3Ac0mDsbRLhNXbM3voosIwBMwEfHiMnAPcAkTM +jI4AD1CwRhBahvAScQzpB0Gomo0EGK0KdPYEhXnbzbxe/i1rokHeiLGuJlQMGwKH +jXT4zilp6p4mY81GuVrJ10NiOweCwku66df3+VMTpfbhzl6zPbbc37Wnq/418D90 +iZChKzfua0Q= +-----END CERTIFICATE----- diff --git a/tmp/clientprivate.pem b/tmp/clientprivate.pem new file mode 100644 index 0000000..d7e40a9 --- /dev/null +++ b/tmp/clientprivate.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDQLKfbIvoeHSFx8Lmzww4aAMRXYMtt2JYMrr3VuiBPyZBw7XmZ +8hTsEMTlPVpVI7RxQAlhBfXPwTiICMXlUgJemCmbtyCz0NxVuYWtLQcAgES/X+UI +GyOk9uOBEKAraEdu/TzHdkNjgSoockMhY5jvUyf7vFryB/kCm70c+ksufwIDAQAB +AoGAN8tQLdaBDOMn3J71Vq23sNZ1ySmDPGypQrru3EKneFsAoJO1XMJaQy73Zq17 +8YfBS/0qxltPl6Ak46jeSWkigvYppSk6tvcovJVRVJV1C3lvIJT8yBQrTXbU41ba +HJYt9kIaI0Y3dJV1s+2SlxE5aKUXH4P0mc6SLAd3CwZAPGkCQQDp578oaZ83AAFb ++be07dXVbkDlc/9GJZkZJGVaCrA2mJUOtILmTkj7E4b0ERk1iyXhn0iBBNWqJMjW +FlpxYqAlAkEA49aw5PB4xeE69hz52DUi0b4rSSU/F+Xm39NVyBTMEDoH4lAqUrvy +EP5BuGXv0Gu022uwK4V7L3BaVi9foN1w0wJBAJxf6LZ11pGImWAKFL0K/BhSO2Sr +JhCZdj8OzAtkdeYqIAzStWiPEc95gJGPFNFtE4hBWtGWj9nN+c7W5uRC0VkCQQDd +xMi0Xzk/aydIDhZHFBmLTO63KPEL4vTiIsQZl9y1Yrbv25YGp/hMACCaS2atWVlW +RrDY+/lWyKa1wzDy1PiPAkEA3Xym3qOs0BnDQ2rFOcsnZpG1Lub85rejpKT/x3PJ +OeOZzUkjMFHQKpIiXWSsZjQSzuQ2n5I1yrc4ogSx4nrEwQ== +-----END RSA PRIVATE KEY-----