chanjs 2.0.6 → 2.0.8
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 +48 -0
- package/core/service copy.js +217 -0
- package/core/service.js +41 -86
- package/index.js +3 -2
- package/middleware/log.js +19 -2
- package/middleware/waf.js +156 -172
- package/package.json +1 -1
- package/utils/response.js +46 -0
package/config/code.js
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
// 简化的状态码定义(数字作为键)
|
2
|
+
export const CODE = {
|
3
|
+
200: "操作成功", // 成功
|
4
|
+
201: "操作失败", // 通用失败
|
5
|
+
|
6
|
+
// 业务逻辑错误
|
7
|
+
1001: "业务处理失败", // 业务处理失败
|
8
|
+
|
9
|
+
// 参数错误
|
10
|
+
2001: "参数无效", // 参数无效
|
11
|
+
2002: "参数缺失", // 参数缺失
|
12
|
+
|
13
|
+
// 认证授权错误
|
14
|
+
3001: "认证失败", // 认证失败
|
15
|
+
3002: "令牌已过期", // 令牌过期
|
16
|
+
3003: "权限不足", // 权限不足
|
17
|
+
|
18
|
+
// 资源相关错误
|
19
|
+
4001: "资源不存在", // 资源不存在
|
20
|
+
4002: "资源已锁定", // 资源锁定
|
21
|
+
4003: "资源已存在", // 资源重复
|
22
|
+
|
23
|
+
// 系统错误
|
24
|
+
5001: "系统内部错误", // 系统错误
|
25
|
+
5002: "服务繁忙,请稍后再试", // 服务繁忙
|
26
|
+
|
27
|
+
// 数据库错误
|
28
|
+
6001: "数据库连接失败", // 连接错误
|
29
|
+
6002: "数据库访问被拒绝", // 访问被拒
|
30
|
+
6003: "存在关联数据,操作失败", // 引用错误
|
31
|
+
6004: "数据库字段错误", // 字段错误
|
32
|
+
6005: "数据重复,违反唯一性约束", // 数据重复
|
33
|
+
6006: "目标表不存在", // 表不存在
|
34
|
+
6007: "数据库操作超时", // 操作超时
|
35
|
+
6008: "数据库语法错误,请检查查询语句", // 语法错误
|
36
|
+
6009: "数据库连接已关闭,请重试", // 连接已关闭
|
37
|
+
};
|
38
|
+
|
39
|
+
// 数据库错误码映射(仅映射状态码)
|
40
|
+
export const DB_ERROR = {
|
41
|
+
ECONNREFUSED: 6001,
|
42
|
+
ER_ACCESS_DENIED_ERROR: 6002,
|
43
|
+
ER_ROW_IS_REFERENCED_2: 6003,
|
44
|
+
ER_BAD_FIELD_ERROR: 6004,
|
45
|
+
ER_DUP_ENTRY: 6005,
|
46
|
+
ER_NO_SUCH_TABLE: 6006,
|
47
|
+
ETIMEOUT: 6007,
|
48
|
+
};
|
@@ -0,0 +1,217 @@
|
|
1
|
+
import {CODE,DB_ERROR} from "../config/code.js";
|
2
|
+
|
3
|
+
// 响应工具函数
|
4
|
+
const createResponse = (success, data, msg, code) => ({
|
5
|
+
success,
|
6
|
+
data: data || (success ? {} : null),
|
7
|
+
msg: msg || (success ? CODE[200] : CODE[1001]),
|
8
|
+
code: code || (success ? 200 : 1001)
|
9
|
+
});
|
10
|
+
|
11
|
+
const successResponse = (data, msg) => createResponse(true, data, msg);
|
12
|
+
const failResponse = (msg, data, code) => createResponse(false, data, msg, code);
|
13
|
+
|
14
|
+
// 错误处理函数
|
15
|
+
const getDefaultErrorCode = (error) => {
|
16
|
+
if (error.message.includes('syntax') || error.message.includes('SQL')) {
|
17
|
+
return 6008;
|
18
|
+
} else if (error.message.includes('Connection closed')) {
|
19
|
+
return 6009;
|
20
|
+
} else if (error.message.includes('permission')) {
|
21
|
+
return 3003;
|
22
|
+
}
|
23
|
+
return 5001;
|
24
|
+
};
|
25
|
+
|
26
|
+
const handleError = (err) => {
|
27
|
+
console.error('Database Error:', err);
|
28
|
+
const errorCode = DB_ERROR[err.code] || getDefaultErrorCode(err);
|
29
|
+
|
30
|
+
return failResponse(
|
31
|
+
CODE[errorCode],
|
32
|
+
{ sql: err.sql, sqlMessage: err.sqlMessage },
|
33
|
+
errorCode
|
34
|
+
);
|
35
|
+
};
|
36
|
+
|
37
|
+
// 基础查询构建函数
|
38
|
+
const buildQuery = (knex, model, query = {}) => {
|
39
|
+
let dbQuery = knex(model);
|
40
|
+
if (Object.keys(query).length > 0) {
|
41
|
+
dbQuery = dbQuery.where(query);
|
42
|
+
}
|
43
|
+
return dbQuery;
|
44
|
+
};
|
45
|
+
|
46
|
+
/**
|
47
|
+
* 查询表所有记录(慎用)
|
48
|
+
*/
|
49
|
+
export const all = async (knex, model, query = {}) => {
|
50
|
+
try {
|
51
|
+
const res = await buildQuery(knex, model, query).select();
|
52
|
+
return successResponse(res);
|
53
|
+
} catch (err) {
|
54
|
+
return handleError(err);
|
55
|
+
}
|
56
|
+
};
|
57
|
+
|
58
|
+
/**
|
59
|
+
* 获取单个记录
|
60
|
+
*/
|
61
|
+
export const one = async (knex, model, query = {}) => {
|
62
|
+
try {
|
63
|
+
const res = await buildQuery(knex, model, query).first();
|
64
|
+
return successResponse(res);
|
65
|
+
} catch (err) {
|
66
|
+
return handleError(err);
|
67
|
+
}
|
68
|
+
};
|
69
|
+
|
70
|
+
/**
|
71
|
+
* 根据条件查询记录
|
72
|
+
*/
|
73
|
+
export const findById = async (knex, model, { query = {}, field = [], len = 1 }) => {
|
74
|
+
try {
|
75
|
+
let dataQuery = knex(model).where(query);
|
76
|
+
|
77
|
+
if (field.length > 0) dataQuery = dataQuery.select(field);
|
78
|
+
if (len === 1) dataQuery = dataQuery.first();
|
79
|
+
else if (len > 1) dataQuery = dataQuery.limit(len);
|
80
|
+
|
81
|
+
const res = await dataQuery;
|
82
|
+
return successResponse(res || (len === 1 ? {} : []));
|
83
|
+
} catch (err) {
|
84
|
+
return handleError(err);
|
85
|
+
}
|
86
|
+
};
|
87
|
+
|
88
|
+
/**
|
89
|
+
* 创建新记录
|
90
|
+
*/
|
91
|
+
export const insert = async (knex, model, data = {}) => {
|
92
|
+
try {
|
93
|
+
if (Object.keys(data).length === 0) {
|
94
|
+
return failResponse(CODE[2002], null, 2002);
|
95
|
+
}
|
96
|
+
const result = await knex(model).insert(data);
|
97
|
+
return successResponse(result?.length > 0 || !!result);
|
98
|
+
} catch (err) {
|
99
|
+
return handleError(err);
|
100
|
+
}
|
101
|
+
};
|
102
|
+
|
103
|
+
/**
|
104
|
+
* 插入多条记录
|
105
|
+
*/
|
106
|
+
export const insertMany = async (knex, model, records = []) => {
|
107
|
+
try {
|
108
|
+
if (records.length === 0) {
|
109
|
+
return failResponse(CODE[2002], null, 2002);
|
110
|
+
}
|
111
|
+
const result = await knex(model).insert(records);
|
112
|
+
return successResponse(result);
|
113
|
+
} catch (err) {
|
114
|
+
return handleError(err);
|
115
|
+
}
|
116
|
+
};
|
117
|
+
|
118
|
+
/**
|
119
|
+
* 根据指定条件删除记录
|
120
|
+
*/
|
121
|
+
export const del = async (knex, model, query = {}) => {
|
122
|
+
try {
|
123
|
+
if (Object.keys(query).length === 0) {
|
124
|
+
return failResponse(CODE[2002], null, 2002);
|
125
|
+
}
|
126
|
+
const affectedRows = await knex(model).where(query).del();
|
127
|
+
return successResponse(affectedRows > 0);
|
128
|
+
} catch (err) {
|
129
|
+
return handleError(err);
|
130
|
+
}
|
131
|
+
};
|
132
|
+
|
133
|
+
/**
|
134
|
+
* 根据指定条件更新记录
|
135
|
+
*/
|
136
|
+
export const update = async (knex, model, query = {}, params = {}) => {
|
137
|
+
try {
|
138
|
+
if (Object.keys(query).length === 0 || Object.keys(params).length === 0) {
|
139
|
+
return failResponse(CODE[2001], null, 2001);
|
140
|
+
}
|
141
|
+
const result = await knex(model).where(query).update(params);
|
142
|
+
return successResponse(!!result);
|
143
|
+
} catch (err) {
|
144
|
+
return handleError(err);
|
145
|
+
}
|
146
|
+
};
|
147
|
+
|
148
|
+
/**
|
149
|
+
* 批量更新多条记录
|
150
|
+
*/
|
151
|
+
export const updateMany = async (knex, model, updates = []) => {
|
152
|
+
if (!Array.isArray(updates) || updates.length === 0) {
|
153
|
+
return failResponse(CODE[2002], null, 2002);
|
154
|
+
}
|
155
|
+
|
156
|
+
const trx = await knex.transaction();
|
157
|
+
try {
|
158
|
+
for (const { query, params } of updates) {
|
159
|
+
const result = await trx(model).where(query).update(params);
|
160
|
+
if (result === 0) {
|
161
|
+
await trx.rollback();
|
162
|
+
return failResponse(`更新失败: ${JSON.stringify(query)}`);
|
163
|
+
}
|
164
|
+
}
|
165
|
+
await trx.commit();
|
166
|
+
return successResponse(true);
|
167
|
+
} catch (err) {
|
168
|
+
await trx.rollback();
|
169
|
+
return handleError(err);
|
170
|
+
}
|
171
|
+
};
|
172
|
+
|
173
|
+
/**
|
174
|
+
* 分页查询
|
175
|
+
*/
|
176
|
+
export const query = async (knex, model, { current = 1, pageSize = 10, query = {}, field = [] }) => {
|
177
|
+
try {
|
178
|
+
const offset = (current - 1) * pageSize;
|
179
|
+
let countQuery = knex(model).count("* as total");
|
180
|
+
let dataQuery = knex(model);
|
181
|
+
|
182
|
+
if (Object.keys(query).length > 0) {
|
183
|
+
Object.entries(query).forEach(([key, value]) => {
|
184
|
+
countQuery = countQuery.where(key, value);
|
185
|
+
dataQuery = dataQuery.where(key, value);
|
186
|
+
});
|
187
|
+
}
|
188
|
+
|
189
|
+
if (field.length > 0) dataQuery = dataQuery.select(field);
|
190
|
+
|
191
|
+
const [totalResult, list] = await Promise.all([
|
192
|
+
countQuery.first(),
|
193
|
+
dataQuery.offset(offset).limit(pageSize)
|
194
|
+
]);
|
195
|
+
|
196
|
+
const total = totalResult?.total || 0;
|
197
|
+
return successResponse({ list, total, current, pageSize });
|
198
|
+
} catch (err) {
|
199
|
+
return handleError(err);
|
200
|
+
}
|
201
|
+
};
|
202
|
+
|
203
|
+
/**
|
204
|
+
* 计数查询
|
205
|
+
*/
|
206
|
+
export const count = async (knex, model, query = []) => {
|
207
|
+
try {
|
208
|
+
let dataQuery = knex(model);
|
209
|
+
if (query.length > 0) {
|
210
|
+
query.forEach(condition => dataQuery = dataQuery.where(condition));
|
211
|
+
}
|
212
|
+
const result = await dataQuery.count("* as total").first();
|
213
|
+
return successResponse(Number(result?.total) || 0);
|
214
|
+
} catch (err) {
|
215
|
+
return handleError(err);
|
216
|
+
}
|
217
|
+
};
|
package/core/service.js
CHANGED
@@ -1,57 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
"ER_ACCESS_DENIED_ERROR": "无权限访问,账号或密码错误。",
|
5
|
-
"ER_ROW_IS_REFERENCED_2": "无法删除或更新记录,存在关联数据。",
|
6
|
-
"ER_BAD_FIELD_ERROR": "SQL语句中包含无效字段,请检查查询条件或列名。",
|
7
|
-
"ER_DUP_ENTRY": "插入失败:数据重复,违反唯一性约束。",
|
8
|
-
"ER_NO_SUCH_TABLE": "操作失败:目标表不存在。",
|
9
|
-
"ETIMEOUT": "数据库操作超时,请稍后再试。"
|
10
|
-
};
|
11
|
-
|
12
|
-
const getDefaultErrorMessage = (error) => {
|
13
|
-
if (error.message.includes('syntax') || error.message.includes('SQL')) {
|
14
|
-
return '数据库语法错误,请检查您的查询语句。';
|
15
|
-
} else if (error.message.includes('Connection closed')) {
|
16
|
-
return '数据库连接已关闭,请重试。';
|
17
|
-
} else if (error.message.includes('permission')) {
|
18
|
-
return '数据库权限不足,请检查配置。';
|
19
|
-
}
|
20
|
-
return '数据库发生未知错误,请稍后重试。';
|
21
|
-
};
|
22
|
-
|
23
|
-
const errorResponse = (err) => {
|
24
|
-
console.error('DB Error:', err);
|
25
|
-
const message = errCode[err.code] || getDefaultErrorMessage(err);
|
26
|
-
return {
|
27
|
-
success: false,
|
28
|
-
msg: message,
|
29
|
-
code: 500,
|
30
|
-
data: {
|
31
|
-
sql: err.sql,
|
32
|
-
sqlMessage: err.sqlMessage
|
33
|
-
}
|
34
|
-
};
|
35
|
-
};
|
36
|
-
|
37
|
-
const failResponse = (msg = "操作失败", data = {}) => {
|
38
|
-
console.warn('Operation failed:', msg);
|
39
|
-
return {
|
40
|
-
success: false,
|
41
|
-
msg,
|
42
|
-
code: 201,
|
43
|
-
data
|
44
|
-
};
|
45
|
-
};
|
46
|
-
|
47
|
-
const successResponse = (data = {}, msg = "操作成功") => ({
|
48
|
-
success: true,
|
49
|
-
msg,
|
50
|
-
code: 200,
|
51
|
-
data
|
52
|
-
});
|
53
|
-
|
54
|
-
// ====================== 共享数据库方法 (只创建一次) ======================
|
1
|
+
import { success, fail, error } from "../utils/response.js";
|
2
|
+
import { CODE } from "../config/code.js";
|
3
|
+
// ====================== 共享数据库方法 ======================
|
55
4
|
const databaseMethods = {
|
56
5
|
/**
|
57
6
|
* 查询表所有记录(慎用)
|
@@ -65,9 +14,9 @@ const databaseMethods = {
|
|
65
14
|
dbQuery = dbQuery.where(query);
|
66
15
|
}
|
67
16
|
const res = await dbQuery.select();
|
68
|
-
return
|
17
|
+
return success(res);
|
69
18
|
} catch (err) {
|
70
|
-
return
|
19
|
+
return error(err);
|
71
20
|
}
|
72
21
|
},
|
73
22
|
|
@@ -75,9 +24,7 @@ const databaseMethods = {
|
|
75
24
|
* 获取单个记录
|
76
25
|
* @param {Object} query - 查询条件
|
77
26
|
* @returns {Promise} 查询结果
|
78
|
-
|
79
|
-
* */
|
80
|
-
|
27
|
+
*/
|
81
28
|
async one(query = {}) {
|
82
29
|
try {
|
83
30
|
let dbQuery = this.knex(this.model);
|
@@ -85,9 +32,9 @@ const databaseMethods = {
|
|
85
32
|
dbQuery = dbQuery.where(query);
|
86
33
|
}
|
87
34
|
const res = await dbQuery.first();
|
88
|
-
return
|
35
|
+
return success(res);
|
89
36
|
} catch (err) {
|
90
|
-
return
|
37
|
+
return error(err);
|
91
38
|
}
|
92
39
|
},
|
93
40
|
|
@@ -106,9 +53,9 @@ const databaseMethods = {
|
|
106
53
|
else if (len > 1) dataQuery = dataQuery.limit(len);
|
107
54
|
|
108
55
|
const res = await dataQuery;
|
109
|
-
return
|
56
|
+
return success(res || (len === 1 ? {} : []));
|
110
57
|
} catch (err) {
|
111
|
-
return
|
58
|
+
return error(err);
|
112
59
|
}
|
113
60
|
},
|
114
61
|
|
@@ -119,13 +66,16 @@ const databaseMethods = {
|
|
119
66
|
*/
|
120
67
|
async insert(data = {}) {
|
121
68
|
try {
|
69
|
+
console.log('data--->',data)
|
122
70
|
if (Object.keys(data).length === 0) {
|
123
|
-
return
|
71
|
+
return fail(CODE[2002], { code: 2002 });
|
124
72
|
}
|
73
|
+
|
74
|
+
console.log('this.model--->',this.model)
|
125
75
|
const result = await this.knex(this.model).insert(data);
|
126
|
-
return
|
76
|
+
return success(result?.length > 0 || !!result);
|
127
77
|
} catch (err) {
|
128
|
-
return
|
78
|
+
return error(err);
|
129
79
|
}
|
130
80
|
},
|
131
81
|
|
@@ -137,12 +87,12 @@ const databaseMethods = {
|
|
137
87
|
async insertMany(records = []) {
|
138
88
|
try {
|
139
89
|
if (records.length === 0) {
|
140
|
-
return
|
90
|
+
return fail(CODE[2002], { code: 2002 });
|
141
91
|
}
|
142
92
|
const result = await this.knex(this.model).insert(records);
|
143
|
-
return
|
93
|
+
return success(result);
|
144
94
|
} catch (err) {
|
145
|
-
return
|
95
|
+
return error(err);
|
146
96
|
}
|
147
97
|
},
|
148
98
|
|
@@ -154,12 +104,12 @@ const databaseMethods = {
|
|
154
104
|
async delete(query = {}) {
|
155
105
|
try {
|
156
106
|
if (Object.keys(query).length === 0) {
|
157
|
-
return
|
107
|
+
return fail(CODE[2002], { code: 2002 });
|
158
108
|
}
|
159
109
|
const affectedRows = await this.knex(this.model).where(query).del();
|
160
|
-
return
|
110
|
+
return success(affectedRows > 0);
|
161
111
|
} catch (err) {
|
162
|
-
return
|
112
|
+
return error(err);
|
163
113
|
}
|
164
114
|
},
|
165
115
|
|
@@ -173,12 +123,17 @@ const databaseMethods = {
|
|
173
123
|
async update({ query, params } = {}) {
|
174
124
|
try {
|
175
125
|
if (!query || !params || Object.keys(query).length === 0) {
|
176
|
-
return
|
126
|
+
return fail(CODE[2001], { code: 2001 });
|
177
127
|
}
|
128
|
+
console.log('query--->',query)
|
129
|
+
console.log('this.model--->',this.model)
|
130
|
+
|
131
|
+
console.log('params---->',params)
|
178
132
|
const result = await this.knex(this.model).where(query).update(params);
|
179
|
-
|
133
|
+
console.log('result--->',result)
|
134
|
+
return success(!!result);
|
180
135
|
} catch (err) {
|
181
|
-
return
|
136
|
+
return error(err);
|
182
137
|
}
|
183
138
|
},
|
184
139
|
|
@@ -189,7 +144,7 @@ const databaseMethods = {
|
|
189
144
|
*/
|
190
145
|
async updateMany(updates = []) {
|
191
146
|
if (!Array.isArray(updates) || updates.length === 0) {
|
192
|
-
return
|
147
|
+
return fail(CODE[2002], { code: 2002 });
|
193
148
|
}
|
194
149
|
|
195
150
|
const trx = await this.knex.transaction();
|
@@ -198,14 +153,14 @@ const databaseMethods = {
|
|
198
153
|
const result = await trx(this.model).where(query).update(params);
|
199
154
|
if (result === 0) {
|
200
155
|
await trx.rollback();
|
201
|
-
return
|
156
|
+
return fail(`更新失败: ${JSON.stringify(query)}`);
|
202
157
|
}
|
203
158
|
}
|
204
159
|
await trx.commit();
|
205
|
-
return
|
160
|
+
return success(true);
|
206
161
|
} catch (err) {
|
207
162
|
await trx.rollback();
|
208
|
-
return
|
163
|
+
return error(err);
|
209
164
|
}
|
210
165
|
},
|
211
166
|
|
@@ -239,9 +194,9 @@ const databaseMethods = {
|
|
239
194
|
]);
|
240
195
|
|
241
196
|
const total = totalResult?.total || 0;
|
242
|
-
return
|
197
|
+
return success({ list, total, current, pageSize });
|
243
198
|
} catch (err) {
|
244
|
-
return
|
199
|
+
return error(err);
|
245
200
|
}
|
246
201
|
},
|
247
202
|
|
@@ -257,14 +212,14 @@ const databaseMethods = {
|
|
257
212
|
query.forEach(condition => dataQuery = dataQuery.where(condition));
|
258
213
|
}
|
259
214
|
const result = await dataQuery.count("* as total").first();
|
260
|
-
return
|
215
|
+
return success(Number(result?.total) || 0);
|
261
216
|
} catch (err) {
|
262
|
-
return
|
217
|
+
return error(err);
|
263
218
|
}
|
264
219
|
}
|
265
220
|
};
|
266
221
|
|
267
|
-
// ====================== 工厂函数
|
222
|
+
// ====================== 工厂函数 ======================
|
268
223
|
/**
|
269
224
|
* 创建数据库服务实例
|
270
225
|
* @param {Object} knex - Knex实例
|
@@ -273,7 +228,7 @@ const databaseMethods = {
|
|
273
228
|
*/
|
274
229
|
export default function Service(knex, model) {
|
275
230
|
if (!knex || !model) {
|
276
|
-
throw new Error('
|
231
|
+
throw new Error('Service: knex instance and model name are required');
|
277
232
|
}
|
278
233
|
|
279
234
|
// 创建继承数据库方法的轻量对象
|
@@ -294,4 +249,4 @@ export default function Service(knex, model) {
|
|
294
249
|
});
|
295
250
|
|
296
251
|
return service;
|
297
|
-
}
|
252
|
+
}
|
package/index.js
CHANGED
@@ -65,7 +65,8 @@ class Chan {
|
|
65
65
|
logger,
|
66
66
|
cors,
|
67
67
|
} = Chan.config;
|
68
|
-
|
68
|
+
|
69
|
+
this.app.set('trust proxy', true);
|
69
70
|
log(this.app, logger);
|
70
71
|
setFavicon(this.app);
|
71
72
|
setCookie(this.app, cookieKey);
|
@@ -166,7 +167,7 @@ class Chan {
|
|
166
167
|
}
|
167
168
|
|
168
169
|
run(cb) {
|
169
|
-
const port =
|
170
|
+
const port = parseInt(process.env.PORT) || 3000;
|
170
171
|
this.app.listen(port, () => {
|
171
172
|
cb ? cb(port) : console.log(`Server is running on port ${port}`);
|
172
173
|
});
|
package/middleware/log.js
CHANGED
@@ -1,4 +1,21 @@
|
|
1
1
|
import morgan from "morgan";
|
2
|
-
|
3
|
-
|
2
|
+
import { getIp } from "../helper/ip.js";
|
3
|
+
morgan.token("ip", (req, res) => {
|
4
|
+
return getIp(req);
|
5
|
+
});
|
6
|
+
// 自定义 morgan 格式:IP Method URL Status Length - Response-Time ms
|
7
|
+
morgan.format("chancms", (tokens, req, res) => {
|
8
|
+
return [
|
9
|
+
tokens.ip(req, res), // 客户端 IP
|
10
|
+
tokens.method(req, res), // GET/POST
|
11
|
+
tokens.url(req, res), // 完整 URL(含 query)
|
12
|
+
tokens.status(req, res), // 状态码(如 403)
|
13
|
+
tokens.res(req, res, "content-length") || "-", // 响应体大小
|
14
|
+
"-", // 占位符(原日志中的 '-')
|
15
|
+
tokens["response-time"](req, res),
|
16
|
+
"ms", // 响应时间
|
17
|
+
].join(" ");
|
18
|
+
});
|
19
|
+
export const log = (app, logger) => {
|
20
|
+
app.use(morgan(logger.level));
|
4
21
|
};
|
package/middleware/waf.js
CHANGED
@@ -1,195 +1,179 @@
|
|
1
1
|
import url from "url";
|
2
|
-
import {getIp} from "../helper/ip.js";
|
3
|
-
|
4
|
-
//
|
5
|
-
const keywords =
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
"
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
"../",
|
55
|
-
"db_",
|
56
|
-
"smtp",
|
57
|
-
"meta",
|
58
|
-
"debug",
|
59
|
-
"secret",
|
60
|
-
"/xampp/",
|
61
|
-
"/metadata/",
|
62
|
-
"/internal/",
|
63
|
-
"/aws/",
|
64
|
-
"/debug/",
|
65
|
-
"/configs/",
|
66
|
-
"/cgi-bin/",
|
67
|
-
"/tmp/",
|
68
|
-
"/staging/",
|
69
|
-
"/mail/",
|
70
|
-
"/docker/",
|
71
|
-
"/.secure/",
|
72
|
-
"/php-cgi/",
|
73
|
-
"/wp-",
|
74
|
-
"/backup/",
|
75
|
-
"redirect",
|
76
|
-
"/phpMyAdmin/",
|
77
|
-
"/setup/",
|
78
|
-
"concat(",
|
79
|
-
"version(",
|
80
|
-
"sleep(",
|
81
|
-
"benchmark(",
|
82
|
-
"0x7e",
|
83
|
-
"extractvalue(",
|
84
|
-
"(select",
|
85
|
-
"a%",
|
86
|
-
"union",
|
87
|
-
"drop",
|
88
|
-
"alter",
|
89
|
-
"truncate",
|
90
|
-
"exec",
|
91
|
-
];
|
92
|
-
|
93
|
-
// 预处理:合并字符串关键词和正则关键词为两个单一正则(核心优化)
|
94
|
-
const { combinedStrRegex, combinedRegRegex } = (() => {
|
2
|
+
import { getIp } from "../helper/ip.js";
|
3
|
+
|
4
|
+
// 改进的关键词列表,区分需要全词匹配的关键词
|
5
|
+
const keywords = {
|
6
|
+
// 需要全词匹配的关键词(避免短词误报)
|
7
|
+
wholeWord: [
|
8
|
+
// 系统命令/工具(容易产生短词误报)
|
9
|
+
"opt", "cmd", "rm", "mdc", "netcat", "nc", "mdb", "bin", "mk", "sys","sh","chomd",
|
10
|
+
"php-cgi"
|
11
|
+
],
|
12
|
+
|
13
|
+
// 普通关键词(包含特殊字符或较长关键词)
|
14
|
+
normal: [
|
15
|
+
// 文件扩展名 & 敏感文件
|
16
|
+
".php", ".asp", ".aspx", ".jsp", ".jspx", ".do", ".action", ".cgi",
|
17
|
+
".py", ".pl", ".md", ".log", ".conf", ".config", ".env", ".jsa",
|
18
|
+
".go", ".jhtml", ".shtml", ".cfm", ".svn", ".keys", ".hidden",
|
19
|
+
".bod", ".ll", ".backup", ".json", ".xml", ".bak", ".aws",
|
20
|
+
".database", ".cookie", ".rsp", ".old", ".tf", ".sql", ".vscode",
|
21
|
+
".docker", ".map", ".save", ".gz", ".yml", ".tar", ".sh", ".idea", ".s3",
|
22
|
+
|
23
|
+
// 敏感目录
|
24
|
+
"/administrator", "/wp-admin",
|
25
|
+
|
26
|
+
// 高危路径/应用
|
27
|
+
"phpMyAdmin", "setup", "wp-", "cgi-bin", "xampp", "staging", "internal",
|
28
|
+
"debug", "metadata", "secret", "smtp", "redirect", "configs",
|
29
|
+
|
30
|
+
// SQL 注入
|
31
|
+
"sleep(", "benchmark(", "concat(", "extractvalue(", "updatexml(",
|
32
|
+
"version(", "union select", "union all", "select @@", "drop",
|
33
|
+
"alter", "truncate", "exec", "(select", "information_schema",
|
34
|
+
"load_file(", "into outfile", "into dumpfile",
|
35
|
+
|
36
|
+
// 命令注入
|
37
|
+
"cmd=", "system(", "exec(", "shell_exec(", "passthru(", "base64_decode",
|
38
|
+
"eval(", "assert(", "preg_replace", "bash -i", "rm -rf", "wget ", "curl ",
|
39
|
+
"chmod ", "phpinfo()",
|
40
|
+
|
41
|
+
// 路径遍历
|
42
|
+
"../", "..\\", "/etc/passwd", "/etc/shadow",
|
43
|
+
|
44
|
+
// XSS
|
45
|
+
"<script", "javascript:", "onerror=", "onload=", "alert(", "document.cookie",
|
46
|
+
|
47
|
+
// 特殊编码
|
48
|
+
"0x7e", "UNION%20SELECT", "%27OR%27", "{{", "}}", "1+1"
|
49
|
+
]
|
50
|
+
};
|
51
|
+
|
52
|
+
// === 预处理:构建正则缓存 ===
|
53
|
+
const { wholeWordRegexCache, normalRegexCache } = (() => {
|
95
54
|
const regexSpecialChars = /[.*+?^${}()|[\]\\]/g;
|
96
|
-
const
|
97
|
-
const
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
55
|
+
const wholeWordRegexCache = {};
|
56
|
+
const normalRegexCache = {};
|
57
|
+
|
58
|
+
// 处理需要全词匹配的关键词
|
59
|
+
keywords.wholeWord.forEach((keyword) => {
|
60
|
+
// 转义特殊字符
|
61
|
+
const escaped = keyword.replace(regexSpecialChars, "\\$&");
|
62
|
+
// 使用单词边界确保全词匹配,忽略大小写
|
63
|
+
wholeWordRegexCache[keyword] = new RegExp(`\\b${escaped}\\b`, "i");
|
64
|
+
});
|
65
|
+
|
66
|
+
// 处理普通关键词
|
67
|
+
keywords.normal.forEach((keyword) => {
|
68
|
+
// 转义特殊字符
|
69
|
+
const escaped = keyword.replace(regexSpecialChars, "\\$&");
|
70
|
+
// 普通匹配,忽略大小写
|
71
|
+
normalRegexCache[keyword] = new RegExp(escaped, "i");
|
108
72
|
});
|
109
73
|
|
110
|
-
|
111
|
-
const buildCombinedRegex = (parts) => {
|
112
|
-
return parts.length
|
113
|
-
? new RegExp(`(?:${parts.join("|")})`, "i") // 非捕获组+不区分大小写
|
114
|
-
: new RegExp("^$"); // 匹配空字符串(永远不命中)
|
115
|
-
};
|
116
|
-
|
117
|
-
return {
|
118
|
-
combinedStrRegex: buildCombinedRegex(strKwParts),
|
119
|
-
combinedRegRegex: buildCombinedRegex(regKwParts),
|
120
|
-
};
|
74
|
+
return { wholeWordRegexCache, normalRegexCache };
|
121
75
|
})();
|
122
76
|
|
77
|
+
/**
|
78
|
+
* WAF 安全中间件(优化版,减少误报)
|
79
|
+
*/
|
123
80
|
const safe = (req, res, next) => {
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
81
|
+
try {
|
82
|
+
const { WAF_LEVEL = 1 } = Chan.config || {};
|
83
|
+
|
84
|
+
// 设置基础安全头
|
85
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
86
|
+
res.setHeader("X-XSS-Protection", "1; mode=block");
|
87
|
+
res.setHeader("X-Frame-Options", "DENY");
|
88
|
+
|
89
|
+
// 构建检测文本:path + query + body
|
90
|
+
let checkText = req.path || "";
|
91
|
+
|
92
|
+
// 添加 query
|
93
|
+
if (req.query && Object.keys(req.query).length > 0) {
|
94
|
+
const queryStr = Object.entries(req.query)
|
95
|
+
.map(([k, v]) => `${k}=${v}`)
|
96
|
+
.join(" ");
|
97
|
+
checkText += ` ${queryStr}`;
|
98
|
+
}
|
139
99
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
if (bodyStr.length < 10000) {
|
153
|
-
bodyText = ` ${bodyStr}`;
|
154
|
-
}
|
155
|
-
} catch (e) {
|
156
|
-
// 忽略序列化错误
|
100
|
+
// 添加 body(非 multipart)
|
101
|
+
let bodyText = "";
|
102
|
+
const contentType = req.headers["content-type"] || "";
|
103
|
+
const isMultipart = contentType.includes("multipart/form-data");
|
104
|
+
|
105
|
+
if (!isMultipart && req.body) {
|
106
|
+
try {
|
107
|
+
const bodyStr =
|
108
|
+
typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
109
|
+
// 限制检测的body长度,避免性能问题
|
110
|
+
if (bodyStr.length < 10000) {
|
111
|
+
bodyText = ` ${bodyStr}`;
|
157
112
|
}
|
113
|
+
} catch (e) {
|
114
|
+
// 忽略序列化错误
|
158
115
|
}
|
116
|
+
}
|
159
117
|
|
160
|
-
|
161
|
-
|
118
|
+
const fullText = checkText + bodyText;
|
119
|
+
|
120
|
+
// 空文本直接跳过检测
|
121
|
+
if (!fullText.trim()) return next();
|
162
122
|
|
163
|
-
|
123
|
+
let matchedKeyword = null;
|
164
124
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
125
|
+
// 1. 检测需要全词匹配的关键词
|
126
|
+
for (const [kw, regex] of Object.entries(wholeWordRegexCache)) {
|
127
|
+
if (regex.test(fullText)) {
|
128
|
+
matchedKeyword = kw;
|
129
|
+
break;
|
170
130
|
}
|
171
|
-
|
172
|
-
|
173
|
-
|
131
|
+
}
|
132
|
+
|
133
|
+
// 2. 检测普通关键词
|
134
|
+
if (!matchedKeyword) {
|
135
|
+
for (const [kw, regex] of Object.entries(normalRegexCache)) {
|
136
|
+
if (regex.test(fullText)) {
|
137
|
+
matchedKeyword = kw;
|
138
|
+
break;
|
139
|
+
}
|
174
140
|
}
|
141
|
+
}
|
175
142
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
143
|
+
// === 拦截处理 ===
|
144
|
+
if (matchedKeyword) {
|
145
|
+
const clientIp = getIp(req);
|
146
|
+
const userAgent = req.get("User-Agent") || "";
|
147
|
+
|
148
|
+
console.error("[WAF 拦截] 疑似恶意请求:", {
|
149
|
+
url: req.url,
|
150
|
+
ip: clientIp,
|
151
|
+
userAgent: userAgent.substring(0, 100),
|
152
|
+
method: req.method,
|
153
|
+
matchedKeyword,
|
154
|
+
sample: fullText.substring(0, 200) + (fullText.length > 200 ? "..." : ""),
|
155
|
+
});
|
156
|
+
|
157
|
+
// 根据WAF级别决定拦截方式
|
158
|
+
if (WAF_LEVEL >= 2) {
|
183
159
|
return res.status(403).send("非法风险请求,已拦截");
|
160
|
+
} else {
|
161
|
+
// 低级别仅记录不拦截,便于观察误报
|
162
|
+
console.warn("[WAF 警告] 低级别模式下未拦截请求");
|
163
|
+
return next();
|
184
164
|
}
|
185
|
-
|
186
|
-
next();
|
187
|
-
} catch (error) {
|
188
|
-
console.error("[安全中间件异常]", error);
|
189
|
-
res.status(500).send("服务器内部错误");
|
190
165
|
}
|
191
|
-
};
|
192
166
|
|
167
|
+
next();
|
168
|
+
} catch (error) {
|
169
|
+
console.error("[WAF 异常]", error);
|
170
|
+
res.status(500).send("服务器内部错误");
|
171
|
+
}
|
172
|
+
};
|
173
|
+
|
174
|
+
/**
|
175
|
+
* 导出 WAF
|
176
|
+
*/
|
193
177
|
export const waf = (app) => {
|
194
178
|
app.use(safe);
|
195
179
|
};
|
package/package.json
CHANGED
@@ -0,0 +1,46 @@
|
|
1
|
+
import { CODE, DB_ERROR } from "../config/code.js";
|
2
|
+
|
3
|
+
// ====================== 模块级别工具函数 ======================
|
4
|
+
export const getDefaultErrorCode = (error) => {
|
5
|
+
if (error.message.includes("syntax") || error.message.includes("SQL")) {
|
6
|
+
return 6008;
|
7
|
+
} else if (error.message.includes("Connection closed")) {
|
8
|
+
return 6009;
|
9
|
+
} else if (error.message.includes("permission")) {
|
10
|
+
return 3003;
|
11
|
+
}
|
12
|
+
return 5001;
|
13
|
+
};
|
14
|
+
|
15
|
+
export const error = (err) => {
|
16
|
+
console.error("DB Error:", err);
|
17
|
+
// 从映射表获取状态码或使用默认错误码
|
18
|
+
const errorCode = DB_ERROR[err.code] || getDefaultErrorCode(err);
|
19
|
+
|
20
|
+
return {
|
21
|
+
success: false,
|
22
|
+
msg: CODE[errorCode], // 从CODE对象获取错误消息
|
23
|
+
code: errorCode,
|
24
|
+
data: {
|
25
|
+
sql: err.sql,
|
26
|
+
sqlMessage: err.sqlMessage,
|
27
|
+
},
|
28
|
+
};
|
29
|
+
};
|
30
|
+
|
31
|
+
export const fail = (msg = CODE[201], data = {}) => {
|
32
|
+
console.warn("Operation failed:", msg);
|
33
|
+
return {
|
34
|
+
success: false,
|
35
|
+
msg,
|
36
|
+
code: 201, // 使用通用失败码
|
37
|
+
data,
|
38
|
+
};
|
39
|
+
};
|
40
|
+
|
41
|
+
export const success = (data = {}, msg = CODE[200]) => ({
|
42
|
+
success: true,
|
43
|
+
msg,
|
44
|
+
code: 200, // 使用标准成功码
|
45
|
+
data,
|
46
|
+
});
|