topbit 1.0.0 → 3.0.0
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/LICENSE +128 -0
- package/README.cn.md +1519 -0
- package/README.md +1483 -0
- package/bin/app.js +17 -0
- package/bin/loadinfo.sh +18 -0
- package/bin/new-ctl.js +234 -0
- package/bin/newapp.js +22 -0
- package/demo/allow.js +98 -0
- package/demo/cert/localhost-cert.pem +19 -0
- package/demo/cert/localhost-privkey.pem +28 -0
- package/demo/controller/api.js +15 -0
- package/demo/extends.js +5 -0
- package/demo/group-api.js +161 -0
- package/demo/group-api2.js +109 -0
- package/demo/http2.js +34 -0
- package/demo/http2_proxy_backend.js +45 -0
- package/demo/http2proxy.js +48 -0
- package/demo/http_proxy_backend.js +44 -0
- package/demo/httpproxy.js +47 -0
- package/demo/loader.js +27 -0
- package/demo/log.js +118 -0
- package/demo/memlimit.js +31 -0
- package/demo/min.js +7 -0
- package/demo/serv.js +15 -0
- package/images/middleware.jpg +0 -0
- package/images/topbit-middleware.png +0 -0
- package/images/topbit.png +0 -0
- package/package.json +42 -11
- package/src/_loadExtends.js +21 -0
- package/src/bodyparser.js +420 -0
- package/src/connfilter.js +125 -0
- package/src/context1.js +166 -0
- package/src/context2.js +182 -0
- package/src/ctxpool.js +39 -0
- package/src/ext.js +318 -0
- package/src/extends/Http2Pool.js +365 -0
- package/src/extends/__randstring.js +24 -0
- package/src/extends/cookie.js +44 -0
- package/src/extends/cors.js +334 -0
- package/src/extends/errorlog.js +252 -0
- package/src/extends/http2limit.js +126 -0
- package/src/extends/http2proxy.js +691 -0
- package/src/extends/jwt.js +217 -0
- package/src/extends/mixlogger.js +63 -0
- package/src/extends/paramcheck.js +266 -0
- package/src/extends/proxy.js +662 -0
- package/src/extends/realip.js +34 -0
- package/src/extends/referer.js +68 -0
- package/src/extends/resource.js +398 -0
- package/src/extends/session.js +174 -0
- package/src/extends/setfinal.js +50 -0
- package/src/extends/sni.js +48 -0
- package/src/extends/sse.js +293 -0
- package/src/extends/timing.js +111 -0
- package/src/extends/tofile.js +123 -0
- package/src/fastParseUrl.js +426 -0
- package/src/headerLimit.js +18 -0
- package/src/http1.js +336 -0
- package/src/http2.js +337 -0
- package/src/httpc.js +251 -0
- package/src/lib/npargv.js +354 -0
- package/src/lib/zipdata.js +45 -0
- package/src/loader/loader.js +999 -0
- package/src/logger.js +32 -0
- package/src/loggermsg.js +349 -0
- package/src/makeId.js +200 -0
- package/src/midcore.js +213 -0
- package/src/middleware1.js +103 -0
- package/src/middleware2.js +116 -0
- package/src/monitor.js +380 -0
- package/src/movefile.js +30 -0
- package/src/optionsCheck.js +54 -0
- package/src/randstring.js +23 -0
- package/src/router.js +682 -0
- package/src/sendmsg.js +27 -0
- package/src/strong.js +72 -0
- package/src/token/token.js +461 -0
- package/src/topbit.js +1293 -0
- package/src/versionCheck.js +31 -0
- package/test/test-bigctx.js +29 -0
- package/test/test-daemon-args.js +7 -0
- package/test/test-ext.js +81 -0
- package/test/test-find.js +69 -0
- package/test/test-route-sort.js +71 -0
- package/test/test-route.js +49 -0
- package/test/test-route2.js +51 -0
- package/test/test-run-args.js +7 -0
- package/test/test-url.js +52 -0
- package/main.js +0 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 跨域请求中,在以下情况下,origin不会出现:
|
|
5
|
+
* https页面请求http接口。
|
|
6
|
+
* 但是在目前的策略中,在https页面中,引入http资源会引入不安全因素。
|
|
7
|
+
* 浏览器会报错:已阻止载入混合活动内容。
|
|
8
|
+
*
|
|
9
|
+
* 跨域请求中,origin字段是必须的。
|
|
10
|
+
*
|
|
11
|
+
* 在https跨域测试中,若要使用自签名的证书,必须先通过浏览器访问后端API,并把证书添加信任。
|
|
12
|
+
*
|
|
13
|
+
* 这之后,fetch才会成功请求。
|
|
14
|
+
*
|
|
15
|
+
* 这里讨论的跨域和referer问题只有在浏览器环境才会有效,通过命令请求完全不会理会这些处理过程。
|
|
16
|
+
*
|
|
17
|
+
* 严格的权限控制要通过token以及其他数据检测手段。
|
|
18
|
+
*
|
|
19
|
+
* 所以为了保证服务既可以适用于跨域也可以同源,必须要针对referer进行检测。
|
|
20
|
+
*
|
|
21
|
+
* 你可以设置在允许的referer内也返回消息头,因为有些应用根本不给你发送这个origin消息头,比如小程序。
|
|
22
|
+
*
|
|
23
|
+
* Access-Control-Allow-Credentials需要单独考虑,这个消息头需要用在浏览器环境下的fetch、Request使用了credentials选项为true的情况下。
|
|
24
|
+
* 这个时候,access-control-allow-origin不能是*而是具体的host。
|
|
25
|
+
* 这个通信过程是前后端需要协商好的,否则会出现cors报错。
|
|
26
|
+
*
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
class Cors {
|
|
30
|
+
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
|
|
33
|
+
//某些情况下,因为状态码是204导致连接提前关闭。
|
|
34
|
+
this.statusCode = 200;
|
|
35
|
+
|
|
36
|
+
this.allow = '*';
|
|
37
|
+
|
|
38
|
+
this.allowHeaders = 'authorization,content-type';
|
|
39
|
+
|
|
40
|
+
this.allowHeaderTable = {};
|
|
41
|
+
|
|
42
|
+
this.requestHeaders = '*';
|
|
43
|
+
|
|
44
|
+
this.credentials = false;
|
|
45
|
+
|
|
46
|
+
//Access-Control-Expose-Headers 指定哪些消息头可以暴露给请求端。
|
|
47
|
+
this.exposeHeaders = '';
|
|
48
|
+
|
|
49
|
+
this.allowEmptyReferer = true;
|
|
50
|
+
|
|
51
|
+
this.emptyRefererGroup = null;
|
|
52
|
+
|
|
53
|
+
this.referer = '';
|
|
54
|
+
|
|
55
|
+
this.methods = [
|
|
56
|
+
'GET', 'POST', 'DELETE', 'PUT', 'OPTIONS', 'PATCH', 'HEAD'
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
if (typeof options !== 'object') {
|
|
60
|
+
options = {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.optionsCache = null;
|
|
64
|
+
|
|
65
|
+
for (let k in options) {
|
|
66
|
+
switch (k) {
|
|
67
|
+
case 'allow':
|
|
68
|
+
if (options[k] === '*' || Array.isArray(options[k])) {
|
|
69
|
+
this.allow = options[k];
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'credentials':
|
|
74
|
+
this.credentials = !!options[k];
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'allowEmptyReferer':
|
|
78
|
+
this.allowEmptyReferer = !!options[k];
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
//允许提交空referer的路由分组
|
|
82
|
+
case 'emptyRefererGroup':
|
|
83
|
+
if (typeof options[k] === 'string') options[k] = [ options[k] ];
|
|
84
|
+
if (Array.isArray(options[k])) this.emptyRefererGroup = options[k];
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'referer':
|
|
88
|
+
if (options[k] === '*') {
|
|
89
|
+
this[k] = '*';
|
|
90
|
+
} else {
|
|
91
|
+
if (typeof options[k] === 'string') options[k] = [ options[k] ];
|
|
92
|
+
if (Array.isArray(options[k])) this[k] = options[k];
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case 'requestHeaders':
|
|
97
|
+
this.requestHeaders = options[k];
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case 'methods':
|
|
101
|
+
if ((options[k] instanceof Array) || typeof options[k] === 'string') {
|
|
102
|
+
this.methods = options[k];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'optionsCache':
|
|
108
|
+
case 'maxAge':
|
|
109
|
+
if (!isNaN(options[k]))
|
|
110
|
+
this.optionsCache = options[k];
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'allowHeaders':
|
|
114
|
+
if (Array.isArray(options[k])) {
|
|
115
|
+
if (options[k].length > 0)
|
|
116
|
+
this.allowHeaders = options[k].join(',');
|
|
117
|
+
} else if (typeof options[k] === 'string') {
|
|
118
|
+
this.allowHeaders = options[k].trim() || '*';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* if (this.allowHeaders !== '*') {
|
|
122
|
+
this.allowHeaders.split(',')
|
|
123
|
+
.filter(p => p.length > 0)
|
|
124
|
+
.map(x => x.trim())
|
|
125
|
+
.forEach(x => {
|
|
126
|
+
this.allowHeaderTable[x] = x;
|
|
127
|
+
});
|
|
128
|
+
} */
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'exposeHeaders':
|
|
132
|
+
this.exposeHeaders = options[k];
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this.methods instanceof Array) {
|
|
139
|
+
this.methodString = this.methods.join(',');
|
|
140
|
+
} else {
|
|
141
|
+
this.methodString = this.methods;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.allowTable = {};
|
|
145
|
+
//记录是否用于referer检测。
|
|
146
|
+
this.refererTable = {};
|
|
147
|
+
this.refererList = [];
|
|
148
|
+
|
|
149
|
+
if (Array.isArray(this.allow)) {
|
|
150
|
+
let lastSlash = 0;
|
|
151
|
+
let midIndex, midChar, host, useForReferer;
|
|
152
|
+
|
|
153
|
+
for (let aw of this.allow) {
|
|
154
|
+
useForReferer = true;
|
|
155
|
+
|
|
156
|
+
if (!aw) continue;
|
|
157
|
+
|
|
158
|
+
if (typeof aw === 'string') host = aw.trim();
|
|
159
|
+
else if (typeof aw === 'object') {
|
|
160
|
+
host = aw.url || '';
|
|
161
|
+
useForReferer = !!aw.referer;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!host) continue;
|
|
165
|
+
|
|
166
|
+
lastSlash = host.length - 1;
|
|
167
|
+
while (host[lastSlash] === '/' && lastSlash > 0) lastSlash--;
|
|
168
|
+
|
|
169
|
+
//不允许 / 结尾。
|
|
170
|
+
if (lastSlash < host.length - 1) host = host.substring(0, lastSlash+1);
|
|
171
|
+
if (!host.trim()) continue;
|
|
172
|
+
|
|
173
|
+
midIndex = parseInt(host.length / 2);
|
|
174
|
+
midChar = host[midIndex];
|
|
175
|
+
|
|
176
|
+
this.allowTable[host] = {
|
|
177
|
+
url: host,
|
|
178
|
+
length: host.length,
|
|
179
|
+
lastIndex: host.length - 1,
|
|
180
|
+
last: host[host.length - 1],
|
|
181
|
+
midIndex: midIndex,
|
|
182
|
+
midChar: midChar,
|
|
183
|
+
slashIndex: host.indexOf('/', 8),
|
|
184
|
+
referer: useForReferer
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (useForReferer) {
|
|
188
|
+
this.refererTable[host] = this.allowTable[host];
|
|
189
|
+
this.refererList.push(this.allowTable[host]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.allow = Object.keys(this.allowTable);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
checkOrigin(url) {
|
|
199
|
+
return this.allowTable[url] ? true : false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
checkReferer(url) {
|
|
203
|
+
if (this.allow === '*') return true;
|
|
204
|
+
|
|
205
|
+
let aobj;
|
|
206
|
+
let ulen = url.length;
|
|
207
|
+
|
|
208
|
+
let refererTotal = this.refererList.length;
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < refererTotal; i++) {
|
|
211
|
+
aobj = this.refererList[i];
|
|
212
|
+
|
|
213
|
+
if (aobj.length > ulen || url[aobj.lastIndex] !== aobj.last) continue;
|
|
214
|
+
|
|
215
|
+
if (url[aobj.midIndex] !== aobj.midChar) continue;
|
|
216
|
+
|
|
217
|
+
if (aobj.slashIndex > 0 && url[aobj.slashIndex] !== '/') continue;
|
|
218
|
+
|
|
219
|
+
if (url.indexOf(aobj.url) === 0) return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* for (let u in this.refererTable) {
|
|
223
|
+
aobj = this.refererTable[u];
|
|
224
|
+
//允许的referer长度比真实的值要短,所以超过的必然不是,如果最后一个字符不匹配可以直接跳过。
|
|
225
|
+
if (aobj.length > ulen || url[aobj.length - 1] !== aobj.last) continue;
|
|
226
|
+
|
|
227
|
+
if (url[aobj.midIndex] !== aobj.midChar) continue;
|
|
228
|
+
|
|
229
|
+
if (aobj.slashIndex > 0 && url[aobj.slashIndex] !== '/') continue;
|
|
230
|
+
|
|
231
|
+
//substring 之后 判等 比 indexOf 要慢。
|
|
232
|
+
if (url.indexOf(u) === 0) return true;
|
|
233
|
+
} */
|
|
234
|
+
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 要区分两种状态:跨域请求和同源请求。
|
|
240
|
+
* 在同源请求:ctx.headers.referer必然是包含页面路径。
|
|
241
|
+
* 若直接请求此资源则不会返回数据。
|
|
242
|
+
*
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
mid() {
|
|
246
|
+
let self = this;
|
|
247
|
+
|
|
248
|
+
return async (ctx, next) => {
|
|
249
|
+
//使用ctx.box.corsAllow控制,给中间件处理留出扩展空间。
|
|
250
|
+
//跨域请求,必须存在origin。
|
|
251
|
+
if (ctx.headers.origin) {
|
|
252
|
+
if (!(self.allow === '*'
|
|
253
|
+
|| self.allowTable[ctx.headers.origin]
|
|
254
|
+
|| ctx.box.corsAllow) )
|
|
255
|
+
{
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
/**
|
|
260
|
+
* 在浏览器里,如果是跨域,则必然会遵循跨域原则,所以origin和referer的规则都会有效。
|
|
261
|
+
* 若不是浏览器,仅凭CORS规范是无法约束非法请求的。
|
|
262
|
+
*/
|
|
263
|
+
//有一种情况,直接返回的页面并不具备referer,所以前端页面的请求不能有跨域扩展。
|
|
264
|
+
//直接通过file方式进行,也不会有referer。
|
|
265
|
+
//如果referer前缀就是host说明是本网站访问
|
|
266
|
+
|
|
267
|
+
//非跨域请求,或仅仅是没有携带origin
|
|
268
|
+
let referer = ctx.headers.referer || ''
|
|
269
|
+
|
|
270
|
+
//处理同源请求。允许提交空的referer或者允许某些路由分组可以提交空referer(针对前端页面)
|
|
271
|
+
//或者是检测到ctx.box.corsAllow,host和referer都是客户端的控制,检测必须要依赖服务端对host的配置。
|
|
272
|
+
|
|
273
|
+
if (!(
|
|
274
|
+
(!referer
|
|
275
|
+
&& (self.allowEmptyReferer || (self.emptyRefererGroup && self.emptyRefererGroup.indexOf(ctx.group) >= 0) )
|
|
276
|
+
)
|
|
277
|
+
|| ctx.box.corsAllow || (referer && self.checkReferer(referer))
|
|
278
|
+
)
|
|
279
|
+
) {
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let req_headers = ctx.headers['access-control-request-headers']
|
|
286
|
+
|
|
287
|
+
if (req_headers && req_headers.indexOf('x-credentials') >= 0) {
|
|
288
|
+
//如果前端使用了credentials为include选项,同时使用x-credentials消息头通知后台,此时要做特殊处理。
|
|
289
|
+
ctx.headers['x-credentials'] = 'include'
|
|
290
|
+
//ctx.setHeader('access-control-request-headers', req_headers);
|
|
291
|
+
}
|
|
292
|
+
//服务端也要包含此消息头。
|
|
293
|
+
ctx.setHeader('access-control-request-headers', self.requestHeaders)
|
|
294
|
+
|
|
295
|
+
if (self.credentials || ctx.headers['x-credentials'] === 'include') {
|
|
296
|
+
let host = '*'
|
|
297
|
+
|
|
298
|
+
if (ctx.headers.origin) {
|
|
299
|
+
host = ctx.headers.origin
|
|
300
|
+
} else if (ctx.headers.referer) {
|
|
301
|
+
// https:// 不必搜索。
|
|
302
|
+
let ind = ctx.headers.referer.indexOf('/', 8)
|
|
303
|
+
if (ind < 0) {
|
|
304
|
+
host = ctx.headers.referer
|
|
305
|
+
} else {
|
|
306
|
+
host = ctx.headers.referer.substring(0, ind)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
ctx.setHeader('access-control-allow-credentials', 'true')
|
|
311
|
+
ctx.setHeader('access-control-allow-origin', host)
|
|
312
|
+
} else {
|
|
313
|
+
ctx.setHeader('access-control-allow-origin', '*')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
ctx.setHeader('access-control-allow-methods', self.methodString)
|
|
317
|
+
ctx.setHeader('access-control-allow-headers', self.allowHeaders)
|
|
318
|
+
|
|
319
|
+
if (self.exposeHeaders)
|
|
320
|
+
ctx.setHeader('access-control-expose-headers', self.exposeHeaders)
|
|
321
|
+
if (ctx.method === 'OPTIONS') {
|
|
322
|
+
self.optionsCache && ctx.setHeader('access-control-max-age', self.optionsCache)
|
|
323
|
+
ctx.status(self.statusCode)
|
|
324
|
+
} else {
|
|
325
|
+
return await next(ctx)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = Cors
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cluster = require('node:cluster')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const fsp = fs.promises
|
|
6
|
+
|
|
7
|
+
const fmtTime = (m = 'long') => {
|
|
8
|
+
let t = new Date()
|
|
9
|
+
let year = t.getFullYear()
|
|
10
|
+
let month = t.getMonth() + 1
|
|
11
|
+
let day = t.getDate()
|
|
12
|
+
let hour = t.getHours()
|
|
13
|
+
let min = t.getMinutes()
|
|
14
|
+
let sec = t.getSeconds()
|
|
15
|
+
|
|
16
|
+
let mt = `${year}_${month > 9 ? '' : '0'}${month}_${day > 9 ? '' : '0'}${day}`
|
|
17
|
+
|
|
18
|
+
if (m === 'short') {
|
|
19
|
+
return mt
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let md = `${mt}_${hour > 9 ? '' : '0'}${hour}`
|
|
23
|
+
if (m === 'middle') {
|
|
24
|
+
return md
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `${md}_${min > 9 ? '' : '0'}${min}_${sec > 9 ? '' : '0'}${sec}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 错误收集程序,它运行在两种模式:单独的记录和发送给master进程。
|
|
32
|
+
*
|
|
33
|
+
* 格式:
|
|
34
|
+
! --ERR-TAG-- | code | constructor name | message | time | extra info(ip, usera-gent...)
|
|
35
|
+
stack info
|
|
36
|
+
|
|
37
|
+
文件存储的命名格式:{prefix_name}_year_month_day_hour_minute_second.log
|
|
38
|
+
*
|
|
39
|
+
*/
|
|
40
|
+
class ErrorLog {
|
|
41
|
+
constructor(options) {
|
|
42
|
+
if (!options || typeof options !== 'object') options = {}
|
|
43
|
+
|
|
44
|
+
this.flog = null
|
|
45
|
+
this.dir = './tmp'
|
|
46
|
+
this.prefix = 'errorlog_'
|
|
47
|
+
this.maxHistory = 100
|
|
48
|
+
this.maxLines = 10000
|
|
49
|
+
this.historyList = []
|
|
50
|
+
this.debug = false
|
|
51
|
+
this.selfLog = false
|
|
52
|
+
this.stdioOutput = false
|
|
53
|
+
|
|
54
|
+
for (let k in options) {
|
|
55
|
+
switch (k) {
|
|
56
|
+
case 'dir':
|
|
57
|
+
case 'prefix':
|
|
58
|
+
if (typeof options[k] === 'string' && options[k]) {
|
|
59
|
+
this[k] = options[k]
|
|
60
|
+
}
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
case 'debug':
|
|
64
|
+
case 'selfLog':
|
|
65
|
+
case 'stdioOutput':
|
|
66
|
+
this[k] = !!options[k]
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
case 'maxHistory':
|
|
70
|
+
case 'maxLines':
|
|
71
|
+
if (typeof options[k] === 'number' && options[k] > 0) {
|
|
72
|
+
this[k] = options[k]
|
|
73
|
+
}
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.logname = this.prefix + 'now.log'
|
|
79
|
+
this.logfile = this.dir + '/' + this.logname
|
|
80
|
+
|
|
81
|
+
this.initDirAndHistory()
|
|
82
|
+
|
|
83
|
+
this.count = 0
|
|
84
|
+
this.checkLock = false
|
|
85
|
+
|
|
86
|
+
this.initLogStream()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
initDirAndHistory() {
|
|
90
|
+
if (!(cluster.isPrimary || this.selfLog)) return false;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
fs.accessSync(this.dir)
|
|
94
|
+
} catch (err) {
|
|
95
|
+
try {
|
|
96
|
+
fs.mkdirSync(this.dir, {mode: 0o755})
|
|
97
|
+
} catch (err) {
|
|
98
|
+
this.debug && console.error(err)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
let flist = fs.readdirSync(this.dir, {withFileTypes: true})
|
|
104
|
+
for (let f of flist) {
|
|
105
|
+
if (!f.isFile()) continue
|
|
106
|
+
|
|
107
|
+
if (f.name.substring(f.name.length - 4) !== '.log') continue
|
|
108
|
+
|
|
109
|
+
if (f.name === this.logfile) continue
|
|
110
|
+
|
|
111
|
+
if (f.name.indexOf(this.prefix) !== 0) continue
|
|
112
|
+
|
|
113
|
+
this.historyList.push(`${this.dir}/${f.name}`)
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
this.debug && console.error(err)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
*
|
|
122
|
+
* @param {object} app - Titbit实例
|
|
123
|
+
*/
|
|
124
|
+
init(app) {
|
|
125
|
+
if (app.strong && typeof app.strong === 'object') {
|
|
126
|
+
app.strong.errorHandle = this.sendErrorLog.bind(this)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (app.isWorker) {
|
|
130
|
+
app.addService('sendErrorLog', this.sendErrorLog.bind(this))
|
|
131
|
+
} else {
|
|
132
|
+
app.setMsgEvent('errorlog', this.mlog.bind(this))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async initLogStream() {
|
|
137
|
+
if (this.flog) return;
|
|
138
|
+
if (!(cluster.isPrimary || this.selfLog)) return;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
this.flog = fs.createWriteStream(this.logfile, {flags: 'a+', mode: 0o644})
|
|
142
|
+
|
|
143
|
+
this.flog.on('close', () => {
|
|
144
|
+
this.flog = null
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
this.flog.on('error', () => {
|
|
148
|
+
this.flog = null
|
|
149
|
+
})
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.debug && console.error(err)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
clearHistory() {
|
|
156
|
+
if (this.historyList.length < this.maxHistory) return;
|
|
157
|
+
|
|
158
|
+
let i = 0
|
|
159
|
+
let total = 5
|
|
160
|
+
let hfile
|
|
161
|
+
|
|
162
|
+
while (i < total) {
|
|
163
|
+
hfile = this.historyList.shift()
|
|
164
|
+
if (!hfile) return;
|
|
165
|
+
fs.unlink(hfile, err => {})
|
|
166
|
+
i += 1
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async _checkLines() {
|
|
171
|
+
if (!this.flog) return;
|
|
172
|
+
if (this.count < this.maxLines) return;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
let old_log = `${this.dir}/${this.prefix}${fmtTime()}.log`
|
|
176
|
+
|
|
177
|
+
await fsp.rename(this.logfile, old_log)
|
|
178
|
+
|
|
179
|
+
this.historyList.push(old_log)
|
|
180
|
+
} catch (err) {
|
|
181
|
+
this.debug && console.error(err)
|
|
182
|
+
} finally {
|
|
183
|
+
this.flog && !this.flog.destroyed && this.flog.destroy()
|
|
184
|
+
this.flog = null
|
|
185
|
+
this.count = 0
|
|
186
|
+
this.initLogStream()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async checkLog() {
|
|
191
|
+
if (this.checkLock) return;
|
|
192
|
+
|
|
193
|
+
this.checkLock = true
|
|
194
|
+
|
|
195
|
+
await this._checkLines()
|
|
196
|
+
this.clearHistory()
|
|
197
|
+
|
|
198
|
+
this.checkLock = false
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fmtLog(msg) {
|
|
202
|
+
let {error={}, errname='-'} = msg
|
|
203
|
+
|
|
204
|
+
return `! ${errname} | ${error.code || '-'} | ${error.name} | `
|
|
205
|
+
+ `${error.message} | ${fmtTime()} | ${error.extrainfo || '-'}\n`
|
|
206
|
+
+ `${error.stack || ''}\n`
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
//mlog is master log
|
|
210
|
+
async mlog(worker, msg, handle=null) {
|
|
211
|
+
try {
|
|
212
|
+
let logtext = this.fmtLog(msg)
|
|
213
|
+
|
|
214
|
+
if (!this.flog) this.initLogStream()
|
|
215
|
+
|
|
216
|
+
this.flog && this.flog.write(logtext) && (this.count += 1);
|
|
217
|
+
|
|
218
|
+
this.checkLog()
|
|
219
|
+
} catch (err) {
|
|
220
|
+
this.debug && console.error(err)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
sendErrorLog(e, errname='--ERR-ERROR--') {
|
|
225
|
+
if (e.code === 'EPIPE') {
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.stdioOutput && console.error(errname, e)
|
|
230
|
+
|
|
231
|
+
let errmsg = {
|
|
232
|
+
type: 'errorlog',
|
|
233
|
+
error: {
|
|
234
|
+
code: e.code || '',
|
|
235
|
+
name: e.constructor.name,
|
|
236
|
+
message: e.message,
|
|
237
|
+
stack: e.stack || '',
|
|
238
|
+
extrainfo: e.extrainfo || ''
|
|
239
|
+
},
|
|
240
|
+
errname
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!process.send || this.selfLog) {
|
|
244
|
+
return this.mlog(null, errmsg)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
process.send(errmsg)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = ErrorLog
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* 基于IP的限制,titbit已经能够处理。
|
|
6
|
+
*
|
|
7
|
+
* 此限流针对的是因为http/2协议设计上特点导致单个连接请求密集。
|
|
8
|
+
*
|
|
9
|
+
* 当请求密集时,关闭连接即可,若频繁发起连接请求,此时titbit层面的IP连接限制自会处理。
|
|
10
|
+
*
|
|
11
|
+
* 连接但不请求,空闲自然会有超时处理。
|
|
12
|
+
*
|
|
13
|
+
* 所以需要限制的场景:
|
|
14
|
+
* - 时间段内的密集请求。
|
|
15
|
+
* - 在空闲超时限制内,不频繁的请求,一直持有连接。
|
|
16
|
+
*
|
|
17
|
+
* 设定一个session生命期,超过则关闭连接。
|
|
18
|
+
*
|
|
19
|
+
**/
|
|
20
|
+
|
|
21
|
+
class StreamLimit {
|
|
22
|
+
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.cache = new Map()
|
|
25
|
+
|
|
26
|
+
if (typeof options !== 'object') {
|
|
27
|
+
options = {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.timeSlice = 1000
|
|
31
|
+
|
|
32
|
+
//单位时间内的最大请求次数。
|
|
33
|
+
this.maxRequest = 50
|
|
34
|
+
|
|
35
|
+
this.socketLife = 3600000
|
|
36
|
+
|
|
37
|
+
for (let k in options) {
|
|
38
|
+
switch (k) {
|
|
39
|
+
case 'timeSlice':
|
|
40
|
+
case 'maxRequest':
|
|
41
|
+
case 'socketLife':
|
|
42
|
+
if (typeof options[k] === 'number') {
|
|
43
|
+
this[k] = options[k]
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_sid(sess) {
|
|
52
|
+
return `${sess.socket.remoteAddress} ${sess.socket.remotePort}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set(id) {
|
|
56
|
+
let s = this.cache.get(id);
|
|
57
|
+
|
|
58
|
+
if (!s) {
|
|
59
|
+
let d = {
|
|
60
|
+
time: Date.now(),
|
|
61
|
+
count: 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
d.startTime = d.time
|
|
65
|
+
|
|
66
|
+
this.cache.set(id, d)
|
|
67
|
+
return d
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
s.count += 1
|
|
71
|
+
|
|
72
|
+
return s
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
remove(id) {
|
|
76
|
+
return this.cache.delete(id)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
checkAndSet(id) {
|
|
80
|
+
let s = this.set(id)
|
|
81
|
+
|
|
82
|
+
if (s.count <= 1) return true
|
|
83
|
+
|
|
84
|
+
let tm = Date.now()
|
|
85
|
+
|
|
86
|
+
if (this.socketLife > 0 && (s.startTime + this.socketLife) < tm) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ( (s.time + this.timeSlice) > tm ) {
|
|
91
|
+
if (this.maxRequest > 0 && s.count > this.maxRequest) {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
s.count = 1
|
|
96
|
+
s.time = tm
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
init(app) {
|
|
103
|
+
app.on('session', session => {
|
|
104
|
+
|
|
105
|
+
let id = this._sid(session)
|
|
106
|
+
|
|
107
|
+
session.socket && session.socket.on('close', () => {
|
|
108
|
+
this.remove(id)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
session.on('stream', stm => {
|
|
112
|
+
let st = this.checkAndSet(id)
|
|
113
|
+
if (st === null) {
|
|
114
|
+
session.close(() => {
|
|
115
|
+
!session.destroyed && session.destroy()
|
|
116
|
+
})
|
|
117
|
+
} else if (!st) {
|
|
118
|
+
session.destroy()
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = StreamLimit
|