@aetherframework/middleware 1.0.2 → 1.0.4
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/README.md +0 -3
- package/docs/readme/README.md +14 -3
- package/docs/readme/README_zh.md +0 -3
- package/examples/advanced-server.js +122 -112
- package/examples/basic-server.js +322 -64
- package/index.js +9 -11
- package/package.json +1 -1
- package/src/core/AetherCompiler.js +117 -63
- package/src/core/AetherContext.js +221 -93
- package/src/core/AetherPipeline.js +261 -285
- package/src/core/AetherRouter.js +358 -256
- package/src/core/AetherStore.js +114 -67
- package/src/middleware/compression.js +165 -91
- package/src/middleware/json.js +180 -169
- package/src/middleware/rate-limit.js +76 -146
- package/src/middleware/security.js +33 -54
- package/src/middleware/session.js +89 -86
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license MIT
|
|
3
3
|
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
4
|
* @module @aetherframework/middleware/middleware/security.js
|
|
6
5
|
*/
|
|
7
6
|
|
|
7
|
+
// [V8-OPT] Ultra-fast boolean parser to prevent "false" string trap
|
|
8
|
+
function isEnvEnabled(key, defaultValue = false) {
|
|
9
|
+
const val = process.env[key];
|
|
10
|
+
if (val === undefined || val === null) return defaultValue;
|
|
11
|
+
if (val === 'true' || val === '1') return true;
|
|
12
|
+
if (val === 'false' || val === '0' || val === '') return false;
|
|
13
|
+
return ['true', '1', 'yes', 'on'].includes(val.toLowerCase().trim());
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
function parsePermissionsPolicy(directivesString) {
|
|
10
|
-
if (!directivesString || typeof directivesString !== "string")
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
17
|
+
if (!directivesString || typeof directivesString !== "string") return null;
|
|
13
18
|
const directives = {};
|
|
14
19
|
const pairs = directivesString.split(/[,;]/);
|
|
15
20
|
for (const pair of pairs) {
|
|
@@ -25,88 +30,62 @@ function parsePermissionsPolicy(directivesString) {
|
|
|
25
30
|
return Object.keys(directives).length > 0 ? directives : null;
|
|
26
31
|
}
|
|
27
32
|
|
|
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
33
|
function createSecurityMiddleware(options = {}) {
|
|
34
|
-
|
|
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
|
-
|
|
34
|
+
// [FIX] Strictly align with .env keys (removed 'SECURITY_' prefix and matched exact names)
|
|
44
35
|
const defaults = {
|
|
45
36
|
hsts: {
|
|
46
|
-
enabled:
|
|
47
|
-
maxAge:
|
|
37
|
+
enabled: isEnvEnabled('HSTS_ENABLED', false),
|
|
38
|
+
maxAge: parseInt(process.env.HSTS_MAX_AGE) || 31536000,
|
|
48
39
|
includeSubDomains: true,
|
|
49
40
|
preload: false,
|
|
50
41
|
},
|
|
51
|
-
noSniff: { enabled:
|
|
52
|
-
xssFilter: { enabled:
|
|
53
|
-
frameguard: {
|
|
54
|
-
|
|
42
|
+
noSniff: { enabled: isEnvEnabled('X_CONTENT_TYPE_OPTIONS_ENABLED', false) },
|
|
43
|
+
xssFilter: { enabled: isEnvEnabled('X_XSS_PROTECTION_ENABLED', false) },
|
|
44
|
+
frameguard: {
|
|
45
|
+
enabled: isEnvEnabled('X_FRAME_OPTIONS_ENABLED', false),
|
|
46
|
+
action: process.env.X_FRAME_OPTIONS || "DENY"
|
|
47
|
+
},
|
|
48
|
+
hidePoweredBy: isEnvEnabled('HIDE_POWERED_BY_ENABLED', false),
|
|
55
49
|
referrerPolicy: {
|
|
56
|
-
enabled:
|
|
57
|
-
value:
|
|
50
|
+
enabled: isEnvEnabled('REFERRER_POLICY_ENABLED', false),
|
|
51
|
+
value: process.env.REFERRER_POLICY || "strict-origin-when-cross-origin",
|
|
58
52
|
},
|
|
59
53
|
permissionsPolicy: {
|
|
60
|
-
enabled:
|
|
61
|
-
directives: { camera: "()", microphone: "()", geolocation: "()" },
|
|
54
|
+
enabled: isEnvEnabled('PERMISSIONS_POLICY_ENABLED', false),
|
|
55
|
+
directives: parsePermissionsPolicy(process.env.PERMISSIONS_POLICY) || { camera: "()", microphone: "()", geolocation: "()" },
|
|
62
56
|
},
|
|
63
57
|
};
|
|
64
58
|
|
|
65
|
-
// Deep merge simple version for performance
|
|
66
59
|
const config = { ...defaults, ...options };
|
|
67
60
|
|
|
68
|
-
//
|
|
61
|
+
// [V8-OPT] Pre-calculate static headers outside the request loop
|
|
69
62
|
const staticHeaders = [];
|
|
70
63
|
|
|
71
64
|
if (config.hsts.enabled) {
|
|
72
65
|
let val = `max-age=${config.hsts.maxAge}${config.hsts.includeSubDomains ? "; includeSubDomains" : ""}${config.hsts.preload ? "; preload" : ""}`;
|
|
73
66
|
staticHeaders.push(["Strict-Transport-Security", val]);
|
|
74
67
|
}
|
|
75
|
-
if (config.noSniff.enabled)
|
|
76
|
-
|
|
77
|
-
if (config.
|
|
78
|
-
|
|
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]);
|
|
68
|
+
if (config.noSniff.enabled) staticHeaders.push(["X-Content-Type-Options", "nosniff"]);
|
|
69
|
+
if (config.xssFilter.enabled) staticHeaders.push(["X-XSS-Protection", "1; mode=block"]);
|
|
70
|
+
if (config.frameguard.enabled) staticHeaders.push(["X-Frame-Options", config.frameguard.action.toUpperCase()]);
|
|
71
|
+
if (config.referrerPolicy.enabled) staticHeaders.push(["Referrer-Policy", config.referrerPolicy.value]);
|
|
86
72
|
if (config.permissionsPolicy.enabled) {
|
|
87
|
-
const p = Object.entries(config.permissionsPolicy.directives)
|
|
88
|
-
.map(([f, v]) => `${f}=${v}`)
|
|
89
|
-
.join(", ");
|
|
73
|
+
const p = Object.entries(config.permissionsPolicy.directives).map(([f, v]) => `${f}=${v}`).join(", ");
|
|
90
74
|
staticHeaders.push(["Permissions-Policy", p]);
|
|
91
75
|
}
|
|
92
76
|
|
|
93
|
-
/**
|
|
94
|
-
* 💡 修复后的核心中间件函数
|
|
95
|
-
* 使用 (context, next) 签名替代旧版的 (context, signal)
|
|
96
|
-
*/
|
|
97
77
|
return async function securityMiddleware(context, next) {
|
|
98
|
-
|
|
99
|
-
// 1. 批量写入预计算的 Header
|
|
78
|
+
// 1. Batch write pre-calculated headers
|
|
100
79
|
for (let i = 0; i < staticHeaders.length; i++) {
|
|
101
80
|
context.setHeader(staticHeaders[i][0], staticHeaders[i][1]);
|
|
102
81
|
}
|
|
103
82
|
|
|
104
|
-
// 2.
|
|
105
|
-
if (config.hidePoweredBy && context._response) {
|
|
83
|
+
// 2. Remove sensitive headers
|
|
84
|
+
if (config.hidePoweredBy && context._response && typeof context._response.removeHeader === 'function') {
|
|
106
85
|
context._response.removeHeader("X-Powered-By");
|
|
107
86
|
}
|
|
108
87
|
|
|
109
|
-
// 3.
|
|
88
|
+
// 3. Call next
|
|
110
89
|
if (typeof next === "function") {
|
|
111
90
|
return next();
|
|
112
91
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license MIT
|
|
3
3
|
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
4
|
* @module @aetherframework/middleware/middleware/session
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import crypto from "crypto";
|
|
8
|
+
import redis from "redis";
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// [V8-OPT] Zero allocation cookie lookup
|
|
11
11
|
function getCookieValue(cookieHeader, name) {
|
|
12
12
|
if (!cookieHeader) return undefined;
|
|
13
13
|
const target = name + "=";
|
|
@@ -16,12 +16,7 @@ function getCookieValue(cookieHeader, name) {
|
|
|
16
16
|
while (pos < len) {
|
|
17
17
|
pos = cookieHeader.indexOf(target, pos);
|
|
18
18
|
if (pos === -1) break;
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
pos === 0 ||
|
|
22
|
-
cookieHeader.charCodeAt(pos - 1) === 32 ||
|
|
23
|
-
cookieHeader.charCodeAt(pos - 1) === 59
|
|
24
|
-
) {
|
|
19
|
+
if (pos === 0 || cookieHeader.charCodeAt(pos - 1) === 32 || cookieHeader.charCodeAt(pos - 1) === 59) {
|
|
25
20
|
pos += target.length;
|
|
26
21
|
let end = cookieHeader.indexOf(";", pos);
|
|
27
22
|
if (end === -1) end = len;
|
|
@@ -32,126 +27,136 @@ function getCookieValue(cookieHeader, name) {
|
|
|
32
27
|
return undefined;
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
//Abandon long UUID, use 16-byte high-performance hexadecimal ID, shorten cookie length, reduce network and compression middleware overhead
|
|
36
30
|
const genId = () => crypto.randomBytes(16).toString("hex");
|
|
37
31
|
|
|
32
|
+
// ... (MemoryStore and RedisStore remain exactly the same as your original code) ...
|
|
38
33
|
class MemoryStore {
|
|
39
|
-
constructor() {
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
async
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
34
|
+
constructor() { this.cache = new Map(); }
|
|
35
|
+
async get(id) { const s = this.cache.get(id); if (!s) return null; if (Date.now() > s.exp) { this.cache.delete(id); return null; } return s.data; }
|
|
36
|
+
async set(id, data, ttl) { this.cache.set(id, { data, exp: Date.now() + ttl }); }
|
|
37
|
+
async delete(id) { this.cache.delete(id); }
|
|
38
|
+
prune() { const now = Date.now(); for (const [k, v] of this.cache) if (now > v.exp) this.cache.delete(k); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class RedisStore {
|
|
42
|
+
constructor(client) { this.client = client; this.prefix = "sess:"; }
|
|
43
|
+
async get(id) { try { const data = await this.client.get(`${this.prefix}${id}`); return data ? JSON.parse(data) : null; } catch (err) { return null; } }
|
|
44
|
+
async set(id, data, ttl) { try { await this.client.setEx(`${this.prefix}${id}`, Math.floor(ttl / 1000), JSON.stringify(data)); } catch (err) {} }
|
|
45
|
+
async delete(id) { try { await this.client.del(`${this.prefix}${id}`); } catch (err) {} }
|
|
46
|
+
prune() {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// [V8-OPT] Safe boolean parser
|
|
50
|
+
function isEnvEnabled(key, defaultValue = false) {
|
|
51
|
+
const val = process.env[key];
|
|
52
|
+
if (val === undefined || val === null) return defaultValue;
|
|
53
|
+
if (val === 'true' || val === '1') return true;
|
|
54
|
+
if (val === 'false' || val === '0' || val === '') return false;
|
|
55
|
+
return ['true', '1', 'yes', 'on'].includes(val.toLowerCase().trim());
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
export class SessionManager {
|
|
64
59
|
constructor(options = {}) {
|
|
65
60
|
const { store, ...restOptions } = options;
|
|
61
|
+
|
|
66
62
|
this.config = {
|
|
67
|
-
|
|
63
|
+
// [FIX] Use strict boolean parsing
|
|
64
|
+
enabled: isEnvEnabled('SESSION_ENABLED', false),
|
|
68
65
|
maxAge: parseInt(process.env.SESSION_MAX_AGE) || 86400000,
|
|
69
66
|
cookieName: process.env.SESSION_COOKIE_NAME || "aether_sid",
|
|
67
|
+
cookieDomain: process.env.SESSION_COOKIE_DOMAIN || null,
|
|
68
|
+
cookiePath: process.env.SESSION_COOKIE_PATH || "/",
|
|
69
|
+
cookieSecure: isEnvEnabled('SESSION_COOKIE_SECURE', false),
|
|
70
|
+
cookieSameSite: process.env.SESSION_COOKIE_SAME_SITE || "Lax",
|
|
70
71
|
...restOptions,
|
|
71
72
|
};
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
|
|
74
|
+
const redisEnabled = isEnvEnabled('REDIS_ENABLED', false);
|
|
75
|
+
const redisHost = process.env.REDIS_HOST;
|
|
76
|
+
const redisPort = process.env.REDIS_PORT;
|
|
77
|
+
|
|
78
|
+
if (redisEnabled && redisHost && redisPort) {
|
|
79
|
+
try {
|
|
80
|
+
const redisClient = redis.createClient({
|
|
81
|
+
socket: { host: redisHost, port: parseInt(redisPort) },
|
|
82
|
+
database: parseInt(process.env.REDIS_DB) || 0,
|
|
83
|
+
password: process.env.REDIS_PASSWORD || undefined,
|
|
84
|
+
});
|
|
85
|
+
redisClient.connect().catch((err) => console.error("[SessionManager] Redis connect error:", err));
|
|
86
|
+
this.config.store = new RedisStore(redisClient);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
this.config.store = store || new MemoryStore();
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
this.config.store = store || new MemoryStore();
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
if (this.config.store instanceof MemoryStore) {
|
|
75
|
-
this.cleanup = setInterval(
|
|
76
|
-
() => this.config.store.prune(),
|
|
77
|
-
60000,
|
|
78
|
-
).unref();
|
|
95
|
+
this.cleanup = setInterval(() => this.config.store.prune(), 60000).unref();
|
|
79
96
|
}
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
middleware() {
|
|
83
|
-
|
|
84
|
-
return (ctx, next) => next && (next.next ? next.next() : next());
|
|
100
|
+
const { enabled, store, maxAge, cookieName, cookieDomain, cookiePath, cookieSecure, cookieSameSite } = this.config;
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
// [FIX] If disabled, actively clear the browser's residual cookie and bypass
|
|
103
|
+
if (!enabled) {
|
|
104
|
+
return async (ctx, next) => {
|
|
105
|
+
// Check if browser sent the residual cookie
|
|
106
|
+
const sid = getCookieValue(ctx.getHeader("cookie"), cookieName);
|
|
107
|
+
if (sid) {
|
|
108
|
+
// Actively destroy the cookie in the browser by setting Max-Age=0
|
|
109
|
+
ctx.setHeader(
|
|
110
|
+
"Set-Cookie",
|
|
111
|
+
`${cookieName}=; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=${cookieSameSite}; Max-Age=0; Path=${cookiePath}${cookieDomain ? `; Domain=${cookieDomain}` : ""}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return typeof next === "function" ? next() : undefined;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cookieSuffixParts = [
|
|
119
|
+
"HttpOnly", cookieSecure ? "Secure" : "", `SameSite=${cookieSameSite}`,
|
|
120
|
+
`Max-Age=${Math.floor(maxAge / 1000)}`, `Path=${cookiePath}`, cookieDomain ? `Domain=${cookieDomain}` : ""
|
|
121
|
+
].filter(Boolean).join("; ");
|
|
88
122
|
|
|
89
123
|
return async (ctx, next) => {
|
|
90
124
|
ctx.state ??= {};
|
|
91
|
-
|
|
92
125
|
const sid = getCookieValue(ctx.getHeader("cookie"), cookieName);
|
|
93
126
|
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
127
|
let isNew = false;
|
|
97
|
-
if (!sessionData) {
|
|
98
|
-
sessionData = {};
|
|
99
|
-
isNew = true;
|
|
100
|
-
}
|
|
128
|
+
if (!sessionData) { sessionData = {}; isNew = true; }
|
|
101
129
|
|
|
102
130
|
const sessionState = { id: sid, data: sessionData, dirty: false };
|
|
103
131
|
|
|
104
132
|
ctx.session = {
|
|
105
133
|
get: (key) => sessionState.data[key],
|
|
106
|
-
set: (key, val) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
110
|
-
delete: (key) => {
|
|
111
|
-
delete sessionState.data[key];
|
|
112
|
-
sessionState.dirty = true;
|
|
113
|
-
},
|
|
114
|
-
clear: () => {
|
|
115
|
-
sessionState.data = {};
|
|
116
|
-
sessionState.dirty = true;
|
|
117
|
-
},
|
|
134
|
+
set: (key, val) => { sessionState.data[key] = val; sessionState.dirty = true; },
|
|
135
|
+
delete: (key) => { delete sessionState.data[key]; sessionState.dirty = true; },
|
|
136
|
+
clear: () => { sessionState.data = {}; sessionState.dirty = true; },
|
|
118
137
|
destroy: async () => {
|
|
119
138
|
if (sessionState.id) await store.delete(sessionState.id);
|
|
120
|
-
sessionState.id = null;
|
|
121
|
-
|
|
122
|
-
sessionState.dirty = false;
|
|
123
|
-
ctx.setHeader(
|
|
124
|
-
"Set-Cookie",
|
|
125
|
-
`${cookieName}=; HttpOnly; Secure; SameSite=Strict; Max-Age=0; Path=/`,
|
|
126
|
-
);
|
|
139
|
+
sessionState.id = null; sessionState.data = {}; sessionState.dirty = false;
|
|
140
|
+
ctx.setHeader("Set-Cookie", `${cookieName}=; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=${cookieSameSite}; Max-Age=0; Path=${cookiePath}${cookieDomain ? `; Domain=${cookieDomain}` : ""}`);
|
|
127
141
|
},
|
|
128
142
|
regenerate: async () => {
|
|
129
143
|
if (sessionState.id) await store.delete(sessionState.id);
|
|
130
144
|
sessionState.id = genId();
|
|
131
145
|
await store.set(sessionState.id, sessionState.data, maxAge);
|
|
132
|
-
ctx.setHeader(
|
|
133
|
-
|
|
134
|
-
`${cookieName}=${sessionState.id}${cookieSuffix}`,
|
|
135
|
-
);
|
|
136
|
-
sessionState.dirty = false;
|
|
137
|
-
isNew = false;
|
|
146
|
+
ctx.setHeader("Set-Cookie", `${cookieName}=${sessionState.id}; ${cookieSuffixParts}`);
|
|
147
|
+
sessionState.dirty = false; isNew = false;
|
|
138
148
|
},
|
|
149
|
+
getId: () => sessionState.id,
|
|
150
|
+
getAllData: () => ({ ...sessionState.data }),
|
|
139
151
|
};
|
|
140
152
|
|
|
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
153
|
try {
|
|
143
|
-
if (next)
|
|
144
|
-
next.next ? await next.next() : await next();
|
|
145
|
-
}
|
|
154
|
+
if (typeof next === "function") await next();
|
|
146
155
|
} finally {
|
|
147
156
|
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
157
|
if (isNew || !sessionState.id) {
|
|
150
158
|
sessionState.id = genId();
|
|
151
|
-
ctx.setHeader(
|
|
152
|
-
"Set-Cookie",
|
|
153
|
-
`${cookieName}=${sessionState.id}${cookieSuffix}`,
|
|
154
|
-
);
|
|
159
|
+
ctx.setHeader("Set-Cookie", `${cookieName}=${sessionState.id}; ${cookieSuffixParts}`);
|
|
155
160
|
}
|
|
156
161
|
await store.set(sessionState.id, sessionState.data, maxAge);
|
|
157
162
|
}
|
|
@@ -159,9 +164,7 @@ export class SessionManager {
|
|
|
159
164
|
};
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
destroy() {
|
|
163
|
-
if (this.cleanup) clearInterval(this.cleanup);
|
|
164
|
-
}
|
|
167
|
+
destroy() { if (this.cleanup) clearInterval(this.cleanup); }
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
export default SessionManager;
|