mm_os 3.2.4 → 3.2.6

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.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * API速率限制中间件
3
+ * 用于防止DoS攻击,限制客户端在一定时间内的请求频率
4
+ * @param {Object} server 服务实例
5
+ * @param {Object} config 配置参数
6
+ */
7
+ module.exports = function(server, config) {
8
+ // 初始化速率限制配置
9
+ const cg = {
10
+ // 默认配置
11
+ windowMs: 15 * 60 * 1000, // 时间窗口,默认15分钟
12
+ maxRequests: 100, // 每个时间窗口内的最大请求数
13
+ message: '请求过于频繁,请稍后再试', // 超过限制时的提示信息
14
+ statusCode: 429, // 超过限制时的HTTP状态码
15
+ // 合并用户配置
16
+ ...(config && config.rateLimit ? config.rateLimit : {})
17
+ };
18
+
19
+ // 生成基于IP的唯一标识符
20
+ function getClientKey(ctx) {
21
+ // 优先使用X-Forwarded-For头部(如果有代理的话)
22
+ const forwardedFor = ctx.headers['x-forwarded-for'];
23
+ if (forwardedFor) {
24
+ // 通常格式为:X-Forwarded-For: client, proxy1, proxy2
25
+ return forwardedFor.split(',')[0].trim();
26
+ }
27
+ // 直接使用IP地址
28
+ return ctx.ip;
29
+ }
30
+
31
+ // 使用Redis存储请求计数
32
+ async function incrementRequestWithRedis(clientKey) {
33
+ try {
34
+ // 检查Redis是否可用
35
+ if ($.cache && typeof $.cache.addInt === 'function') {
36
+ const key = `rate_limit:${clientKey}`;
37
+
38
+ // 使用addInt方法增加计数(mm_redis包提供的方法)
39
+ const count = await $.cache.addInt(key, 1);
40
+
41
+ // 设置过期时间
42
+ await $.cache.ttl(key, Math.ceil(cg.windowMs / 1000));
43
+
44
+ return count || 0;
45
+ }
46
+ } catch (error) {
47
+ $.log.error('Redis速率限制失败:', error);
48
+ }
49
+
50
+ // Redis不可用时返回null,将使用内存存储
51
+ return null;
52
+ }
53
+
54
+ // 中间件主逻辑
55
+ server.use(async (ctx, next) => {
56
+ try {
57
+ // 跳过静态文件和favicon请求
58
+ if (ctx.path === '/favicon.ico' || ctx.path.startsWith('/static/')) {
59
+ await next();
60
+ return;
61
+ }
62
+
63
+ // 获取客户端唯一标识符
64
+ const clientKey = getClientKey(ctx);
65
+
66
+ let requestCount = await incrementRequestWithRedis(clientKey);
67
+
68
+ // 设置响应头部,告知客户端当前的限制状态
69
+ ctx.set('X-RateLimit-Limit', cg.maxRequests);
70
+ ctx.set('X-RateLimit-Remaining', Math.max(0, cg.maxRequests - requestCount));
71
+
72
+ // 检查是否超过限制
73
+ if (requestCount > cg.maxRequests) {
74
+ $.log.warn(`API速率限制触发: ${clientKey} 请求 ${ctx.path} 次数过多`);
75
+
76
+ ctx.status = cg.statusCode;
77
+ ctx.body = {
78
+ code: cg.statusCode,
79
+ msg: cg.message
80
+ };
81
+
82
+ // 记录超过限制的请求
83
+ if ($.log && $.log.warn) {
84
+ $.log.warn(`速率限制触发: IP=${clientKey}, Path=${ctx.path}, Method=${ctx.method}`);
85
+ }
86
+
87
+ return;
88
+ }
89
+
90
+ // 继续处理请求
91
+ await next();
92
+ } catch (error) {
93
+ $.log.error('速率限制中间件错误:', error);
94
+ // 确保请求可以继续处理
95
+ await next();
96
+ }
97
+ });
98
+
99
+ // 记录中间件初始化信息
100
+ $.log.info(`速率限制中间件已加载: ${cg.maxRequests}请求/${cg.windowMs/1000}秒`);
101
+
102
+ return server;
103
+ };
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "rate_limit",
3
+ "title": "API速率限制",
4
+ "description": "限制客户端在一定时间内的请求频率,防止DoS攻击",
5
+ "version": "1.0",
6
+ "type": "web",
7
+ "process_type": "common_before",
8
+ "sort": 50,
9
+ "script": "index.js"
10
+ }
@@ -51,12 +51,25 @@ function getClientIP(req) {
51
51
  module.exports = function(server, config) {
52
52
  /* WAF(web防火墙) */
53
53
  server.use(async (ctx, next) => {
54
- var url = ctx.url;
55
- var danger = waf_check(url);
56
- if (danger) {
57
- var ip = getClientIP(ctx.req);
58
- $.log.warn(`检测到来自IP ${ip} 的攻击`, "规则:", danger.toString());
59
- } else {
54
+ try {
55
+ var url = ctx.url;
56
+ var danger = waf_check(url);
57
+ if (danger) {
58
+ var ip = getClientIP(ctx.req);
59
+ $.log.warn(`检测到来自IP ${ip} 的攻击`, "规则:", danger.toString());
60
+ // 阻止攻击请求,返回403禁止访问
61
+ ctx.status = 403;
62
+ ctx.body = {
63
+ code: 403,
64
+ msg: '访问被WAF阻止,请求包含潜在的攻击特征',
65
+ rule: danger.toString()
66
+ };
67
+ } else {
68
+ await next();
69
+ }
70
+ } catch (error) {
71
+ $.log.error('WAF中间件错误:', error);
72
+ // 出错时默认允许请求继续处理
60
73
  await next();
61
74
  }
62
75
  });
@@ -1,8 +1,7 @@
1
- const os = require('os');
2
1
  const {
3
2
  exec
4
3
  } = require('child_process');
5
- const platform = os.platform();
4
+ const platform = require('os').platform();
6
5
 
7
6
  /**
8
7
  * 获取客户端IP
@@ -60,55 +59,108 @@ module.exports = function(server, config) {
60
59
  if (limit && duration) {
61
60
  /* WAF(web防火墙) */
62
61
  server.use(async (ctx, next) => {
63
- var pass = true;
64
- // 获取IP
65
- var ip = getClientIP(ctx.req);
66
- var num = 1;
67
- var now = new Date();
68
- var date = now.toStr('yyyy-MM-dd');
69
- var time;
70
- var str = await $.cache.get("ip_" + ip);
71
- var json;
72
- if (str) {
73
- if (typeof(str) === "string") {
74
- json = JSON.parse(str);
75
- } else {
76
- json = str;
77
- }
78
- if (json.date !== date) {
79
- num = 1;
80
- } else {
81
- // 判断时间间隔是否在范围外
82
- if (json.time.toTime().interval(now) > duration) {
83
- num = 1;
84
- } else {
85
- // 如果是在周期内,访问次数+1,并判断是否超出上限
86
- num = json.num + 1;
87
- if (num > limit) {
88
- // 超出上限禁止访问,并加入黑名单
89
- pass = false;
90
- if (block) {
91
- setting_blacklist(ip);
62
+ try {
63
+ var pass = true;
64
+ // 获取IP
65
+ var ip = getClientIP(ctx.req);
66
+ var num = 1;
67
+ var now = new Date();
68
+ var date = now.toStr('yyyy-MM-dd');
69
+ var time;
70
+ var json;
71
+ try {
72
+ var str = await $.cache.get("ip_" + ip);
73
+ if (str) {
74
+ if (typeof(str) === "string") {
75
+ try {
76
+ json = JSON.parse(str);
77
+ } catch (jsonError) {
78
+ $.log.error('WAF IP中间件JSON解析错误:', jsonError);
79
+ json = null;
80
+ }
81
+ } else {
82
+ json = str;
83
+ }
84
+ if (json) {
85
+ try {
86
+ if (json.date !== date) {
87
+ num = 1;
88
+ } else {
89
+ // 判断时间间隔是否在范围外
90
+ // 安全地处理json.time,无论它是字符串还是对象
91
+ if (typeof json.time === 'string') {
92
+ // 如果json.time是字符串,尝试解析它
93
+ const savedTime = new Date(json.time);
94
+ if (!isNaN(savedTime.getTime()) && (now - savedTime) > duration) {
95
+ num = 1;
96
+ } else {
97
+ // 如果是在周期内,访问次数+1,并判断是否超出上限
98
+ num = json.num + 1;
99
+ if (num > limit) {
100
+ // 超出上限禁止访问,并加入黑名单
101
+ pass = false;
102
+ if (block) {
103
+ setting_blacklist(ip);
104
+ }
105
+ }
106
+ }
107
+ } else if (json.time && json.time.toTime && typeof json.time.toTime().interval === 'function') {
108
+ // 原有逻辑,处理对象类型的时间
109
+ if (json.time.toTime().interval(now) > duration) {
110
+ num = 1;
111
+ } else {
112
+ // 如果是在周期内,访问次数+1,并判断是否超出上限
113
+ num = json.num + 1;
114
+ if (num > limit) {
115
+ // 超出上限禁止访问,并加入黑名单
116
+ pass = false;
117
+ if (block) {
118
+ setting_blacklist(ip);
119
+ }
120
+ }
121
+ }
122
+ } else {
123
+ // 默认情况:如果时间格式不正确,重置计数
124
+ num = 1;
125
+ }
126
+ }
127
+ } catch (timeError) {
128
+ $.log.error('WAF IP中间件时间处理错误:', timeError);
129
+ // 出错时重置计数以保证安全
130
+ num = 1;
92
131
  }
93
132
  }
94
133
  }
134
+ } catch (cacheError) {
135
+ $.log.error('WAF IP中间件缓存操作错误:', cacheError);
136
+ // 缓存出错时,默认允许请求通过
95
137
  }
96
- }
97
- if (!time) {
98
- time = now.toStr('yyyy-MM-dd hh:mm:ss');
99
- }
100
- if (pass) {
101
- await $.cache.set("ip_" + ip, JSON.stringify({
102
- date,
103
- time,
104
- num
105
- }), duration);
106
- ctx.request.ip = ip;
107
- ctx.ip = ip;
138
+ if (!time) {
139
+ time = now.toStr('yyyy-MM-dd hh:mm:ss');
140
+ }
141
+ if (pass) {
142
+ try {
143
+ await $.cache.set("ip_" + ip, JSON.stringify({
144
+ date,
145
+ time,
146
+ num
147
+ }), duration);
148
+ } catch (setCacheError) {
149
+ $.log.error('WAF IP中间件缓存设置错误:', setCacheError);
150
+ // 缓存设置失败不影响请求处理
151
+ }
152
+ ctx.request.ip = ip;
153
+ ctx.ip = ip;
154
+ await next();
155
+ } else {
156
+ ctx.status = 429;
157
+ ctx.body = '请求频率过高,请稍后再试。';
158
+ }
159
+ } catch (error) {
160
+ $.log.error('WAF IP中间件错误:', error);
161
+ // 出错时默认允许请求通过
162
+ ctx.ip = getClientIP(ctx.req);
108
163
  await next();
109
- } else {
110
- ctx.status = 429;
111
- ctx.body = '请求频率过高,请稍后再试。';
112
164
  }
113
165
  });
114
166
  }
@@ -5,14 +5,27 @@
5
5
  */
6
6
  module.exports = function(server, config) {
7
7
  server.use(async (ctx, next) => {
8
- await next();
9
- if (ctx.path !== "/favicon.ico") {
10
- await $.eventer.run('web_after', ctx, ctx.db);
11
- var event = $.event_admin('api');
12
- var ret = await event.after(ctx.path, ctx, ctx.db);
13
- if (ret) {
14
- ctx.body = ret;
8
+ try {
9
+ // 先让其他中间件执行
10
+ await next();
11
+
12
+ // 然后执行after逻辑
13
+ try {
14
+ if (ctx.path !== "/favicon.ico") {
15
+ await $.eventer.run('web_after', ctx, ctx.db);
16
+ var event = $.event_admin('api');
17
+ var ret = await event.after(ctx.path, ctx, ctx.db);
18
+ if (ret) {
19
+ ctx.body = ret;
20
+ }
21
+ }
22
+ } catch (afterError) {
23
+ $.log.error('web_after事件执行错误:', afterError);
24
+ // 即使after事件出错,也不影响响应结果
15
25
  }
26
+ } catch (error) {
27
+ $.log.error('web_after中间件错误:', error);
28
+ // 注意:这里不需要再次调用next(),因为已经在try块中调用过了
16
29
  }
17
30
  });
18
31
 
@@ -14,10 +14,22 @@ module.exports = function(server, config) {
14
14
  * 发送文件
15
15
  */
16
16
  server.use(async (ctx, next) => {
17
- ctx.send = async function(src) {
18
- await send(ctx, src);
17
+ try {
18
+ ctx.send = async function(src) {
19
+ try {
20
+ await send(ctx, src);
21
+ } catch (sendError) {
22
+ $.log.error('文件发送错误:', sendError);
23
+ // 可以选择抛出错误让上层处理,或者静默处理
24
+ throw sendError;
25
+ }
26
+ }
27
+ await next();
28
+ } catch (error) {
29
+ $.log.error('web_base中间件(文件发送设置)错误:', error);
30
+ // 出错时默认允许请求继续处理
31
+ await next();
19
32
  }
20
- await next();
21
33
  });
22
34
 
23
35
  // 设置session 保存时长2小时
@@ -56,9 +68,21 @@ module.exports = function(server, config) {
56
68
  // 解析 application/json、application/x-www-form-urlencoded、text/plain
57
69
  // 接收主体
58
70
  server.use(async (ctx, next) => {
59
- if (!ctx.request.body) {
60
- await func(ctx, next);
61
- } else {
71
+ try {
72
+ if (!ctx.request.body) {
73
+ try {
74
+ await func(ctx, next);
75
+ } catch (parseError) {
76
+ $.log.error('请求体解析错误:', parseError);
77
+ // 解析错误时,确保请求继续处理
78
+ await next();
79
+ }
80
+ } else {
81
+ await next();
82
+ }
83
+ } catch (error) {
84
+ $.log.error('web_base中间件(请求体解析)错误:', error);
85
+ // 出错时默认允许请求继续处理
62
86
  await next();
63
87
  }
64
88
  });
@@ -6,16 +6,22 @@
6
6
  module.exports = function(server, config) {
7
7
  // 使用路由(主要)
8
8
  server.use(async (ctx, next) => {
9
- if (ctx.path !== "/favicon.ico") {
10
- await $.eventer.run('web_check', ctx, ctx.db);
11
- var event = $.event_admin('api');
12
- var ret = await event.check(ctx.path, ctx, ctx.db);
13
- // console.log("check阶段", ret);
14
- if (ret) {
15
- ctx.body = ret;
9
+ try {
10
+ if (ctx.path !== "/favicon.ico") {
11
+ await $.eventer.run('web_check', ctx, ctx.db);
12
+ var event = $.event_admin('api');
13
+ var ret = await event.check(ctx.path, ctx, ctx.db);
14
+ // console.log("check阶段", ret);
15
+ if (ret) {
16
+ ctx.body = ret;
17
+ }
16
18
  }
19
+ await next();
20
+ } catch (error) {
21
+ $.log.error('web_check中间件错误:', error);
22
+ // 确保请求可以继续处理
23
+ await next();
17
24
  }
18
- await next();
19
25
  });
20
26
 
21
27
  return server;
@@ -6,16 +6,22 @@
6
6
  module.exports = function(server, config) {
7
7
  // 使用路由(主要)
8
8
  server.use(async (ctx, next) => {
9
- if (ctx.path !== "/favicon.ico") {
10
- await $.eventer.run('web_main', ctx, ctx.db);
11
- var event = $.event_admin('api');
12
- var ret = await event.main(ctx.path, ctx, ctx.db);
13
- // console.log("main阶段", ret);
14
- if (ret) {
15
- ctx.body = ret;
9
+ try {
10
+ if (ctx.path !== "/favicon.ico") {
11
+ await $.eventer.run('web_main', ctx, ctx.db);
12
+ var event = $.event_admin('api');
13
+ var ret = await event.main(ctx.path, ctx, ctx.db);
14
+ // console.log("main阶段", ret);
15
+ if (ret) {
16
+ ctx.body = ret;
17
+ }
16
18
  }
19
+ await next();
20
+ } catch (error) {
21
+ $.log.error('web_main中间件错误:', error);
22
+ // 确保请求可以继续处理
23
+ await next();
17
24
  }
18
- await next();
19
25
  });
20
26
 
21
27
  return server;
@@ -13,11 +13,24 @@ module.exports = function(server, config) {
13
13
  if (config && config.proxy) {
14
14
  var options = config.proxy;
15
15
  if (options.targets) {
16
- server.use(proxy(options, function(op, ctx, next) {
17
- if (ctx.session && ctx.session.user) {
18
- ctx.request.header['user_id'] = ctx.session.user.user_id;
16
+ server.use(async (ctx, next) => {
17
+ try {
18
+ // 先设置用户信息头
19
+ if (ctx.session && ctx.session.user) {
20
+ ctx.request.header['user_id'] = ctx.session.user.user_id;
21
+ }
22
+ // 创建一个代理处理器
23
+ const proxyHandler = proxy(options, function(op, ctxProxy, nextProxy) {
24
+ return nextProxy();
25
+ });
26
+ // 执行代理
27
+ await proxyHandler(ctx, next);
28
+ } catch (error) {
29
+ $.log.error('web_proxy中间件错误:', error);
30
+ // 出错时默认允许请求继续处理
31
+ await next();
19
32
  }
20
- }));
33
+ });
21
34
  }
22
35
  }
23
36
  return server;
@@ -63,18 +63,24 @@ function render_body(ret, res, t) {
63
63
  module.exports = function(server, config) {
64
64
  // 使用路由(主要)
65
65
  server.use(async (ctx, next) => {
66
- if (ctx.path !== "/favicon.ico") {
67
- await $.eventer.run('web_render', ctx, ctx.db);
68
- var event = $.event_admin('api');
69
- var ret = await event.render(ctx.path, ctx, ctx.db);
70
- if (!ctx.body && ret) {
71
- ctx.body = ret;
72
- }
73
- if (ctx.body) {
74
- ctx.body = render_body(ctx.body, ctx.response, ctx.request.type);
66
+ try {
67
+ if (ctx.path !== "/favicon.ico") {
68
+ await $.eventer.run('web_render', ctx, ctx.db);
69
+ var event = $.event_admin('api');
70
+ var ret = await event.render(ctx.path, ctx, ctx.db);
71
+ if (!ctx.body && ret) {
72
+ ctx.body = ret;
73
+ }
74
+ if (ctx.body) {
75
+ ctx.body = render_body(ctx.body, ctx.response, ctx.request.type);
76
+ }
75
77
  }
78
+ await next();
79
+ } catch (error) {
80
+ $.log.error('web_render中间件错误:', error);
81
+ // 确保请求可以继续处理
82
+ await next();
76
83
  }
77
- await next();
78
84
  });
79
85
 
80
86
  return server;
@@ -12,8 +12,21 @@ module.exports = function(server, config) {
12
12
  //使用 websocket 服务
13
13
  $.socket = new $.Socket();
14
14
  $.socket.update();
15
- server.ws.use((ctx, next) => {
16
- $.socket.run(ctx, next);
15
+ server.ws.use(async (ctx, next) => {
16
+ try {
17
+ // 确保$.socket.run是异步执行的
18
+ await Promise.resolve($.socket.run(ctx, next));
19
+ } catch (error) {
20
+ $.log.error('web_socket中间件错误:', error);
21
+ // WebSocket连接错误时,尝试关闭连接以避免资源泄漏
22
+ try {
23
+ if (ctx.websocket && ctx.websocket.readyState === ctx.websocket.OPEN) {
24
+ ctx.websocket.close(1011, 'Internal server error');
25
+ }
26
+ } catch (closeError) {
27
+ // 忽略关闭连接时的错误
28
+ }
29
+ }
17
30
  });
18
31
  }
19
32
 
@@ -1,4 +1,9 @@
1
+ /**
2
+ * 增强的静态资源处理中间件
3
+ * 提供缓存控制、压缩、预加载等性能优化功能
4
+ */
1
5
  const statics = require('mm_statics');
6
+ const compress = require('koa-compress');
2
7
 
3
8
  /**
4
9
  * 静态资源请求
@@ -6,20 +11,104 @@ const statics = require('mm_statics');
6
11
  * @param {Object} config 配置参数
7
12
  */
8
13
  module.exports = function(server, config) {
14
+ // 配置默认值
15
+ const cg = Object.assign({
16
+ static: true,
17
+ maxAge: 365 * 24 * 60 * 60, // 默认缓存一年
18
+ compress: true,
19
+ compressThreshold: 1024, // 大于1KB的文件才压缩
20
+ enablePreload: true, // 启用预加载提示
21
+ staticPaths: [] // 多路径配置
22
+ }, config);
23
+
9
24
  // 处理静态文件
10
- if (config.static) {
11
- server.use(statics(
12
- config.static_path.fullname(), {
13
- maxAge: config.max_age || 0,
14
- gzip: config.compress || false,
15
- brotli: config.compress || false
25
+ if (cg.static) {
26
+ // 添加压缩中间件
27
+ if (cg.compress && typeof compress === 'function') {
28
+ server.use(compress({
29
+ filter: function(content_type) {
30
+ return /text|json|javascript|css|xml/.test(content_type);
31
+ },
32
+ threshold: cg.compressThreshold,
33
+ gzip: { flush: require('zlib').constants.Z_SYNC_FLUSH },
34
+ brotli: { flush: require('zlib').constants.BROTLI_OPERATION_FLUSH }
16
35
  }));
17
- // 使用多路径静态文件处理器
36
+ }
37
+
38
+ // 主静态路径处理
39
+ if (config.static_path) {
40
+ // 为mm_statics添加错误处理包装器
41
+ const staticHandler = statics(
42
+ config.static_path.fullname(), {
43
+ maxAge: cg.maxAge,
44
+ gzip: cg.compress,
45
+ brotli: cg.compress
46
+ });
47
+
48
+ server.use(async (ctx, next) => {
49
+ // 预加载提示处理
50
+ if (cg.enablePreload && ctx.path === '/') {
51
+ ctx.append('Link', '</static/css/main.css>; rel=preload; as=style');
52
+ ctx.append('Link', '</static/js/main.js>; rel=preload; as=script');
53
+ }
54
+ try {
55
+ await staticHandler(ctx, next);
56
+ } catch (staticError) {
57
+ if (staticError.code !== 'ENOENT') { // 忽略文件不存在的错误
58
+ $.log.error('静态资源服务错误:', staticError);
59
+ }
60
+ // 出错时继续处理请求
61
+ await next();
62
+ }
63
+ });
64
+ }
65
+
66
+ // 多路径静态文件处理
67
+ if (Array.isArray(cg.staticPaths) && cg.staticPaths.length > 0) {
68
+ cg.staticPaths.forEach((pathConfig, index) => {
69
+ try {
70
+ const staticPathHandler = statics(
71
+ pathConfig.path.fullname(),
72
+ Object.assign({
73
+ maxAge: cg.maxAge,
74
+ gzip: cg.compress,
75
+ brotli: cg.compress
76
+ }, pathConfig.options || {})
77
+ );
78
+
79
+ server.use(async (ctx, next) => {
80
+ try {
81
+ await staticPathHandler(ctx, next);
82
+ } catch (e) {
83
+ if (e.code !== 'ENOENT') {
84
+ $.log.error(`静态资源路径[${pathConfig.path}]服务错误:`, e);
85
+ }
86
+ await next();
87
+ }
88
+ });
89
+ } catch (e) {
90
+ $.log.error(`静态资源多路径配置错误[${index}]:`, e);
91
+ }
92
+ });
93
+ }
94
+
95
+ // 使用多路径静态文件处理器(保留原有功能)
18
96
  if ($.Static) {
19
97
  const Static = $.Static;
20
98
  $.static = new Static();
21
99
  $.static.update();
22
- server.use($.static.run);
100
+
101
+ // 为$.static.run添加错误处理包装器
102
+ server.use(async (ctx, next) => {
103
+ try {
104
+ // 确保静态文件处理器是异步执行的
105
+ await Promise.resolve($.static.run(ctx, next));
106
+ } catch (staticRunError) {
107
+ $.log.error('多路径静态资源服务错误:', staticRunError);
108
+ // 出错时继续处理请求
109
+ await next();
110
+ }
111
+ });
23
112
  }
24
113
  }
25
114
  return server;