@aetherframework/middleware 1.0.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/.env.example +88 -0
- package/LICENSE +21 -0
- package/README.md +693 -0
- package/docs/readme/README.md +679 -0
- package/docs/readme/README_zh.md +680 -0
- package/examples/advanced-router-demo.js +119 -0
- package/examples/advanced-server.js +272 -0
- package/examples/basic-server.js +134 -0
- package/examples/benchmark.js +85 -0
- package/examples/router-demo.js +369 -0
- package/index.js +67 -0
- package/package.json +59 -0
- package/src/core/AetherCompiler.js +118 -0
- package/src/core/AetherContext.js +242 -0
- package/src/core/AetherPipeline.js +375 -0
- package/src/core/AetherRouter.js +347 -0
- package/src/core/AetherStore.js +204 -0
- package/src/middleware/body-parser.js +299 -0
- package/src/middleware/compression.js +248 -0
- package/src/middleware/cors.js +162 -0
- package/src/middleware/json.js +214 -0
- package/src/middleware/jwt.js +929 -0
- package/src/middleware/params.js +227 -0
- package/src/middleware/rate-limit.js +167 -0
- package/src/middleware/router.js +36 -0
- package/src/middleware/security.js +116 -0
- package/src/middleware/session.js +167 -0
- package/src/utils/atomic-ops.js +127 -0
- package/src/utils/env-loader.js +128 -0
- package/src/utils/memory-pool.js +93 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/middleware/middleware/middleware/params.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function createParamsMiddleware(options = {}) {
|
|
9
|
+
const defaults = {
|
|
10
|
+
// Parameter parsing configuration
|
|
11
|
+
parseNumbers: true, // Auto-convert numbers
|
|
12
|
+
parseBooleans: true, // Auto-convert booleans
|
|
13
|
+
parseArrays: true, // Auto-convert arrays
|
|
14
|
+
trimStrings: true, // Auto-trim strings
|
|
15
|
+
|
|
16
|
+
// Validation configuration
|
|
17
|
+
validation: {
|
|
18
|
+
enabled: true,
|
|
19
|
+
onError: (context, errors) => {
|
|
20
|
+
context.setStatus(400).json({
|
|
21
|
+
error: "Validation Error",
|
|
22
|
+
message: "Invalid request parameters",
|
|
23
|
+
details: errors
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const config = { ...defaults, ...options };
|
|
30
|
+
|
|
31
|
+
// Parameter type conversion
|
|
32
|
+
function parseValue(value, options) {
|
|
33
|
+
if (value === undefined || value === null) return value;
|
|
34
|
+
|
|
35
|
+
let result = String(value);
|
|
36
|
+
|
|
37
|
+
// Trim strings
|
|
38
|
+
if (options.trimStrings) {
|
|
39
|
+
result = result.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse numbers
|
|
43
|
+
if (options.parseNumbers && /^-?\d+(\.\d+)?$/.test(result)) {
|
|
44
|
+
const num = parseFloat(result);
|
|
45
|
+
if (!isNaN(num)) return num;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse booleans
|
|
49
|
+
if (options.parseBooleans) {
|
|
50
|
+
const lower = result.toLowerCase();
|
|
51
|
+
if (lower === "true") return true;
|
|
52
|
+
if (lower === "false") return false;
|
|
53
|
+
if (lower === "1") return true;
|
|
54
|
+
if (lower === "0") return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse arrays (comma-separated)
|
|
58
|
+
if (options.parseArrays && result.includes(",")) {
|
|
59
|
+
return result.split(",").map(item => parseValue(item.trim(), options));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate parameters
|
|
66
|
+
function validateParams(params, rules) {
|
|
67
|
+
if (!config.validation.enabled || !rules) return null;
|
|
68
|
+
|
|
69
|
+
const errors = [];
|
|
70
|
+
|
|
71
|
+
for (const [key, rule] of Object.entries(rules)) {
|
|
72
|
+
const value = params[key];
|
|
73
|
+
const isRequired = rule.required !== false;
|
|
74
|
+
|
|
75
|
+
// Required check
|
|
76
|
+
if (isRequired && (value === undefined || value === null || value === "")) {
|
|
77
|
+
errors.push({
|
|
78
|
+
field: key,
|
|
79
|
+
message: rule.message || `${key} is required`,
|
|
80
|
+
type: "required"
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Type checking
|
|
86
|
+
if (value !== undefined && value !== null) {
|
|
87
|
+
if (rule.type === "number" && typeof value !== "number") {
|
|
88
|
+
errors.push({
|
|
89
|
+
field: key,
|
|
90
|
+
message: rule.message || `${key} must be a number`,
|
|
91
|
+
type: "type"
|
|
92
|
+
});
|
|
93
|
+
} else if (rule.type === "string" && typeof value !== "string") {
|
|
94
|
+
errors.push({
|
|
95
|
+
field: key,
|
|
96
|
+
message: rule.message || `${key} must be a string`,
|
|
97
|
+
type: "type"
|
|
98
|
+
});
|
|
99
|
+
} else if (rule.type === "boolean" && typeof value !== "boolean") {
|
|
100
|
+
errors.push({
|
|
101
|
+
field: key,
|
|
102
|
+
message: rule.message || `${key} must be a boolean`,
|
|
103
|
+
type: "type"
|
|
104
|
+
});
|
|
105
|
+
} else if (rule.type === "array" && !Array.isArray(value)) {
|
|
106
|
+
errors.push({
|
|
107
|
+
field: key,
|
|
108
|
+
message: rule.message || `${key} must be an array`,
|
|
109
|
+
type: "type"
|
|
110
|
+
});
|
|
111
|
+
} else if (rule.type === "object" && (typeof value !== "object" || Array.isArray(value))) {
|
|
112
|
+
errors.push({
|
|
113
|
+
field: key,
|
|
114
|
+
message: rule.message || `${key} must be an object`,
|
|
115
|
+
type: "type"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Length validation
|
|
120
|
+
if (rule.minLength !== undefined && String(value).length < rule.minLength) {
|
|
121
|
+
errors.push({
|
|
122
|
+
field: key,
|
|
123
|
+
message: rule.message || `${key} must be at least ${rule.minLength} characters`,
|
|
124
|
+
type: "minLength"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (rule.maxLength !== undefined && String(value).length > rule.maxLength) {
|
|
129
|
+
errors.push({
|
|
130
|
+
field: key,
|
|
131
|
+
message: rule.message || `${key} must be at most ${rule.maxLength} characters`,
|
|
132
|
+
type: "maxLength"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Range validation
|
|
137
|
+
if (rule.min !== undefined && Number(value) < rule.min) {
|
|
138
|
+
errors.push({
|
|
139
|
+
field: key,
|
|
140
|
+
message: rule.message || `${key} must be at least ${rule.min}`,
|
|
141
|
+
type: "min"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (rule.max !== undefined && Number(value) > rule.max) {
|
|
146
|
+
errors.push({
|
|
147
|
+
field: key,
|
|
148
|
+
message: rule.message || `${key} must be at most ${rule.max}`,
|
|
149
|
+
type: "max"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Pattern validation
|
|
154
|
+
if (rule.pattern && !new RegExp(rule.pattern).test(String(value))) {
|
|
155
|
+
errors.push({
|
|
156
|
+
field: key,
|
|
157
|
+
message: rule.message || `${key} does not match the required pattern`,
|
|
158
|
+
type: "pattern"
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Enum validation
|
|
163
|
+
if (rule.enum && !rule.enum.includes(value)) {
|
|
164
|
+
errors.push({
|
|
165
|
+
field: key,
|
|
166
|
+
message: rule.message || `${key} must be one of: ${rule.enum.join(", ")}`,
|
|
167
|
+
type: "enum"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return errors.length > 0 ? errors : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return async function paramsMiddleware(context, next) {
|
|
177
|
+
// Merge all parameters
|
|
178
|
+
const allParams = {
|
|
179
|
+
...context.params || {},
|
|
180
|
+
...context.query || {},
|
|
181
|
+
...(context.getState("parsedBody") || {})
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Parameter type conversion
|
|
185
|
+
const parsedParams = {};
|
|
186
|
+
for (const [key, value] of Object.entries(allParams)) {
|
|
187
|
+
parsedParams[key] = parseValue(value, config);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Set to context
|
|
191
|
+
context.allParams = parsedParams;
|
|
192
|
+
|
|
193
|
+
// Get validation rules (from route metadata)
|
|
194
|
+
const validationRules = context.route?.validationRules;
|
|
195
|
+
|
|
196
|
+
// Execute validation
|
|
197
|
+
if (validationRules) {
|
|
198
|
+
const errors = validateParams(parsedParams, validationRules);
|
|
199
|
+
if (errors) {
|
|
200
|
+
return config.validation.onError(context, errors);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Add convenience methods
|
|
205
|
+
context.getParam = (key, defaultValue) => {
|
|
206
|
+
return parsedParams[key] !== undefined ? parsedParams[key] : defaultValue;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
context.hasParam = (key) => {
|
|
210
|
+
return parsedParams[key] !== undefined;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
context.requireParam = (key) => {
|
|
214
|
+
const value = parsedParams[key];
|
|
215
|
+
if (value === undefined) {
|
|
216
|
+
throw new Error(`Parameter ${key} is required`);
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (typeof next === "function") {
|
|
222
|
+
await next();
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default createParamsMiddleware;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/middleware/middleware/rate-limit.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
|
|
10
|
+
// Ultimate optimization: Use the most primitive, zero object allocation for-loop for single-point cookie lookup, reducing GC overhead to absolute zero
|
|
11
|
+
function getCookieValue(cookieHeader, name) {
|
|
12
|
+
if (!cookieHeader) return undefined;
|
|
13
|
+
const target = name + "=";
|
|
14
|
+
const len = cookieHeader.length;
|
|
15
|
+
let pos = 0;
|
|
16
|
+
while (pos < len) {
|
|
17
|
+
pos = cookieHeader.indexOf(target, pos);
|
|
18
|
+
if (pos === -1) break;
|
|
19
|
+
// Ensure it's an independent cookie name, not a prefix of another
|
|
20
|
+
if (
|
|
21
|
+
pos === 0 ||
|
|
22
|
+
cookieHeader.charCodeAt(pos - 1) === 32 ||
|
|
23
|
+
cookieHeader.charCodeAt(pos - 1) === 59
|
|
24
|
+
) {
|
|
25
|
+
pos += target.length;
|
|
26
|
+
let end = cookieHeader.indexOf(";", pos);
|
|
27
|
+
if (end === -1) end = len;
|
|
28
|
+
return cookieHeader.substring(pos, end).trim();
|
|
29
|
+
}
|
|
30
|
+
pos += 1;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Abandon long UUID, use 16-byte high-performance hexadecimal ID, shorten cookie length, reduce network and compression middleware overhead
|
|
36
|
+
const genId = () => crypto.randomBytes(16).toString("hex");
|
|
37
|
+
|
|
38
|
+
class MemoryStore {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.cache = new Map();
|
|
41
|
+
}
|
|
42
|
+
async get(id) {
|
|
43
|
+
const s = this.cache.get(id);
|
|
44
|
+
if (!s) return null;
|
|
45
|
+
if (Date.now() > s.exp) {
|
|
46
|
+
this.cache.delete(id);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return s.data;
|
|
50
|
+
}
|
|
51
|
+
async set(id, data, ttl) {
|
|
52
|
+
this.cache.set(id, { data, exp: Date.now() + ttl });
|
|
53
|
+
}
|
|
54
|
+
async delete(id) {
|
|
55
|
+
this.cache.delete(id);
|
|
56
|
+
}
|
|
57
|
+
prune() {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (const [k, v] of this.cache) if (now > v.exp) this.cache.delete(k);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class SessionManager {
|
|
64
|
+
constructor(options = {}) {
|
|
65
|
+
const { store, ...restOptions } = options;
|
|
66
|
+
this.config = {
|
|
67
|
+
enabled: process.env.SESSION_ENABLED !== "false",
|
|
68
|
+
maxAge: parseInt(process.env.SESSION_MAX_AGE) || 86400000,
|
|
69
|
+
cookieName: process.env.SESSION_COOKIE_NAME || "aether_sid",
|
|
70
|
+
...restOptions,
|
|
71
|
+
};
|
|
72
|
+
this.config.store =
|
|
73
|
+
store && typeof store === "object" ? store : new MemoryStore();
|
|
74
|
+
if (this.config.store instanceof MemoryStore) {
|
|
75
|
+
this.cleanup = setInterval(
|
|
76
|
+
() => this.config.store.prune(),
|
|
77
|
+
60000,
|
|
78
|
+
).unref();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
middleware() {
|
|
83
|
+
if (!this.config.enabled)
|
|
84
|
+
return (ctx, next) => next && (next.next ? next.next() : next());
|
|
85
|
+
|
|
86
|
+
const { store, maxAge, cookieName } = this.config;
|
|
87
|
+
const cookieSuffix = `; HttpOnly; Secure; SameSite=Strict; Max-Age=${Math.floor(maxAge / 1000)}; Path=/`;
|
|
88
|
+
|
|
89
|
+
return async (ctx, next) => {
|
|
90
|
+
ctx.state ??= {};
|
|
91
|
+
|
|
92
|
+
const sid = getCookieValue(ctx.getHeader("cookie"), cookieName);
|
|
93
|
+
let sessionData = sid ? await store.get(sid) : null;
|
|
94
|
+
|
|
95
|
+
// 🚀 Strategy adjustment: If not obtained, first give an empty object, never write to storage early, never set cookie early
|
|
96
|
+
let isNew = false;
|
|
97
|
+
if (!sessionData) {
|
|
98
|
+
sessionData = {};
|
|
99
|
+
isNew = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sessionState = { id: sid, data: sessionData, dirty: false };
|
|
103
|
+
|
|
104
|
+
ctx.session = {
|
|
105
|
+
get: (key) => sessionState.data[key],
|
|
106
|
+
set: (key, val) => {
|
|
107
|
+
sessionState.data[key] = val;
|
|
108
|
+
sessionState.dirty = true;
|
|
109
|
+
},
|
|
110
|
+
delete: (key) => {
|
|
111
|
+
delete sessionState.data[key];
|
|
112
|
+
sessionState.dirty = true;
|
|
113
|
+
},
|
|
114
|
+
clear: () => {
|
|
115
|
+
sessionState.data = {};
|
|
116
|
+
sessionState.dirty = true;
|
|
117
|
+
},
|
|
118
|
+
destroy: async () => {
|
|
119
|
+
if (sessionState.id) await store.delete(sessionState.id);
|
|
120
|
+
sessionState.id = null;
|
|
121
|
+
sessionState.data = {};
|
|
122
|
+
sessionState.dirty = false;
|
|
123
|
+
ctx.setHeader(
|
|
124
|
+
"Set-Cookie",
|
|
125
|
+
`${cookieName}=; HttpOnly; Secure; SameSite=Strict; Max-Age=0; Path=/`,
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
regenerate: async () => {
|
|
129
|
+
if (sessionState.id) await store.delete(sessionState.id);
|
|
130
|
+
sessionState.id = genId();
|
|
131
|
+
await store.set(sessionState.id, sessionState.data, maxAge);
|
|
132
|
+
ctx.setHeader(
|
|
133
|
+
"Set-Cookie",
|
|
134
|
+
`${cookieName}=${sessionState.id}${cookieSuffix}`,
|
|
135
|
+
);
|
|
136
|
+
sessionState.dirty = false;
|
|
137
|
+
isNew = false;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Use the most reliable and well-isolated try...finally to ensure pipeline smoothness, while strictly limiting persistence logic to the "actually modified" checkpoint
|
|
142
|
+
try {
|
|
143
|
+
if (next) {
|
|
144
|
+
next.next ? await next.next() : await next();
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
if (sessionState.dirty) {
|
|
148
|
+
// If it's a new session and the route has written data, only now generate the ID and issue the cookie
|
|
149
|
+
if (isNew || !sessionState.id) {
|
|
150
|
+
sessionState.id = genId();
|
|
151
|
+
ctx.setHeader(
|
|
152
|
+
"Set-Cookie",
|
|
153
|
+
`${cookieName}=${sessionState.id}${cookieSuffix}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
await store.set(sessionState.id, sessionState.data, maxAge);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
destroy() {
|
|
163
|
+
if (this.cleanup) clearInterval(this.cleanup);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default SessionManager;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/middleware/middleware/router.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import AetherRouter from "../core/AetherRouter.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create router middleware for AetherJS
|
|
13
|
+
* @param {Object} routes - Route definitions
|
|
14
|
+
* @param {Object} options - Router options
|
|
15
|
+
* @returns {Function} - Router middleware function
|
|
16
|
+
*/
|
|
17
|
+
function createRouterMiddleware(routes = {}, options = {}) {
|
|
18
|
+
const router = new AetherRouter(options);
|
|
19
|
+
|
|
20
|
+
// Register routes from configuration
|
|
21
|
+
for (const [methodPath, handler] of Object.entries(routes)) {
|
|
22
|
+
const [method, path] = methodPath.split(" ");
|
|
23
|
+
if (method && path && handler) {
|
|
24
|
+
router[method.toLowerCase()](path, handler);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return router.middleware();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
createRouterMiddleware.Router = AetherRouter;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export default createRouterMiddleware;
|
|
36
|
+
export { createRouterMiddleware as createRouter, AetherRouter };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/middleware/middleware/security.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
function parsePermissionsPolicy(directivesString) {
|
|
10
|
+
if (!directivesString || typeof directivesString !== "string") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const directives = {};
|
|
14
|
+
const pairs = directivesString.split(/[,;]/);
|
|
15
|
+
for (const pair of pairs) {
|
|
16
|
+
const trimmed = pair.trim();
|
|
17
|
+
if (!trimmed) continue;
|
|
18
|
+
const equalIndex = trimmed.indexOf("=");
|
|
19
|
+
if (equalIndex !== -1) {
|
|
20
|
+
const feature = trimmed.substring(0, equalIndex).trim();
|
|
21
|
+
const value = trimmed.substring(equalIndex + 1).trim();
|
|
22
|
+
if (feature && value) directives[feature] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return Object.keys(directives).length > 0 ? directives : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create security headers middleware for AetherJS
|
|
30
|
+
* @param {Object} options - Security headers configuration
|
|
31
|
+
* @returns {Function} - Standard AetherJS middleware (ctx, next)
|
|
32
|
+
*/
|
|
33
|
+
function createSecurityMiddleware(options = {}) {
|
|
34
|
+
const envConfig = {
|
|
35
|
+
hstsEnabled: process.env.SECURITY_HSTS_ENABLED,
|
|
36
|
+
hstsMaxAge: process.env.SECURITY_HSTS_MAX_AGE,
|
|
37
|
+
noSniffEnabled: process.env.SECURITY_NO_SNIFF,
|
|
38
|
+
xssFilterEnabled: process.env.SECURITY_XSS_FILTER,
|
|
39
|
+
frameguardAction: process.env.SECURITY_FRAMEGUARD_ACTION,
|
|
40
|
+
hidePoweredBy: process.env.SECURITY_HIDE_POWERED_BY,
|
|
41
|
+
referrerPolicy: process.env.SECURITY_REFERRER_POLICY,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const defaults = {
|
|
45
|
+
hsts: {
|
|
46
|
+
enabled: envConfig.hstsEnabled !== "false",
|
|
47
|
+
maxAge: envConfig.hstsMaxAge ? parseInt(envConfig.hstsMaxAge) : 31536000,
|
|
48
|
+
includeSubDomains: true,
|
|
49
|
+
preload: false,
|
|
50
|
+
},
|
|
51
|
+
noSniff: { enabled: envConfig.noSniffEnabled !== "false" },
|
|
52
|
+
xssFilter: { enabled: envConfig.xssFilterEnabled !== "false" },
|
|
53
|
+
frameguard: { enabled: true, action: envConfig.frameguardAction || "DENY" },
|
|
54
|
+
hidePoweredBy: envConfig.hidePoweredBy !== "false",
|
|
55
|
+
referrerPolicy: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
value: envConfig.referrerPolicy || "strict-origin-when-cross-origin",
|
|
58
|
+
},
|
|
59
|
+
permissionsPolicy: {
|
|
60
|
+
enabled: true,
|
|
61
|
+
directives: { camera: "()", microphone: "()", geolocation: "()" },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Deep merge simple version for performance
|
|
66
|
+
const config = { ...defaults, ...options };
|
|
67
|
+
|
|
68
|
+
// 🚀 性能优化:预计算所有 Header 字符串,避免在请求响应循环中构造字符串
|
|
69
|
+
const staticHeaders = [];
|
|
70
|
+
|
|
71
|
+
if (config.hsts.enabled) {
|
|
72
|
+
let val = `max-age=${config.hsts.maxAge}${config.hsts.includeSubDomains ? "; includeSubDomains" : ""}${config.hsts.preload ? "; preload" : ""}`;
|
|
73
|
+
staticHeaders.push(["Strict-Transport-Security", val]);
|
|
74
|
+
}
|
|
75
|
+
if (config.noSniff.enabled)
|
|
76
|
+
staticHeaders.push(["X-Content-Type-Options", "nosniff"]);
|
|
77
|
+
if (config.xssFilter.enabled)
|
|
78
|
+
staticHeaders.push(["X-XSS-Protection", "1; mode=block"]);
|
|
79
|
+
if (config.frameguard.enabled)
|
|
80
|
+
staticHeaders.push([
|
|
81
|
+
"X-Frame-Options",
|
|
82
|
+
config.frameguard.action.toUpperCase(),
|
|
83
|
+
]);
|
|
84
|
+
if (config.referrerPolicy.enabled)
|
|
85
|
+
staticHeaders.push(["Referrer-Policy", config.referrerPolicy.value]);
|
|
86
|
+
if (config.permissionsPolicy.enabled) {
|
|
87
|
+
const p = Object.entries(config.permissionsPolicy.directives)
|
|
88
|
+
.map(([f, v]) => `${f}=${v}`)
|
|
89
|
+
.join(", ");
|
|
90
|
+
staticHeaders.push(["Permissions-Policy", p]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 💡 修复后的核心中间件函数
|
|
95
|
+
* 使用 (context, next) 签名替代旧版的 (context, signal)
|
|
96
|
+
*/
|
|
97
|
+
return async function securityMiddleware(context, next) {
|
|
98
|
+
console.log("Security middleware executing for URL:", context.url);
|
|
99
|
+
// 1. 批量写入预计算的 Header
|
|
100
|
+
for (let i = 0; i < staticHeaders.length; i++) {
|
|
101
|
+
context.setHeader(staticHeaders[i][0], staticHeaders[i][1]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. 移除敏感 Header
|
|
105
|
+
if (config.hidePoweredBy && context._response) {
|
|
106
|
+
context._response.removeHeader("X-Powered-By");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. 💡 修复点:调用标准 next() 而不是 signal.next()
|
|
110
|
+
if (typeof next === "function") {
|
|
111
|
+
return next();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default createSecurityMiddleware;
|