aetherframework-middleware 1.0.0
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/.env.example +88 -0
- package/LICENSE +21 -0
- package/README.md +578 -0
- package/examples/advanced-server.js +272 -0
- package/examples/basic-server.js +134 -0
- package/examples/benchmark.js +85 -0
- package/index.js +59 -0
- package/package.json +62 -0
- package/src/core/AetherCompiler.js +118 -0
- package/src/core/AetherContext.js +240 -0
- package/src/core/AetherPipeline.js +371 -0
- package/src/core/AetherStore.js +200 -0
- package/src/middleware/body-parser.js +295 -0
- package/src/middleware/compression.js +243 -0
- package/src/middleware/cors.js +155 -0
- package/src/middleware/json.js +207 -0
- package/src/middleware/jwt.js +222 -0
- package/src/middleware/rate-limit.js +232 -0
- package/src/middleware/security.js +114 -0
- package/src/middleware/session.js +167 -0
- package/src/utils/atomic-ops.js +125 -0
- package/src/utils/env-loader.js +124 -0
- package/src/utils/memory-pool.js +93 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// json.js - JSON parsing middleware for AetherJS
|
|
2
|
+
// High-performance JSON parsing with size limits and error handling
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create JSON parsing middleware for AetherJS
|
|
6
|
+
* @param {Object} options - JSON parser configuration
|
|
7
|
+
* @returns {Function} - JSON parser middleware function
|
|
8
|
+
*/
|
|
9
|
+
function createJsonMiddleware(options = {}) {
|
|
10
|
+
// Load configuration from environment variables
|
|
11
|
+
const envConfig = {
|
|
12
|
+
limit: process.env.BODY_LIMIT_JSON,
|
|
13
|
+
strict: process.env.JSON_STRICT,
|
|
14
|
+
reviver: process.env.JSON_REVIVER,
|
|
15
|
+
enable: process.env.JSON_ENABLE,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Default configuration
|
|
19
|
+
const defaults = {
|
|
20
|
+
enabled: envConfig.enable !== "false",
|
|
21
|
+
limit: parseSize(envConfig.limit || "1mb"),
|
|
22
|
+
strict: envConfig.strict !== "false",
|
|
23
|
+
reviver: envConfig.reviver ? eval(`(${envConfig.reviver})`) : null,
|
|
24
|
+
|
|
25
|
+
// Error handling
|
|
26
|
+
onError: (context, error) => {
|
|
27
|
+
context.setStatus(400).json({
|
|
28
|
+
error: "Bad Request",
|
|
29
|
+
message: "Invalid JSON format",
|
|
30
|
+
details: error.message,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Size limit exceeded handler
|
|
35
|
+
onLimitExceeded: (context, limit) => {
|
|
36
|
+
context.setStatus(413).json({
|
|
37
|
+
error: "Payload Too Large",
|
|
38
|
+
message: `JSON payload exceeds ${limit} bytes limit`,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Parse size string to bytes
|
|
44
|
+
function parseSize(size) {
|
|
45
|
+
const units = {
|
|
46
|
+
b: 1,
|
|
47
|
+
kb: 1024,
|
|
48
|
+
mb: 1024 * 1024,
|
|
49
|
+
gb: 1024 * 1024 * 1024,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// 1. 确保 size 是字符串,并转换为小写以便匹配
|
|
53
|
+
const lowerSize = String(size).toLowerCase();
|
|
54
|
+
|
|
55
|
+
// 2. 执行正则匹配
|
|
56
|
+
const match = lowerSize.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
|
|
57
|
+
|
|
58
|
+
if (!match) {
|
|
59
|
+
throw new Error(`Invalid size format: ${size}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. 从匹配数组中提取数值部分 (index 1) 和单位部分 (index 2)
|
|
63
|
+
const value = parseFloat(match[1]); // 提取第一个捕获组(数字)
|
|
64
|
+
const unit = match[2]; // 提取第二个捕获组(单位)
|
|
65
|
+
|
|
66
|
+
// 4. 计算并返回字节数
|
|
67
|
+
return value * (units[unit] || 1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Merge with provided options
|
|
71
|
+
const config = { ...defaults, ...options };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse JSON from request body
|
|
75
|
+
* @param {Object} request - HTTP request object
|
|
76
|
+
* @returns {Promise<Object>} - Parsed JSON object
|
|
77
|
+
*/
|
|
78
|
+
async function parseJson(request) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const chunks = [];
|
|
81
|
+
let totalLength = 0;
|
|
82
|
+
|
|
83
|
+
request.on("data", (chunk) => {
|
|
84
|
+
totalLength += chunk.length;
|
|
85
|
+
|
|
86
|
+
if (totalLength > config.limit) {
|
|
87
|
+
request.destroy();
|
|
88
|
+
reject(new Error(`JSON payload exceeds ${config.limit} bytes limit`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
chunks.push(chunk);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
request.on("end", () => {
|
|
96
|
+
try {
|
|
97
|
+
const buffer = Buffer.concat(chunks);
|
|
98
|
+
const text = buffer.toString("utf8");
|
|
99
|
+
|
|
100
|
+
if (config.strict && text.trim() === "") {
|
|
101
|
+
reject(new Error("Empty JSON payload"));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const parsed = config.reviver
|
|
106
|
+
? JSON.parse(text, config.reviver)
|
|
107
|
+
: JSON.parse(text);
|
|
108
|
+
|
|
109
|
+
resolve(parsed);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
reject(error);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
request.on("error", (error) => {
|
|
116
|
+
reject(error);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* JSON middleware function
|
|
123
|
+
* @param {AetherContext} context - AetherJS execution context
|
|
124
|
+
* @param {Object} signal - Signal object for flow control
|
|
125
|
+
*/
|
|
126
|
+
return async function jsonMiddleware(context, signal) {
|
|
127
|
+
if (!config.enabled) {
|
|
128
|
+
return signal.next();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Skip if not JSON content type
|
|
132
|
+
const contentType = context.getHeader("content-type") || "";
|
|
133
|
+
if (!contentType.includes("application/json")) {
|
|
134
|
+
return signal.next();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Skip if no body is expected
|
|
138
|
+
if (context.method === "GET" || context.method === "HEAD") {
|
|
139
|
+
return signal.next();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const contentLength = parseInt(context.getHeader("content-length")) || 0;
|
|
143
|
+
|
|
144
|
+
// Skip if no content
|
|
145
|
+
if (contentLength === 0) {
|
|
146
|
+
return signal.next();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check size limit
|
|
150
|
+
if (contentLength > config.limit) {
|
|
151
|
+
return config.onLimitExceeded(context, config.limit);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
// Parse JSON body
|
|
156
|
+
const json = await parseJson(context._request);
|
|
157
|
+
|
|
158
|
+
// Store parsed JSON in context
|
|
159
|
+
context.setState("json", json);
|
|
160
|
+
context.setState("body", json);
|
|
161
|
+
|
|
162
|
+
// Add JSON methods to context
|
|
163
|
+
context.jsonBody = json;
|
|
164
|
+
context.getJson = () => json;
|
|
165
|
+
|
|
166
|
+
await signal.next();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.message.includes("exceeds")) {
|
|
169
|
+
return config.onLimitExceeded(context, config.limit);
|
|
170
|
+
} else {
|
|
171
|
+
return config.onError(context, error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add utility functions to the middleware
|
|
178
|
+
createJsonMiddleware.parse = function (text, reviver) {
|
|
179
|
+
try {
|
|
180
|
+
return reviver ? JSON.parse(text, reviver) : JSON.parse(text);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(`JSON parse error: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
createJsonMiddleware.stringify = function (value, replacer, space) {
|
|
187
|
+
try {
|
|
188
|
+
return JSON.stringify(value, replacer, space);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new Error(`JSON stringify error: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
createJsonMiddleware.isValid = function (text) {
|
|
195
|
+
try {
|
|
196
|
+
JSON.parse(text);
|
|
197
|
+
return true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
createJsonMiddleware.format = function (json, space = 2) {
|
|
204
|
+
return JSON.stringify(json, null, space);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export default createJsonMiddleware;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 辅助函数:解析算法配置
|
|
5
|
+
*/
|
|
6
|
+
function parseAlgorithms(algorithmsString) {
|
|
7
|
+
if (!algorithmsString) return ["HS256"];
|
|
8
|
+
return algorithmsString.split(",").map((alg) => alg.trim());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create JWT middleware for AetherJS
|
|
13
|
+
* @param {Object} options - JWT configuration options
|
|
14
|
+
* @returns {Function} - JWT middleware function
|
|
15
|
+
*/
|
|
16
|
+
function createJwtMiddleware(options = {}) {
|
|
17
|
+
// Load configuration from environment variables
|
|
18
|
+
const envConfig = {
|
|
19
|
+
enabled: process.env.JWT_ENABLED,
|
|
20
|
+
secret: process.env.JWT_SECRET,
|
|
21
|
+
algorithms: process.env.JWT_ALGORITHMS,
|
|
22
|
+
audience: process.env.JWT_AUDIENCE,
|
|
23
|
+
issuer: process.env.JWT_ISSUER,
|
|
24
|
+
expiresIn: process.env.JWT_EXPIRES_IN,
|
|
25
|
+
ignoreExpiration: process.env.JWT_IGNORE_EXPIRATION,
|
|
26
|
+
credentialsRequired: process.env.JWT_CREDENTIALS_REQUIRED,
|
|
27
|
+
tokenHeader: process.env.JWT_TOKEN_HEADER,
|
|
28
|
+
tokenQuery: process.env.JWT_TOKEN_QUERY,
|
|
29
|
+
tokenCookie: process.env.JWT_TOKEN_COOKIE,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Default configuration
|
|
33
|
+
const defaults = {
|
|
34
|
+
enabled: envConfig.enabled !== "false",
|
|
35
|
+
secret:
|
|
36
|
+
envConfig.secret || "your-super-secret-jwt-key-change-in-production",
|
|
37
|
+
algorithms: parseAlgorithms(envConfig.algorithms),
|
|
38
|
+
algorithm: envConfig.algorithms
|
|
39
|
+
? parseAlgorithms(envConfig.algorithms)[0]
|
|
40
|
+
: "HS256",
|
|
41
|
+
audience: envConfig.audience,
|
|
42
|
+
issuer: envConfig.issuer,
|
|
43
|
+
expiresIn: envConfig.expiresIn || "7d",
|
|
44
|
+
ignoreExpiration: envConfig.ignoreExpiration === "true",
|
|
45
|
+
credentialsRequired: envConfig.credentialsRequired !== "false",
|
|
46
|
+
tokenHeader: envConfig.tokenHeader || "authorization",
|
|
47
|
+
tokenQuery: envConfig.tokenQuery || "token",
|
|
48
|
+
tokenCookie: envConfig.tokenCookie || "token",
|
|
49
|
+
|
|
50
|
+
// Token extraction methods
|
|
51
|
+
extractors: [
|
|
52
|
+
(context) => {
|
|
53
|
+
const header = context.getHeader(defaults.tokenHeader);
|
|
54
|
+
if (header && header.startsWith("Bearer ")) {
|
|
55
|
+
return header.substring(7);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
(context) => {
|
|
60
|
+
return context.query[defaults.tokenQuery] || null;
|
|
61
|
+
},
|
|
62
|
+
(context) => {
|
|
63
|
+
const cookies = parseCookies(context.getHeader("cookie") || "");
|
|
64
|
+
return cookies[defaults.tokenCookie] || null;
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
|
|
68
|
+
validationOptions: {
|
|
69
|
+
algorithms: parseAlgorithms(envConfig.algorithms),
|
|
70
|
+
audience: envConfig.audience,
|
|
71
|
+
issuer: envConfig.issuer,
|
|
72
|
+
ignoreExpiration: envConfig.ignoreExpiration === "true",
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
onError: (context, error) => {
|
|
76
|
+
context.setStatus(401).json({
|
|
77
|
+
error: "Unauthorized",
|
|
78
|
+
message: error.message,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
onMissing: (context) => {
|
|
83
|
+
context.setStatus(401).json({
|
|
84
|
+
error: "Unauthorized",
|
|
85
|
+
message: "No token provided",
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const config = { ...defaults, ...options };
|
|
91
|
+
|
|
92
|
+
function parseCookies(cookieHeader) {
|
|
93
|
+
const cookies = {};
|
|
94
|
+
if (!cookieHeader) return cookies;
|
|
95
|
+
|
|
96
|
+
const pairs = cookieHeader.split(";");
|
|
97
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
98
|
+
const eqIdx = pairs[i].indexOf("=");
|
|
99
|
+
if (eqIdx === -1) continue;
|
|
100
|
+
const key = pairs[i].substring(0, eqIdx).trim();
|
|
101
|
+
const value = pairs[i].substring(eqIdx + 1).trim();
|
|
102
|
+
// 只保留第一次见到的 Cookie,防止同名覆盖攻击/隐患
|
|
103
|
+
if (!cookies[key]) {
|
|
104
|
+
cookies[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return cookies;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractToken(context) {
|
|
111
|
+
for (let i = 0; i < config.extractors.length; i++) {
|
|
112
|
+
const token = config.extractors[i](context);
|
|
113
|
+
if (token) return token;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 💡 极致优化:去掉异步 Promise,改为同步校验,杜绝线程池排队阻塞
|
|
120
|
+
*/
|
|
121
|
+
function verifyTokenSync(token) {
|
|
122
|
+
return jwt.verify(token, config.secret, config.validationOptions);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 签发通常不属于超高频热点,可保留异步或同步。这里维持原异步以保障向下兼容性。
|
|
127
|
+
*/
|
|
128
|
+
async function signToken(payload, signOptions = {}) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const algorithm = signOptions.algorithm || config.algorithm || "HS256";
|
|
131
|
+
const options = {
|
|
132
|
+
algorithm: algorithm,
|
|
133
|
+
expiresIn: config.expiresIn,
|
|
134
|
+
audience: config.audience,
|
|
135
|
+
issuer: config.issuer,
|
|
136
|
+
...signOptions,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
jwt.sign(payload, config.secret, options, (error, token) => {
|
|
140
|
+
if (error) reject(error);
|
|
141
|
+
else resolve(token);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return async function jwtMiddleware(context, next) {
|
|
147
|
+
if (!config.enabled) {
|
|
148
|
+
return typeof next === "function" ? next() : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const token = extractToken(context);
|
|
152
|
+
|
|
153
|
+
if (!token) {
|
|
154
|
+
if (config.credentialsRequired) {
|
|
155
|
+
return config.onMissing(context);
|
|
156
|
+
}
|
|
157
|
+
return typeof next === "function" ? next() : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// 💡 同步执行,无等待延迟
|
|
162
|
+
const decoded = verifyTokenSync(token);
|
|
163
|
+
|
|
164
|
+
context.setState("jwt", decoded);
|
|
165
|
+
context.setState("user", decoded);
|
|
166
|
+
context.setState("token", token);
|
|
167
|
+
|
|
168
|
+
context.jwt = {
|
|
169
|
+
payload: decoded,
|
|
170
|
+
token: token,
|
|
171
|
+
refresh: async (newPayload = {}) => {
|
|
172
|
+
const payload = { ...decoded, ...newPayload };
|
|
173
|
+
const newToken = await signToken(payload);
|
|
174
|
+
context.setState("jwt", payload);
|
|
175
|
+
context.setState("token", newToken);
|
|
176
|
+
return newToken;
|
|
177
|
+
},
|
|
178
|
+
verify: async () => {
|
|
179
|
+
return verifyTokenSync(token);
|
|
180
|
+
},
|
|
181
|
+
expiresAt: () => (decoded.exp ? new Date(decoded.exp * 1000) : null),
|
|
182
|
+
isExpired: () =>
|
|
183
|
+
decoded.exp ? Date.now() >= decoded.exp * 1000 : false,
|
|
184
|
+
issuedAt: () => (decoded.iat ? new Date(decoded.iat * 1000) : null),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (typeof next === "function") {
|
|
188
|
+
await next();
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return config.onError(context, error);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 静态方法修正:与运行时实例解耦,统一走静态安全配置
|
|
197
|
+
createJwtMiddleware.sign = async function (payload, options = {}) {
|
|
198
|
+
const secret =
|
|
199
|
+
process.env.JWT_SECRET || "your-super-secret-jwt-key-change-in-production";
|
|
200
|
+
const algorithm = options.algorithm || "HS256";
|
|
201
|
+
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
jwt.sign(payload, secret, { algorithm, ...options }, (error, token) => {
|
|
204
|
+
if (error) reject(error);
|
|
205
|
+
else resolve(token);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
createJwtMiddleware.verify = function (token, options = {}) {
|
|
211
|
+
const secret =
|
|
212
|
+
process.env.JWT_SECRET || "your-super-secret-jwt-key-change-in-production";
|
|
213
|
+
const algorithms = options.algorithms || ["HS256"];
|
|
214
|
+
// 静态暴露同样支持同步直出
|
|
215
|
+
return jwt.verify(token, secret, { algorithms, ...options });
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
createJwtMiddleware.decode = function (token, options = {}) {
|
|
219
|
+
return jwt.decode(token, options);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export default createJwtMiddleware;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 高性能内存存储 - 基于惰性双重校验与微任务抽样清理
|
|
3
|
+
* O(1) 复杂度,彻底消灭定时器引发的 Long Task 阻塞
|
|
4
|
+
*/
|
|
5
|
+
class MemoryStore {
|
|
6
|
+
constructor(windowMs) {
|
|
7
|
+
this.windowMs = windowMs;
|
|
8
|
+
this.hits = new Map();
|
|
9
|
+
this.keysIterator = null; // 用于增量迭代清理
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async increment(key) {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
let record = this.hits.get(key);
|
|
15
|
+
|
|
16
|
+
if (!record) {
|
|
17
|
+
this.hits.set(key, {
|
|
18
|
+
count: 1,
|
|
19
|
+
resetTime: now + this.windowMs,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 💡 核心优化:万分之一的概率触发“增量、分批”微清理,不阻塞主线程
|
|
23
|
+
if (Math.random() < 0.0001) {
|
|
24
|
+
this._incrementalCleanup(now, 50); // 每次最多只检查 50 个 Key
|
|
25
|
+
}
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 💡 核心优化:惰性过期检查。如果当前 Key 已经过了生命周期,直接就地重置
|
|
30
|
+
if (now > record.resetTime) {
|
|
31
|
+
record.count = 1;
|
|
32
|
+
record.resetTime = now + this.windowMs;
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
record.count++;
|
|
37
|
+
return record.count;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async decrement(key) {
|
|
41
|
+
const record = this.hits.get(key);
|
|
42
|
+
if (record) {
|
|
43
|
+
record.count = Math.max(0, record.count - 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async resetKey(key) {
|
|
48
|
+
this.hits.delete(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async resetAll() {
|
|
52
|
+
this.hits.clear();
|
|
53
|
+
this.keysIterator = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getRemaining(key, max) {
|
|
57
|
+
const record = this.hits.get(key);
|
|
58
|
+
if (!record || Date.now() > record.resetTime) return max;
|
|
59
|
+
return Math.max(0, max - record.count);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getResetTime(key) {
|
|
63
|
+
const record = this.hits.get(key);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (!record || now > record.resetTime) return now + this.windowMs;
|
|
66
|
+
return record.resetTime;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 💡 分批、渐进式迭代清理,避免单次循环过大
|
|
71
|
+
*/
|
|
72
|
+
_incrementalCleanup(now, limit) {
|
|
73
|
+
if (!this.keysIterator) {
|
|
74
|
+
this.keysIterator = this.hits.keys();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let count = 0;
|
|
78
|
+
while (count < limit) {
|
|
79
|
+
const next = this.keysIterator.next();
|
|
80
|
+
if (next.done) {
|
|
81
|
+
this.keysIterator = null; // 遍历完了,清空以便下次重新开始
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const key = next.value;
|
|
86
|
+
const record = this.hits.get(key);
|
|
87
|
+
if (record && now > record.resetTime) {
|
|
88
|
+
this.hits.delete(key);
|
|
89
|
+
}
|
|
90
|
+
count++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create rate limiting middleware for AetherJS
|
|
97
|
+
* @param {Object} options - Rate limiting configuration
|
|
98
|
+
* @returns {Function} - Rate limiting middleware function
|
|
99
|
+
*/
|
|
100
|
+
function createRateLimitMiddleware(options = {}) {
|
|
101
|
+
const envConfig = {
|
|
102
|
+
enabled: process.env.RATE_LIMIT_ENABLED,
|
|
103
|
+
windowMs: process.env.RATE_LIMIT_WINDOW_MS,
|
|
104
|
+
max: process.env.RATE_LIMIT_MAX_REQUESTS,
|
|
105
|
+
message: process.env.RATE_LIMIT_MESSAGE,
|
|
106
|
+
statusCode: process.env.RATE_LIMIT_STATUS_CODE,
|
|
107
|
+
skipSuccessfulRequests: process.env.RATE_LIMIT_SKIP_SUCCESSFUL,
|
|
108
|
+
skipFailedRequests: process.env.RATE_LIMIT_SKIP_FAILED,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const defaults = {
|
|
112
|
+
enabled: envConfig.enabled !== "false",
|
|
113
|
+
windowMs: envConfig.windowMs
|
|
114
|
+
? parseInt(envConfig.windowMs)
|
|
115
|
+
: 15 * 60 * 1000,
|
|
116
|
+
max: envConfig.max ? parseInt(envConfig.max) : 100,
|
|
117
|
+
message: envConfig.message || "Too many requests, please try again later.",
|
|
118
|
+
statusCode: envConfig.statusCode ? parseInt(envConfig.statusCode) : 429,
|
|
119
|
+
skipSuccessfulRequests: envConfig.skipSuccessfulRequests === "true",
|
|
120
|
+
skipFailedRequests: envConfig.skipFailedRequests === "true",
|
|
121
|
+
store: new MemoryStore(
|
|
122
|
+
envConfig.windowMs ? parseInt(envConfig.windowMs) : 15 * 60 * 1000,
|
|
123
|
+
),
|
|
124
|
+
|
|
125
|
+
keyGenerator: (context) => {
|
|
126
|
+
// 兼容 AetherContext 的标准方法与原生底层属性访问
|
|
127
|
+
const getHeader =
|
|
128
|
+
typeof context.getHeader === "function"
|
|
129
|
+
? context.getHeader.bind(context)
|
|
130
|
+
: (k) => context.headers?.[k] || context.res?.getHeader?.(k);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
context.ip ||
|
|
134
|
+
getHeader("x-forwarded-for") ||
|
|
135
|
+
(context._request &&
|
|
136
|
+
context._request.socket &&
|
|
137
|
+
context._request.socket.remoteAddress) ||
|
|
138
|
+
(context.req &&
|
|
139
|
+
context.req.socket &&
|
|
140
|
+
context.req.socket.remoteAddress) ||
|
|
141
|
+
"unknown"
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
skip: (context) => {
|
|
146
|
+
const skipPaths = ["/health", "/metrics", "/favicon.ico"];
|
|
147
|
+
return skipPaths.includes(context.path || context.url);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
handler: (context, options) => {
|
|
151
|
+
context.setStatus(options.statusCode);
|
|
152
|
+
context.setHeader(
|
|
153
|
+
"Retry-After",
|
|
154
|
+
Math.ceil(options.windowMs / 1000).toString(),
|
|
155
|
+
);
|
|
156
|
+
context.json({
|
|
157
|
+
error: options.message,
|
|
158
|
+
retryAfter: Math.ceil(options.windowMs / 1000),
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const opts = { ...defaults, ...options };
|
|
164
|
+
|
|
165
|
+
return async function rateLimitMiddleware(context, next) {
|
|
166
|
+
if (!opts.enabled) {
|
|
167
|
+
return typeof next === "function" ? next() : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (opts.skip && opts.skip(context)) {
|
|
171
|
+
return typeof next === "function" ? next() : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const key = opts.keyGenerator(context);
|
|
175
|
+
const current = await opts.store.increment(key);
|
|
176
|
+
|
|
177
|
+
const remaining = Math.max(0, opts.max - current);
|
|
178
|
+
const resetTime = await opts.store.getResetTime(key);
|
|
179
|
+
|
|
180
|
+
// 🛡️ 核心修复 1:安全的武器级 Header 写入。支持全小写规范,避免多方扫描时发生二义性冲突
|
|
181
|
+
const getHeader =
|
|
182
|
+
typeof context.getHeader === "function"
|
|
183
|
+
? context.getHeader.bind(context)
|
|
184
|
+
: () => null;
|
|
185
|
+
|
|
186
|
+
if (!getHeader("x-ratelimit-limit") && !getHeader("X-RateLimit-Limit")) {
|
|
187
|
+
context.setHeader("X-RateLimit-Limit", opts.max.toString());
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
!getHeader("x-ratelimit-remaining") &&
|
|
191
|
+
!getHeader("X-RateLimit-Remaining")
|
|
192
|
+
) {
|
|
193
|
+
context.setHeader("X-RateLimit-Remaining", remaining.toString());
|
|
194
|
+
}
|
|
195
|
+
if (!getHeader("x-ratelimit-reset") && !getHeader("X-RateLimit-Reset")) {
|
|
196
|
+
context.setHeader(
|
|
197
|
+
"X-RateLimit-Reset",
|
|
198
|
+
Math.ceil(resetTime / 1000).toString(),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 🛡️ 核心修复 2:前置触发熔断,直接阻断,不再向下游和回溯传递
|
|
203
|
+
if (current > opts.max) {
|
|
204
|
+
opts.handler(context, opts);
|
|
205
|
+
if (typeof context.terminate === "function") context.terminate();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 执行下游中间件
|
|
210
|
+
if (typeof next === "function") {
|
|
211
|
+
await next();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 🛡️ 核心修复 3:回溯精准补偿。为了不破坏外层缓存提炼,业务流回溯完毕后必须立刻 return 干净
|
|
215
|
+
const statusCode =
|
|
216
|
+
context.statusCode ||
|
|
217
|
+
(context._response && context._response.statusCode) ||
|
|
218
|
+
(context.res && context.res.statusCode);
|
|
219
|
+
|
|
220
|
+
if (opts.skipSuccessfulRequests && statusCode >= 200 && statusCode < 300) {
|
|
221
|
+
await opts.store.decrement(key);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (opts.skipFailedRequests && statusCode >= 400) {
|
|
225
|
+
await opts.store.decrement(key);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return; // 💥 强力斩断任何微任务隐式溢出,保证 V8 垃圾回收的高效性
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default createRateLimitMiddleware;
|