UNPKG

titbit-toolkit

Version:

titbit框架的工具集,包括跨域、静态资源处理,权限过滤,请求计时,cookie,session,jwt等大量中间件

692 lines (532 loc) 16.6 kB
'use strict' const http2 = require('node:http2') const Http2Pool = require('./Http2Pool') let error_502_text = `<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Error 502</title> </head> <body> <div style="width:100%;font-size:105%;color:#737373;padding:0.8rem;"> <h2>502 Bad Gateway</h2><br> <p>代理请求不可达。</p> </div> </body> </html>` let error_503_text = `<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Error 503</title> </head> <body> <div style="width:100%;font-size:105%;color:#737373;padding:0.8rem;"> <h2>503 Service Unavailable</h2><br> <p>此服务暂时不可用。</p> </div> </body> </html>` function fmtpath(path) { path = path.trim() if (path.length == 0) { return '/*' } if (path[0] !== '/') { path = `/${path}` } if (path.length > 1 && path[path.length - 1] !== '/') { path = `${path}/` } if (path.indexOf('/:') >= 0) { return path.substring(0, path.length-1) } return `${path}*` } let Http2Proxy = function (options = {}) { if (!(this instanceof Http2Proxy)) return Http2Proxy(options) if (typeof options !== 'object') options = {} this.urlpreg = /(unix|http|https):\/\/[a-zA-Z0-9\-\_]+/ this.hostProxy = {} this.proxyBalance = {} this.pathTable = {} this.methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE' ] this.maxBody = 50000000 //是否启用全代理模式。 this.full = false this.timeout = 30000 this.connectTimeout = 15000 this.maxAliveStreams = 100 this.starPath = false this.addIP = false this.debug = false this.config = {} this.connectOptions = { family: 4 } for (let k in options) { switch (k) { case 'config': this.config = options[k] break case 'starPath': this.starPath = !!options[k] break case 'maxBody': case 'timeout': case 'connectTimeout': case 'maxAliveStreams': if (typeof options[k] === 'number' && !isNaN(options[k])) { this[k] = options[k] } break case 'addIP': case 'full': case 'debug': this[k] = !!options[k] break case 'connectOptions': if (options[k] && typeof options[k] === 'object') { for (let a in options[k]) this.connectOptions[a] = options[k][a] } break } } this.setHostProxy(this.config) } Http2Proxy.prototype.fmtConfig = function (cfg, k) { if (typeof cfg[k] === 'string') { cfg[k] = [ { path : '/', url : cfg[k] } ] } else if (! (cfg[k] instanceof Array) ) { cfg[k] = [ cfg[k] ] } } Http2Proxy.prototype.checkConfig = function (tmp, k) { if (typeof tmp !== 'object' || (tmp instanceof Array) ) { console.error(`${k} ${JSON.stringify(tmp)} 错误的配置格式`) return false } if (tmp.path === undefined) { tmp.path = '/' } tmp.path = tmp.path.trim().replace(/(\/){2,}/g, '/') if (tmp.path.length > 2 && tmp.path[tmp.path.length - 1] === '/') { tmp.path = tmp.substring(0, tmp.path.length-1) } if (tmp.url === undefined) { console.error(`${k} ${tmp.path}:没有指定要代理转发的url。`) return false } if (this.urlpreg.test(tmp.url) === false) { console.error(`${tmp.url} : 错误的url,请检查。`) return false } if (tmp.url[ tmp.url.length - 1 ] == '/') { tmp.url = tmp.url.substring(0, tmp.url.length - 1) } if (tmp.headers !== undefined) { if (typeof tmp.headers !== 'object') { console.error(`${k} ${tmp.url} ${tmp.path}:headers属性要求是object类型,使用key-value形式提供。`) return false } } return true } Http2Proxy.prototype.checkAndSetConfig = function (backend_obj, tmp) { if (tmp.headers && tmp.headers.toString() === '[object Object]') { backend_obj.headers = {} for (let h in tmp.headers) { backend_obj.headers[h] = tmp.headers[h] } } if (tmp.max && typeof tmp.max === 'number' && tmp.max > 1) backend_obj.max = tmp.max if (tmp.debug !== undefined) backend_obj.debug = tmp.debug if (tmp.weight && typeof tmp.weight === 'number' && tmp.weight > 1) backend_obj.weight = parseInt(tmp.weight) if (tmp.reconnDelay !== undefined && typeof tmp.reconnDelay === 'number') backend_obj.reconnDelay = tmp.reconnDelay if (tmp.timeout !== undefined && typeof tmp.timeout === 'number') backend_obj.timeout = tmp.timeout if (tmp.rewrite && typeof tmp.rewrite === 'function') backend_obj.rewrite = tmp.rewrite if (tmp.connectTimeout && typeof tmp.connectTimeout === 'number' && !isNaN(tmp.connectTimeout)) { backend_obj.connectTimeout = tmp.connectTimeout } } Http2Proxy.prototype.setHostProxy = function (cfg) { if (typeof cfg !== 'object') return false let pt = '' let tmp = '' let backend_obj = null let tmp_cfg for (let k in cfg) { tmp_cfg = Array.isArray(cfg[k]) ? cfg[k] : [ cfg[k] ] for (let i = 0; i < tmp_cfg.length; i++) { tmp = tmp_cfg[i] if (!this.checkConfig(tmp, k)) continue if (this.hostProxy[k] === undefined) { this.hostProxy[k] = {} this.proxyBalance[k] = {} } pt = fmtpath(tmp.path) backend_obj = { url: tmp.url, headers: null, path: tmp.path, pathLength: tmp.path.length, rewrite: false, weight: 1, weightCount: 0, reconnDelay: 500, max: 50, maxConnect: tmp.maxConnect || 0, debug: this.debug, h2Pool: null, timeout: this.timeout, connectTimeout: this.connectTimeout, maxAliveStreams: this.maxAliveStreams, alive: false, connectOptions: { timeout: this.timeout, ...this.connectOptions } } if (tmp.connectOptions && typeof tmp.connectOptions === 'object') { for (let o in tmp.connectOptions) { backend_obj.connectOptions[o] = tmp.connectOptions[o] } } this.checkAndSetConfig(backend_obj, tmp) backend_obj.h2Pool = new Http2Pool({ debug: backend_obj.debug, max: backend_obj.max, url: backend_obj.url, connectOptions: backend_obj.connectOptions, parent: backend_obj, reconnDelay: backend_obj.reconnDelay, quiet: true, timeout: backend_obj.timeout, connectTimeout: backend_obj.connectTimeout, maxAliveStreams: backend_obj.maxAliveStreams, maxConnect: backend_obj.maxConnect }) backend_obj.h2Pool.createPool() if (this.hostProxy[k][pt] === undefined) { this.hostProxy[k][pt] = [ backend_obj ] this.proxyBalance[k][pt] = { stepIndex : 0, useWeight : false } } else if (this.hostProxy[k][pt] instanceof Array) { this.hostProxy[k][pt].push(backend_obj) } if (backend_obj.weight > 1) this.proxyBalance[k][pt].useWeight = true this.pathTable[pt] = 1 } //end sub for } //end for } Http2Proxy.prototype.checkAlive = function (pr) { if (!pr.h2Pool) return false return pr.h2Pool.ok() } Http2Proxy.prototype.getBackend = function (c, host) { let prlist = this.hostProxy[host][c.routepath] let pxybalance = this.proxyBalance[host][c.routepath] let pr if (prlist.length === 1) { pr = prlist[0] } else { if (pxybalance.stepIndex >= prlist.length) { pxybalance.stepIndex = 0 } pr = prlist[pxybalance.stepIndex] if (pxybalance.useWeight) { if (pr.weightCount >= pr.weight) { pr.weightCount = 0 pxybalance.stepIndex += 1 } else { pr.weightCount += 1 } } else { pxybalance.stepIndex += 1 } } if (!pr.alive) { pr.h2Pool && pr.h2Pool.delayConnect() for (let i = prlist.length - 1; i >= 0 ; i--) { pr = prlist[i] if (pr.alive) { return pr } else { pr.h2Pool && pr.h2Pool.delayConnect() } } return null } return pr } //把http1的消息头转换为http2支持的 Http2Proxy.prototype.fmtHeaders = function (headers, ctx) { let http2_headers = { ':method': ctx.method, ':path': headers[':path'] || ctx.request.url || ctx.path, } for (let k in headers) { //if (typeof k !== 'string') continue switch (k) { case 'connection': case 'keep-alive': case 'upgrade': case 'transfer-encoding': case 'proxy-connection': case ':path': case ':method': case 'method': break case 'host': http2_headers[':authority'] = headers[k] break default: http2_headers[k] = headers[k] } } return http2_headers } Http2Proxy.prototype.mid = function () { let self = this let timeoutError = new Error('request timeout') timeoutError.code = 'ETIMEOUT' return async (c, next) => { let host = c.host let hind = c.host.length - 1 if (hind > 4) { let eind = hind - 5 while (hind >= eind) { if (c.host[hind] === ':') { host = c.host.substring(0, hind) break } hind-- } } if (!self.hostProxy[host] || !self.hostProxy[host][c.routepath]) { if (self.full) { return c.status(502).send(error_502_text) } return await next() } let pr = self.getBackend(c, host) if (!pr) { pr = self.getBackend(c, host) if (!pr) { await c.ext.delay(9) pr = self.getBackend(c, host) if (!pr) { for (let i = 0; i < 200; i++) { await c.ext.delay(6 + i) pr = self.getBackend(c, host) if (pr) break } } if (!pr) return c.status(503).send(error_503_text) } } if (self.addIP && c.headers['x-real-ip']) { c.headers['x-real-ip'] += `,${c.ip}` } else { c.headers['x-real-ip'] = c.ip } let hii = pr.h2Pool let session_client try { if (pr.headers) { for (let k in pr.headers) c.headers[k] = pr.headers[k] } if (pr.rewrite) { let rpath = pr.rewrite(c, c.major >= 2 ? c.headers[':path'] : c.request.url) if (rpath) { let path_typ = typeof rpath if (path_typ === 'object' && rpath.redirect) { return c.setHeader('location', rpath.redirect) } else if (path_typ === 'string') { c.headers[':path'] = rpath } } } session_client = await hii.getSession() if (session_client.deny) { for (let i = 0; i < 50; i++) { await c.ext.delay(5 + i) session_client = await hii.getSession() if (!session_client.deny) break } if (session_client.deny) return c.status(429).send('服务繁忙,请稍后再试') } await new Promise(async (rv, rj) => { let resolved = false let rejected = false let request_stream = c.stream let stm = null stm = await hii.request( c.major === 2 ? c.headers : this.fmtHeaders(c.headers, c), session_client ).catch(err => { rejected = true rj(err) stm = null }) if (!stm) { rj(new Error('request failed')) return false } let timeout_handler = () => { timeout_timer = null //强制的异常结束,这意味着session的其他stream也会出现问题。 try { !stm.closed && stm.close(http2.constants.NGHTTP2_CANCEL) stm.destroy() if (session_client.session && !session_client.session.destroyed) { session_client.session.destroy() } } catch(e) {} if (!resolved && !rejected) { rejected = true rj(new Error('force destroy stream, request timeout')) } } session_client.aliveStreams++ let timeout_timer = setTimeout(timeout_handler, pr.timeout + 5000) c.stream.on('timeout', () => { stm.close(http2.constants.NGHTTP2_CANCEL) //stm.destroy() }) c.stream.on('close', () => { if (request_stream && request_stream.rstCode !== http2.constants.NGHTTP2_NO_ERROR) { stm.close(request_stream.rstCode) } }) c.stream.on('error', err => { if (timeout_timer) { clearTimeout(timeout_timer) timeout_timer = null } stm.close(http2.constants.NGHTTP2_INTERNAL_ERROR) stm.destroy() }) c.stream.on('aborted', err => { !request_stream.destroyed && request_stream.destroy() //stm.close(http2.constants.NGHTTP2_CANCEL) stm.destroy() }) stm.setTimeout(pr.timeout, () => { //stm.close(http2.constants.NGHTTP2_CANCEL) stm.destroy() }) stm.on('aborted', err => { if (timeout_timer) { clearTimeout(timeout_timer) timeout_timer = null } !stm.destroyed && stm.destroy() if (!resolved && !rejected) { rejected = true rj(err) } }) stm.on('close', () => { if (timeout_timer) { clearTimeout(timeout_timer) timeout_timer = null } stm.removeAllListeners() if (stm.rstCode === http2.constants.NGHTTP2_NO_ERROR) { if (!resolved && !rejected) { resolved = true rv() } } else { if (!resolved && !rejected) { rejected = true rj(new Error(`stream close, exit code ${stm.rstCode}`)) } } session_client.aliveStreams-- }) stm.on('response', (headers, flags) => { timeout_timer && clearTimeout(timeout_timer) timeout_timer = setTimeout(timeout_handler, pr.timeout + 5000) if (c.reply && c.reply.writable) { if (c.reply.respond) { c.reply.respond(headers) } else if (c.reply.setHeader) { c.status(headers[':status']) for (let k in headers) { if (typeof k !== 'string' || k[0] === ':') continue c.reply.setHeader(k, headers[k]) } } } }) stm.on('frameError', err => { stm.close(http2.constants.NGHTTP2_INTERNAL_ERROR) stm.destroy() }) stm.on('error', err => { self.debug && console.error('------ error ------',err) //stm.close(http2.constants.NGHTTP2_INTERNAL_ERROR) stm.destroy(err) }) c.request.on('data', chunk => { stm.write(chunk) }) c.request.on('end', () => { stm.end() }) let data_count = 0 stm.on('data', chunk => { data_count++ c.reply && c.reply.writable && c.reply.write(chunk) if (data_count >= 111) { data_count = 0 timeout_timer && clearTimeout(timeout_timer) timeout_timer = setTimeout(timeout_handler, pr.timeout + 5000) } }) stm.on('end', () => { if (timeout_timer) { clearTimeout(timeout_timer) timeout_timer = null } stm.close() if (!resolved && !rejected) { resolved = true rv() } }) }) } catch (err) { self.debug && console.error(err||'request null error') c.status(503).send(error_503_text) } finally { session_client = null } } } Http2Proxy.prototype.init = function (app) { app.config.timeout = this.timeout for (let p in this.pathTable) { app.router.map(this.methods, p, async c => {}, '@titbit_h2_proxy'); } app.use(this.mid(), { pre: true, group: `titbit_h2_proxy` }) } module.exports = Http2Proxy