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