topbit 3.2.1 → 3.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.cn.md +0 -10
- package/demo/http2.js +33 -1
- package/demo/http2_proxy_backend.js +1 -1
- package/demo/http2proxy.js +2 -2
- package/package.json +1 -1
- package/src/extends/http2limit.js +1 -1
- package/src/extends/http2proxy.js +21 -6
- package/src/extends/proxy.js +19 -3
- package/src/lib/balancer.js +76 -0
- package/src/topbit.js +10 -2
package/README.cn.md
CHANGED
|
@@ -1933,16 +1933,6 @@ app.run(1234)
|
|
|
1933
1933
|
|
|
1934
1934
|
### 7. SNI (HTTPS 多域名支持)
|
|
1935
1935
|
|
|
1936
|
-
**描述**:用于在同一 IP 地址和端口上支持多个 HTTPS 域名证书的中间件。
|
|
1937
|
-
|
|
1938
|
-
感谢提供源代码。根据代码逻辑,`SNI` 扩展通过 `init` 方法将 `SNICallback` 注入到应用的 `config.server` 配置中,从而利用 Node.js 原生 TLS 的能力实现多域名证书支持。
|
|
1939
|
-
|
|
1940
|
-
以下是补全后的 **SNI** 文档部分:
|
|
1941
|
-
|
|
1942
|
-
---
|
|
1943
|
-
|
|
1944
|
-
### 7. SNI (HTTPS 多域名支持)
|
|
1945
|
-
|
|
1946
1936
|
**描述**:用于在同一 IP 地址和端口上支持多个 HTTPS 域名证书的中间件。它利用 TLS 协议的 Server Name Indication 特性,根据客户端请求的域名动态加载对应的 SSL 证书。
|
|
1947
1937
|
|
|
1948
1938
|
**注意**:初始化时会同步读取证书文件,请确保路径正确。如果某个域名的证书读取失败,会在控制台输出错误信息,但不会阻塞其他域名的加载。
|
package/demo/http2.js
CHANGED
|
@@ -19,7 +19,15 @@ let app = new Topbit({
|
|
|
19
19
|
loadInfoFile: '--mem',
|
|
20
20
|
cert: './cert/localhost-cert.pem',
|
|
21
21
|
key: './cert/localhost-privkey.pem',
|
|
22
|
-
http2: true
|
|
22
|
+
http2: true,
|
|
23
|
+
server: {
|
|
24
|
+
peerMaxConcurrentStreams: 200,
|
|
25
|
+
settings: {
|
|
26
|
+
maxConcurrentStreams: 201,
|
|
27
|
+
maxHeaderListSize: 16384,
|
|
28
|
+
maxHeaderSize: 16384
|
|
29
|
+
}
|
|
30
|
+
}
|
|
23
31
|
})
|
|
24
32
|
|
|
25
33
|
if (app.isWorker) {
|
|
@@ -42,8 +50,32 @@ if (app.isWorker) {
|
|
|
42
50
|
})
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
app.on('connection', sock => {
|
|
54
|
+
console.log(sock)
|
|
55
|
+
})
|
|
45
56
|
|
|
46
57
|
app.sched('none')
|
|
47
58
|
.autoWorker(3)
|
|
48
59
|
.printServInfo()
|
|
49
60
|
.daemon(1234, 2)
|
|
61
|
+
|
|
62
|
+
let settings = {
|
|
63
|
+
maxConcurrentStreams: 200,
|
|
64
|
+
maxHeaderListSize: 16384
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
app.on('session', sess => {
|
|
68
|
+
console.log(sess.localSettings, sess.remoteSettings)
|
|
69
|
+
|
|
70
|
+
sess.on('localSettings', s => {
|
|
71
|
+
console.log('local', s)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
sess.on('remoteSettings', s => {
|
|
75
|
+
console.log('remote', s)
|
|
76
|
+
})
|
|
77
|
+
/* sess.settings(settings, (err, setting, dura) => {
|
|
78
|
+
console.log(setting, dura)
|
|
79
|
+
}) */
|
|
80
|
+
|
|
81
|
+
})
|
package/demo/http2proxy.js
CHANGED
|
@@ -14,7 +14,7 @@ let app = new Topbit({
|
|
|
14
14
|
if (app.isWorker) {
|
|
15
15
|
let h2proxy = new Http2Proxy({
|
|
16
16
|
config: {
|
|
17
|
-
'
|
|
17
|
+
'v.com': [
|
|
18
18
|
{
|
|
19
19
|
url: 'http://localhost:3001',
|
|
20
20
|
weight: 10,
|
|
@@ -45,4 +45,4 @@ if (app.isWorker) {
|
|
|
45
45
|
h2proxy.init(app)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
app.daemon(1234, 2)
|
|
48
|
+
app.printServInfo().daemon(1234, 2)
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@ let error_503_text = `<!DOCTYPE html><html>
|
|
|
29
29
|
<p>此服务暂时不可用。</p>
|
|
30
30
|
</div>
|
|
31
31
|
</body>
|
|
32
|
-
</html>`
|
|
32
|
+
</html>`
|
|
33
33
|
|
|
34
34
|
function fmtpath(path) {
|
|
35
35
|
path = path.trim()
|
|
@@ -83,6 +83,8 @@ let Http2Proxy = function (options = {}) {
|
|
|
83
83
|
|
|
84
84
|
this.maxBody = 50000000
|
|
85
85
|
|
|
86
|
+
this.realIPHeader = 'x-real-ip'
|
|
87
|
+
|
|
86
88
|
//是否启用全代理模式。
|
|
87
89
|
this.full = false
|
|
88
90
|
|
|
@@ -103,8 +105,18 @@ let Http2Proxy = function (options = {}) {
|
|
|
103
105
|
family: 4
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
this.balancer = (options.balancer
|
|
109
|
+
&& options.balancer.select
|
|
110
|
+
&& typeof options.balancer.select === 'function')
|
|
111
|
+
? options.balancer
|
|
112
|
+
: null
|
|
113
|
+
|
|
106
114
|
for (let k in options) {
|
|
107
115
|
switch (k) {
|
|
116
|
+
case 'realIPHeader':
|
|
117
|
+
this.realIPHeader = options[k]
|
|
118
|
+
break
|
|
119
|
+
|
|
108
120
|
case 'config':
|
|
109
121
|
this.config = options[k]
|
|
110
122
|
break
|
|
@@ -323,9 +335,12 @@ Http2Proxy.prototype.checkAlive = function (pr) {
|
|
|
323
335
|
|
|
324
336
|
Http2Proxy.prototype.getBackend = function (c, host) {
|
|
325
337
|
let prlist = this.hostProxy[host][c.routepath]
|
|
326
|
-
|
|
327
338
|
let pxybalance = this.proxyBalance[host][c.routepath]
|
|
328
339
|
|
|
340
|
+
if (this.balancer) {
|
|
341
|
+
return this.balancer.select(c, prlist, pxybalance)
|
|
342
|
+
}
|
|
343
|
+
|
|
329
344
|
let pr
|
|
330
345
|
|
|
331
346
|
if (prlist.length === 1) {
|
|
@@ -399,7 +414,7 @@ Http2Proxy.prototype.mid = function () {
|
|
|
399
414
|
|
|
400
415
|
if (!self.hostProxy[host] || !self.hostProxy[host][c.routepath]) {
|
|
401
416
|
if (self.full) {
|
|
402
|
-
return c.status(502).to(error_502_text)
|
|
417
|
+
return c.status(502).to(error_502_text)
|
|
403
418
|
}
|
|
404
419
|
|
|
405
420
|
return await next(c)
|
|
@@ -408,10 +423,10 @@ Http2Proxy.prototype.mid = function () {
|
|
|
408
423
|
let pr = self.getBackend(c, host)
|
|
409
424
|
if (!pr) return c.status(503).to(error_503_text)
|
|
410
425
|
|
|
411
|
-
if (self.addIP && c.headers[
|
|
412
|
-
c.headers[
|
|
426
|
+
if (self.addIP && c.headers[self.realIPHeader]) {
|
|
427
|
+
c.headers[self.realIPHeader] += `,${c.ip}`
|
|
413
428
|
} else {
|
|
414
|
-
c.headers[
|
|
429
|
+
c.headers[self.realIPHeader] = c.ip
|
|
415
430
|
}
|
|
416
431
|
|
|
417
432
|
let hii = pr.h2Pool
|
package/src/extends/proxy.js
CHANGED
|
@@ -40,6 +40,8 @@ class Proxy {
|
|
|
40
40
|
|
|
41
41
|
this.methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE']
|
|
42
42
|
|
|
43
|
+
this.realIPHeader = 'x-real-ip'
|
|
44
|
+
|
|
43
45
|
this.hostProxy = {}
|
|
44
46
|
|
|
45
47
|
this.proxyBalance = {}
|
|
@@ -104,8 +106,18 @@ class Proxy {
|
|
|
104
106
|
options = {}
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
this.balancer = (options.balancer
|
|
110
|
+
&& options.balancer.select
|
|
111
|
+
&& typeof options.balancer.select === 'function')
|
|
112
|
+
? options.balancer
|
|
113
|
+
: null
|
|
114
|
+
|
|
107
115
|
for (let k in options) {
|
|
108
116
|
switch (k) {
|
|
117
|
+
case 'realIPHeader':
|
|
118
|
+
this.realIPHeader = options[k]
|
|
119
|
+
break
|
|
120
|
+
|
|
109
121
|
case 'host':
|
|
110
122
|
case 'config':
|
|
111
123
|
this.config = options[k]
|
|
@@ -375,6 +387,10 @@ class Proxy {
|
|
|
375
387
|
getBackend(c, host) {
|
|
376
388
|
let prlist = this.hostProxy[host][c.routepath]
|
|
377
389
|
let pb = this.proxyBalance[host][c.routepath]
|
|
390
|
+
if (this.balancer) {
|
|
391
|
+
return this.balancer.select(c, prlist, pxybalance)
|
|
392
|
+
}
|
|
393
|
+
|
|
378
394
|
let pr
|
|
379
395
|
|
|
380
396
|
if (prlist.length === 1) {
|
|
@@ -448,10 +464,10 @@ class Proxy {
|
|
|
448
464
|
urlobj.headers = c.headers
|
|
449
465
|
urlobj.method = c.method
|
|
450
466
|
|
|
451
|
-
if (self.addIP && urlobj.headers[
|
|
452
|
-
urlobj.headers[
|
|
467
|
+
if (self.addIP && urlobj.headers[self.realIPHeader]) {
|
|
468
|
+
urlobj.headers[self.realIPHeader] += `,${c.ip}`
|
|
453
469
|
} else {
|
|
454
|
-
urlobj.headers[
|
|
470
|
+
urlobj.headers[self.realIPHeader] = c.ip
|
|
455
471
|
}
|
|
456
472
|
|
|
457
473
|
let hci = urlobj.protocol == 'https:' ? https : http
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 确定性负载均衡器
|
|
7
|
+
* 支持基于用户 ID 的哈希分发
|
|
8
|
+
*/
|
|
9
|
+
class ConsistentBalancer {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
// 定义如何从请求上下文 c 中提取唯一标识
|
|
12
|
+
// 默认尝试提取 header 中的 user-id,或者使用 IP
|
|
13
|
+
this.identityFn = options.identityFn || ((c) => {
|
|
14
|
+
return c.user ? c.user.id : c.ip
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
this.hashAlgorithm = options.hashAlgorithm || 'sha256'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 负载均衡选择算法
|
|
22
|
+
* @param {Object} c 请求上下文
|
|
23
|
+
* @param {Array} prlist 备选后端列表
|
|
24
|
+
* @param {Object} pxybalance 状态表
|
|
25
|
+
*/
|
|
26
|
+
select(c, prlist, pxybalance) {
|
|
27
|
+
if (!prlist || prlist.length === 0) return null
|
|
28
|
+
|
|
29
|
+
if (prlist.length === 1) return prlist[0]
|
|
30
|
+
|
|
31
|
+
// 1. 提取标识符
|
|
32
|
+
const identity = this.identityFn(c)
|
|
33
|
+
|
|
34
|
+
if (!identity) {
|
|
35
|
+
// 如果没有标识符,退回到随机/轮询(或者直接用原逻辑)
|
|
36
|
+
return this.fallback(prlist, pxybalance)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. 过滤健康的后端 (基于你原有的 checkAlive 逻辑)
|
|
40
|
+
// 注意:pr.h2Pool.ok() 是判断连接池是否正常的关键
|
|
41
|
+
const aliveBackends = prlist.filter(pr => pr.h2Pool && pr.h2Pool.ok())
|
|
42
|
+
|
|
43
|
+
const targets = aliveBackends.length > 0 ? aliveBackends : prlist
|
|
44
|
+
|
|
45
|
+
// 3. 确定性哈希计算 (Rendezvous Hashing)
|
|
46
|
+
let maxWeight = -1
|
|
47
|
+
let selected = targets[0]
|
|
48
|
+
|
|
49
|
+
for (let pr of targets) {
|
|
50
|
+
// 计算 Hash(identity + server_url)
|
|
51
|
+
let hash = crypto.createHash(this.hashAlgorithm)
|
|
52
|
+
.update(identity + pr.url)
|
|
53
|
+
.digest()
|
|
54
|
+
.readUInt32BE(0)
|
|
55
|
+
|
|
56
|
+
// 结合权重计算分值 (HRW Hashing 变体)
|
|
57
|
+
// 使用公式:Score = Hash * (Weight^(1/n)) 或者简单乘法
|
|
58
|
+
let score = hash * (pr.weight || 1)
|
|
59
|
+
|
|
60
|
+
if (score > maxWeight) {
|
|
61
|
+
maxWeight = score
|
|
62
|
+
selected = pr
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return selected
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 兜底逻辑
|
|
70
|
+
fallback(prlist, pxybalance) {
|
|
71
|
+
if (pxybalance.stepIndex >= prlist.length) pxybalance.stepIndex = 0
|
|
72
|
+
return prlist[pxybalance.stepIndex++]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = ConsistentBalancer
|
package/src/topbit.js
CHANGED
|
@@ -35,6 +35,7 @@ const TopbitExtends = require('./_loadExtends.js')
|
|
|
35
35
|
const npargv = require('./lib/npargv.js')
|
|
36
36
|
const zipdata = require('./lib/zipdata.js')
|
|
37
37
|
const ErrorLog = require('./lib/errorlog.js')
|
|
38
|
+
const Balancer = require('./lib/balancer.js')
|
|
38
39
|
|
|
39
40
|
let __instance__ = 0;
|
|
40
41
|
|
|
@@ -780,14 +781,20 @@ class Topbit {
|
|
|
780
781
|
if (typeof callback === 'function') {
|
|
781
782
|
this.httpServ.requestError = callback;
|
|
782
783
|
}
|
|
783
|
-
return;
|
|
784
|
+
return this;
|
|
784
785
|
}
|
|
785
|
-
|
|
786
|
+
|
|
787
|
+
if (this.server && this.server.on) {
|
|
788
|
+
this.server.on(evt, callback);
|
|
789
|
+
return this;
|
|
790
|
+
}
|
|
791
|
+
|
|
786
792
|
if (!this.eventTable[evt]) {
|
|
787
793
|
this.eventTable[evt] = [ callback ];
|
|
788
794
|
} else {
|
|
789
795
|
this.eventTable[evt].push(callback);
|
|
790
796
|
}
|
|
797
|
+
return this;
|
|
791
798
|
}
|
|
792
799
|
|
|
793
800
|
/**
|
|
@@ -1329,5 +1336,6 @@ Topbit.npargv = npargv;
|
|
|
1329
1336
|
Topbit.zipdata = zipdata;
|
|
1330
1337
|
Topbit.ErrorLog = ErrorLog;
|
|
1331
1338
|
Topbit.extensions = TopbitExtends;
|
|
1339
|
+
Topbit.ProxyBalancer = Balancer;
|
|
1332
1340
|
|
|
1333
1341
|
module.exports = Topbit;
|