UNPKG

connect-qos

Version:

Connect middleware that helps maintain a high quality of service during heavy traffic

184 lines (183 loc) 7.46 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "ConnectQOS", { enumerable: true, get: function() { return ConnectQOS; } }); const _toobusyjs = /*#__PURE__*/ _interop_require_default(require("toobusy-js")); const _metrics = require("./metrics"); const _util = require("./util"); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } class ConnectQOS { constructor(opts){ const { minLag = 70, maxLag = 300, errorStatusCode = 503, errorResponseDelay = 0, httpBehindProxy = false, httpsBehindProxy = false, ...metricOptions } = opts || {}; this.#minLag = minLag; this.#maxLag = maxLag; this.#lagRange = maxLag - minLag; this.#errorStatusCode = errorStatusCode; this.#httpBehindProxy = httpBehindProxy; this.#httpsBehindProxy = httpsBehindProxy; this.#errorResponseDelay = errorResponseDelay; // we only require `toobusy.lag` feature and can ignore toobusy() via maxLag // toobusy.maxLag(this.#maxLag); this.#metrics = new _metrics.Metrics(metricOptions); this.#hostRateRange = this.#metrics.maxHostRate - this.#metrics.minHostRate; this.#ipRateRange = this.#metrics.maxIpRate - this.#metrics.minIpRate; } #minLag; #maxLag; #lagRange; #hostRateRange; #ipRateRange; #errorStatusCode; #httpBehindProxy; #httpsBehindProxy; #errorResponseDelay; #metrics; get minLag() { return this.#minLag; } get maxLag() { return this.#maxLag; } get errorStatusCode() { return this.#errorStatusCode; } get httpBehindProxy() { return this.#httpBehindProxy; } get httpsBehindProxy() { return this.#httpsBehindProxy; } get metrics() { return this.#metrics; } getMiddleware({ beforeThrottle, destroySocket = true } = {}) { const self = this; function sendError(res) { res.statusCode = self.#errorStatusCode; res.end(); if (destroySocket) { if (res.stream) { res.stream.session?.destroy(); } else if (res.socket?.destroyed === false) { res.socket.destroySoon(); } } } return function QOSMiddleware(req, res, next) { const reason = self.shouldThrottleRequest(req); if (reason) { if (!beforeThrottle || beforeThrottle(self, req, reason) !== false) { // if no throttle handler OR the throttle handler does not explicitly reject, do it return void self.#errorResponseDelay ? setTimeout(sendError, self.#errorResponseDelay, res).unref() : sendError(res); } } // continue next(); }; } shouldThrottleRequest(req) { const host = this.resolveHost(req); const hostStatus = this.getHostStatus(host, false); // defer tracking const ipStatus = this.getIpStatus(req, false); // defer tracking // never throttle whitelisted actors if (hostStatus === _metrics.ActorStatus.Whitelisted || ipStatus === _metrics.ActorStatus.Whitelisted) return false; if (hostStatus === _metrics.ActorStatus.Bad) return _metrics.BadActorType.badHost; if (ipStatus === _metrics.ActorStatus.Bad) return _metrics.BadActorType.badIp; // If host is exceeding host ratio and IP rate override is either not set or exceeded, return hostViolation status const maxIpRateHostViolation = this.#metrics.maxIpRateHostViolation; if (this.metrics.hostRatioViolations.has(host) && (!maxIpRateHostViolation || this.getIpStatus(req, false, Math.max(0, maxIpRateHostViolation - this.#metrics.minIpRate)) === _metrics.ActorStatus.Bad)) { return _metrics.BadActorType.hostViolation; } // only track if NOT throttling this.trackRequest(req); // do not throttle user return false; } get lag() { return _toobusyjs.default.lag(); } get lagRatio() { // lagRatio = 0-1 const lag = _toobusyjs.default.lag(); // if lag exceeds maxLag will cap ratio at 1 return lag > this.#minLag ? Math.min(1, (lag - this.#minLag) / this.#lagRange) : 0; } resolveHost(source) { return typeof source === 'string' ? (0, _util.normalizeHost)(source) : (0, _util.resolveHostFromRequest)(source); } getHostStatus(source, track = true) { const sourceStr = this.resolveHost(source); const sourceInfo = this.#metrics.getHostInfo(sourceStr); const status = getStatus(sourceInfo, { minRate: this.#metrics.minHostRate, rateRange: this.#hostRateRange, lagRatio: this.lagRatio }); if (sourceInfo === _metrics.ActorStatus.Whitelisted) return _metrics.ActorStatus.Whitelisted; if (track && status === _metrics.ActorStatus.Good) { // only track if we're NOT throttling // forward cache to avoid additional lookup this.#metrics.trackHost(sourceStr, sourceInfo); } return status; } isBadHost(host, track = true) { return this.getHostStatus(host, track) === _metrics.ActorStatus.Bad; } resolveIp(source) { return typeof source === 'string' ? source : (0, _util.resolveIpFromRequest)(source, source.scheme === 'https' ? this.#httpsBehindProxy : this.#httpBehindProxy); } getIpStatus(source, track = true, rateRange = this.#ipRateRange) { const sourceStr = this.resolveIp(source); const sourceInfo = this.#metrics.getIpInfo(sourceStr); const status = getStatus(sourceInfo, { minRate: this.#metrics.minIpRate, lagRatio: this.lagRatio, rateRange }); if (track && status === _metrics.ActorStatus.Good) { // only track if we're NOT throttling // forward cache to avoid additional lookup this.#metrics.trackIp(sourceStr, sourceInfo); } return status; } isBadIp(ip, track = true) { return this.getIpStatus(ip, track) === _metrics.ActorStatus.Bad; } trackRequest(req) { const host = this.resolveHost(req); this.#metrics.trackHost(host); const ip = this.resolveIp(req); this.#metrics.trackIp(ip); } } function getStatus(sourceInfo, { minRate, rateRange, lagRatio }) { if (sourceInfo === _metrics.ActorStatus.Whitelisted) return _metrics.ActorStatus.Whitelisted; // if no history OR rate limiting disabled assume it's good if (!sourceInfo || !minRate) return _metrics.ActorStatus.Good; // lagRatio = 0-1 (min-max) // minRate = 10 // maxRate = 30 // rateRange = (maxRate - minRate) = 20 // ((1-0.00) * 20) + 10 = 30 // minLag RPS // ((1-0.25) * 20) + 10 = 25 // ((1-0.50) * 20) + 10 = 20 // ((1-0.75) * 20) + 10 = 15 // ((1-1.00) * 20) + 10 = 10 // maxLag RPS const dynamicRate = (1 - lagRatio) * rateRange + minRate; // min/max not required since lagRatio is guaranteed // to be 0-1 regardless of (under|over)flow return sourceInfo.rate > dynamicRate ? _metrics.ActorStatus.Bad : _metrics.ActorStatus.Good; } //# sourceMappingURL=connect.js.map