chanjs 2.5.1 → 2.5.2

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/checker.js ADDED
@@ -0,0 +1,68 @@
1
+ import { keywordRegexCache } from "./keywords.js";
2
+
3
+ /**
4
+ * 安全检查工具
5
+ * 提供关键词检测和路径忽略检查功能
6
+ */
7
+
8
+ /**
9
+ * 检查文本中是否包含恶意关键词
10
+ * @param {string} fullText - 要检查的完整文本
11
+ * @returns {Object|null} 检测结果,包含 category 和 keyword,未检测到返回 null
12
+ * @description
13
+ * 按优先级检查 SQL注入、XSS、命令注入等恶意关键词
14
+ * 优先检查 sqlInjection、xss、commandInjection 类别
15
+ * @example
16
+ * const result = checkKeywords('SELECT * FROM users WHERE 1=1');
17
+ * console.log(result); // { category: 'sqlInjection', keyword: 'union select' }
18
+ */
19
+ export function checkKeywords(fullText) {
20
+ if (!fullText?.trim()) return null;
21
+
22
+ const priorityCategories = ['sqlInjection', 'xss', 'commandInjection'];
23
+ const otherCategories = Object.keys(keywordRegexCache).filter(c => !priorityCategories.includes(c));
24
+
25
+ for (const category of priorityCategories) {
26
+ const patterns = keywordRegexCache[category];
27
+ if (!patterns) continue;
28
+ for (const { keyword, regex } of patterns) {
29
+ if (regex.test(fullText)) {
30
+ return { category, keyword };
31
+ }
32
+ }
33
+ }
34
+
35
+ for (const category of otherCategories) {
36
+ const patterns = keywordRegexCache[category];
37
+ if (!patterns) continue;
38
+ for (const { keyword, regex } of patterns) {
39
+ if (regex.test(fullText)) {
40
+ return { category, keyword };
41
+ }
42
+ }
43
+ }
44
+
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * 检查路径是否在忽略列表中
50
+ * @param {string} path - 要检查的路径
51
+ * @param {Array<string>} ignorePaths - 忽略路径数组
52
+ * @returns {boolean} 是否应该忽略该路径
53
+ * @description
54
+ * 检查给定路径是否匹配忽略列表中的任意路径
55
+ * 支持精确匹配和前缀匹配
56
+ * @example
57
+ * isIgnored('/api/health', ['/api/health', '/metrics']); // true
58
+ * isIgnored('/api/users', ['/api/health']); // false
59
+ */
60
+ export function isIgnored(path, ignorePaths) {
61
+ if (!path || !ignorePaths?.length) return false;
62
+
63
+ const normalizedPath = path.trim().toLowerCase().replace(/\/$/, '');
64
+ return ignorePaths.some(p => {
65
+ const normalizedP = p.trim().toLowerCase().replace(/\/$/, '');
66
+ return normalizedPath === normalizedP || normalizedPath.startsWith(`${normalizedP}/`);
67
+ });
68
+ }
package/helper/ip.js CHANGED
@@ -31,7 +31,11 @@
31
31
  */
32
32
  export function getIp(req) {
33
33
  if (req.ip) {
34
- const ip = req.ip;
34
+ let ip = req.ip;
35
+
36
+ if (ip.startsWith('::ffff:')) {
37
+ ip = ip.substring(7);
38
+ }
35
39
 
36
40
  const isLocalAddress = ip === '::1' ||
37
41
  ip === '127.0.0.1' ||
package/keywords.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * 安全关键词规则定义
3
+ * 定义各类恶意攻击的关键词检测规则
4
+ */
5
+
6
+ export const KEYWORD_RULES = {
7
+ /**
8
+ * 整词匹配规则(需完整匹配)
9
+ */
10
+ wholeWord: [
11
+ "netcat", "nc", "php-cgi", "process", "require",
12
+ "child_process", "execSync", "mainModule"
13
+ ],
14
+ /**
15
+ * 敏感文件扩展名
16
+ */
17
+ extensions: [
18
+ ".php", ".asp", ".aspx", ".jsp", ".jspx", ".do", ".action", ".cgi",
19
+ ".py", ".pl", ".cfm", ".jhtml", ".shtml",".sql"
20
+ ],
21
+ /**
22
+ * 敏感目录名称
23
+ */
24
+ directories: [
25
+ "/administrator", "/wp-admin", "phpMyAdmin", "cgi-bin",
26
+ "setup", "staging", "internal", "debug", "metadata", "secret"
27
+ ],
28
+ /**
29
+ * SQL 注入关键词
30
+ */
31
+ sqlInjection: [
32
+ "sleep(", "benchmark(", "concat(", "extractvalue(", "updatexml(", "version(",
33
+ "union select", "union all", "select @@", "drop ", "alter ", "truncate ",
34
+ "(select", "information_schema", "load_file(", "into outfile", "into dumpfile"
35
+ ],
36
+ /**
37
+ * 命令注入关键词
38
+ */
39
+ commandInjection: [
40
+ "cmd=", "system(", "exec(", "shell_exec(", "passthru(",
41
+ "eval(", "assert(", "preg_replace", "bash -i", "rm -rf",
42
+ "wget ", "curl ", "chmod ", "base64_decode", "phpinfo()",
43
+ "kill ", "killall", "shutdown", "reboot", "halt", "fdisk",
44
+ "mkfs", "dd ", "ssh ", "scp ", "rsync", "nc ",
45
+ "netcat", "nmap", "iptables", "systemctl", "service",
46
+ "init", "crontab", "at ", "su ", "sudo", "useradd",
47
+ "userdel", "usermod", "groupadd", "groupdel", "passwd",
48
+ "chpasswd", "mount ", "umount", "ln -s"
49
+ ],
50
+ /**
51
+ * 路径遍历关键词
52
+ */
53
+ pathTraversal: [
54
+ "../", "..\\", "/etc/passwd", "/etc/shadow", "/etc/hosts",
55
+ "/etc/", "/var/www/", "/app/", "/root/", "__dirname", "__filename"
56
+ ],
57
+ /**
58
+ * XSS 攻击关键词
59
+ */
60
+ xss: [
61
+ "<script", "javascript:", "onerror=", "onload=", "onclick=",
62
+ "alert(", "document.cookie", "document.write"
63
+ ],
64
+ /**
65
+ * 编码绕过关键词
66
+ */
67
+ encoding: [
68
+ "0x7e", "UNION%20SELECT", "%27OR%27", "{{", "}}", "${", "1+1"
69
+ ],
70
+ /**
71
+ * 敏感标识符
72
+ */
73
+ sensitiveIdentifiers: [
74
+ "wp-", "smtp", "redirect", "configs", ".well-known/",
75
+ "fs.readFile", "fs.existsSync", "process.env", "process.argv"
76
+ ],
77
+ };
78
+
79
+ /**
80
+ * 关键词正则表达式缓存
81
+ * 将关键词规则编译为正则表达式以提高检测性能
82
+ */
83
+ export let keywordRegexCache = (() => {
84
+ const regexSpecialChars = /[.*+?^${}()|[\]\\]/g;
85
+ const cache = {};
86
+
87
+ for (const [category, keywords] of Object.entries(KEYWORD_RULES)) {
88
+ if (!Array.isArray(keywords)) continue;
89
+
90
+ cache[category] = keywords.map((keyword) => {
91
+ const escaped = keyword.replace(regexSpecialChars, "\\$&").replace(/ /g, "\\s");
92
+ return {
93
+ keyword,
94
+ regex: new RegExp(
95
+ category === 'wholeWord' ? `\\b${escaped}\\b` : escaped,
96
+ "i"
97
+ ),
98
+ };
99
+ });
100
+ }
101
+
102
+ return cache;
103
+ })();
104
+
105
+ /**
106
+ * 更新关键词缓存
107
+ * @param {Object} newRules - 新的关键词规则
108
+ * @description
109
+ * 允许动态添加或更新关键词检测规则
110
+ * @example
111
+ * updateKeywordCache({
112
+ * customKeywords: ['malicious', 'attack']
113
+ * });
114
+ */
115
+ export function updateKeywordCache(newRules = {}) {
116
+ const regexSpecialChars = /[.*+?^${}()|[\]\\]/g;
117
+
118
+ for (const [category, keywords] of Object.entries(newRules)) {
119
+ if (!Array.isArray(keywords)) continue;
120
+
121
+ keywordRegexCache[category] = keywords.map((keyword) => {
122
+ const escaped = keyword.replace(regexSpecialChars, "\\$&").replace(/ /g, "\\s");
123
+ return {
124
+ keyword,
125
+ regex: new RegExp(
126
+ category === 'wholeWord' ? `\\b${escaped}\\b` : escaped,
127
+ "i"
128
+ ),
129
+ };
130
+ });
131
+ }
132
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.5.1",
4
+ "version": "2.5.2",
5
5
  "description": "chanjs基于express5 纯js研发的轻量级mvc框架。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
package/rate-limit.js ADDED
@@ -0,0 +1,116 @@
1
+ import { isIgnored } from "./checker.js";
2
+ import { getIp } from "../helper/ip.js";
3
+
4
+ /**
5
+ * 访问频率限制工具
6
+ * 提供基于 IP 和 Cookie 的访问限流功能
7
+ */
8
+
9
+ const COOKIE_NAME = `${process.env.APP_NAME || 'app'}_ratelimit`;
10
+
11
+ /**
12
+ * 解析时间窗口参数
13
+ * @private
14
+ * @param {number|string} time - 时间值
15
+ * @returns {number} 毫秒数
16
+ * @description
17
+ * 支持以下格式:
18
+ * - 数字:直接作为毫秒数
19
+ * - 数字+s:秒
20
+ * - 数字+m:分钟
21
+ * - 数字+h:小时
22
+ * - 数字+d:天
23
+ */
24
+ function parseWindowMs(time) {
25
+ if (typeof time === 'number') return time;
26
+ if (typeof time !== 'string') return 60 * 60 * 1000;
27
+
28
+ const match = time.match(/^(\d+)([smhd])$/);
29
+ if (!match) return 60 * 60 * 1000;
30
+
31
+ const value = parseInt(match[1], 10);
32
+ const unit = match[2];
33
+
34
+ const multipliers = {
35
+ 's': 1000,
36
+ 'm': 60 * 1000,
37
+ 'h': 60 * 60 * 1000,
38
+ 'd': 24 * 60 * 60 * 1000,
39
+ };
40
+
41
+ return value * (multipliers[unit] || 1000);
42
+ }
43
+
44
+ /**
45
+ * 创建限流中间件
46
+ * @param {Object} rateLimitConfig - 限流配置
47
+ * @param {number|string} rateLimitConfig.windowMs - 时间窗口
48
+ * @param {number} rateLimitConfig.max - 最大请求次数
49
+ * @param {Array<string>} [rateLimitConfig.ignorePaths] - 忽略的路径数组
50
+ * @returns {Function} Express 中间件函数
51
+ * @description
52
+ * 基于客户端 IP 实现滑动窗口限流算法
53
+ * 使用 Cookie 存储限流状态,避免内存泄漏
54
+ * @example
55
+ * const rateLimiter = createRateLimitMiddleware({
56
+ * windowMs: '1m',
57
+ * max: 60,
58
+ * ignorePaths: ['/health']
59
+ * });
60
+ */
61
+ export function createRateLimitMiddleware(rateLimitConfig) {
62
+ const config = {
63
+ ...rateLimitConfig,
64
+ windowMs: parseWindowMs(rateLimitConfig?.windowMs),
65
+ };
66
+
67
+ return (req, res, next) => {
68
+ try {
69
+ if (isIgnored(req.path, config.ignorePaths)) {
70
+ return next();
71
+ }
72
+
73
+ const now = Date.now();
74
+ const ip = getIp(req);
75
+ const cookieValue = req.cookies && req.cookies[COOKIE_NAME];
76
+ let currentData = null;
77
+
78
+ if (cookieValue) {
79
+ try {
80
+ currentData = JSON.parse(cookieValue);
81
+ if (currentData && currentData.ip !== ip) {
82
+ currentData = null;
83
+ }
84
+ } catch (e) {
85
+ currentData = null;
86
+ }
87
+ }
88
+
89
+ let newData;
90
+ if (!currentData || now > currentData.resetTime) {
91
+ newData = { count: 1, resetTime: now + config.windowMs, ip };
92
+ } else if (currentData.count >= config.max) {
93
+ console.error(`[WAF 限流拦截] 路径:${req.path} 计数:${currentData.count}/${config.max} IP:${ip}`);
94
+ return res.status(429).json({
95
+ code: 429,
96
+ success: false,
97
+ msg: '请求过于频繁,请稍后重试',
98
+ retryAfter: Math.ceil((currentData.resetTime - now) / 1000),
99
+ });
100
+ } else {
101
+ newData = { count: currentData.count + 1, resetTime: currentData.resetTime, ip };
102
+ }
103
+
104
+ res.cookie(COOKIE_NAME, JSON.stringify(newData), {
105
+ httpOnly: true,
106
+ maxAge: config.windowMs,
107
+ sameSite: 'strict',
108
+ });
109
+
110
+ next();
111
+ } catch (error) {
112
+ console.error(`[WAF 限流异常] 路径:${req.path} 错误:${error.message}`);
113
+ next();
114
+ }
115
+ };
116
+ }
package/response.js ADDED
@@ -0,0 +1,180 @@
1
+ import { CODE, DB_ERROR } from "../config/code.js";
2
+
3
+ /**
4
+ * 响应工具函数
5
+ * 提供统一的响应格式和错误处理
6
+ */
7
+
8
+ const ERROR_MESSAGES = {
9
+ 6001: "数据库连接失败",
10
+ 6002: "数据库访问被拒绝",
11
+ 6003: "存在关联数据,操作失败",
12
+ 6004: "数据库字段错误",
13
+ 6005: "数据重复,违反唯一性约束",
14
+ 6006: "目标表不存在",
15
+ 6007: "数据库操作超时",
16
+ 6008: "数据库语法错误,请检查查询语句",
17
+ 6009: "数据库连接已关闭,请重试",
18
+ 4003: "资源已存在",
19
+ 5001: "系统内部错误",
20
+ };
21
+
22
+ /**
23
+ * 获取默认错误代码
24
+ * @private
25
+ * @param {Error} error - 错误对象
26
+ * @returns {number} 错误代码
27
+ */
28
+ const getDefaultErrorCode = (error) => {
29
+ if (!error?.message) return 5001;
30
+ if (error.message.includes("syntax") || error.message.includes("SQL")) {
31
+ return 6008;
32
+ } else if (error.message.includes("Connection closed")) {
33
+ return 6009;
34
+ } else if (error.message.includes("permission")) {
35
+ return 3003;
36
+ }
37
+ return 5001;
38
+ };
39
+
40
+ /**
41
+ * 解析数据库错误
42
+ * @param {Error} error - 数据库错误对象
43
+ * @returns {Object} 包含 code、msg 和 statusCode 的对象
44
+ * @description
45
+ * 根据数据库错误代码映射为业务状态码
46
+ * 返回对应的错误消息和 HTTP 状态码
47
+ */
48
+ export function parseDatabaseError(error) {
49
+ const errorCode = error?.code && DB_ERROR[error.code]
50
+ ? DB_ERROR[error.code]
51
+ : error?.message?.includes("syntax") || error?.message?.includes("SQL")
52
+ ? 6008
53
+ : error?.message?.includes("Connection closed")
54
+ ? 6009
55
+ : error?.message?.includes("permission")
56
+ ? 3003
57
+ : 5001;
58
+
59
+ return {
60
+ code: errorCode,
61
+ msg: ERROR_MESSAGES[errorCode] || error?.message || "服务器内部错误",
62
+ statusCode: errorCode >= 6000 ? 500 : errorCode >= 4000 ? 400 : 500,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * 生成错误响应
68
+ * @param {Object} options - 响应选项
69
+ * @param {Error} options.err - 错误对象
70
+ * @param {Object} [options.data={}] - 响应数据
71
+ * @param {number} [options.code=500] - 错误代码
72
+ * @returns {Object} 错误响应对象
73
+ * @description
74
+ * 根据错误类型生成标准错误响应
75
+ * 开发环境下包含数据库错误详情
76
+ */
77
+ export const error = ({ err, data = {}, code = 500 } = {}) => {
78
+ if (err) {
79
+ console.error("[DB Error]", err?.message || err);
80
+ const errorCode = err?.code && DB_ERROR[err.code] ? DB_ERROR[err.code] : getDefaultErrorCode(err);
81
+ const msg = CODE[errorCode] || "操作失败";
82
+
83
+ return {
84
+ success: false,
85
+ msg,
86
+ code: errorCode,
87
+ data: process.env.NODE_ENV === 'development' ? {
88
+ sql: err?.sql,
89
+ sqlMessage: err?.sqlMessage,
90
+ message: err?.message,
91
+ } : {},
92
+ };
93
+ }
94
+
95
+ const msg = CODE[code] || "操作失败";
96
+ return {
97
+ success: false,
98
+ msg,
99
+ code,
100
+ data,
101
+ };
102
+ };
103
+
104
+ /**
105
+ * 生成失败响应
106
+ * @param {Object} options - 响应选项
107
+ * @param {string} [options.msg="操作失败"] - 错误消息
108
+ * @param {Object} [options.data={}] - 响应数据
109
+ * @param {number} [options.code=201] - 错误代码
110
+ * @returns {Object} 失败响应对象
111
+ */
112
+ export const fail = ({ msg = "操作失败", data = {}, code = 201 } = {}) => {
113
+ return {
114
+ success: false,
115
+ msg,
116
+ code,
117
+ data,
118
+ };
119
+ };
120
+
121
+ /**
122
+ * 生成成功响应
123
+ * @param {Object} options - 响应选项
124
+ * @param {Object} [options.data={}] - 响应数据
125
+ * @param {string} [options.msg="操作成功"] - 成功消息
126
+ * @returns {Object} 成功响应对象
127
+ */
128
+ export const success = ({ data = {}, msg = "操作成功" } = {}) => ({
129
+ success: true,
130
+ msg,
131
+ code: 200,
132
+ data,
133
+ });
134
+
135
+ /**
136
+ * 生成 404 响应
137
+ * @param {Object} req - Express 请求对象
138
+ * @returns {Object} 404 响应对象
139
+ */
140
+ export function notFoundResponse(req) {
141
+ return {
142
+ success: false,
143
+ msg: "接口不存在",
144
+ code: 404,
145
+ data: { path: req.path, method: req.method },
146
+ };
147
+ }
148
+
149
+ /**
150
+ * 生成错误响应(Express 错误处理中间件用)
151
+ * @param {Error} err - 错误对象
152
+ * @param {Object} req - Express 请求对象
153
+ * @returns {Object} 错误响应对象
154
+ */
155
+ export function errorResponse(err, req) {
156
+ const errorInfo = parseDatabaseError(err);
157
+
158
+ console.error(`[Error Handler] ${errorInfo.msg} - ${err?.message}`, {
159
+ code: errorInfo.code,
160
+ path: req?.path,
161
+ method: req?.method,
162
+ sql: err?.sql,
163
+ sqlMessage: err?.sqlMessage,
164
+ stack: err?.stack,
165
+ });
166
+
167
+ return {
168
+ success: false,
169
+ msg: errorInfo.msg,
170
+ code: errorInfo.code,
171
+ data: process.env.NODE_ENV === "development"
172
+ ? {
173
+ message: err?.message,
174
+ sql: err?.sql,
175
+ sqlMessage: err?.sqlMessage,
176
+ stack: err?.stack,
177
+ }
178
+ : {},
179
+ };
180
+ }
package/xss-filter.js ADDED
@@ -0,0 +1,42 @@
1
+ import xss from 'xss';
2
+
3
+ /**
4
+ * XSS 过滤工具
5
+ * 提供跨站脚本攻击过滤功能
6
+ */
7
+
8
+ /**
9
+ * 过滤 XSS 攻击代码
10
+ * @param {*} data - 要过滤的数据,可以是字符串、数组或对象
11
+ * @returns {*} 过滤后的数据
12
+ * @description
13
+ * 递归过滤数据中的所有字符串值
14
+ * 使用 xss 库清除危险的 HTML 和 JavaScript 代码
15
+ * 支持字符串、数组和对象的递归处理
16
+ * @example
17
+ * const clean = filterXSS({
18
+ * name: '<script>alert(1)</script>',
19
+ * items: ['<img src=x onerror=alert(1)>']
20
+ * });
21
+ */
22
+ export function filterXSS(data) {
23
+ if (typeof data === 'string') {
24
+ return xss(data);
25
+ }
26
+
27
+ if (Array.isArray(data)) {
28
+ return data.map(item => filterXSS(item));
29
+ }
30
+
31
+ if (data && typeof data === 'object') {
32
+ const result = {};
33
+ for (const key in data) {
34
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
35
+ result[key] = filterXSS(data[key]);
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+
41
+ return data;
42
+ }