topbit 3.1.0 → 3.1.1
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 +1 -1
- package/README.md +1 -1
- package/demo/group-api.js +1 -1
- package/demo/group-api2.js +1 -1
- package/demo/monitor.js +6 -3
- package/package.json +1 -1
- package/src/extends/Http2Pool.js +143 -309
- package/src/extends/http2proxy.js +39 -141
- package/src/extends/proxy.js +16 -17
- package/test/test-route-sort.js +1 -1
- package/test/test-route2.js +1 -1
package/README.cn.md
CHANGED
package/README.md
CHANGED
package/demo/group-api.js
CHANGED
package/demo/group-api2.js
CHANGED
package/demo/monitor.js
CHANGED
|
@@ -12,11 +12,14 @@ let {args} = npargv({
|
|
|
12
12
|
'--loadstdio': {
|
|
13
13
|
name: 'loadstdio',
|
|
14
14
|
default: false
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
'--http2': {
|
|
18
|
+
name: 'http2',
|
|
19
|
+
default: false
|
|
15
20
|
}
|
|
16
21
|
})
|
|
17
22
|
|
|
18
|
-
console.log(args)
|
|
19
|
-
|
|
20
23
|
const app = new Topbit({
|
|
21
24
|
debug : true,
|
|
22
25
|
allow : new Set(['127.0.0.1']),
|
|
@@ -24,7 +27,7 @@ const app = new Topbit({
|
|
|
24
27
|
unitTime: 10,
|
|
25
28
|
useLimit: true,
|
|
26
29
|
maxConn: 2000,
|
|
27
|
-
|
|
30
|
+
http2: args.http2,
|
|
28
31
|
loadMonitor: true,
|
|
29
32
|
loadInfoType : args.loadtype,
|
|
30
33
|
globalLog : false,
|
package/package.json
CHANGED
package/src/extends/Http2Pool.js
CHANGED
|
@@ -6,360 +6,194 @@ const crypto = require('node:crypto')
|
|
|
6
6
|
class Http2Pool {
|
|
7
7
|
|
|
8
8
|
constructor(options = {}) {
|
|
9
|
-
// 存储session的Map
|
|
10
9
|
if (!options || typeof options !== 'object') options = {}
|
|
11
|
-
if (!options.connectOptions) options.connectOptions = {}
|
|
12
|
-
|
|
13
|
-
this.pool = new Map()
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
this.max = (options.max && !isNaN(options.max) && options.max > 0) ? options.max : 50
|
|
27
|
-
this.poolMax = Math.floor(this.max * 1.5 + 0.5)
|
|
28
|
-
this.maxConnect = (options.maxConnect && !isNaN(options.maxConnect) && options.maxConnect > 0)
|
|
29
|
-
? options.maxConnect
|
|
30
|
-
: this.poolMax + 500
|
|
31
|
-
|
|
32
|
-
this.url = options.url || ''
|
|
33
|
-
this.debug = options.debug || false
|
|
34
|
-
// 连接选项
|
|
11
|
+
// 配置初始化
|
|
12
|
+
this.maxConnect = options.maxConnect || 100 // 最大物理连接数
|
|
13
|
+
this.maxAliveStreams = options.maxAliveStreams || 100 // 单连接最大并发流
|
|
14
|
+
this.url = options.url
|
|
15
|
+
this.debug = !!options.debug
|
|
16
|
+
this.reconnDelay = options.reconnDelay || 500
|
|
17
|
+
|
|
18
|
+
// 核心数据结构
|
|
19
|
+
this.sessions = [] // 使用数组代替Map,利用索引做 Round-Robin
|
|
20
|
+
this.cursor = 0 // 轮询指针
|
|
21
|
+
|
|
35
22
|
this.connectOptions = {
|
|
36
23
|
rejectUnauthorized: false,
|
|
37
|
-
|
|
38
|
-
peerMaxConcurrentStreams: 100,
|
|
39
|
-
timeout: this.timeout,
|
|
24
|
+
timeout: options.timeout || 30000,
|
|
40
25
|
...options.connectOptions
|
|
41
26
|
}
|
|
27
|
+
|
|
28
|
+
this.waitQueue = [] // 等待可用连接的队列
|
|
29
|
+
}
|
|
42
30
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (options.parent && typeof options.parent === 'object') {
|
|
51
|
-
this.parent = options.parent
|
|
31
|
+
/**
|
|
32
|
+
* 初始化连接池 (预热)
|
|
33
|
+
*/
|
|
34
|
+
createPool(initialSize = 5) {
|
|
35
|
+
for (let i = 0; i < initialSize; i++) {
|
|
36
|
+
this._createConnection()
|
|
52
37
|
}
|
|
53
|
-
|
|
54
|
-
this.maxAliveStreams = options.maxAliveStreams || 100
|
|
55
|
-
|
|
56
|
-
this.quiet = false
|
|
57
|
-
if (options.quiet)
|
|
58
|
-
this.quiet = !!options.quiet
|
|
59
38
|
}
|
|
60
39
|
|
|
61
40
|
/**
|
|
62
|
-
*
|
|
41
|
+
* 内部建立连接
|
|
63
42
|
*/
|
|
64
|
-
|
|
65
|
-
if (this.
|
|
66
|
-
return {
|
|
67
|
-
deny: true,
|
|
68
|
-
error: `超出最大连接限制:${this.maxConnect}`
|
|
69
|
-
}
|
|
70
|
-
}
|
|
43
|
+
_createConnection() {
|
|
44
|
+
if (this.sessions.length >= this.maxConnect) return null
|
|
71
45
|
|
|
72
46
|
const session = http2.connect(this.url, this.connectOptions)
|
|
73
|
-
|
|
74
|
-
// 生成唯一session id
|
|
75
|
-
const sessionId = crypto.randomBytes(16).toString('hex')
|
|
76
47
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
id: sessionId,
|
|
81
|
-
session,
|
|
82
|
-
streamCount: 0,
|
|
83
|
-
url: this.url,
|
|
48
|
+
const wrapper = {
|
|
49
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
50
|
+
session: session,
|
|
84
51
|
connected: false,
|
|
85
|
-
|
|
86
|
-
|
|
52
|
+
aliveStreams: 0, // 当前并发数
|
|
53
|
+
weight: 1 // 预留权重字段
|
|
87
54
|
}
|
|
88
55
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
let resolved = false
|
|
96
|
-
let rejected = false
|
|
97
|
-
|
|
98
|
-
await new Promise((resolve, reject) => {
|
|
99
|
-
session.once('connect', () => {
|
|
100
|
-
if (timeout_timer) {
|
|
101
|
-
clearTimeout(timeout_timer)
|
|
102
|
-
timeout_timer = null
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (this.failedCount > 0) {
|
|
106
|
-
this.failedCount--
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
this.innerConnectDelay = 0
|
|
110
|
-
|
|
111
|
-
sessionState.connected = true
|
|
112
|
-
this.parent && !this.parent.alive && (this.parent.alive = true)
|
|
113
|
-
|
|
114
|
-
resolve()
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
session.once('error', err => {
|
|
118
|
-
if (timeout_timer) {
|
|
119
|
-
clearTimeout(timeout_timer)
|
|
120
|
-
timeout_timer = null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (this.pool.size < 1) {
|
|
124
|
-
this.parent && (this.parent.alive = false)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
this.failedCount++
|
|
128
|
-
|
|
129
|
-
if (this.failedCount < 10) {
|
|
130
|
-
this.innerConnectDelay = this.failedCount
|
|
131
|
-
} if (this.failedCount < 60000) {
|
|
132
|
-
this.innerConnectDelay = Math.floor(this.failedCount / 10)
|
|
133
|
-
} else { this.innerConnectDelay = 6000 }
|
|
134
|
-
|
|
135
|
-
!rejected && (rejected = true) && reject(err)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
session.once('goaway', err => {
|
|
139
|
-
if (timeout_timer) {
|
|
140
|
-
clearTimeout(timeout_timer)
|
|
141
|
-
timeout_timer = null
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
!rejected && (rejected = true) && reject(err||new Error('goaway'))
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
session.once('frameError', err => {
|
|
148
|
-
if (timeout_timer) {
|
|
149
|
-
clearTimeout(timeout_timer)
|
|
150
|
-
timeout_timer = null
|
|
151
|
-
}
|
|
152
|
-
!rejected && (rejected = true) && reject(err)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
if (!timeout_timer) {
|
|
156
|
-
timeout_timer = setTimeout(() => {
|
|
157
|
-
timeout_timer = null
|
|
158
|
-
!session.destroyed && session.destroy()
|
|
159
|
-
!rejected && (rejected = true) && reject(new Error('connect timeout'))
|
|
160
|
-
}, this.connectTimeout)
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
} catch (err) {
|
|
164
|
-
sessionState.error = err
|
|
165
|
-
sessionState.session = null
|
|
166
|
-
this.debug && console.error(err)
|
|
167
|
-
} finally {
|
|
168
|
-
this.reconnecting = false
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (this.pool.size < this.poolMax && sessionState.connected) {
|
|
172
|
-
this.pool.set(sessionId, sessionState)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return sessionState
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
createPool(max=0) {
|
|
179
|
-
if (max <= 0) max = this.max
|
|
180
|
-
|
|
181
|
-
for (let i = 0; i < max; i++) {
|
|
182
|
-
this.connect()
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
delayConnect() {
|
|
187
|
-
if (this.reconnecting) return false
|
|
188
|
-
|
|
189
|
-
let delay_time = this.reconnDelay + this.innerConnectDelay
|
|
190
|
-
|
|
191
|
-
if (delay_time > 0) {
|
|
192
|
-
if (!this.delayTimer) {
|
|
193
|
-
this.reconnecting = true
|
|
194
|
-
this.delayTimer = setTimeout(() => {
|
|
195
|
-
this.delayTimer = null
|
|
196
|
-
this.connect()
|
|
197
|
-
}, delay_time)
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
this.reconnecting = true
|
|
201
|
-
this.connect()
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* 处理session的各种事件
|
|
207
|
-
*/
|
|
208
|
-
_handleSessionEvents(sessionState) {
|
|
209
|
-
const { session, id } = sessionState
|
|
210
|
-
|
|
211
|
-
session.on('close', () => {
|
|
212
|
-
// session关闭时从pool中移除
|
|
213
|
-
this.pool.delete(id)
|
|
214
|
-
|
|
215
|
-
if (this.pool.size < 1) {
|
|
216
|
-
this.delayConnect()
|
|
56
|
+
session.once('connect', () => {
|
|
57
|
+
wrapper.connected = true
|
|
58
|
+
// 触发队列中的等待者
|
|
59
|
+
while (this.waitQueue.length > 0 && wrapper.aliveStreams < this.maxAliveStreams) {
|
|
60
|
+
const resolve = this.waitQueue.shift()
|
|
61
|
+
resolve(wrapper)
|
|
217
62
|
}
|
|
218
63
|
})
|
|
219
64
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
65
|
+
// 错误处理与清理
|
|
66
|
+
const cleanup = () => {
|
|
67
|
+
if (wrapper._destroyed) return
|
|
68
|
+
wrapper._destroyed = true
|
|
69
|
+
|
|
70
|
+
// 从池中移除
|
|
71
|
+
const idx = this.sessions.indexOf(wrapper)
|
|
72
|
+
if (idx !== -1) {
|
|
73
|
+
this.sessions.splice(idx, 1)
|
|
74
|
+
// 修正指针,防止跳过
|
|
75
|
+
if (this.cursor >= idx && this.cursor > 0) this.cursor--
|
|
76
|
+
}
|
|
225
77
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
78
|
+
if (!session.destroyed) session.destroy()
|
|
79
|
+
|
|
80
|
+
// 自动补充连接 (维持最小连接数,可选)
|
|
81
|
+
if (this.sessions.length < 2) {
|
|
82
|
+
// 防止雪崩,延迟重连
|
|
83
|
+
setTimeout(() => this._createConnection(), this.reconnDelay)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
230
86
|
|
|
231
|
-
session.on('
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
87
|
+
session.on('close', cleanup)
|
|
88
|
+
session.on('error', (err) => {
|
|
89
|
+
if(this.debug) console.error(`[H2Pool] Session Error ${this.url}:`, err.message)
|
|
90
|
+
cleanup()
|
|
235
91
|
})
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
92
|
+
session.on('goaway', cleanup)
|
|
93
|
+
|
|
94
|
+
// 超时保活策略:只有完全空闲才销毁,否则发送 ping
|
|
95
|
+
session.setTimeout(this.connectOptions.timeout, () => {
|
|
96
|
+
if (wrapper.aliveStreams > 0) {
|
|
97
|
+
// 还有流量,不销毁,尝试 ping 保持活跃
|
|
98
|
+
session.ping && session.ping(() => {})
|
|
99
|
+
} else {
|
|
100
|
+
session.close() // 优雅关闭
|
|
101
|
+
}
|
|
244
102
|
})
|
|
245
|
-
}
|
|
246
103
|
|
|
247
|
-
|
|
248
|
-
return
|
|
249
|
-
&& !session.destroyed
|
|
250
|
-
&& !session.closed
|
|
251
|
-
&& session.socket
|
|
252
|
-
&& !session.socket.destroyed
|
|
104
|
+
this.sessions.push(wrapper)
|
|
105
|
+
return wrapper
|
|
253
106
|
}
|
|
254
107
|
|
|
255
108
|
/**
|
|
256
|
-
*
|
|
109
|
+
* 获取最佳可用 Session (Round-Robin)
|
|
257
110
|
*/
|
|
258
111
|
async getSession() {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
for (const [id, state] of items) {
|
|
262
|
-
if (state.connected
|
|
263
|
-
&& state.streamCount < this.maxStreamId
|
|
264
|
-
&& this.isSessionHealthy(state.session))
|
|
265
|
-
{
|
|
266
|
-
if (state.aliveStreams < this.maxAliveStreams) {
|
|
267
|
-
return state
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
state.connected = false
|
|
271
|
-
if (!state.session.destroyed) {
|
|
272
|
-
state.session.close()
|
|
112
|
+
let tried = 0
|
|
113
|
+
const len = this.sessions.length
|
|
273
114
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
let sess = state.session
|
|
279
|
-
setTimeout(() => {
|
|
280
|
-
!sess.destroyed && sess.destroy()
|
|
281
|
-
sess = null
|
|
282
|
-
}, this.timeout + 5000)
|
|
283
|
-
} */
|
|
284
|
-
}
|
|
115
|
+
// 1. 尝试轮询获取可用连接
|
|
116
|
+
while (tried < len) {
|
|
117
|
+
this.cursor = (this.cursor + 1) % len
|
|
118
|
+
const wrapper = this.sessions[this.cursor]
|
|
285
119
|
|
|
286
|
-
|
|
287
|
-
|
|
120
|
+
if (wrapper && wrapper.connected && !wrapper.session.destroyed && wrapper.aliveStreams < this.maxAliveStreams) {
|
|
121
|
+
return wrapper
|
|
122
|
+
}
|
|
123
|
+
tried++
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. 如果没有可用连接,且未达上限,创建新连接
|
|
127
|
+
if (this.sessions.length < this.maxConnect) {
|
|
128
|
+
const newWrapper = this._createConnection()
|
|
129
|
+
if (newWrapper) {
|
|
130
|
+
// 等待连接建立 (设置一个短超时)
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
reject(new Error('Connection timeout'))
|
|
134
|
+
}, 5000)
|
|
135
|
+
|
|
136
|
+
const onConnect = () => {
|
|
137
|
+
clearTimeout(timer)
|
|
138
|
+
newWrapper.session.removeListener('connect', onConnect)
|
|
139
|
+
resolve(newWrapper)
|
|
140
|
+
}
|
|
141
|
+
newWrapper.session.once('connect', onConnect)
|
|
142
|
+
newWrapper.session.once('error', (err) => {
|
|
143
|
+
clearTimeout(timer)
|
|
144
|
+
reject(err)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
288
147
|
}
|
|
289
148
|
}
|
|
290
149
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
//创建请求stream
|
|
308
|
-
return sessionState.session.request(headers)
|
|
150
|
+
// 3. 还是没有,进入队列等待 (削峰填谷)
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
// 3秒后还没拿到连接就报错
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
const idx = this.waitQueue.indexOf(resolve)
|
|
155
|
+
if (idx !== -1) this.waitQueue.splice(idx, 1)
|
|
156
|
+
reject(new Error('No available h2 session (Queued timeout)'))
|
|
157
|
+
}, 3000)
|
|
158
|
+
|
|
159
|
+
// 包装 resolve 以清理 timer
|
|
160
|
+
this.waitQueue.push((wrapper) => {
|
|
161
|
+
clearTimeout(timer)
|
|
162
|
+
resolve(wrapper)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
309
165
|
}
|
|
310
166
|
|
|
311
167
|
/**
|
|
312
|
-
*
|
|
168
|
+
* 发起请求 (核心入口)
|
|
313
169
|
*/
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
state.session.close()
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
this.pool.clear()
|
|
322
|
-
}
|
|
170
|
+
async request(headers) {
|
|
171
|
+
const wrapper = await this.getSession()
|
|
172
|
+
wrapper.aliveStreams++
|
|
323
173
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
174
|
+
try {
|
|
175
|
+
const stream = wrapper.session.request(headers)
|
|
176
|
+
|
|
177
|
+
// 监听流关闭,减少计数
|
|
178
|
+
stream.once('close', () => {
|
|
179
|
+
wrapper.aliveStreams--
|
|
180
|
+
// 如果有等待队列,唤醒一个
|
|
181
|
+
if (this.waitQueue.length > 0) {
|
|
182
|
+
const resolve = this.waitQueue.shift()
|
|
183
|
+
resolve(wrapper)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
return stream
|
|
188
|
+
} catch (e) {
|
|
189
|
+
wrapper.aliveStreams--
|
|
190
|
+
throw e
|
|
327
191
|
}
|
|
328
|
-
|
|
329
|
-
return this.ok()
|
|
330
192
|
}
|
|
331
193
|
|
|
332
194
|
ok() {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
for (const [id, state] of items) {
|
|
336
|
-
if (state.connected) return true
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return false
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* 获取当前pool状态
|
|
344
|
-
*/
|
|
345
|
-
status() {
|
|
346
|
-
const status = {
|
|
347
|
-
total: this.pool.size,
|
|
348
|
-
sessions: []
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
let items = this.pool.entries()
|
|
352
|
-
|
|
353
|
-
for (const [id, state] of items) {
|
|
354
|
-
status.sessions.push({
|
|
355
|
-
id: state.id,
|
|
356
|
-
streamCount: state.streamCount,
|
|
357
|
-
connected: state.connected
|
|
358
|
-
})
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return status
|
|
195
|
+
return this.sessions.some(s => s.connected && !s.session.destroyed)
|
|
362
196
|
}
|
|
363
197
|
}
|
|
364
198
|
|
|
365
|
-
module.exports = Http2Pool
|
|
199
|
+
module.exports = Http2Pool
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const http2 = require('node:http2')
|
|
4
|
-
const Http2Pool = require('./Http2Pool')
|
|
4
|
+
const Http2Pool = require('./Http2Pool.js')
|
|
5
5
|
|
|
6
6
|
let error_502_text = `<!DOCTYPE html><html>
|
|
7
7
|
<head>
|
|
@@ -52,6 +52,19 @@ function fmtpath(path) {
|
|
|
52
52
|
return `${path}*`
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// 主机名提取 (IPv6 兼容优化版)
|
|
56
|
+
function extractHostname(host) {
|
|
57
|
+
if (!host) return ''
|
|
58
|
+
if (host.charCodeAt(0) === 91) { // '[' IPv6
|
|
59
|
+
const end = host.indexOf(']')
|
|
60
|
+
return end > -1 ? host.substring(0, end + 1) : host
|
|
61
|
+
}
|
|
62
|
+
const idx = host.indexOf(':')
|
|
63
|
+
if (idx === -1) return host
|
|
64
|
+
if (host.indexOf(':', idx + 1) !== -1) return host // 裸 IPv6
|
|
65
|
+
return host.substring(0, idx)
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
let Http2Proxy = function (options = {}) {
|
|
56
69
|
|
|
57
70
|
if (!(this instanceof Http2Proxy)) return Http2Proxy(options)
|
|
@@ -188,8 +201,8 @@ Http2Proxy.prototype.checkAndSetConfig = function (backend_obj, tmp) {
|
|
|
188
201
|
|
|
189
202
|
}
|
|
190
203
|
|
|
191
|
-
if (tmp.
|
|
192
|
-
backend_obj.
|
|
204
|
+
if (tmp.maxConnect && typeof tmp.maxConnect === 'number' && tmp.maxConnect > 1)
|
|
205
|
+
backend_obj.maxConnect = tmp.maxConnect
|
|
193
206
|
|
|
194
207
|
if (tmp.debug !== undefined) backend_obj.debug = tmp.debug
|
|
195
208
|
|
|
@@ -244,8 +257,7 @@ Http2Proxy.prototype.setHostProxy = function (cfg) {
|
|
|
244
257
|
weight: 1,
|
|
245
258
|
weightCount: 0,
|
|
246
259
|
reconnDelay: 500,
|
|
247
|
-
|
|
248
|
-
maxConnect: tmp.maxConnect || 0,
|
|
260
|
+
maxConnect: tmp.maxConnect || 10,
|
|
249
261
|
debug: this.debug,
|
|
250
262
|
h2Pool: null,
|
|
251
263
|
timeout: this.timeout,
|
|
@@ -268,7 +280,6 @@ Http2Proxy.prototype.setHostProxy = function (cfg) {
|
|
|
268
280
|
|
|
269
281
|
backend_obj.h2Pool = new Http2Pool({
|
|
270
282
|
debug: backend_obj.debug,
|
|
271
|
-
max: backend_obj.max,
|
|
272
283
|
url: backend_obj.url,
|
|
273
284
|
connectOptions: backend_obj.connectOptions,
|
|
274
285
|
parent: backend_obj,
|
|
@@ -338,23 +349,6 @@ Http2Proxy.prototype.getBackend = function (c, host) {
|
|
|
338
349
|
}
|
|
339
350
|
}
|
|
340
351
|
|
|
341
|
-
if (!pr.alive) {
|
|
342
|
-
pr.h2Pool && pr.h2Pool.delayConnect()
|
|
343
|
-
|
|
344
|
-
for (let i = prlist.length - 1; i >= 0 ; i--) {
|
|
345
|
-
|
|
346
|
-
pr = prlist[i]
|
|
347
|
-
|
|
348
|
-
if (pr.alive) {
|
|
349
|
-
return pr
|
|
350
|
-
} else {
|
|
351
|
-
pr.h2Pool && pr.h2Pool.delayConnect()
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return null
|
|
356
|
-
}
|
|
357
|
-
|
|
358
352
|
return pr
|
|
359
353
|
}
|
|
360
354
|
|
|
@@ -401,22 +395,7 @@ Http2Proxy.prototype.mid = function () {
|
|
|
401
395
|
|
|
402
396
|
return async (c, next) => {
|
|
403
397
|
|
|
404
|
-
let host = c.host
|
|
405
|
-
|
|
406
|
-
let hind = c.host.length - 1
|
|
407
|
-
|
|
408
|
-
if (hind > 4) {
|
|
409
|
-
let eind = hind - 5
|
|
410
|
-
|
|
411
|
-
while (hind >= eind) {
|
|
412
|
-
if (c.host[hind] === ':') {
|
|
413
|
-
host = c.host.substring(0, hind)
|
|
414
|
-
break
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
hind--
|
|
418
|
-
}
|
|
419
|
-
}
|
|
398
|
+
let host = extractHostname(c.host)
|
|
420
399
|
|
|
421
400
|
if (!self.hostProxy[host] || !self.hostProxy[host][c.routepath]) {
|
|
422
401
|
if (self.full) {
|
|
@@ -427,25 +406,7 @@ Http2Proxy.prototype.mid = function () {
|
|
|
427
406
|
}
|
|
428
407
|
|
|
429
408
|
let pr = self.getBackend(c, host)
|
|
430
|
-
|
|
431
|
-
if (!pr) {
|
|
432
|
-
pr = self.getBackend(c, host)
|
|
433
|
-
|
|
434
|
-
if (!pr) {
|
|
435
|
-
await c.ext.delay(9)
|
|
436
|
-
pr = self.getBackend(c, host)
|
|
437
|
-
|
|
438
|
-
if (!pr) {
|
|
439
|
-
for (let i = 0; i < 200; i++) {
|
|
440
|
-
await c.ext.delay(6 + i)
|
|
441
|
-
pr = self.getBackend(c, host)
|
|
442
|
-
if (pr) break
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (!pr) return c.status(503).to(error_503_text)
|
|
447
|
-
}
|
|
448
|
-
}
|
|
409
|
+
if (!pr) return c.status(503).to(error_503_text)
|
|
449
410
|
|
|
450
411
|
if (self.addIP && c.headers['x-real-ip']) {
|
|
451
412
|
c.headers['x-real-ip'] += `,${c.ip}`
|
|
@@ -454,7 +415,6 @@ Http2Proxy.prototype.mid = function () {
|
|
|
454
415
|
}
|
|
455
416
|
|
|
456
417
|
let hii = pr.h2Pool
|
|
457
|
-
let session_client
|
|
458
418
|
|
|
459
419
|
try {
|
|
460
420
|
if (pr.headers) {
|
|
@@ -462,73 +422,38 @@ Http2Proxy.prototype.mid = function () {
|
|
|
462
422
|
}
|
|
463
423
|
|
|
464
424
|
if (pr.rewrite) {
|
|
465
|
-
let rpath = pr.rewrite(c, c.major
|
|
425
|
+
let rpath = pr.rewrite(c, c.major > 1 ? c.headers[':path'] : c.req.url)
|
|
466
426
|
|
|
467
427
|
if (rpath) {
|
|
468
428
|
let path_typ = typeof rpath
|
|
469
429
|
if (path_typ === 'object' && rpath.redirect) {
|
|
470
430
|
return c.setHeader('location', rpath.redirect)
|
|
471
431
|
} else if (path_typ === 'string') {
|
|
472
|
-
c.
|
|
432
|
+
if (c.major > 1)
|
|
433
|
+
c.headers[':path'] = rpath
|
|
434
|
+
else c.req.url = rpath
|
|
473
435
|
}
|
|
474
436
|
}
|
|
475
437
|
}
|
|
476
438
|
|
|
477
|
-
session_client = await hii.getSession()
|
|
478
|
-
|
|
479
|
-
if (session_client.deny) {
|
|
480
|
-
for (let i = 0; i < 50; i++) {
|
|
481
|
-
await c.ext.delay(5 + i)
|
|
482
|
-
session_client = await hii.getSession()
|
|
483
|
-
if (!session_client.deny) break
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (session_client.deny)
|
|
487
|
-
return c.status(429).to('服务繁忙,请稍后再试')
|
|
488
|
-
}
|
|
489
|
-
|
|
490
439
|
await new Promise(async (rv, rj) => {
|
|
491
440
|
let resolved = false
|
|
492
441
|
let rejected = false
|
|
493
442
|
let request_stream = c.stream
|
|
494
443
|
let stm = null
|
|
495
444
|
|
|
496
|
-
stm = await hii.request(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
stm = null
|
|
503
|
-
})
|
|
445
|
+
stm = await hii.request(c.major > 1 ? c.headers : this.fmtHeaders(c.headers, c))
|
|
446
|
+
.catch(err => {
|
|
447
|
+
rejected = true
|
|
448
|
+
rj(err)
|
|
449
|
+
stm = null
|
|
450
|
+
})
|
|
504
451
|
|
|
505
452
|
if (!stm) {
|
|
506
453
|
rj(new Error('request failed'))
|
|
507
454
|
return false
|
|
508
455
|
}
|
|
509
456
|
|
|
510
|
-
let timeout_handler = () => {
|
|
511
|
-
timeout_timer = null
|
|
512
|
-
|
|
513
|
-
//强制的异常结束,这意味着session的其他stream也会出现问题。
|
|
514
|
-
try {
|
|
515
|
-
!stm.closed && stm.close(http2.constants.NGHTTP2_CANCEL)
|
|
516
|
-
stm.destroy()
|
|
517
|
-
if (session_client.session && !session_client.session.destroyed) {
|
|
518
|
-
session_client.session.destroy()
|
|
519
|
-
}
|
|
520
|
-
} catch(e) {}
|
|
521
|
-
|
|
522
|
-
if (!resolved && !rejected) {
|
|
523
|
-
rejected = true
|
|
524
|
-
rj(new Error('force destroy stream, request timeout'))
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
session_client.aliveStreams++
|
|
529
|
-
|
|
530
|
-
let timeout_timer = setTimeout(timeout_handler, pr.timeout + 5000)
|
|
531
|
-
|
|
532
457
|
c.stream.on('timeout', () => {
|
|
533
458
|
stm.close(http2.constants.NGHTTP2_CANCEL)
|
|
534
459
|
//stm.destroy()
|
|
@@ -541,11 +466,6 @@ Http2Proxy.prototype.mid = function () {
|
|
|
541
466
|
})
|
|
542
467
|
|
|
543
468
|
c.stream.on('error', err => {
|
|
544
|
-
if (timeout_timer) {
|
|
545
|
-
clearTimeout(timeout_timer)
|
|
546
|
-
timeout_timer = null
|
|
547
|
-
}
|
|
548
|
-
|
|
549
469
|
stm.close(http2.constants.NGHTTP2_INTERNAL_ERROR)
|
|
550
470
|
stm.destroy()
|
|
551
471
|
})
|
|
@@ -562,11 +482,6 @@ Http2Proxy.prototype.mid = function () {
|
|
|
562
482
|
})
|
|
563
483
|
|
|
564
484
|
stm.on('aborted', err => {
|
|
565
|
-
if (timeout_timer) {
|
|
566
|
-
clearTimeout(timeout_timer)
|
|
567
|
-
timeout_timer = null
|
|
568
|
-
}
|
|
569
|
-
|
|
570
485
|
!stm.destroyed && stm.destroy()
|
|
571
486
|
|
|
572
487
|
if (!resolved && !rejected) {
|
|
@@ -576,13 +491,6 @@ Http2Proxy.prototype.mid = function () {
|
|
|
576
491
|
})
|
|
577
492
|
|
|
578
493
|
stm.on('close', () => {
|
|
579
|
-
if (timeout_timer) {
|
|
580
|
-
clearTimeout(timeout_timer)
|
|
581
|
-
timeout_timer = null
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
stm.removeAllListeners()
|
|
585
|
-
|
|
586
494
|
if (stm.rstCode === http2.constants.NGHTTP2_NO_ERROR) {
|
|
587
495
|
if (!resolved && !rejected) {
|
|
588
496
|
resolved = true
|
|
@@ -594,13 +502,9 @@ Http2Proxy.prototype.mid = function () {
|
|
|
594
502
|
rj(new Error(`stream close, exit code ${stm.rstCode}`))
|
|
595
503
|
}
|
|
596
504
|
}
|
|
597
|
-
|
|
598
|
-
session_client.aliveStreams--
|
|
599
505
|
})
|
|
600
506
|
|
|
601
507
|
stm.on('response', (headers, flags) => {
|
|
602
|
-
timeout_timer && clearTimeout(timeout_timer)
|
|
603
|
-
timeout_timer = setTimeout(timeout_handler, pr.timeout + 5000)
|
|
604
508
|
if (c.res && c.res.writable) {
|
|
605
509
|
if (c.res.respond) {
|
|
606
510
|
c.res.respond(headers)
|
|
@@ -636,25 +540,21 @@ Http2Proxy.prototype.mid = function () {
|
|
|
636
540
|
stm.end()
|
|
637
541
|
})
|
|
638
542
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
data_count++
|
|
642
|
-
c.res && c.res.writable && c.res.write(chunk)
|
|
543
|
+
const onDrain = () => stm.resume()
|
|
544
|
+
if (c.res) c.res.on('drain', onDrain)
|
|
643
545
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
546
|
+
stm.on('data', chunk => {
|
|
547
|
+
if (c.res && c.res.writable) {
|
|
548
|
+
if (c.res.write(chunk) === false) {
|
|
549
|
+
stm.pause()
|
|
550
|
+
}
|
|
648
551
|
}
|
|
649
552
|
})
|
|
650
553
|
|
|
651
554
|
stm.on('end', () => {
|
|
652
|
-
if (
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
stm.close()
|
|
555
|
+
if (c.res) c.res.removeListener('drain', onDrain)
|
|
556
|
+
|
|
557
|
+
!stm.closed && stm.close()
|
|
658
558
|
|
|
659
559
|
if (!resolved && !rejected) {
|
|
660
560
|
resolved = true
|
|
@@ -666,8 +566,6 @@ Http2Proxy.prototype.mid = function () {
|
|
|
666
566
|
} catch (err) {
|
|
667
567
|
self.debug && console.error(err||'request null error')
|
|
668
568
|
c.status(503).to(error_503_text)
|
|
669
|
-
} finally {
|
|
670
|
-
session_client = null
|
|
671
569
|
}
|
|
672
570
|
|
|
673
571
|
}
|
package/src/extends/proxy.js
CHANGED
|
@@ -4,6 +4,20 @@ const urlparse = require('node:url');
|
|
|
4
4
|
const http = require('node:http');
|
|
5
5
|
const https = require('node:https');
|
|
6
6
|
|
|
7
|
+
// 主机名提取 (IPv6 兼容优化版)
|
|
8
|
+
function extractHostname(host) {
|
|
9
|
+
if (!host) return ''
|
|
10
|
+
if (host.charCodeAt(0) === 91) { // '[' IPv6
|
|
11
|
+
const end = host.indexOf(']')
|
|
12
|
+
return end > -1 ? host.substring(0, end + 1) : host
|
|
13
|
+
}
|
|
14
|
+
const idx = host.indexOf(':')
|
|
15
|
+
if (idx === -1) return host
|
|
16
|
+
if (host.indexOf(':', idx + 1) !== -1) return host // 裸 IPv6
|
|
17
|
+
return host.substring(0, idx)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
7
21
|
/**
|
|
8
22
|
* {
|
|
9
23
|
* host : {}
|
|
@@ -406,22 +420,7 @@ class Proxy {
|
|
|
406
420
|
|
|
407
421
|
return async (c, next) => {
|
|
408
422
|
|
|
409
|
-
let host = c.host
|
|
410
|
-
|
|
411
|
-
let hind = c.host.length - 1
|
|
412
|
-
|
|
413
|
-
if (hind > 4) {
|
|
414
|
-
let eind = hind - 5
|
|
415
|
-
|
|
416
|
-
while (hind >= eind) {
|
|
417
|
-
if (c.host[hind] === ':') {
|
|
418
|
-
host = c.host.substring(0, hind)
|
|
419
|
-
break
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
hind--
|
|
423
|
-
}
|
|
424
|
-
}
|
|
423
|
+
let host = extractHostname(c.host)
|
|
425
424
|
|
|
426
425
|
if (self.hostProxy[host]===undefined || self.hostProxy[host][c.routepath]===undefined) {
|
|
427
426
|
if (self.full) {
|
|
@@ -433,7 +432,7 @@ class Proxy {
|
|
|
433
432
|
let pr = self.getBackend(c, host)
|
|
434
433
|
|
|
435
434
|
if (pr === null) {
|
|
436
|
-
for (let i = 0; i <
|
|
435
|
+
for (let i = 0; i < 50; i++) {
|
|
437
436
|
await new Promise((rv, rj) => {setTimeout(rv, 10)})
|
|
438
437
|
pr = self.getBackend(c, host)
|
|
439
438
|
if (pr) break
|
package/test/test-route-sort.js
CHANGED
package/test/test-route2.js
CHANGED