chanjs 2.7.8 → 2.7.11
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.md +261 -363
- package/config/index.js +4 -2
- package/core/App.js +35 -0
- package/core/Container.js +56 -29
- package/core/Database.js +58 -8
- package/core/EventBus.js +88 -0
- package/core/Lang.js +56 -0
- package/core/Repository.js +34 -2
- package/core/Task.js +87 -0
- package/core/errors.js +0 -5
- package/doc/00-README.md +208 -0
- package/doc/01-/346/240/270/345/277/203/347/261/273Controller-Service-Repository.md +432 -0
- package/doc/02-/345/223/215/345/272/224/344/270/216/351/224/231/350/257/257.md +255 -0
- package/doc/03-/345/256/211/345/205/250/346/250/241/345/235/227.md +264 -0
- package/doc/04-/345/255/230/345/202/250/344/270/216/347/274/223/345/255/230.md +157 -0
- package/doc/05-/345/267/245/345/205/267/344/270/216/346/240/241/351/252/214.md +309 -0
- package/doc/06-/345/272/224/347/224/250/347/224/237/345/221/275/345/221/250/346/234/237.md +207 -0
- package/doc/07-/344/272/213/344/273/266/347/263/273/347/273/237EventBus.md +324 -0
- package/doc/08-/345/256/232/346/227/266/344/273/273/345/212/241Task.md +262 -0
- package/doc/09-/345/233/275/351/231/205/345/214/226Lang.md +220 -0
- package/index.js +30 -1
- package/middleware/log.js +48 -31
- package/middleware/waf.js +22 -90
- package/package.json +20 -2
- package/response/code.js +0 -12
- package/response/response.js +8 -2
- package/security/checker.js +14 -7
- package/security/keywords.js +2 -3
- package/utils/logger.js +60 -91
- package/utils/pages.js +13 -12
- package/utils/signal.js +21 -2
- package/USAGE.md +0 -533
- package/doc/Cache.md +0 -333
- package/doc/Common.md +0 -638
- package/doc/Controller.md +0 -223
- package/doc/Help.md +0 -390
- package/doc/QuickStart.md +0 -116
- package/doc/Repository.md +0 -560
- package/doc/Service.md +0 -240
- package/publish.bat +0 -4
- package/todo.md +0 -1
package/security/checker.js
CHANGED
|
@@ -15,21 +15,28 @@ const BODY_EXCLUDED_CATEGORIES = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* 恶意关键词检测:单次遍历分类列表,利用捕获组直接定位命中关键词
|
|
19
19
|
* @param {string} fullText 待检测文本
|
|
20
|
-
* @param {{scope?: 'body'|'url'}} [options]
|
|
20
|
+
* @param {{scope?: 'body'|'url', categories?: string[]}} [options]
|
|
21
|
+
* - scope='body' 时跳过路径/文件特征类规则,降低正文误报;
|
|
22
|
+
* - categories 显式指定要扫描的分类(用于 URL 场景,只保留真正危险分类,避免正常词误伤);
|
|
23
|
+
* - 二者都不传则扫描全部分类。
|
|
21
24
|
* @returns {{category: string; keyword: string}|null}
|
|
22
25
|
*/
|
|
23
|
-
export const checkKeywords = (fullText, { scope } = {}) => {
|
|
26
|
+
export const checkKeywords = (fullText, { scope, categories } = {}) => {
|
|
24
27
|
const text = fullText?.trim();
|
|
25
28
|
if (!text) return null;
|
|
26
29
|
|
|
27
|
-
const cats =
|
|
28
|
-
?
|
|
29
|
-
:
|
|
30
|
+
const cats = Array.isArray(categories)
|
|
31
|
+
? categories
|
|
32
|
+
: (scope === 'body'
|
|
33
|
+
? SORTED_CATEGORIES.filter(c => !BODY_EXCLUDED_CATEGORIES.includes(c))
|
|
34
|
+
: SORTED_CATEGORIES);
|
|
30
35
|
|
|
31
36
|
for (const cat of cats) {
|
|
32
|
-
const
|
|
37
|
+
const re = keywordRegexCache[cat]?.merged;
|
|
38
|
+
if (!re) continue;
|
|
39
|
+
const match = re.exec(text);
|
|
33
40
|
if (!match) continue;
|
|
34
41
|
// capture[0]是整体匹配,capture[1..]是每个关键词的捕获组
|
|
35
42
|
const idx = match.findIndex((g, i) => i > 0 && g);
|
package/security/keywords.js
CHANGED
|
@@ -22,11 +22,10 @@ const KEYWORD_RULES = Object.freeze({
|
|
|
22
22
|
commandInjection: [
|
|
23
23
|
"cmd=", "system(", "exec(", "shell_exec(", "passthru(",
|
|
24
24
|
"eval(", "assert(", "preg_replace", "bash -i", "rm -rf",
|
|
25
|
-
"wget ", "
|
|
25
|
+
"wget ", "base64_decode", "phpinfo()",
|
|
26
26
|
"killall", "shutdown", "fdisk", "mkfs", "dd ",
|
|
27
27
|
"scp ", "rsync", "nc ", "netcat", "nmap", "iptables",
|
|
28
|
-
"systemctl"
|
|
29
|
-
"userdel", "usermod", "groupadd", "groupdel", "passwd", "chpasswd"
|
|
28
|
+
"systemctl"
|
|
30
29
|
],
|
|
31
30
|
pathTraversal: [
|
|
32
31
|
"../", "..\\", "/etc/passwd", "/etc/shadow", "/etc/hosts",
|
package/utils/logger.js
CHANGED
|
@@ -1,117 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
* 统一日志工具
|
|
3
|
-
* 1. 本地dev:彩色友好打印
|
|
4
|
-
* 2. 线上prd:JSON单行字符串输出,供日志采集器解析
|
|
5
|
-
* 3. 日志分级控制 LOG_LEVEL,线上关闭调试日志减少磁盘占用
|
|
6
|
-
* 4. error/fatal 自动走 stderr
|
|
7
|
-
* 5. 支持自定义模块标签、本地文件落盘、错误堆栈结构化解析
|
|
8
|
-
*/
|
|
9
|
-
import fs from "fs";
|
|
10
|
-
import path from "path";
|
|
11
|
-
import { parseStack } from "../core/errors.js";
|
|
12
|
-
|
|
13
|
-
const LOG_LEVELS = Object.freeze({
|
|
14
|
-
trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5, silent: 99,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const COLORS = Object.freeze({
|
|
18
|
-
trace: "\x1b[90m",
|
|
19
|
-
debug: "\x1b[34m",
|
|
20
|
-
info: "\x1b[32m",
|
|
21
|
-
warn: "\x1b[33m",
|
|
22
|
-
error: "\x1b[31m",
|
|
23
|
-
fatal: "\x1b[41m\x1b[37m",
|
|
24
|
-
reset: "\x1b[0m",
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const ERROR_LEVELS = new Set(["error", "fatal"]);
|
|
1
|
+
import pino from "pino";
|
|
28
2
|
|
|
29
3
|
const NODE_ENV = (process.env.NODE_ENV || "dev").toLowerCase();
|
|
30
4
|
const IS_PROD = ["prd", "prod", "production"].includes(NODE_ENV);
|
|
31
|
-
const
|
|
32
|
-
const CURRENT_LEVEL = LOG_LEVELS[(process.env.LOG_LEVEL || DEFAULT_LOG_LEVEL).toLowerCase()] ?? LOG_LEVELS.info;
|
|
5
|
+
const LOG_LEVEL = process.env.LOG_LEVEL || (IS_PROD ? "info" : "debug");
|
|
33
6
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
7
|
+
/**
|
|
8
|
+
* pino 基础配置
|
|
9
|
+
* - dev:pino-pretty 彩色输出,单行易读
|
|
10
|
+
* - prod:纯 JSON,pm2 捕获 stdout
|
|
11
|
+
*/
|
|
12
|
+
const baseOptions = {
|
|
13
|
+
level: LOG_LEVEL,
|
|
14
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
15
|
+
};
|
|
39
16
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
17
|
+
if (!IS_PROD) {
|
|
18
|
+
baseOptions.transport = {
|
|
19
|
+
target: "pino-pretty",
|
|
20
|
+
options: {
|
|
21
|
+
colorize: true,
|
|
22
|
+
translateTime: "yyyy-mm-dd HH:mm:ss.l",
|
|
23
|
+
// dev 终端只做单进程开发,module 恒为 "Chan" 无区分意义,隐藏以精简输出
|
|
24
|
+
// (prod 仍保留 module 字段,便于日志聚合时按模块过滤)
|
|
25
|
+
ignore: "pid,hostname,module",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
45
28
|
}
|
|
46
29
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
30
|
+
/** pino 根实例 */
|
|
31
|
+
export const root = pino(baseOptions);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 将多参数调用适配为 pino 格式
|
|
35
|
+
* 兼容现有用法:logger.info("msg")、logger.error("msg", err)、logger.warn("msg", obj)
|
|
36
|
+
*
|
|
37
|
+
* @param {string} level - 日志级别
|
|
38
|
+
* @param {import("pino").Logger} instance - pino 实例
|
|
39
|
+
* @param {...any} args - 业务传入的参数
|
|
40
|
+
*/
|
|
41
|
+
function emit(level, instance, ...args) {
|
|
42
|
+
const msgParts = [];
|
|
43
|
+
const mergeObj = {};
|
|
61
44
|
|
|
62
|
-
/** 解析日志入参,分离普通文本与错误堆栈 */
|
|
63
|
-
function parseArgs(args) {
|
|
64
|
-
let stackInfo = null;
|
|
65
|
-
const parts = [];
|
|
66
45
|
for (const arg of args) {
|
|
67
46
|
if (arg instanceof Error) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
47
|
+
// Error 对象转为 pino 的 err 字段(自动序列化 stack)
|
|
48
|
+
mergeObj.err = arg;
|
|
49
|
+
msgParts.push(arg.message);
|
|
71
50
|
} else if (arg !== null && typeof arg === "object") {
|
|
72
|
-
|
|
73
|
-
catch { parts.push(String(arg)); }
|
|
51
|
+
Object.assign(mergeObj, arg);
|
|
74
52
|
} else {
|
|
75
|
-
|
|
53
|
+
msgParts.push(String(arg));
|
|
76
54
|
}
|
|
77
55
|
}
|
|
78
|
-
return { message: parts.join(" "), stackInfo };
|
|
79
|
-
}
|
|
80
56
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!isLevelEnable(level)) return;
|
|
57
|
+
const msg = msgParts.join(" ");
|
|
58
|
+
const hasMerge = Object.keys(mergeObj).length > 0;
|
|
84
59
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (IS_PROD) {
|
|
91
|
-
const logLine = JSON.stringify({ time, env: NODE_ENV, level, label, message, stack: stackInfo, pid: process.pid });
|
|
92
|
-
printFn(logLine);
|
|
93
|
-
return;
|
|
60
|
+
if (hasMerge) {
|
|
61
|
+
instance[level](mergeObj, msg);
|
|
62
|
+
} else {
|
|
63
|
+
instance[level](msg);
|
|
94
64
|
}
|
|
95
|
-
|
|
96
|
-
const color = COLORS[level] ?? "";
|
|
97
|
-
printFn(`${color}[${time}] [${level.toUpperCase()}] [${label}]${COLORS.reset}`, ...args);
|
|
98
|
-
if (stackInfo) console.error(`${color}>>>> 错误堆栈详情:${COLORS.reset}`, stackInfo);
|
|
99
|
-
writeFile(`[${time}] [${level.toUpperCase()}] [${label}] ${message}`);
|
|
100
65
|
}
|
|
101
66
|
|
|
102
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* 创建带固定标签的日志实例
|
|
69
|
+
* @param {string} [label="Chan"] - 模块标签
|
|
70
|
+
* @returns {{trace: Function, debug: Function, info: Function, warn: Function, error: Function, fatal: Function}}
|
|
71
|
+
*/
|
|
103
72
|
export function createLogger(label = "Chan") {
|
|
104
|
-
const
|
|
105
|
-
const make = level => (...args) => emit(level, safeLabel, args);
|
|
73
|
+
const instance = root.child({ module: label });
|
|
106
74
|
return {
|
|
107
|
-
trace:
|
|
108
|
-
debug:
|
|
109
|
-
info:
|
|
110
|
-
warn:
|
|
111
|
-
error:
|
|
112
|
-
fatal:
|
|
75
|
+
trace: (...args) => emit("trace", instance, ...args),
|
|
76
|
+
debug: (...args) => emit("debug", instance, ...args),
|
|
77
|
+
info: (...args) => emit("info", instance, ...args),
|
|
78
|
+
warn: (...args) => emit("warn", instance, ...args),
|
|
79
|
+
error: (...args) => emit("error", instance, ...args),
|
|
80
|
+
fatal: (...args) => emit("fatal", instance, ...args),
|
|
113
81
|
};
|
|
114
82
|
}
|
|
115
83
|
|
|
84
|
+
/** 默认导出:全局 logger 实例(标签 "Chan") */
|
|
116
85
|
const logger = createLogger("Chan");
|
|
117
86
|
export default logger;
|
package/utils/pages.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 分页工具函数
|
|
3
|
-
*
|
|
3
|
+
* 输出无 ul/li 的干净 HTML:普通页码直接用 <a>,当前页/禁用/省略号用 <span class="current|disabled|ellipsis">
|
|
4
|
+
* 样式由项目 CSS(如 .blince-pagination)统一维护,不依赖 Tailwind 等第三方框架
|
|
4
5
|
*/
|
|
5
|
-
const LI_DISABLED = 'inline-flex items-center px-3 py-1.5 text-sm text-slate-400 cursor-not-allowed rounded-md border border-slate-100 bg-slate-50';
|
|
6
|
-
const LI_ELLIPSIS = 'inline-flex items-center px-2 py-1.5 text-sm text-slate-400';
|
|
7
|
-
const LI_CURRENT = 'inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-lime-600 border border-lime-600 rounded-md';
|
|
8
|
-
const LI_LINK = 'inline-flex items-center text-sm text-slate-700 bg-white border border-slate-200 rounded-md hover:border-lime-600 hover:text-lime-600 transition-colors';
|
|
9
|
-
const A_LINK = 'block px-3 py-1.5';
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* 渲染分页HTML
|
|
@@ -26,8 +22,13 @@ export function pages(current, total, pageSize, href, query = '') {
|
|
|
26
22
|
if (totalPage <= 1) return '';
|
|
27
23
|
if (current > totalPage) current = totalPage;
|
|
28
24
|
|
|
29
|
-
const pageLink = (i, text) =>
|
|
30
|
-
|
|
25
|
+
const pageLink = (i, text) => i === current
|
|
26
|
+
? `<span class="current">${text || i}</span>`
|
|
27
|
+
: `<a href="${href}${i}.html${query}">${text || i}</a>`;
|
|
28
|
+
const navLink = (i, text) => i
|
|
29
|
+
? pageLink(i, text)
|
|
30
|
+
: `<span class="disabled">${text}</span>`;
|
|
31
|
+
const ellipsis = () => `<span class="ellipsis">...</span>`;
|
|
31
32
|
const items = [];
|
|
32
33
|
|
|
33
34
|
// 上一页
|
|
@@ -38,14 +39,14 @@ export function pages(current, total, pageSize, href, query = '') {
|
|
|
38
39
|
for (let i = 1; i <= totalPage; i++) items.push(pageLink(i));
|
|
39
40
|
} else if (current <= 4) {
|
|
40
41
|
for (let i = 1; i <= 5; i++) items.push(pageLink(i));
|
|
41
|
-
items.push(
|
|
42
|
+
items.push(ellipsis(), pageLink(totalPage));
|
|
42
43
|
} else if (current >= totalPage - 3) {
|
|
43
|
-
items.push(pageLink(1),
|
|
44
|
+
items.push(pageLink(1), ellipsis());
|
|
44
45
|
for (let i = totalPage - 4; i <= totalPage; i++) items.push(pageLink(i));
|
|
45
46
|
} else {
|
|
46
|
-
items.push(pageLink(1),
|
|
47
|
+
items.push(pageLink(1), ellipsis());
|
|
47
48
|
for (let i = current - 1; i <= current + 1; i++) items.push(pageLink(i));
|
|
48
|
-
items.push(
|
|
49
|
+
items.push(ellipsis(), pageLink(totalPage));
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// 下一页
|
package/utils/signal.js
CHANGED
|
@@ -10,9 +10,16 @@ import { SHUTDOWN_TIMEOUT } from "../config/index.js";
|
|
|
10
10
|
let registered = false;
|
|
11
11
|
let shuttingDown = false;
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* 优雅停机资源关闭顺序:
|
|
15
|
+
* 1. HTTP 服务 → 拒绝新连接
|
|
16
|
+
* 2. 定时任务 → 停止调度,防止停机期间仍触发任务
|
|
17
|
+
* 3. 缓存存储 → 关闭 Redis/内存
|
|
18
|
+
* 4. 数据库连接 → 释放连接池
|
|
19
|
+
* 5. 事件总线 → 清理监听器防内存泄漏
|
|
20
|
+
*/
|
|
14
21
|
async function doShutdown(chan) {
|
|
15
|
-
const { dbManager, server } = chan;
|
|
22
|
+
const { dbManager, server, task, event } = chan;
|
|
16
23
|
|
|
17
24
|
if (server) {
|
|
18
25
|
try {
|
|
@@ -25,6 +32,12 @@ async function doShutdown(chan) {
|
|
|
25
32
|
|
|
26
33
|
const errors = [];
|
|
27
34
|
|
|
35
|
+
// 停止定时任务(同步操作,异常极少,单独捕获)
|
|
36
|
+
if (task) {
|
|
37
|
+
try { task.stopAll(); }
|
|
38
|
+
catch (e) { errors.push({ type: "task", err: e.message }); }
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
try { await store.close(); }
|
|
29
42
|
catch (e) { errors.push({ type: "store", err: e.message }); }
|
|
30
43
|
|
|
@@ -33,6 +46,12 @@ async function doShutdown(chan) {
|
|
|
33
46
|
results.filter(item => !item.success).forEach(item => errors.push({ type: `db:${item.name}`, err: item.error }));
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
// 清理事件总线全部监听器(防内存泄漏)
|
|
50
|
+
if (event) {
|
|
51
|
+
try { event.destroy(); }
|
|
52
|
+
catch (e) { errors.push({ type: "event", err: e.message }); }
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
if (errors.length) logger.warn(`[Chan] 停机存在${errors.length}处资源关闭失败`, errors);
|
|
37
56
|
setApp(null);
|
|
38
57
|
}
|