UNPKG

titbit-toolkit

Version:

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

663 lines (512 loc) 15.1 kB
'use strict'; const urlparse = require('node:url'); const http = require('node:http'); const https = require('node:https'); /** * { * host : {} * } * { * host : '' * } * * { * host : [ * {} * ] * } * */ class Proxy { constructor(options = {}) { this.methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE'] this.hostProxy = {} this.proxyBalance = {} this.pathTable = {} this.config = {} this.urlpreg = /(unix|http|https):\/\/[a-zA-Z0-9\-\_]+/ this.maxBody = 50000000 //是否启用全代理模式。 this.full = false this.timeout = 15000 this.addIP = false this.debug = false this.autoClearListeners = false //记录定时器 this.proxyIntervals = {} this.connectOptions = { family: 4 } this.error = { '502' : `<!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>`, '503' :`<!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>` } if (typeof options !== 'object') { options = {} } for (let k in options) { switch (k) { case 'host': case 'config': this.config = options[k] break case 'methods': Array.isArray(options[k]) && (this.methods = options[k]); break case 'maxBody': if (typeof options[k] == 'number' && parseInt(options[k]) >= 0) { this.maxBody = parseInt(options[k]) } break case 'full': case 'debug': case 'autoClearListeners': this[k] = !!options[k] break case 'timeout': if (typeof options[k] === 'number' && options[k] >= 0) { this.timeout = options[k] } break case 'addIP': this.addIP = options[k] break case 'connectOptions': if (options[k] && typeof options[k] === 'object') { for (let o in options[k]) this.connectOptions[o] = options[k][o] } break default:; } } this.setHostProxy(this.config) } 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}*` } setHostProxy(cfg) { if (typeof cfg !== 'object') { return } let pt = '' let tmp = '' let backend_obj = null for (let k in cfg) { if (typeof cfg[k] === 'string') { cfg[k] = [ { path : '/', url : cfg[k] } ] } else if (!(cfg[k] instanceof Array) && typeof cfg[k] === 'object') { cfg[k] = [ cfg[k] ] } else if ( !(cfg[k] instanceof Array) ) { continue } /** * { * path : '', * url : '', * aliveCheckPath : '', * headers : {} * } */ for (let i = 0; i < cfg[k].length; i++) { tmp = cfg[k][i] if (typeof tmp !== 'object' || (tmp instanceof Array) ) { console.error(`${k} ${JSON.stringify(tmp)} 错误的配置格式`) continue } if (tmp.path === undefined) { tmp.path = '/' } if (tmp.url === undefined) { console.error(`${k} ${tmp.path}:没有指定要代理转发的url。`) continue } if (this.urlpreg.test(tmp.url) === false) { console.error(`${tmp.url} : 错误的url,请检查。`) continue } pt = this.fmtpath(tmp.path) 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形式提供。` ); continue } } if (this.hostProxy[k] === undefined) { this.hostProxy[k] = {} this.proxyBalance[k] = {} } tmp.urlobj = this.parseUrl(tmp.url) tmp.urlobj.timeout = tmp.timeout || this.timeout backend_obj = { url : tmp.url, urlobj : tmp.urlobj, headers : {}, path : tmp.path, weight: 1, weightCount : 0, alive : true, aliveCheckInterval : 5, aliveCheckPath : '/', intervalCount : 0, rewrite: (tmp.rewrite && typeof tmp.rewrite === 'function') ? tmp.rewrite : null, connectOptions: {...this.connectOptions} } if (tmp.connectOptions && typeof tmp.connectOptions) { for (let o in tmp.connectOptions) { backend_obj.connectOptions[o] = tmp.connectOptions[o] } } if (tmp.headers !== undefined) { for (let h in tmp.headers) { backend_obj.headers[h] = tmp.headers[h] } } if (typeof tmp.aliveCheckPath === 'string' && tmp.aliveCheckPath.length > 0) { if (tmp.aliveCheckPath[0] !== '/') { tmp.aliveCheckPath = `/${tmp.aliveCheckPath}` } backend_obj.aliveCheckPath = tmp.aliveCheckPath } if (tmp.weight && typeof tmp.weight === 'number' && tmp.weight > 1) { backend_obj.weight = parseInt(tmp.weight) } if (tmp.aliveCheckInterval !== undefined && typeof tmp.aliveCheckInterval === 'number') { if (tmp.aliveCheckInterval >= 0 && tmp.aliveCheckInterval <= 7200) { backend_obj.aliveCheckInterval = tmp.aliveCheckInterval } } 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 } parseUrl(url) { let u = new urlparse.URL(url) let urlobj = { hash : u.hash, hostname: u.hostname, protocol: u.protocol, path : u.pathname, method : 'GET', headers : {}, } if (u.search.length > 0) { urlobj.path += u.search } if (u.protocol === 'unix:') { urlobj.protocol = 'http:' let sockarr = u.pathname.split('.sock') urlobj.socketPath = `${sockarr[0]}.sock` urlobj.path = sockarr[1] } else { urlobj.host = u.host urlobj.port = u.port } if (u.protocol === 'https:') { urlobj.requestCert = false urlobj.rejectUnauthorized = false } return urlobj } copyUrlobj(uobj) { let u = { hash: uobj.hash, hostname: uobj.hostname, protocol: uobj.protocol, path: uobj.path, method: 'GET', headers: {}, timeout: uobj.timeout } if (uobj.host) { u.host = uobj.host u.port = uobj.port } else { u.socketPath = uobj.socketPath } if (uobj.protocol === 'https:') { u.requestCert = false u.rejectUnauthorized = false } return u } getBackend(c, host) { let prlist = this.hostProxy[host][c.routepath] let pb = this.proxyBalance[host][c.routepath] let pr if (prlist.length === 1) { pr = prlist[0] } else { if (pb.stepIndex >= prlist.length) { pb.stepIndex = 0 } pr = prlist[pb.stepIndex] if (pb.useWeight) { if (pr.weightCount >= pr.weight) { pr.weightCount = 0 pb.stepIndex++ } else { pr.weightCount++ } } else { pb.stepIndex++ } } if (pr.alive === false) { for (let i = 0; i < prlist.length; i++) { pr = prlist[i] if (pr.alive === true) { return pr } } return null } return pr } mid() { 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]===undefined || self.hostProxy[host][c.routepath]===undefined) { if (self.full) { return c.status(502).send(self.error['502']) } return await next() } let pr = self.getBackend(c, host) if (pr === null) { for (let i = 0; i < 200; i++) { await new Promise((rv, rj) => {setTimeout(rv, 10)}) pr = self.getBackend(c, host) if (pr) break } if (!pr) return c.status(503).send(self.error['503']) } let urlobj = self.copyUrlobj(pr.urlobj) urlobj.path = c.request.url urlobj.headers = c.headers urlobj.method = c.method if (self.addIP && urlobj.headers['x-real-ip']) { urlobj.headers['x-real-ip'] += `,${c.ip}` } else { urlobj.headers['x-real-ip'] = c.ip } let hci = urlobj.protocol == 'https:' ? https : http for (let k in pr.connectOptions) { urlobj[k] = pr.connectOptions[k] } if (pr.rewrite) { let rw = pr.rewrite(c, c.request.url) if (rw) { let path_typ = typeof rw if (path_typ === 'string') { urlobj.path = rw } else if (path_typ === 'object' && rw.redirect) { return c.setHeader('location', rw.redirect) } } } let h = hci.request(urlobj) return await new Promise((rv, rj) => { let resolved = false let rejected = false c.request.on('timeout', () => { !h.destroyed && h.destroy(timeoutError) }) c.response.on('timeout', () => { !h.destroyed && h.destroy(timeoutError) }) h.on('timeout', () => { !h.destroyed && h.destroy(timeoutError) }) h.on('close', () => { if (!resolved && !rejected) { resolved = true rv() } }) h.on('response', res => { c.status(res.statusCode) for (let k in res.headers) { c.setHeader(k, res.headers[k]) } res.on('data', chunk => { c.response.write(chunk) }) res.on('end', () => { c.response.end() if (!resolved && !rejected) { resolved = true rv() } }) res.on('error', err => { if (!resolved && !rejected){ rejected = true rj(err) } }) }) h.on('error', (err) => { if (!resolved && !rejected) { rejected = true rj(err) } }) c.request.on('data', chunk => { h.write(chunk) }) c.request.on('end', () => { h.end() }) }).catch(err => { self.debug && console.error(err); c.status(503).send(self.error['503']); }) .finally(() => { this.autoClearListeners && h.removeAllListeners && h.removeAllListeners(); !h.destroyed && h.destroy(); }) } } timerRequest(pxy, timeout=false) { let h = http let opts = { timeout : this.timeout + 30_000, method: 'TRACE', headers: { 'user-agent': 'Node.js/Titbit,Titbit-Toolkit: Proxy,AliveCheck' } } if (pxy.urlobj.protocol === 'https:') { h = https opts.rejectUnauthorized = false opts.requestCert = false } for (let o in pxy.connectOptions) { opts[o] = pxy.connectOptions[o] } let aliveUrl = `${pxy.urlobj.protocol}//${pxy.urlobj.host}${pxy.aliveCheckPath}` let req = h.request(aliveUrl, opts) req.on('error', err => { pxy.alive = false //当出现连接错误,立即发起一个请求,测试是否是某些特殊情况导致的异常,比如服务重启导致瞬间请求失败。 if (!timeout) { setTimeout(() => { this.timerRequest(pxy, true) }, 500) } }) req.on('response', res => { pxy.alive = true res.on('error', err => { }) res.on('data', chunk => { pxy.alive = true }) res.on('end', () => { pxy.alive = true }) }) req.end() } setTimer(pxys) { let count = 0 for (let p of pxys) { if (p.aliveCheckInterval > 0) count++ } if (count === 0) return null let self = this return setInterval(() => { for (let i = 0; i < pxys.length; i++) { if (pxys[i].aliveCheckInterval <= 0) continue pxys[i].intervalCount++ if (pxys[i].intervalCount >= pxys[i].aliveCheckInterval) { pxys[i].intervalCount = 0 self.timerRequest(pxys[i]) } } }, 1000) } init(app) { app.config.timeout = this.timeout for (let p in this.pathTable) { app.router.map(this.methods, p, async c => {}, '@titbit_proxy') } app.use(this.mid(), {pre: true, group: `titbit_proxy`}) for (let k in this.hostProxy) { this.proxyIntervals[k] = {} for (let p in this.hostProxy[k]) { this.proxyIntervals[k][p] = this.setTimer(this.hostProxy[k][p]) } } } } module.exports = Proxy