diff --git a/lib/docker.ts b/lib/docker.ts index 67de6bc..8b13466 100644 --- a/lib/docker.ts +++ b/lib/docker.ts @@ -23,7 +23,7 @@ export class DockerModule { const Dolphin = require('dolphin'); this.redbird = redbird; - this.log = redbird.log; + this.log = redbird.logger; const targets: Record> = (this.targets = {}); this.ports = {}; diff --git a/lib/etcd-backend.ts b/lib/etcd-backend.ts index da33cdd..128d006 100644 --- a/lib/etcd-backend.ts +++ b/lib/etcd-backend.ts @@ -17,7 +17,7 @@ export class ETCDModule { // Create Redbird Instance and Log this.redbird = redbird; - const log = redbird.log; + const log = redbird.logger; const _this = this; // Create node-etcd Instance diff --git a/lib/interfaces/proxy-options.ts b/lib/interfaces/proxy-options.ts index 8924aa7..93061fb 100644 --- a/lib/interfaces/proxy-options.ts +++ b/lib/interfaces/proxy-options.ts @@ -2,19 +2,7 @@ import http, { IncomingMessage, ServerResponse } from 'http'; import httpProxy, { ProxyTargetUrl } from 'http-proxy'; import { Socket } from 'net'; import pino from 'pino'; - -type ResolverFnResult = string | { path: string; url: string } | null | undefined; - -export type ResolverFn = ( - host: string, - url: string, - req?: IncomingMessage -) => ResolverFnResult | Promise; - -export interface Resolver { - fn: ResolverFn; - priority: number; -} +import { Resolver } from './resolver.js'; export interface SSLConfig { port?: number; @@ -39,7 +27,7 @@ export interface ProxyOptions { httpProxy?: httpProxy.ServerOptions; // Enable Logging - log?: pino.LoggerOptions; + logger?: pino.Logger; // Enable Cluster Mode cluster?: number; diff --git a/lib/interfaces/proxy-route.ts b/lib/interfaces/proxy-route.ts index a551123..41e7a69 100644 --- a/lib/interfaces/proxy-route.ts +++ b/lib/interfaces/proxy-route.ts @@ -1,3 +1,6 @@ +import { ProxyTargetUrl } from './proxy-target-url.js'; +import { RouteOptions } from './route-options.js'; + /** * ProxyRoute interface * @description @@ -8,16 +11,5 @@ export interface ProxyRoute { path?: string; rr?: number; isResolved?: boolean; - opts?: { - onRequest?: (req: any, res: any, target: ProxyTargetUrl) => void; - }; -} - -export interface ProxyTargetUrl { - host: string; - hostname: string; - port: number; - pathname: string; - useTargetHostHeader: boolean; - href: string; + opts?: RouteOptions; } diff --git a/lib/interfaces/proxy-target-url.ts b/lib/interfaces/proxy-target-url.ts new file mode 100644 index 0000000..c2a8224 --- /dev/null +++ b/lib/interfaces/proxy-target-url.ts @@ -0,0 +1,8 @@ +export interface ProxyTargetUrl { + host: string; + hostname: string; + port: number; + pathname: string; + useTargetHostHeader: boolean; + href: string; +} diff --git a/lib/interfaces/resolver.ts b/lib/interfaces/resolver.ts new file mode 100644 index 0000000..db9bfd4 --- /dev/null +++ b/lib/interfaces/resolver.ts @@ -0,0 +1,19 @@ +import { IncomingMessage } from 'http'; +import { RouteOptions } from './route-options.js'; + +export type ResolverFnResult = + | string + | { path?: string; url: string; opts?: RouteOptions } + | null + | undefined; + +export type ResolverFn = ( + host: string, + url: string, + req?: IncomingMessage +) => ResolverFnResult | Promise; + +export interface Resolver { + fn: ResolverFn; + priority: number; +} diff --git a/lib/interfaces/route-options.ts b/lib/interfaces/route-options.ts new file mode 100644 index 0000000..095ffb4 --- /dev/null +++ b/lib/interfaces/route-options.ts @@ -0,0 +1,12 @@ +import { ProxyTargetUrl } from './proxy-target-url.js'; + +export interface RouteOptions { + useTargetHostHeader?: boolean; + ssl?: { + key?: string; + cert?: string; + ca?: string; + letsencrypt?: { email: string; production: boolean; lazy?: boolean }; + }; + onRequest?: (req: any, res: any, target: ProxyTargetUrl) => void; +} diff --git a/lib/proxy.ts b/lib/proxy.ts index 6362a34..aca6eb7 100755 --- a/lib/proxy.ts +++ b/lib/proxy.ts @@ -22,9 +22,11 @@ import { LRUCache } from 'lru-cache'; // Custom modules. import * as letsencrypt from './letsencrypt.js'; -import { ProxyOptions, ResolverFn, Resolver } from './interfaces/proxy-options.js'; +import { ProxyOptions } from './interfaces/proxy-options.js'; import { Socket } from 'net'; import { ProxyRoute } from './interfaces/proxy-route.js'; +import { RouteOptions } from './interfaces/route-options.js'; +import { Resolver, ResolverFn, ResolverFnResult } from './interfaces/resolver.js'; const { isFunction, isObject, sortBy, uniq, remove, isString } = lodash; @@ -34,7 +36,7 @@ const ONE_DAY = 60 * 60 * 24 * 1000; const ONE_MONTH = ONE_DAY * 30; export class Redbird { - log?: Logger; + logger?: Logger; routing: any = {}; resolvers: Resolver[] = []; certs: any; @@ -61,9 +63,9 @@ export class Redbird { this.opts.httpProxy = {}; } - if (opts.log) { - this.log = pino( - opts.log || { + if (opts.logger) { + this.logger = pino( + opts.logger || { name: 'redbird', } ); @@ -110,7 +112,7 @@ export class Redbird { cluster.on('exit', (worker, code, signal) => { // Fork if a worker dies. - this.log?.error( + this.logger?.error( { code: code, signal: signal, @@ -136,12 +138,12 @@ export class Redbird { const websocketsUpgrade = async (req: any, socket: Socket, head: Buffer) => { socket.on('error', (err) => { - this.log?.error(err, 'WebSockets error'); + this.logger?.error(err, 'WebSockets error'); }); const src = this.getSource(req); const target = await this.getTarget(src, req); - this.log?.info({ headers: req.headers, target: target }, 'upgrade to websockets'); + this.logger?.info({ headers: req.headers, target: target }, 'upgrade to websockets'); if (target) { if (target.useTargetHostHeader === true) { @@ -220,7 +222,12 @@ export class Redbird { // // Plain HTTP Proxy // - const server = (this.server = this.setupHttpProxy(proxy, websocketsUpgrade, this.log, opts)); + const server = (this.server = this.setupHttpProxy( + proxy, + websocketsUpgrade, + this.logger, + opts + )); server.listen(opts.port, opts.host); const handleProxyError = ( @@ -235,7 +242,7 @@ export class Redbird { // Send a 500 http status if headers have been sent // if (!res || !res.writeHead) { - this.log?.error(err, 'Proxy Error'); + this.logger?.error(err, 'Proxy Error'); return; } else { if (err.code === 'ECONNREFUSED') { @@ -249,7 +256,7 @@ export class Redbird { // Do not log this common error // if (err.message !== 'socket hang up') { - this.log?.error(err, 'Proxy Error'); + this.logger?.error(err, 'Proxy Error'); } // @@ -265,7 +272,7 @@ export class Redbird { proxy.on('error', handleProxyError); } - this.log?.info('Started a Redbird reverse proxy server on port %s', opts.port); + this.logger?.info('Started a Redbird reverse proxy server on port %s', opts.port); } } @@ -277,8 +284,8 @@ export class Redbird { const target = await this.getTarget(src, req, res); if (target) { - if (this.shouldRedirectToHttps(this.certs, src, target)) { - redirectToHttps(req, res, this.opts.ssl, this.log); + if (this.shouldRedirectToHttps(target)) { + redirectToHttps(req, res, this.opts.ssl, this.logger); } else { proxy.web(req, res, { target, secure: !!opts.secure }); } @@ -310,9 +317,8 @@ export class Redbird { throw Error('Missing certificate path for Lets Encrypt'); } const letsencryptPort = opts.letsencrypt.port || defaultLetsencryptPort; - this.letsencryptServer = letsencrypt.init(opts.letsencrypt.path, letsencryptPort, this.log); + this.letsencryptServer = letsencrypt.init(opts.letsencrypt.path, letsencryptPort, this.logger); - opts.resolvers = opts.resolvers || []; this.letsencryptHost = '127.0.0.1:' + letsencryptPort; const targetHost = 'http://' + this.letsencryptHost; const challengeResolver = (host: string, url: string) => { @@ -337,7 +343,28 @@ export class Redbird { opts?: any; } = { SNICallback: async (hostname: string, cb: (err: any, ctx?: any) => void) => { - if (!certs[hostname] && this.lazyCerts[hostname]) { + if (!certs[hostname]) { + if (!this.opts?.letsencrypt?.path) { + console.error('Missing certificate path for Lets Encrypt'); + return cb(new Error('No certs for hostname ' + hostname)); + } + + if (!this.lazyCerts[hostname]) { + // Check if we have a resolver that matches the hostname and has letsencrypt enabled + const results = await this.applyResolvers(this.resolvers, hostname, '', null); + const route = results.find((route) => (route)?.opts?.ssl?.letsencrypt); + const sslOpts = (route)?.opts?.ssl; + if (route && sslOpts) { + this.lazyCerts[hostname] = { + email: sslOpts.letsencrypt?.email, + production: sslOpts.letsencrypt?.production, + renewWithin: this.opts?.letsencrypt?.renewWithin || ONE_MONTH, + }; + } else { + return cb(new Error('No certs for hostname ' + hostname)); + } + } + try { await this.updateCertificates( hostname, @@ -407,14 +434,14 @@ export class Redbird { httpsServer.on('upgrade', websocketsUpgrade); httpsServer.on('error', (err: NodeJS.ErrnoException) => { - this.log?.error(err, 'HTTPS Server Error'); + this.logger?.error(err, 'HTTPS Server Error'); }); httpsServer.on('clientError', (err: NodeJS.ErrnoException) => { - this.log?.error(err, 'HTTPS Client Error'); + this.logger?.error(err, 'HTTPS Client Error'); }); - this.log?.info('Listening to HTTPS requests on port %s', sslOpts.port); + this.logger?.info('Listening to HTTPS requests on port %s', sslOpts.port); httpsServer.listen(sslOpts.port, sslOpts.ip); } @@ -464,19 +491,15 @@ export class Redbird { @target {String|URL} A string or a url parsed by node url module. @opts {Object} Route options. */ - register(opts: { - src: string | URL; - target: string | URL; - ssl: { - key?: string; - cert?: string; - ca?: string; - letsencrypt?: { email: string; production: boolean; lazy?: boolean }; - }; - }): Promise; + register( + opts: { + src: string | URL; + target: string | URL; + } & RouteOptions + ): Promise; register(src: string, opts: any): Promise; - register(src: string | URL, target: string | URL, opts: any): Promise; - async register(src: any, target?: any, opts?: any): Promise { + register(src: string | URL, target: string | URL, opts: RouteOptions): Promise; + async register(src: any, target?: any, opts?: RouteOptions): Promise { if (this.opts.cluster && cluster.isPrimary) { return; } @@ -516,7 +539,7 @@ export class Redbird { } if (!ssl.letsencrypt.lazy) { - this.log?.info('Getting Lets Encrypt certificates for %s', src.hostname); + this.logger?.info('Getting Lets Encrypt certificates for %s', src.hostname); await this.updateCertificates( src.hostname, ssl.letsencrypt.email, @@ -525,7 +548,7 @@ export class Redbird { ); } else { // We need to store the letsencrypt options for this domain somewhere - this.log?.info('Lazy loading Lets Encrypt certificates for %s', src.hostname); + this.logger?.info('Lazy loading Lets Encrypt certificates for %s', src.hostname); this.lazyCerts[src.hostname] = { ...ssl.letsencrypt, renewWithin: this.opts.letsencrypt.renewWithin || ONE_MONTH, @@ -564,7 +587,7 @@ export class Redbird { route.urls.push(target); - this.log?.info({ from: src, to: target }, 'Registered a new route'); + this.logger?.info({ from: src, to: target }, 'Registered a new route'); } async updateCertificates( @@ -581,7 +604,7 @@ export class Redbird { this.opts.letsencrypt?.port, production, renew, - this.log + this.logger ); if (certs) { const opts = { @@ -597,10 +620,10 @@ export class Redbird { renewTime = renewTime > 0 ? renewTime : this.opts.letsencrypt.minRenewTime || 60 * 60 * 1000; - this.log?.info('Renewal of %s in %s days', domain, Math.floor(renewTime / ONE_DAY)); + this.logger?.info('Renewal of %s in %s days', domain, Math.floor(renewTime / ONE_DAY)); const renewCertificate = () => { - this.log?.info('Renewing letscrypt certificates for %s', domain); + this.logger?.info('Renewing letscrypt certificates for %s', domain); this.updateCertificates(domain, email, production, renewWithin, true); }; @@ -609,7 +632,7 @@ export class Redbird { // // TODO: Try again, but we need an exponential backof to avoid getting banned. // - this.log?.info('Could not get any certs for %s', domain); + this.logger?.info('Could not get any certs for %s', domain); } } catch (err) { console.error('Error getting LetsEncrypt certificates', err); @@ -659,11 +682,20 @@ export class Redbird { } } - this.log?.info({ from: src, to: target }, 'Unregistered a route'); + this.logger?.info({ from: src, to: target }, 'Unregistered a route'); } return this; } + private applyResolvers( + resolvers: Resolver[], + host: string, + url: string, + req?: IncomingMessage + ): Promise { + return Promise.all(resolvers.map((resolver) => resolver.fn(host, url, req))); + } + /** * Resolves to route * @param host @@ -678,9 +710,7 @@ export class Redbird { try { host = host.toLowerCase(); - const promiseArray = this.resolvers.map((resolver) => resolver.fn.call(this, host, url, req)); - - const resolverResults = await Promise.all(promiseArray); + const resolverResults = await this.applyResolvers(this.resolvers, host, url, req); for (let i = 0; i < resolverResults.length; i++) { const route = resolverResults[i]; @@ -709,7 +739,7 @@ export class Redbird { const route = await this.resolve(src, url, req); if (!route) { - this.log?.warn({ src: src, url: url }, 'no valid route found for given source'); + this.logger?.warn({ src: src, url: url }, 'no valid route found for given source'); return; } @@ -754,12 +784,15 @@ export class Redbird { if (route.opts?.onRequest) { const resultFromRequestHandler = route.opts.onRequest(req, res, target); if (resultFromRequestHandler !== undefined) { - this.log?.info('Proxying %s received result from onRequest handler, returning.', src + url); + this.logger?.info( + 'Proxying %s received result from onRequest handler, returning.', + src + url + ); return resultFromRequestHandler; } } - this.log?.info('Proxying %s to %s', src + url, path.posix.join(target.host, req.url)); + this.logger?.info('Proxying %s to %s', src + url, path.posix.join(target.host, req.url)); return target; } @@ -826,8 +859,8 @@ export class Redbird { } } - shouldRedirectToHttps(certs: any, src: string, target: any) { - return certs && src in certs && target.sslRedirect && target.host != this.letsencryptHost; + shouldRedirectToHttps(target: any) { + return target.sslRedirect && target.host != this.letsencryptHost; } } diff --git a/package-lock.json b/package-lock.json index 998a4d3..f2a6048 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "object-hash": "^3.0.0", "pino": "^9.4.0", "safe-timers": "^1.1.0", - "spdy": "^4.0.2", "valid-url": "^1.0.9" }, "devDependencies": { @@ -1809,6 +1808,7 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1858,12 +1858,6 @@ "node": ">=0.10.0" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT" - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2977,12 +2971,6 @@ "node": ">= 10.13.0" } }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "license": "MIT" - }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -3042,54 +3030,6 @@ "node": ">=0.10.0" } }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "license": "MIT" - }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -3205,6 +3145,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -3456,12 +3397,6 @@ "node": ">=0.10.0" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3734,12 +3669,6 @@ "node": ">=6" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3782,6 +3711,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/mute-stdout": { @@ -3963,12 +3893,6 @@ "node": ">=0.10.0" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "license": "MIT" - }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -4290,12 +4214,6 @@ "node": ">= 0.6.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/process-warning": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", @@ -4731,12 +4649,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "license": "MIT" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4881,50 +4793,6 @@ "node": ">= 10.13.0" } }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/spdy-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -5456,6 +5324,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -5749,15 +5618,6 @@ } } }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", diff --git a/package.json b/package.json index e3eef4f..8f77a04 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "object-hash": "^3.0.0", "pino": "^9.4.0", "safe-timers": "^1.1.0", - "spdy": "^4.0.2", "valid-url": "^1.0.9" }, "devDependencies": { diff --git a/samples/lazy-wildcards.mjs b/samples/lazy-wildcards.mjs new file mode 100644 index 0000000..de5db09 --- /dev/null +++ b/samples/lazy-wildcards.mjs @@ -0,0 +1,57 @@ +'use strict'; + +import { createServer } from 'http'; +import { Redbird } from '../dist/index.js'; +import { isPrimary } from 'cluster'; + +const ONE_DAY = 24 * 60 * 60 * 1000; + +const wildcard_target = { + url: `http://localhost:3000/`, + opts: { + ssl: { + letsencrypt: { + email: 'admin@optimalbits.com', + production: false, + }, + }, + }, +}; + +async function sample1() { + const proxy = new Redbird({ + port: 8080, + cluster: 4, + keepAlive: true, + log: { + name: 'Redbird', + }, + ssl: { + port: 8443, + }, + letsencrypt: { + path: './.letsencrypt', // Path to store Let's Encrypt certificates + port: 9999, // Port for Let's Encrypt challenge responses + renewWithin: 30 * ONE_DAY, // Renew certificates when they are within 1 day of expiration + }, + resolvers: [ + { + fn: (hostname) => { + return wildcard_target; + }, + // A negative priority will put this resolver at the end of the list + priority: -1, + }, + ], + }); +} + +sample1(); + +if (!isPrimary) { + createServer(function (req, res) { + res.writeHead(200); + res.write('hello world'); + res.end(); + }).listen(3000); +} diff --git a/test/custom_resolver.spec.ts b/test/custom_resolver.spec.ts index 14764ef..ccf2ab1 100644 --- a/test/custom_resolver.spec.ts +++ b/test/custom_resolver.spec.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { Redbird, buildRoute } from '../lib/index.js'; // Adjust the import path if necessary -import { ResolverFn } from '../lib/interfaces/proxy-options.js'; import { IncomingMessage } from 'http'; import { ProxyRoute } from '../lib/interfaces/proxy-route.js'; +import { ResolverFn } from '../lib/interfaces/resolver.js'; const opts = { port: 10000 + Math.ceil(Math.random() * 55535), diff --git a/test/custom_resolver_certificates.spec.ts b/test/custom_resolver_certificates.spec.ts new file mode 100644 index 0000000..2a63080 --- /dev/null +++ b/test/custom_resolver_certificates.spec.ts @@ -0,0 +1,262 @@ +// tests/redbird.spec.ts + +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; +import http, { Server } from 'http'; +import https from 'https'; +import fs from 'fs'; +import path from 'path'; + +import { certificate, key } from './fixtures/index.js'; + +const ONE_DAY = 24 * 60 * 60 * 1000; + +const MOCK_CERT_DATA = 'MOCK_CHAIN_DATA'; + +// Note: as we are mocking safe-timers we cannot use intervals larger than 2^31 - 1 ms. + +vi.mock('../lib/letsencrypt.js', () => ({ + getCertificates: vi.fn().mockImplementation(async () => { + return { + privkey: key, + cert: certificate, + chain: MOCK_CERT_DATA, + expiresAt: Date.now() + 22 * ONE_DAY, + }; + }), + init: vi.fn(), +})); + +const getCertificatesMock = vi.mocked((await import('../lib/letsencrypt.js')).getCertificates); + +// Mock 'safe-timers' module +vi.mock('safe-timers', () => { + return { + default: { + setTimeout: (callback, delay, ...args) => { + return setTimeout(callback, delay, ...args); + }, + clearTimeout: (timerId) => { + clearTimeout(timerId); + }, + // Include other methods if needed + }, + }; +}); + +import { Redbird } from '../lib/index.js'; + +const testPort = 54679; +const proxyPort = 8083; +const sslPort = 8443; + +// Helper functions to make HTTP and HTTPS requests +function makeHttpRequest(options) { + return new Promise<{ status: number; headers: any; data: string }>((resolve, reject) => { + const req = http.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + resolve({ status: res.statusCode!, headers: res.headers, data }); + }); + }); + req.on('error', (err) => { + console.error('ERROR', err); + reject(err); + }); + req.end(); + }); +} + +function makeHttpsRequest(options) { + return new Promise<{ status: number; headers: any; data: string }>((resolve, reject) => { + options.rejectUnauthorized = false; // For self-signed certificates + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + resolve({ status: res.statusCode!, headers: res.headers, data }); + }); + }); + req.on('error', (err) => { + reject(err); + }); + req.end(); + }); +} + +const responseMessage = 'Hello from target server'; + +describe('Redbird Lets Encrypt SSL Certificate Generation For Custom Resolvers', () => { + let proxy: Redbird; + let targetServer: Server; + let targetPort: number = testPort; + + beforeAll(async () => { + // Start a simple HTTP server to act as the backend target + targetServer = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(responseMessage); + }); + + await new Promise((resolve) => { + targetServer.listen(testPort, () => { + resolve(null); + }); + }); + + const wildcard_target = { + url: `http://localhost:${targetPort}/`, + opts: { + ssl: { + letsencrypt: { + email: 'admin@optimalbits.com', + production: false, + }, + }, + }, + }; + + // Create a new instance of Redbird with SSL options + proxy = new Redbird({ + port: proxyPort, + ssl: { + port: sslPort, + }, + letsencrypt: { + path: path.join(__dirname, 'letsencrypt'), // Path to store Let's Encrypt certificates + port: 9999, // Port for Let's Encrypt challenge responses + renewWithin: 1 * ONE_DAY, // Renew certificates when they are within 1 day of expiration + }, + resolvers: [ + { + // We will accept any hostname and return the same target. + fn: (hostname) => { + return wildcard_target; + }, + priority: -1, + }, + ], + }); + + // Mocking the certificate files + vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => true); + vi.spyOn(fs, 'readFileSync').mockImplementation((filePath, encoding) => 'MOCK_CERT_DATA'); + }); + + afterAll(async () => { + // Close the proxy and target server + await proxy.close(); + await new Promise((resolve) => targetServer.close(() => resolve(null))); + vi.restoreAllMocks(); + }); + + it('should generate SSL certificates for new subdomains', async () => { + const subdomain = 'secure.example.com'; + + // Make an HTTPS request to the new subdomain + // First HTTPS request to trigger certificate generation + const options = { + hostname: 'localhost', + port: sslPort, + path: '/', + method: 'GET', + headers: { + Host: subdomain, + }, + rejectUnauthorized: false, // Accept self-signed certificates + }; + + const response = await makeHttpsRequest(options); + expect(response.status).toBe(200); + expect(response.data).toBe(responseMessage); + }); + + it('should renew SSL certificates that are halfway to expire', async () => { + getCertificatesMock.mockClear(); + + const subdomain = 'renew.example.com'; + + // Mock getCertificates to return the initial and renewed certificates + getCertificatesMock + .mockResolvedValueOnce({ + privkey: key, + cert: certificate, + chain: MOCK_CERT_DATA, + expiresAt: Date.now() + 10 * ONE_DAY, // Expires in 10 days + }) + .mockResolvedValueOnce({ + privkey: key, + cert: certificate, + chain: MOCK_CERT_DATA, + expiresAt: Date.now() + 15 * ONE_DAY, // Renewed, expires in 90 days + }); + + expect(getCertificatesMock).toHaveBeenCalledTimes(0); + + // Use fake timers to simulate time passage + vi.useFakeTimers(); + + // Initial HTTPS request to trigger certificate generation + const options = { + hostname: 'localhost', + port: sslPort, + path: '/', + method: 'GET', + headers: { + Host: subdomain, + }, + rejectUnauthorized: false, + }; + + const response = await makeHttpsRequest(options); + expect(response.status).toBe(200); + expect(response.data).toBe('Hello from target server'); + + expect(getCertificatesMock).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(8 * ONE_DAY); + + expect(getCertificatesMock).toHaveBeenCalledTimes(1); + + // Advance all timers to execute pending callbacks + vi.advanceTimersByTime(1 * ONE_DAY); + + expect(getCertificatesMock).toHaveBeenCalledTimes(2); + + // Second HTTPS request + const response2 = await makeHttpsRequest(options); + expect(response2.status).toBe(200); + expect(response2.data).toBe('Hello from target server'); + + // Verify getCertificates was called twice + expect(getCertificatesMock).toHaveBeenCalledTimes(2); + + // Restore real timers + vi.useRealTimers(); + }); + + it('should redirect HTTP requests to HTTPS', async () => { + const subdomain = 'secure2.example.com'; + + // Make an HTTPS request to the new subdomain + // First HTTPS request to trigger certificate generation + const options = { + hostname: 'localhost', + port: proxyPort, + path: '/', + method: 'GET', + headers: { + Host: subdomain, + }, + rejectUnauthorized: false, // Accept self-signed certificates + }; + + const response = await makeHttpRequest(options); + expect(response.status).toBe(302); + expect(response.headers.location).toBe(`https://${subdomain}:${sslPort}/`); + }); +}); diff --git a/test/letsencrypt_certificates.spec.ts b/test/letsencrypt_certificates.spec.ts index 22f7753..8fb1d6c 100644 --- a/test/letsencrypt_certificates.spec.ts +++ b/test/letsencrypt_certificates.spec.ts @@ -45,7 +45,9 @@ vi.mock('safe-timers', () => { import { Redbird } from '../lib'; -const TEST_PORT = 54679; +const testPort = 54680; +const sslPort = 8444; +const proxyPort = 8081; // Helper functions to make HTTP and HTTPS requests function makeHttpRequest(options) { @@ -85,12 +87,12 @@ function makeHttpsRequest(options) { }); } -const responseMessage = 'Hello from target server' +const responseMessage = 'Hello from target server'; describe('Redbird Lets Encrypt SSL Certificate Generation', () => { let proxy: Redbird; let targetServer: Server; - let targetPort: number = TEST_PORT; + let targetPort: number = testPort; beforeAll(async () => { // Start a simple HTTP server to act as the backend target @@ -100,16 +102,16 @@ describe('Redbird Lets Encrypt SSL Certificate Generation', () => { }); await new Promise((resolve) => { - targetServer.listen(TEST_PORT, () => { + targetServer.listen(testPort, () => { resolve(null); }); }); // Create a new instance of Redbird with SSL options proxy = new Redbird({ - port: 8080, + port: proxyPort, ssl: { - port: 8443, + port: sslPort, }, letsencrypt: { path: path.join(__dirname, 'letsencrypt'), // Path to store Let's Encrypt certificates @@ -147,7 +149,7 @@ describe('Redbird Lets Encrypt SSL Certificate Generation', () => { // First HTTPS request to trigger certificate generation const options = { hostname: 'localhost', - port: 8443, + port: sslPort, path: '/', method: 'GET', headers: { @@ -205,7 +207,7 @@ describe('Redbird Lets Encrypt SSL Certificate Generation', () => { // Initial HTTPS request to trigger certificate generation const options = { hostname: 'localhost', - port: 8443, + port: sslPort, path: '/', method: 'GET', headers: { @@ -240,7 +242,7 @@ describe('Redbird Lets Encrypt SSL Certificate Generation', () => { getCertificatesMock.mockClear(); // Simulate registering a domain with lazy loading enabled - await proxy.register('https://lazy.example.com', `http://localhost:${TEST_PORT}`, { + await proxy.register('https://lazy.example.com', `http://localhost:${testPort}`, { ssl: { letsencrypt: { email: 'email@example.com', @@ -261,7 +263,7 @@ describe('Redbird Lets Encrypt SSL Certificate Generation', () => { // Make an HTTPS request to trigger lazy loading of certificates const options = { hostname: 'localhost', - port: 8443, + port: sslPort, path: '/', method: 'GET', headers: { Host: 'lazy.example.com' }, // Required for virtual hosts diff --git a/yarn.lock b/yarn.lock index 1b49c1d..0f91b48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -614,7 +614,7 @@ copy-props@^4.0.0: each-props "^3.0.0" is-plain-object "^5.0.0" -core-util-is@~1.0.0, core-util-is@1.0.2: +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== @@ -650,7 +650,7 @@ deasync@^0.1.13: bindings "^1.5.0" node-addon-api "^1.7.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.3.6: +debug@^4.0.1, debug@^4.3.6: version "4.3.7" resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -677,11 +677,6 @@ detect-file@^1.0.0: resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz" integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - doctrine@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" @@ -1308,11 +1303,6 @@ gulplog@^2.2.0: dependencies: glogg "^2.2.0" -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" @@ -1350,21 +1340,6 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - http-proxy@^1.18.0: version "1.18.1" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" @@ -1428,7 +1403,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@2: +inherits@^2.0.3, inherits@^2.0.4, inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1564,11 +1539,6 @@ is-windows@^1.0.1: resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -1746,11 +1716,6 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -1873,11 +1838,6 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - on-exit-leak-free@^2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz" @@ -2051,11 +2011,6 @@ prettier@^2.0.1: resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - process-warning@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz" @@ -2113,28 +2068,6 @@ rasha@^1.2.4: resolved "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz" integrity sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw== -readable-stream@^2.0.1: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" @@ -2328,11 +2261,6 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-replace@^1.0.2, safe-replace@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz" @@ -2353,11 +2281,6 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, "safer-buffer@>= 2.1.2 < 3", "safer-bu resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - semver-greatest-satisfied-range@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz" @@ -2423,29 +2346,6 @@ sparkles@^2.1.0: resolved "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz" integrity sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg== -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - split2@^4.0.0: version "4.2.0" resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz" @@ -2511,13 +2411,6 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - string-width@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz" @@ -2772,7 +2665,7 @@ url-parse@^1.4.3: querystringify "^2.1.1" requires-port "^1.0.0" -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -2908,13 +2801,6 @@ vitest@^2.0.5: vite-node "2.1.1" why-is-node-running "^2.3.0" -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - web-streams-polyfill@^3.0.3: version "3.3.3" resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz"