titbit-toolkit
Version:
titbit框架的工具集,包括跨域、静态资源处理,权限过滤,请求计时,cookie,session,jwt等大量中间件
663 lines (512 loc) • 15.1 kB
JavaScript
'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