diff --git a/lib/utils/http-requestor.js b/lib/utils/http-requestor.js index e93e6851..e664dee2 100644 --- a/lib/utils/http-requestor.js +++ b/lib/utils/http-requestor.js @@ -1,9 +1,11 @@ -const bent = require('bent'); +const {Client, Pool} = require('undici'); const parseUrl = require('parse-url'); const assert = require('assert'); const BaseRequestor = require('./base-requestor'); const {HookMsgTypes} = require('./constants.json'); const snakeCaseKeys = require('./snakecase-keys'); +const pools = new Map(); +const HTTP_TIMEOUT = 10000; const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64'); @@ -22,21 +24,41 @@ class HttpRequestor extends BaseRequestor { this.method = hook.method || 'POST'; this.authHeader = basicAuth(hook.username, hook.password); - const u = parseUrl(this.url); - //const myPort = u.port ? `:${u.port}` : ''; - const baseUrl = this._baseUrl = `${u.protocol}://${u.resource}`; - this.get = bent(baseUrl, 'GET', 'buffer', 200, 201); - this.post = bent(baseUrl, 'POST', 'buffer', 200, 201); - - assert(this._isAbsoluteUrl(this.url)); assert(['GET', 'POST'].includes(this.method)); + + const u = this._parsedUrl = parseUrl(this.url); + this._baseUrl = `${u.protocol}://${u.resource}`; + this._resource = u.resource; + this._protocol = u.protocol; + this._usePools = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL); + + if (this._usePools) { + if (pools.has(this._baseUrl)) { + this.client = pools.get(this._baseUrl); + } + else { + const connections = process.env.HTTP_POOLSIZE ? parseInt(process.env.HTTP_POOLSIZE) : 10; + const pipelining = process.env.HTTP_PIPELINING ? parseInt(process.env.HTTP_PIPELINING) : 1; + const pool = this.client = new Pool(this._baseUrl, { + connections, + pipelining + }); + pools.set(this._baseUrl, pool); + this.logger.debug(`HttpRequestor:created pool for ${this._baseUrl}`); + } + } + else this.client = new Client(`${u.protocol}://${u.resource}`); } get baseUrl() { return this._baseUrl; } + close() { + if (!this._usePools && !this.client?.closed) this.client.close(); + } + /** * Make an HTTP request. * All requests use json bodies. @@ -57,6 +79,7 @@ class HttpRequestor extends BaseRequestor { const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null; const url = hook.url || hook; const method = hook.method || 'POST'; + let buf = ''; assert.ok(url, 'HttpRequestor:request url was not provided'); assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`); @@ -64,14 +87,46 @@ class HttpRequestor extends BaseRequestor { this.logger.debug({url: urlInfo, method: methodInfo, payload}, `HttpRequestor:request ${method} ${url}`); const startAt = process.hrtime(); - let buf; + let newClient; try { + let client, path; + if (this._isRelativeUrl(url)) { + client = this.client; + path = url; + } + else { + const u = parseUrl(url); + if (u.resource === this._resource && u.protocol === this._protocol) { + client = this.client; + path = u.pathname; + } + else { + client = newClient = new Client(`${u.protocol}://${u.resource}`); + path = u.pathname; + } + } const sigHeader = this._generateSigHeader(payload, this.secret); - const headers = {...sigHeader, ...this.authHeader, ...httpHeaders}; - this.logger.debug({url, headers}, 'send webhook'); - buf = this._isRelativeUrl(url) ? - await this.post(url, payload, headers) : - await bent(method, 'buffer', 200, 201, 202)(url, payload, headers); + const hdrs = { + ...sigHeader, + ...this.authHeader, + ...httpHeaders, + ...('POST' === method && {'Content-Type': 'application/json'}) + }; + const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url; + this.logger.debug({url, absUrl, hdrs}, 'send webhook'); + const {statusCode, headers, body} = await client.request({ + path, + method, + headers: hdrs, + ...('POST' === method && {body: JSON.stringify(payload)}), + timeout: HTTP_TIMEOUT, + followRedirects: false + }); + if (![200, 202, 204].includes(statusCode)) throw new Error({statusCode}); + if (headers['content-type'].includes('application/json')) { + buf = await body.json(); + } + if (newClient) newClient.close(); } catch (err) { if (err.statusCode) { this.logger.info({baseUrl: this.baseUrl, url}, @@ -93,20 +148,15 @@ class HttpRequestor extends BaseRequestor { } this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert')); + if (newClient) newClient.close(); throw err; } const rtt = this._roundTrip(startAt); if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']); - if (buf && buf.toString().length > 0) { - try { - const json = JSON.parse(buf.toString()); - this.logger.info({response: json}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`); - return json; - } - catch (err) { - //this.logger.debug({err, url, method}, `HttpRequestor:request returned non-JSON content: '${buf.toString()}'`); - } + if (buf && Array.isArray(buf)) { + this.logger.info({response: buf}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`); + return buf; } } } diff --git a/lib/utils/ws-requestor.js b/lib/utils/ws-requestor.js index 1d6d24b1..d440a082 100644 --- a/lib/utils/ws-requestor.js +++ b/lib/utils/ws-requestor.js @@ -158,7 +158,7 @@ class WsRequestor extends BaseRequestor { close() { this.closedGracefully = true; - this.logger.info('WsRequestor:close closing socket'); + this.logger.debug('WsRequestor:close closing socket'); try { if (this.ws) { this.ws.close(); diff --git a/package-lock.json b/package-lock.json index 7862eedd..a7a3f118 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "sdp-transform": "^2.14.1", "short-uuid": "^4.2.0", "to-snake-case": "^1.0.0", + "undici": "^5.7.0", "uuid": "^8.3.2", "verify-aws-sns-signature": "^0.0.7", "ws": "^8.8.0", @@ -4272,9 +4273,9 @@ "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=" }, "node_modules/moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "engines": { "node": "*" } @@ -5881,6 +5882,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.7.0.tgz", + "integrity": "sha512-ORgxwDkiPS+gK2VxE7iyVeR7JliVn5DqhZ4LgQqYLBXsuK+lwOEmnJ66dhvlpLM0tC3fC7eYF1Bti2frbw2eAA==", + "engines": { + "node": ">=12.18" + } + }, "node_modules/unix-dgram": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz", @@ -9529,9 +9538,9 @@ "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=" }, "moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "ms": { "version": "2.1.2", @@ -10773,6 +10782,11 @@ } } }, + "undici": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.7.0.tgz", + "integrity": "sha512-ORgxwDkiPS+gK2VxE7iyVeR7JliVn5DqhZ4LgQqYLBXsuK+lwOEmnJ66dhvlpLM0tC3fC7eYF1Bti2frbw2eAA==" + }, "unix-dgram": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz", diff --git a/package.json b/package.json index c78dfac3..1b76673b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "scripts": { "start": "node app", - "test": "NODE_ENV=test JAMBONES_HOSTING=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=info ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ", + "test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=info ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test", "jslint": "eslint app.js lib" }, @@ -55,6 +55,7 @@ "sdp-transform": "^2.14.1", "short-uuid": "^4.2.0", "to-snake-case": "^1.0.0", + "undici": "^5.7.0", "uuid": "^8.3.2", "verify-aws-sns-signature": "^0.0.7", "ws": "^8.8.0",