chanjs 2.0.16 → 2.0.18
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/config/code.js +6 -2
- package/core/service.js +36 -56
- package/extend/art-template.js +1 -1
- package/global/import.js +5 -6
- package/helper/data-parse.js +63 -46
- package/helper/db.js +16 -13
- package/helper/file.js +33 -33
- package/helper/html.js +29 -21
- package/helper/index.js +3 -8
- package/helper/ip.js +18 -17
- package/helper/jwt.js +15 -16
- package/helper/loader.js +22 -0
- package/helper/time.js +2 -2
- package/index.js +7 -4
- package/middleware/cookie.js +2 -2
- package/middleware/cors.js +3 -3
- package/middleware/header.js +1 -1
- package/middleware/index.js +21 -21
- package/middleware/setBody.js +4 -5
- package/middleware/template.js +4 -4
- package/middleware/validator.js +2 -2
- package/middleware/waf.js +133 -25
- package/package.json +1 -1
- package/utils/response.js +23 -5
package/config/code.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @description 状态码定义
|
|
3
|
+
*/
|
|
2
4
|
export const CODE = {
|
|
3
5
|
200: "操作成功", // 成功
|
|
4
6
|
201: "操作失败", // 通用失败
|
|
@@ -36,7 +38,9 @@ export const CODE = {
|
|
|
36
38
|
6009: "数据库连接已关闭,请重试", // 连接已关闭
|
|
37
39
|
};
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
/**
|
|
42
|
+
* @description 数据库错误码定义
|
|
43
|
+
*/
|
|
40
44
|
export const DB_ERROR = {
|
|
41
45
|
ECONNREFUSED: 6001,
|
|
42
46
|
ER_ACCESS_DENIED_ERROR: 6002,
|
package/core/service.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { success, fail, error } from "../utils/response.js";
|
|
2
2
|
import { CODE } from "../config/code.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 数据库服务类
|
|
6
|
+
*/
|
|
7
|
+
class Service {
|
|
8
|
+
/**
|
|
9
|
+
* 构造函数
|
|
10
|
+
* @param {Object} knex - Knex实例
|
|
11
|
+
* @param {string} model - 表名/模型名
|
|
12
|
+
*/
|
|
13
|
+
constructor(knex, model) {
|
|
14
|
+
if (!knex || !model) {
|
|
15
|
+
throw new Error("Service: knex instance and model name are required");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.knex = knex;
|
|
19
|
+
this.model = model;
|
|
20
|
+
this.pageSize = 100;
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
/**
|
|
6
24
|
* 查询表所有记录(慎用)
|
|
7
25
|
* @param {Object} query - 查询条件
|
|
@@ -18,7 +36,7 @@ const databaseMethods = {
|
|
|
18
36
|
} catch (err) {
|
|
19
37
|
return error(err);
|
|
20
38
|
}
|
|
21
|
-
}
|
|
39
|
+
}
|
|
22
40
|
|
|
23
41
|
/**
|
|
24
42
|
* 获取单个记录
|
|
@@ -36,7 +54,7 @@ const databaseMethods = {
|
|
|
36
54
|
} catch (err) {
|
|
37
55
|
return error(err);
|
|
38
56
|
}
|
|
39
|
-
}
|
|
57
|
+
}
|
|
40
58
|
|
|
41
59
|
/**
|
|
42
60
|
* 根据ID查询记录
|
|
@@ -51,13 +69,13 @@ const databaseMethods = {
|
|
|
51
69
|
if (field.length > 0) dataQuery = dataQuery.select(field);
|
|
52
70
|
if (len === 1) dataQuery = dataQuery.first();
|
|
53
71
|
else if (len > 1) dataQuery = dataQuery.limit(len);
|
|
54
|
-
|
|
72
|
+
|
|
55
73
|
const res = await dataQuery;
|
|
56
74
|
return success(res || (len === 1 ? {} : []));
|
|
57
75
|
} catch (err) {
|
|
58
76
|
return error(err);
|
|
59
77
|
}
|
|
60
|
-
}
|
|
78
|
+
}
|
|
61
79
|
|
|
62
80
|
/**
|
|
63
81
|
* 创建新记录
|
|
@@ -66,18 +84,15 @@ const databaseMethods = {
|
|
|
66
84
|
*/
|
|
67
85
|
async insert(data = {}) {
|
|
68
86
|
try {
|
|
69
|
-
console.log('data--->',data)
|
|
70
87
|
if (Object.keys(data).length === 0) {
|
|
71
88
|
return fail(CODE[2002], { code: 2002 });
|
|
72
89
|
}
|
|
73
|
-
|
|
74
|
-
console.log('this.model--->',this.model)
|
|
75
90
|
const result = await this.knex(this.model).insert(data);
|
|
76
91
|
return success(result?.length > 0 || !!result);
|
|
77
92
|
} catch (err) {
|
|
78
93
|
return error(err);
|
|
79
94
|
}
|
|
80
|
-
}
|
|
95
|
+
}
|
|
81
96
|
|
|
82
97
|
/**
|
|
83
98
|
* 插入多条记录
|
|
@@ -94,7 +109,7 @@ const databaseMethods = {
|
|
|
94
109
|
} catch (err) {
|
|
95
110
|
return error(err);
|
|
96
111
|
}
|
|
97
|
-
}
|
|
112
|
+
}
|
|
98
113
|
|
|
99
114
|
/**
|
|
100
115
|
* 根据指定条件删除记录
|
|
@@ -111,7 +126,7 @@ const databaseMethods = {
|
|
|
111
126
|
} catch (err) {
|
|
112
127
|
return error(err);
|
|
113
128
|
}
|
|
114
|
-
}
|
|
129
|
+
}
|
|
115
130
|
|
|
116
131
|
/**
|
|
117
132
|
* 根据指定条件更新记录
|
|
@@ -125,17 +140,12 @@ const databaseMethods = {
|
|
|
125
140
|
if (!query || !params || Object.keys(query).length === 0) {
|
|
126
141
|
return fail(CODE[2001], { code: 2001 });
|
|
127
142
|
}
|
|
128
|
-
console.log('query--->',query)
|
|
129
|
-
console.log('this.model--->',this.model)
|
|
130
|
-
|
|
131
|
-
console.log('params---->',params)
|
|
132
143
|
const result = await this.knex(this.model).where(query).update(params);
|
|
133
|
-
console.log('result--->',result)
|
|
134
144
|
return success(!!result);
|
|
135
145
|
} catch (err) {
|
|
136
146
|
return error(err);
|
|
137
147
|
}
|
|
138
|
-
}
|
|
148
|
+
}
|
|
139
149
|
|
|
140
150
|
/**
|
|
141
151
|
* 批量更新多条记录
|
|
@@ -146,7 +156,7 @@ const databaseMethods = {
|
|
|
146
156
|
if (!Array.isArray(updates) || updates.length === 0) {
|
|
147
157
|
return fail(CODE[2002], { code: 2002 });
|
|
148
158
|
}
|
|
149
|
-
|
|
159
|
+
|
|
150
160
|
const trx = await this.knex.transaction();
|
|
151
161
|
try {
|
|
152
162
|
for (const { query, params } of updates) {
|
|
@@ -162,7 +172,7 @@ const databaseMethods = {
|
|
|
162
172
|
await trx.rollback();
|
|
163
173
|
return error(err);
|
|
164
174
|
}
|
|
165
|
-
}
|
|
175
|
+
}
|
|
166
176
|
|
|
167
177
|
/**
|
|
168
178
|
* 分页查询
|
|
@@ -178,7 +188,7 @@ const databaseMethods = {
|
|
|
178
188
|
const offset = (current - 1) * pageSize;
|
|
179
189
|
let countQuery = this.knex(this.model).count("* as total");
|
|
180
190
|
let dataQuery = this.knex(this.model);
|
|
181
|
-
|
|
191
|
+
|
|
182
192
|
if (Object.keys(query).length > 0) {
|
|
183
193
|
Object.entries(query).forEach(([key, value]) => {
|
|
184
194
|
countQuery = countQuery.where(key, value);
|
|
@@ -190,7 +200,7 @@ const databaseMethods = {
|
|
|
190
200
|
|
|
191
201
|
const [totalResult, list] = await Promise.all([
|
|
192
202
|
countQuery.first(),
|
|
193
|
-
dataQuery.offset(offset).limit(pageSize)
|
|
203
|
+
dataQuery.offset(offset).limit(pageSize),
|
|
194
204
|
]);
|
|
195
205
|
|
|
196
206
|
const total = totalResult?.total || 0;
|
|
@@ -198,7 +208,7 @@ const databaseMethods = {
|
|
|
198
208
|
} catch (err) {
|
|
199
209
|
return error(err);
|
|
200
210
|
}
|
|
201
|
-
}
|
|
211
|
+
}
|
|
202
212
|
|
|
203
213
|
/**
|
|
204
214
|
* 计数查询
|
|
@@ -209,7 +219,7 @@ const databaseMethods = {
|
|
|
209
219
|
try {
|
|
210
220
|
let dataQuery = this.knex(this.model);
|
|
211
221
|
if (query.length > 0) {
|
|
212
|
-
query.forEach(condition => dataQuery = dataQuery.where(condition));
|
|
222
|
+
query.forEach((condition) => (dataQuery = dataQuery.where(condition)));
|
|
213
223
|
}
|
|
214
224
|
const result = await dataQuery.count("* as total").first();
|
|
215
225
|
return success(Number(result?.total) || 0);
|
|
@@ -217,36 +227,6 @@ const databaseMethods = {
|
|
|
217
227
|
return error(err);
|
|
218
228
|
}
|
|
219
229
|
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
// ====================== 工厂函数 ======================
|
|
223
|
-
/**
|
|
224
|
-
* 创建数据库服务实例
|
|
225
|
-
* @param {Object} knex - Knex实例
|
|
226
|
-
* @param {string} model - 表名/模型名
|
|
227
|
-
* @returns {Object} 数据库服务实例
|
|
228
|
-
*/
|
|
229
|
-
export default function Service(knex, model) {
|
|
230
|
-
if (!knex || !model) {
|
|
231
|
-
throw new Error('Service: knex instance and model name are required');
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 创建继承数据库方法的轻量对象
|
|
235
|
-
const service = Object.create(databaseMethods);
|
|
236
|
-
|
|
237
|
-
// 设置实例专属状态(不可写)
|
|
238
|
-
Object.defineProperties(service, {
|
|
239
|
-
knex: {
|
|
240
|
-
value: knex,
|
|
241
|
-
writable: false,
|
|
242
|
-
enumerable: true
|
|
243
|
-
},
|
|
244
|
-
model: {
|
|
245
|
-
value: model,
|
|
246
|
-
writable: false,
|
|
247
|
-
enumerable: true
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
return service;
|
|
252
230
|
}
|
|
231
|
+
|
|
232
|
+
export default Service;
|
package/extend/art-template.js
CHANGED
package/global/import.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import fs from "fs/promises";
|
|
3
2
|
import { pathToFileURL } from "url";
|
|
4
3
|
import { createRequire } from "module";
|
|
@@ -9,8 +8,8 @@ import { createRequire } from "module";
|
|
|
9
8
|
* @returns {Promise<object|null>} 模块对象或null
|
|
10
9
|
*/
|
|
11
10
|
const importFile = async (filepath) => {
|
|
12
|
-
if (!filepath || typeof filepath !==
|
|
13
|
-
console.error(
|
|
11
|
+
if (!filepath || typeof filepath !== "string") {
|
|
12
|
+
console.error("错误: 文件路径必须是有效的字符串");
|
|
14
13
|
return null;
|
|
15
14
|
}
|
|
16
15
|
|
|
@@ -20,9 +19,9 @@ const importFile = async (filepath) => {
|
|
|
20
19
|
const module = await import(fileUrl);
|
|
21
20
|
return module.default || module;
|
|
22
21
|
} catch (error) {
|
|
23
|
-
if (error.code ===
|
|
22
|
+
if (error.code === "ENOENT") {
|
|
24
23
|
console.error(`文件不存在: ${filepath}`);
|
|
25
|
-
} else if (error.code ===
|
|
24
|
+
} else if (error.code === "EACCES") {
|
|
26
25
|
console.error(`没有权限访问文件: ${filepath}`);
|
|
27
26
|
} else {
|
|
28
27
|
console.error(`导入文件时出错 [${filepath}]:`, error.message);
|
|
@@ -37,4 +36,4 @@ const importFile = async (filepath) => {
|
|
|
37
36
|
export const importjs = createRequire(import.meta.url);
|
|
38
37
|
|
|
39
38
|
global.requirejs = importjs;
|
|
40
|
-
global.importFile = importFile;
|
|
39
|
+
global.importFile = importFile;
|
package/helper/data-parse.js
CHANGED
|
@@ -5,53 +5,58 @@
|
|
|
5
5
|
* @param {string} valueField - 作为值的字段名
|
|
6
6
|
* @returns {Object} 转换后的对象
|
|
7
7
|
*/
|
|
8
|
-
export const arrToObj = (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const value = item[valueField];
|
|
18
|
-
if (key !== undefined && value !== undefined) {
|
|
19
|
-
result[key] = value;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return result;
|
|
23
|
-
}, {});
|
|
24
|
-
};
|
|
25
|
-
|
|
8
|
+
export const arrToObj = (
|
|
9
|
+
arr,
|
|
10
|
+
keyField = "config_key",
|
|
11
|
+
valueField = "config_value"
|
|
12
|
+
) => {
|
|
13
|
+
// 输入验证
|
|
14
|
+
if (!Array.isArray(arr)) {
|
|
15
|
+
throw new Error("arrToObj 期望接收数组作为第一个参数");
|
|
16
|
+
}
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export function parseJsonFields(obj) {
|
|
33
|
-
const result = {};
|
|
34
|
-
for (const key in obj) {
|
|
35
|
-
if (!obj.hasOwnProperty(key)) continue;
|
|
36
|
-
const value = obj[key];
|
|
37
|
-
// 如果是字符串,并且看起来像 JSON(以 { 或 [ 开头)
|
|
38
|
-
if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
|
|
39
|
-
try {
|
|
40
|
-
result[key] = JSON.parse(value);
|
|
41
|
-
} catch (e) {
|
|
42
|
-
console.warn(`JSON parse failed for field: ${key}`, e);
|
|
43
|
-
result[key] = value; // 保留原始值
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
18
|
+
return arr.reduce((result, item) => {
|
|
19
|
+
if (item && typeof item === "object") {
|
|
20
|
+
const key = item[keyField];
|
|
21
|
+
const value = item[valueField];
|
|
22
|
+
if (key !== undefined && value !== undefined) {
|
|
46
23
|
result[key] = value;
|
|
47
24
|
}
|
|
48
25
|
}
|
|
49
|
-
|
|
50
26
|
return result;
|
|
27
|
+
}, {});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @description 将字符串中的 JSON 字符串转换为对象
|
|
32
|
+
* @param {*} obj - 包含 JSON 字符串的对象
|
|
33
|
+
* @returns
|
|
34
|
+
*/
|
|
35
|
+
export function parseJsonFields(obj) {
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const key in obj) {
|
|
38
|
+
if (!obj.hasOwnProperty(key)) continue;
|
|
39
|
+
const value = obj[key];
|
|
40
|
+
// 如果是字符串,并且看起来像 JSON(以 { 或 [ 开头)
|
|
41
|
+
if (
|
|
42
|
+
typeof value === "string" &&
|
|
43
|
+
(value.startsWith("{") || value.startsWith("["))
|
|
44
|
+
) {
|
|
45
|
+
try {
|
|
46
|
+
result[key] = JSON.parse(value);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.warn(`JSON parse failed for field: ${key}`, e);
|
|
49
|
+
result[key] = value; // 保留原始值
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
result[key] = value;
|
|
53
|
+
}
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
/**
|
|
55
60
|
* @param {Array} arr - 原始数据数组
|
|
56
61
|
* @param {number|string} [pid=0] - 根节点父ID
|
|
57
62
|
* @param {string} [idKey='id'] - ID字段名
|
|
@@ -59,27 +64,39 @@ export const arrToObj = (arr, keyField = 'config_key', valueField = 'config_valu
|
|
|
59
64
|
* @param {string} [childrenKey='children'] - 子节点字段名
|
|
60
65
|
* @returns {Array} 树形结构数组
|
|
61
66
|
*/
|
|
62
|
-
export function buildTree(
|
|
67
|
+
export function buildTree(
|
|
68
|
+
arr,
|
|
69
|
+
pid = 0,
|
|
70
|
+
idKey = "id",
|
|
71
|
+
pidKey = "pid",
|
|
72
|
+
childrenKey = "children"
|
|
73
|
+
) {
|
|
63
74
|
// 基础参数校验
|
|
64
75
|
if (!Array.isArray(arr)) return [];
|
|
65
|
-
|
|
76
|
+
|
|
66
77
|
const tree = [];
|
|
67
|
-
|
|
78
|
+
|
|
68
79
|
for (let i = 0; i < arr.length; i++) {
|
|
69
80
|
const item = arr[i];
|
|
70
81
|
// 找到当前层级的节点
|
|
71
82
|
if (item[pidKey] === pid) {
|
|
72
83
|
// 递归查找子节点(通过slice创建子数组,避免重复遍历已处理项)
|
|
73
|
-
const children = buildTree(
|
|
74
|
-
|
|
84
|
+
const children = buildTree(
|
|
85
|
+
arr.slice(i + 1),
|
|
86
|
+
item[idKey],
|
|
87
|
+
idKey,
|
|
88
|
+
pidKey,
|
|
89
|
+
childrenKey
|
|
90
|
+
);
|
|
91
|
+
|
|
75
92
|
// 有子节点则添加,避免空数组
|
|
76
93
|
if (children.length) {
|
|
77
94
|
item[childrenKey] = children;
|
|
78
95
|
}
|
|
79
|
-
|
|
96
|
+
|
|
80
97
|
tree.push(item);
|
|
81
98
|
}
|
|
82
99
|
}
|
|
83
|
-
|
|
100
|
+
|
|
84
101
|
return tree;
|
|
85
102
|
}
|
package/helper/db.js
CHANGED
|
@@ -19,7 +19,7 @@ const getDefaultErrorMessage = (error) => {
|
|
|
19
19
|
}
|
|
20
20
|
return "数据库发生未知错误,请稍后重试。";
|
|
21
21
|
};
|
|
22
|
-
export const db =({
|
|
22
|
+
export const db = ({
|
|
23
23
|
client = "mysql2",
|
|
24
24
|
host = "127.0.0.1",
|
|
25
25
|
user = "root",
|
|
@@ -30,8 +30,8 @@ export const db =({
|
|
|
30
30
|
charset = "utf8mb4",
|
|
31
31
|
min = 0,
|
|
32
32
|
max = 2,
|
|
33
|
-
filename=
|
|
34
|
-
}) =>{
|
|
33
|
+
filename = "",
|
|
34
|
+
}) => {
|
|
35
35
|
let config = {
|
|
36
36
|
client,
|
|
37
37
|
connection: {
|
|
@@ -40,20 +40,23 @@ export const db =({
|
|
|
40
40
|
user,
|
|
41
41
|
password,
|
|
42
42
|
database,
|
|
43
|
-
charset
|
|
43
|
+
charset,
|
|
44
44
|
},
|
|
45
45
|
debug,
|
|
46
46
|
pool: {
|
|
47
47
|
//默认为{min: 2, max: 10},连接池配置
|
|
48
48
|
min,
|
|
49
49
|
max,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
conn.on(
|
|
53
|
-
console.error(
|
|
50
|
+
// 添加连接池错误处理
|
|
51
|
+
afterCreate: (conn, done) => {
|
|
52
|
+
conn.on("error", (error) => {
|
|
53
|
+
console.error(
|
|
54
|
+
`[连接池错误] 连接出错: ${getDefaultErrorMessage(error)}`,
|
|
55
|
+
error
|
|
56
|
+
);
|
|
54
57
|
});
|
|
55
58
|
done(null, conn);
|
|
56
|
-
}
|
|
59
|
+
},
|
|
57
60
|
},
|
|
58
61
|
log: {
|
|
59
62
|
warn(message) {
|
|
@@ -79,10 +82,10 @@ export const db =({
|
|
|
79
82
|
},
|
|
80
83
|
},
|
|
81
84
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
|
|
86
|
+
if (client === "sqlite3" || client === "better-sqlite3") {
|
|
87
|
+
config.connection.filename = filename;
|
|
88
|
+
}
|
|
86
89
|
|
|
87
90
|
return knex(config);
|
|
88
91
|
};
|
package/helper/file.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import { accessSync, unlinkSync, existsSync, mkdirSync } from
|
|
3
|
-
import path from
|
|
4
|
-
import { fileURLToPath } from
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { accessSync, unlinkSync, existsSync, mkdirSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
5
|
|
|
6
|
-
export const dirname = (url) =>{
|
|
6
|
+
export const dirname = (url) => {
|
|
7
7
|
const __filename = fileURLToPath(url);
|
|
8
8
|
return path.dirname(__filename);
|
|
9
|
-
}
|
|
10
|
-
|
|
9
|
+
};
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* 获取指定路径的文件树结构
|
|
14
|
-
*
|
|
13
|
+
*
|
|
15
14
|
* @param {string} basePath - 要扫描的基础路径
|
|
16
15
|
* @param {boolean} [deep=true] - 是否深度遍历子目录
|
|
17
16
|
* @returns {Promise<Array<Object>>} 文件树数组,每个元素包含文件/目录信息
|
|
@@ -43,13 +42,15 @@ export const getFileTree = async (basePath, deep = true) => {
|
|
|
43
42
|
name: item,
|
|
44
43
|
path: itemPath,
|
|
45
44
|
relativePath: path.relative(ROOT_PATH, itemPath),
|
|
46
|
-
type: itemStats.isDirectory() ?
|
|
45
|
+
type: itemStats.isDirectory() ? "directory" : "file",
|
|
47
46
|
size: itemStats.size,
|
|
48
47
|
modified: itemStats.mtime,
|
|
49
|
-
depth:
|
|
48
|
+
depth:
|
|
49
|
+
itemPath.split(path.sep).length -
|
|
50
|
+
path.resolve(ROOT_PATH).split(path.sep).length,
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
if (treeItem.type ===
|
|
53
|
+
if (treeItem.type === "directory" && deep) {
|
|
53
54
|
treeItem.children = await getFileTree(itemPath, deep);
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -59,9 +60,9 @@ export const getFileTree = async (basePath, deep = true) => {
|
|
|
59
60
|
// 排序:文件夹在前,文件在后,名称按字母顺序排列
|
|
60
61
|
return tree.sort((a, b) => {
|
|
61
62
|
if (a.type === b.type) {
|
|
62
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity:
|
|
63
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
63
64
|
}
|
|
64
|
-
return a.type ===
|
|
65
|
+
return a.type === "directory" ? -1 : 1;
|
|
65
66
|
});
|
|
66
67
|
} catch (error) {
|
|
67
68
|
console.error(`获取文件树失败: ${basePath}`, error);
|
|
@@ -71,7 +72,7 @@ export const getFileTree = async (basePath, deep = true) => {
|
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
74
|
* 读取文件内容(UTF-8编码)
|
|
74
|
-
*
|
|
75
|
+
*
|
|
75
76
|
* @param {string} filePath - 要读取的文件路径
|
|
76
77
|
* @returns {Promise<string>} 文件内容字符串
|
|
77
78
|
* @throws {Error} 当文件不存在、无法读取或不是文件时抛出错误
|
|
@@ -79,17 +80,17 @@ export const getFileTree = async (basePath, deep = true) => {
|
|
|
79
80
|
export const readFileContent = async (filePath) => {
|
|
80
81
|
try {
|
|
81
82
|
// 先检查路径是否安全
|
|
82
|
-
if (!isPathSafe(filePath,APP_PATH)
|
|
83
|
+
if (!isPathSafe(filePath, APP_PATH) && !isPathSafe(filePath, ROOT_PATH)) {
|
|
83
84
|
throw new Error(`路径不安全: ${filePath}`);
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
// 检查是否为文件
|
|
87
88
|
const stats = await fs.stat(filePath);
|
|
88
89
|
if (!stats.isFile()) {
|
|
89
90
|
throw new Error(`不是文件: ${filePath}`);
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
-
return await fs.readFile(filePath,
|
|
92
|
+
|
|
93
|
+
return await fs.readFile(filePath, "utf8");
|
|
93
94
|
} catch (error) {
|
|
94
95
|
console.error(`读取文件失败: ${filePath}`, error);
|
|
95
96
|
throw error;
|
|
@@ -98,7 +99,7 @@ export const readFileContent = async (filePath) => {
|
|
|
98
99
|
|
|
99
100
|
/**
|
|
100
101
|
* 保存内容到文件(UTF-8编码)
|
|
101
|
-
*
|
|
102
|
+
*
|
|
102
103
|
* @param {string} filePath - 要保存的文件路径
|
|
103
104
|
* @param {string} content - 要写入的内容
|
|
104
105
|
* @returns {Promise<void>} 无返回值
|
|
@@ -107,16 +108,16 @@ export const readFileContent = async (filePath) => {
|
|
|
107
108
|
export const saveFileContent = async (filePath, content) => {
|
|
108
109
|
try {
|
|
109
110
|
// 先检查路径是否安全
|
|
110
|
-
if (!isPathSafe(filePath,APP_PATH)
|
|
111
|
+
if (!isPathSafe(filePath, APP_PATH) && !isPathSafe(filePath, ROOT_PATH)) {
|
|
111
112
|
throw new Error(`路径不安全: ${filePath}`);
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
|
|
114
115
|
// 确保目录存在
|
|
115
116
|
const dirname = path.dirname(filePath);
|
|
116
117
|
await fs.mkdir(dirname, { recursive: true });
|
|
117
|
-
|
|
118
|
+
|
|
118
119
|
// 写入文件
|
|
119
|
-
await fs.writeFile(filePath, content,
|
|
120
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
120
121
|
} catch (error) {
|
|
121
122
|
console.error(`保存文件失败: ${filePath}`, error);
|
|
122
123
|
throw error;
|
|
@@ -125,7 +126,7 @@ export const saveFileContent = async (filePath, content) => {
|
|
|
125
126
|
|
|
126
127
|
/**
|
|
127
128
|
* 验证路径是否在基础路径范围内(防止路径遍历攻击)
|
|
128
|
-
*
|
|
129
|
+
*
|
|
129
130
|
* @param {string} requestedPath - 要验证的路径
|
|
130
131
|
* @param {string} basePath - 基础路径,requestedPath必须在此路径下
|
|
131
132
|
* @returns {boolean} 如果路径安全则返回true,否则返回false
|
|
@@ -139,7 +140,7 @@ export const isPathSafe = (requestedPath, basePath) => {
|
|
|
139
140
|
|
|
140
141
|
/**
|
|
141
142
|
* 删除指定路径的图片文件
|
|
142
|
-
*
|
|
143
|
+
*
|
|
143
144
|
* @param {string} link - 图片文件的路径
|
|
144
145
|
* @returns {boolean} 成功删除返回true,否则返回false
|
|
145
146
|
* @description 同步操作,会检查文件是否存在后再删除
|
|
@@ -151,10 +152,10 @@ export function delImg(link) {
|
|
|
151
152
|
console.error(`路径不安全: ${link}`);
|
|
152
153
|
return false;
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
+
|
|
155
156
|
// 检查文件是否存在
|
|
156
157
|
accessSync(link);
|
|
157
|
-
|
|
158
|
+
|
|
158
159
|
// 删除文件
|
|
159
160
|
unlinkSync(link);
|
|
160
161
|
return true;
|
|
@@ -166,7 +167,7 @@ export function delImg(link) {
|
|
|
166
167
|
|
|
167
168
|
/**
|
|
168
169
|
* 递归创建目录(同步操作)
|
|
169
|
-
*
|
|
170
|
+
*
|
|
170
171
|
* @param {string} dirname - 要创建的目录路径
|
|
171
172
|
* @returns {boolean} 成功创建或目录已存在返回true,否则返回false
|
|
172
173
|
* @description 类似于mkdir -p命令,会创建所有不存在的父目录
|
|
@@ -178,18 +179,18 @@ export function mkdirsSync(dirname) {
|
|
|
178
179
|
console.error(`路径不安全: ${dirname}`);
|
|
179
180
|
return false;
|
|
180
181
|
}
|
|
181
|
-
|
|
182
|
+
|
|
182
183
|
if (existsSync(dirname)) {
|
|
183
184
|
return true;
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
+
|
|
186
187
|
// 递归创建父目录
|
|
187
188
|
const parentDir = path.dirname(dirname);
|
|
188
189
|
if (mkdirsSync(parentDir)) {
|
|
189
190
|
mkdirSync(dirname);
|
|
190
191
|
return true;
|
|
191
192
|
}
|
|
192
|
-
|
|
193
|
+
|
|
193
194
|
return false;
|
|
194
195
|
} catch (err) {
|
|
195
196
|
console.error(`创建目录失败: ${dirname}`, err);
|
|
@@ -197,7 +198,6 @@ export function mkdirsSync(dirname) {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
|
|
201
201
|
export default {
|
|
202
202
|
mkdirsSync,
|
|
203
203
|
delImg,
|
|
@@ -205,4 +205,4 @@ export default {
|
|
|
205
205
|
saveFileContent,
|
|
206
206
|
readFileContent,
|
|
207
207
|
getFileTree,
|
|
208
|
-
}
|
|
208
|
+
};
|
package/helper/html.js
CHANGED
|
@@ -5,17 +5,21 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const htmlEncode = (str) => {
|
|
7
7
|
// 非字符串直接返回,避免报错
|
|
8
|
-
if (typeof str !==
|
|
9
|
-
|
|
8
|
+
if (typeof str !== "string") return str;
|
|
9
|
+
|
|
10
10
|
// 一次性替换所有特殊字符,减少函数调用
|
|
11
|
-
return str.replace(
|
|
12
|
-
'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
return str.replace(
|
|
12
|
+
/[&<>"' ]/g,
|
|
13
|
+
(match) =>
|
|
14
|
+
({
|
|
15
|
+
"&": "&",
|
|
16
|
+
"<": "<",
|
|
17
|
+
">": ">",
|
|
18
|
+
'"': """,
|
|
19
|
+
"'": "'",
|
|
20
|
+
" ": " ",
|
|
21
|
+
}[match])
|
|
22
|
+
);
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -25,15 +29,19 @@ export const htmlEncode = (str) => {
|
|
|
25
29
|
*/
|
|
26
30
|
export const htmlDecode = (str) => {
|
|
27
31
|
// 非字符串直接返回,避免报错
|
|
28
|
-
if (typeof str !==
|
|
29
|
-
|
|
32
|
+
if (typeof str !== "string") return str;
|
|
33
|
+
|
|
30
34
|
// 一次性替换所有常见实体,减少函数调用
|
|
31
|
-
return str.replace(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
return str.replace(
|
|
36
|
+
/&|<|>| |'|"/g,
|
|
37
|
+
(match) =>
|
|
38
|
+
({
|
|
39
|
+
"&": "&",
|
|
40
|
+
"<": "<",
|
|
41
|
+
">": ">",
|
|
42
|
+
" ": " ",
|
|
43
|
+
"'": "'",
|
|
44
|
+
""": '"',
|
|
45
|
+
}[match])
|
|
46
|
+
);
|
|
47
|
+
};
|
package/helper/index.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
import { loaderSort, loadConfig } from "./loader.js";
|
|
1
3
|
|
|
2
|
-
|
|
3
|
-
import { loaderSort, loadConfig } from './loader.js'
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
db,
|
|
7
|
-
loaderSort,
|
|
8
|
-
loadConfig,
|
|
9
|
-
}
|
|
4
|
+
export { db, loaderSort, loadConfig };
|
package/helper/ip.js
CHANGED
|
@@ -3,34 +3,35 @@
|
|
|
3
3
|
* @param {Object} req - Express请求对象
|
|
4
4
|
* @returns {string} 格式化后的IP地址,默认返回空字符串
|
|
5
5
|
*/
|
|
6
|
-
export const getIp = (req) =>{
|
|
7
|
-
|
|
6
|
+
export const getIp = (req) => {
|
|
8
7
|
// 优先级从高到低获取可能的IP来源
|
|
9
8
|
const ipSources = [
|
|
10
|
-
req.headers[
|
|
11
|
-
req.headers[
|
|
12
|
-
req.ip,
|
|
13
|
-
req.connection?.remoteAddress,
|
|
14
|
-
req.socket?.remoteAddress,
|
|
15
|
-
req.connection?.socket?.remoteAddress
|
|
9
|
+
req.headers["x-forwarded-for"], // 代理场景下的真实IP(可能多个,逗号分隔)
|
|
10
|
+
req.headers["x-real-ip"], // 部分代理服务器使用
|
|
11
|
+
req.ip, // Express内置的IP获取(已处理代理)
|
|
12
|
+
req.connection?.remoteAddress, // 底层连接的远程地址
|
|
13
|
+
req.socket?.remoteAddress, // 套接字的远程地址
|
|
14
|
+
req.connection?.socket?.remoteAddress, // 连接套接字的远程地址
|
|
16
15
|
];
|
|
17
16
|
|
|
18
17
|
// 从来源中找到第一个有效IP字符串
|
|
19
|
-
let ip = ipSources.find(
|
|
20
|
-
|
|
18
|
+
let ip = ipSources.find(
|
|
19
|
+
(source) => typeof source === "string" && source.trim() !== ""
|
|
20
|
+
);
|
|
21
|
+
|
|
21
22
|
// 若未找到有效IP,直接返回空
|
|
22
|
-
if (!ip) return
|
|
23
|
+
if (!ip) return "";
|
|
23
24
|
|
|
24
25
|
// 处理多IP情况(取第一个)
|
|
25
|
-
ip = ip.split(
|
|
26
|
+
ip = ip.split(",").shift().trim();
|
|
26
27
|
|
|
27
28
|
// IPv6转IPv4处理
|
|
28
|
-
if (ip ===
|
|
29
|
-
return
|
|
29
|
+
if (ip === "::1") {
|
|
30
|
+
return "127.0.0.1"; // 本地环回地址
|
|
30
31
|
}
|
|
31
|
-
if (ip.startsWith(
|
|
32
|
-
return ip.slice(7);
|
|
32
|
+
if (ip.startsWith("::ffff:")) {
|
|
33
|
+
return ip.slice(7); // 去除IPv6映射的IPv4前缀
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
return ip;
|
|
36
|
-
}
|
|
37
|
+
};
|
package/helper/jwt.js
CHANGED
|
@@ -1,41 +1,40 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
4
|
+
*
|
|
6
5
|
* @param {Object} data - 要存储在令牌中的数据(不建议存放敏感信息)
|
|
7
6
|
* @param {string} secretKey - 用于签名的密钥
|
|
8
7
|
* @param {string} time - 令牌过期时间,如 '1h'、'7d' 或秒数
|
|
9
8
|
* @returns {string} 生成的JWT令牌
|
|
10
9
|
*/
|
|
11
|
-
export function setToken(data={}, secretKey=
|
|
10
|
+
export function setToken(data = {}, secretKey = "chancms", time = "7d") {
|
|
12
11
|
try {
|
|
13
12
|
return jwt.sign(data, secretKey, {
|
|
14
13
|
expiresIn: time,
|
|
15
|
-
algorithm:
|
|
14
|
+
algorithm: "HS256",
|
|
16
15
|
});
|
|
17
16
|
} catch (error) {
|
|
18
|
-
console.error(
|
|
17
|
+
console.error("令牌生成失败:", error.message);
|
|
19
18
|
throw new Error(`生成令牌失败: ${error.message}`);
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
// 获取token
|
|
24
|
-
export async function getToken(token, secretKey=
|
|
23
|
+
export async function getToken(token, secretKey = "chancms") {
|
|
25
24
|
return new Promise((resolve, reject) => {
|
|
26
|
-
jwt.verify(token, secretKey,(err, decode) => {
|
|
25
|
+
jwt.verify(token, secretKey, (err, decode) => {
|
|
27
26
|
if (err) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
let errorMessage = "令牌验证失败";
|
|
28
|
+
if (err.name === "TokenExpiredError") {
|
|
29
|
+
errorMessage = "令牌已过期";
|
|
30
|
+
} else if (err.name === "JsonWebTokenError") {
|
|
31
|
+
errorMessage = "无效的令牌";
|
|
32
|
+
}
|
|
33
|
+
console.error(errorMessage, "令牌异常信息:", err.message);
|
|
34
|
+
return reject(new Error(errorMessage));
|
|
36
35
|
} else {
|
|
37
36
|
resolve(decode);
|
|
38
37
|
}
|
|
39
38
|
});
|
|
40
39
|
});
|
|
41
|
-
}
|
|
40
|
+
}
|
package/helper/loader.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @description 实例化一个类,并将该类的所有方法绑定到一个新的对象上。
|
|
7
|
+
* @param {Function} className - 需要实例化的类。
|
|
8
|
+
*@returns {Object} 包含绑定方法的对象。
|
|
9
|
+
*/
|
|
10
|
+
export const bindClass = function (className) {
|
|
11
|
+
let obj = {};
|
|
12
|
+
const cls = new className();
|
|
13
|
+
Object.getOwnPropertyNames(cls.constructor.prototype).forEach(
|
|
14
|
+
(methodName) => {
|
|
15
|
+
if (
|
|
16
|
+
methodName !== "constructor" &&
|
|
17
|
+
typeof cls[methodName] === "function"
|
|
18
|
+
) {
|
|
19
|
+
obj[methodName] = cls[methodName].bind(cls);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
return obj;
|
|
24
|
+
};
|
|
3
25
|
|
|
4
26
|
/**
|
|
5
27
|
*
|
package/helper/time.js
CHANGED
package/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
waf,
|
|
18
18
|
} from "./middleware/index.js";
|
|
19
19
|
import { db, loaderSort, loadConfig } from "./helper/index.js";
|
|
20
|
-
import {dirname} from "./helper/file.js";
|
|
20
|
+
import { dirname } from "./helper/file.js";
|
|
21
21
|
|
|
22
22
|
class Chan {
|
|
23
23
|
//版本号
|
|
@@ -30,6 +30,7 @@ class Chan {
|
|
|
30
30
|
static extend = {}; //组件扩展
|
|
31
31
|
static middleware = {}; //中间件
|
|
32
32
|
|
|
33
|
+
|
|
33
34
|
constructor() {
|
|
34
35
|
this.app = express();
|
|
35
36
|
this.router = express.Router();
|
|
@@ -65,8 +66,8 @@ class Chan {
|
|
|
65
66
|
logger,
|
|
66
67
|
cors,
|
|
67
68
|
} = Chan.config;
|
|
68
|
-
|
|
69
|
-
this.app.set(
|
|
69
|
+
|
|
70
|
+
this.app.set("trust proxy", true);
|
|
70
71
|
log(this.app, logger);
|
|
71
72
|
setFavicon(this.app);
|
|
72
73
|
setCookie(this.app, cookieKey);
|
|
@@ -134,7 +135,9 @@ class Chan {
|
|
|
134
135
|
|
|
135
136
|
async loadFn(_path, key) {
|
|
136
137
|
if (fs.existsSync(_path)) {
|
|
137
|
-
const files = fs
|
|
138
|
+
const files = fs
|
|
139
|
+
.readdirSync(_path)
|
|
140
|
+
.filter((file) => file.endsWith(".js"));
|
|
138
141
|
for (const file of files) {
|
|
139
142
|
const filePath = path.join(_path, file);
|
|
140
143
|
let helperModule = await importFile(filePath);
|
package/middleware/cookie.js
CHANGED
package/middleware/cors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import cors from "cors";
|
|
2
|
-
export const Cors = (app,_cors) => {
|
|
3
|
-
|
|
4
|
-
};
|
|
2
|
+
export const Cors = (app, _cors) => {
|
|
3
|
+
app.use(cors(_cors));
|
|
4
|
+
};
|
package/middleware/header.js
CHANGED
package/middleware/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import {log} from "./log.js";
|
|
2
|
-
import {setCookie} from "./cookie.js";
|
|
3
|
-
import {setFavicon} from "./favicon.js";
|
|
4
|
-
import {setBody} from "./setBody.js";
|
|
5
|
-
import {setStatic} from "./static.js";
|
|
6
|
-
import {setHeader} from "./header.js";
|
|
7
|
-
import {setTemplate} from "./template.js";
|
|
8
|
-
import {validator} from "./validator.js";
|
|
9
|
-
import {Cors} from "./cors.js";
|
|
10
|
-
import {waf} from "./waf.js";
|
|
1
|
+
import { log } from "./log.js";
|
|
2
|
+
import { setCookie } from "./cookie.js";
|
|
3
|
+
import { setFavicon } from "./favicon.js";
|
|
4
|
+
import { setBody } from "./setBody.js";
|
|
5
|
+
import { setStatic } from "./static.js";
|
|
6
|
+
import { setHeader } from "./header.js";
|
|
7
|
+
import { setTemplate } from "./template.js";
|
|
8
|
+
import { validator } from "./validator.js";
|
|
9
|
+
import { Cors } from "./cors.js";
|
|
10
|
+
import { waf } from "./waf.js";
|
|
11
11
|
|
|
12
12
|
export {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
13
|
+
log,
|
|
14
|
+
setCookie,
|
|
15
|
+
setFavicon,
|
|
16
|
+
setBody,
|
|
17
|
+
setStatic,
|
|
18
|
+
setHeader,
|
|
19
|
+
setTemplate,
|
|
20
|
+
Cors,
|
|
21
|
+
validator,
|
|
22
|
+
waf,
|
|
23
|
+
};
|
package/middleware/setBody.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
-
|
|
3
2
|
let setBody = function (app, JSON_LIMIT) {
|
|
3
|
+
// 1. 优先解析 XML 数据(必须在 json/urlencoded 之前)
|
|
4
|
+
app.use(express.raw({ type: "application/xml", limit: JSON_LIMIT }));
|
|
5
|
+
// 2. 再解析 JSON 和表单数据
|
|
4
6
|
app.use(express.json({ limit: JSON_LIMIT }));
|
|
5
7
|
app.use(express.urlencoded({ extended: false }));
|
|
6
|
-
app.use(express.raw({ type: 'application/xml', limit: JSON_LIMIT }));
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
export {
|
|
10
|
-
setBody
|
|
11
|
-
}
|
|
10
|
+
export { setBody };
|
package/middleware/template.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "../extend/art-template.js";
|
|
2
2
|
export let setTemplate = (app, config) => {
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
const all = [...views,
|
|
3
|
+
const { views, env } = config;
|
|
4
|
+
//合并插件中的view
|
|
5
|
+
const all = [...views, "app/modules/web/view"];
|
|
6
6
|
app.set("view options", {
|
|
7
7
|
debug: env === "dev",
|
|
8
8
|
cache: env === "prd",
|
package/middleware/validator.js
CHANGED
|
@@ -4,12 +4,12 @@ export const validator = (schemas) => (req, res, next) => {
|
|
|
4
4
|
headers: schemas.headers,
|
|
5
5
|
params: schemas.params,
|
|
6
6
|
query: schemas.query,
|
|
7
|
-
body: schemas.body
|
|
7
|
+
body: schemas.body,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
for (const [key, schema] of Object.entries(sections)) {
|
|
11
11
|
if (!schema) continue;
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
const result = schema.safeParse(req[key]);
|
|
14
14
|
if (!result.success) {
|
|
15
15
|
// 返回首个错误信息
|
package/middleware/waf.js
CHANGED
|
@@ -6,47 +6,154 @@ const keywords = {
|
|
|
6
6
|
// 需要全词匹配的关键词(避免短词误报)
|
|
7
7
|
wholeWord: [
|
|
8
8
|
// 系统命令/工具(容易产生短词误报)
|
|
9
|
-
"opt",
|
|
10
|
-
"
|
|
9
|
+
"opt",
|
|
10
|
+
"cmd",
|
|
11
|
+
"rm",
|
|
12
|
+
"mdc",
|
|
13
|
+
"netcat",
|
|
14
|
+
"nc",
|
|
15
|
+
"mdb",
|
|
16
|
+
"bin",
|
|
17
|
+
"mk",
|
|
18
|
+
"sys",
|
|
19
|
+
"sh",
|
|
20
|
+
"chomd",
|
|
21
|
+
"php-cgi",
|
|
22
|
+
"process",
|
|
23
|
+
"require",
|
|
24
|
+
"child_process",
|
|
25
|
+
"execSync",
|
|
26
|
+
"mainModule",
|
|
11
27
|
],
|
|
12
|
-
|
|
28
|
+
|
|
13
29
|
// 普通关键词(包含特殊字符或较长关键词)
|
|
14
30
|
normal: [
|
|
15
31
|
// 文件扩展名 & 敏感文件
|
|
16
|
-
".php",
|
|
17
|
-
".
|
|
18
|
-
".
|
|
19
|
-
".
|
|
20
|
-
".
|
|
21
|
-
".
|
|
32
|
+
".php",
|
|
33
|
+
".asp",
|
|
34
|
+
".aspx",
|
|
35
|
+
".jsp",
|
|
36
|
+
".jspx",
|
|
37
|
+
".do",
|
|
38
|
+
".action",
|
|
39
|
+
".cgi",
|
|
40
|
+
".py",
|
|
41
|
+
".pl",
|
|
42
|
+
".md",
|
|
43
|
+
".log",
|
|
44
|
+
".conf",
|
|
45
|
+
".config",
|
|
46
|
+
".env",
|
|
47
|
+
".jsa",
|
|
48
|
+
".go",
|
|
49
|
+
".jhtml",
|
|
50
|
+
".shtml",
|
|
51
|
+
".cfm",
|
|
52
|
+
".svn",
|
|
53
|
+
".keys",
|
|
54
|
+
".hidden",
|
|
55
|
+
".bod",
|
|
56
|
+
".ll",
|
|
57
|
+
".backup",
|
|
58
|
+
".json",
|
|
59
|
+
".xml",
|
|
60
|
+
".bak",
|
|
61
|
+
".aws",
|
|
62
|
+
".database",
|
|
63
|
+
".cookie",
|
|
64
|
+
".rsp",
|
|
65
|
+
".old",
|
|
66
|
+
".tf",
|
|
67
|
+
".sql",
|
|
68
|
+
".vscode",
|
|
69
|
+
".docker",
|
|
70
|
+
".map",
|
|
71
|
+
".save",
|
|
72
|
+
".gz",
|
|
73
|
+
".yml",
|
|
74
|
+
".tar",
|
|
75
|
+
".sh",
|
|
76
|
+
".idea",
|
|
77
|
+
".s3",
|
|
22
78
|
|
|
23
79
|
// 敏感目录
|
|
24
|
-
"/administrator",
|
|
80
|
+
"/administrator",
|
|
81
|
+
"/wp-admin",
|
|
25
82
|
|
|
26
83
|
// 高危路径/应用
|
|
27
|
-
"phpMyAdmin",
|
|
28
|
-
"
|
|
84
|
+
"phpMyAdmin",
|
|
85
|
+
"setup",
|
|
86
|
+
"wp-",
|
|
87
|
+
"cgi-bin",
|
|
88
|
+
"xampp",
|
|
89
|
+
"staging",
|
|
90
|
+
"internal",
|
|
91
|
+
"debug",
|
|
92
|
+
"metadata",
|
|
93
|
+
"secret",
|
|
94
|
+
"smtp",
|
|
95
|
+
"redirect",
|
|
96
|
+
"configs",
|
|
29
97
|
|
|
30
98
|
// SQL 注入
|
|
31
|
-
"sleep(",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
99
|
+
"sleep(",
|
|
100
|
+
"benchmark(",
|
|
101
|
+
"concat(",
|
|
102
|
+
"extractvalue(",
|
|
103
|
+
"updatexml(",
|
|
104
|
+
"version(",
|
|
105
|
+
"union select",
|
|
106
|
+
"union all",
|
|
107
|
+
"select @@",
|
|
108
|
+
"drop",
|
|
109
|
+
"alter",
|
|
110
|
+
"truncate",
|
|
111
|
+
"exec",
|
|
112
|
+
"(select",
|
|
113
|
+
"information_schema",
|
|
114
|
+
"load_file(",
|
|
115
|
+
"into outfile",
|
|
116
|
+
"into dumpfile",
|
|
35
117
|
|
|
36
118
|
// 命令注入
|
|
37
|
-
"cmd=",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
119
|
+
"cmd=",
|
|
120
|
+
"system(",
|
|
121
|
+
"exec(",
|
|
122
|
+
"shell_exec(",
|
|
123
|
+
"passthru(",
|
|
124
|
+
"base64_decode",
|
|
125
|
+
"eval(",
|
|
126
|
+
"assert(",
|
|
127
|
+
"preg_replace",
|
|
128
|
+
"bash -i",
|
|
129
|
+
"rm -rf",
|
|
130
|
+
"wget ",
|
|
131
|
+
"curl ",
|
|
132
|
+
"chmod ",
|
|
133
|
+
"phpinfo()",
|
|
40
134
|
|
|
41
135
|
// 路径遍历
|
|
42
|
-
"../",
|
|
136
|
+
"../",
|
|
137
|
+
"..\\",
|
|
138
|
+
"/etc/passwd",
|
|
139
|
+
"/etc/shadow",
|
|
43
140
|
|
|
44
141
|
// XSS
|
|
45
|
-
"<script",
|
|
142
|
+
"<script",
|
|
143
|
+
"javascript:",
|
|
144
|
+
"onerror=",
|
|
145
|
+
"onload=",
|
|
146
|
+
"alert(",
|
|
147
|
+
"document.cookie",
|
|
46
148
|
|
|
47
149
|
// 特殊编码
|
|
48
|
-
"0x7e",
|
|
49
|
-
|
|
150
|
+
"0x7e",
|
|
151
|
+
"UNION%20SELECT",
|
|
152
|
+
"%27OR%27",
|
|
153
|
+
"{{",
|
|
154
|
+
"}}",
|
|
155
|
+
"1+1",
|
|
156
|
+
],
|
|
50
157
|
};
|
|
51
158
|
|
|
52
159
|
// === 预处理:构建正则缓存 ===
|
|
@@ -116,7 +223,7 @@ const safe = (req, res, next) => {
|
|
|
116
223
|
}
|
|
117
224
|
|
|
118
225
|
const fullText = checkText + bodyText;
|
|
119
|
-
|
|
226
|
+
|
|
120
227
|
// 空文本直接跳过检测
|
|
121
228
|
if (!fullText.trim()) return next();
|
|
122
229
|
|
|
@@ -151,7 +258,8 @@ const safe = (req, res, next) => {
|
|
|
151
258
|
userAgent: userAgent.substring(0, 100),
|
|
152
259
|
method: req.method,
|
|
153
260
|
matchedKeyword,
|
|
154
|
-
sample:
|
|
261
|
+
sample:
|
|
262
|
+
fullText.substring(0, 200) + (fullText.length > 200 ? "..." : ""),
|
|
155
263
|
});
|
|
156
264
|
|
|
157
265
|
// 根据WAF级别决定拦截方式
|
package/package.json
CHANGED
package/utils/response.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { CODE, DB_ERROR } from "../config/code.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @description 获取数据库错误码
|
|
5
|
+
* @param {*} error
|
|
6
|
+
* @returns {Number} 默认错误码
|
|
7
|
+
*/
|
|
8
|
+
const getDefaultErrorCode = (error) => {
|
|
5
9
|
if (error.message.includes("syntax") || error.message.includes("SQL")) {
|
|
6
10
|
return 6008;
|
|
7
11
|
} else if (error.message.includes("Connection closed")) {
|
|
@@ -12,11 +16,14 @@ export const getDefaultErrorCode = (error) => {
|
|
|
12
16
|
return 5001;
|
|
13
17
|
};
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @description 数据库错误响应处理函数
|
|
21
|
+
* @param {*} err
|
|
22
|
+
* @returns {Object} 错误响应对象
|
|
23
|
+
*/
|
|
15
24
|
export const error = (err) => {
|
|
16
25
|
console.error("DB Error:", err);
|
|
17
|
-
// 从映射表获取状态码或使用默认错误码
|
|
18
26
|
const errorCode = DB_ERROR[err.code] || getDefaultErrorCode(err);
|
|
19
|
-
|
|
20
27
|
return {
|
|
21
28
|
success: false,
|
|
22
29
|
msg: CODE[errorCode], // 从CODE对象获取错误消息
|
|
@@ -28,8 +35,13 @@ export const error = (err) => {
|
|
|
28
35
|
};
|
|
29
36
|
};
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @description 错误响应处理函数
|
|
40
|
+
* @param {*} msg
|
|
41
|
+
* @param {*} data
|
|
42
|
+
* @returns {Object} 失败响应对象
|
|
43
|
+
*/
|
|
31
44
|
export const fail = (msg = CODE[201], data = {}) => {
|
|
32
|
-
console.warn("Operation failed:", msg);
|
|
33
45
|
return {
|
|
34
46
|
success: false,
|
|
35
47
|
msg,
|
|
@@ -38,6 +50,12 @@ export const fail = (msg = CODE[201], data = {}) => {
|
|
|
38
50
|
};
|
|
39
51
|
};
|
|
40
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @description 成功响应处理函数
|
|
55
|
+
* @param {*} data
|
|
56
|
+
* @param {*} msg
|
|
57
|
+
* @returns {Object} 成功响应对象
|
|
58
|
+
*/
|
|
41
59
|
export const success = (data = {}, msg = CODE[200]) => ({
|
|
42
60
|
success: true,
|
|
43
61
|
msg,
|