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,295 @@
|
|
|
1
|
+
// body-parser.js - Request body parsing middleware for AetherJS
|
|
2
|
+
// Supports JSON, URL-encoded, text, and raw formats with zero-copy operations
|
|
3
|
+
|
|
4
|
+
import { StringDecoder } from 'string_decoder';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse size string to bytes
|
|
8
|
+
* @param {string|number} size - Size string like '1mb', '2kb', or bytes count
|
|
9
|
+
* @returns {number} - Size in bytes
|
|
10
|
+
*/
|
|
11
|
+
function parseSize(size) {
|
|
12
|
+
if (typeof size === 'number') return size;
|
|
13
|
+
if (typeof size !== 'string') return 0;
|
|
14
|
+
|
|
15
|
+
const units = {
|
|
16
|
+
'b': 1,
|
|
17
|
+
'kb': 1024,
|
|
18
|
+
'mb': 1024 * 1024,
|
|
19
|
+
'gb': 1024 * 1024 * 1024
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const parsedStr = size.toLowerCase();
|
|
23
|
+
const match = parsedStr.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
|
|
24
|
+
if (!match) {
|
|
25
|
+
throw new Error(`Invalid size format: ${size}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 💡 修复:正确读取捕获组
|
|
29
|
+
const value = parseFloat(match[1]); // 第一捕获组:纯数字
|
|
30
|
+
const unit = match[2]; // 第二捕获组:单位 (如 'mb')
|
|
31
|
+
return value * (units[unit] || 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse request body as buffer
|
|
36
|
+
* @param {Object} request - HTTP request object
|
|
37
|
+
* @param {number} limit - Maximum size in bytes
|
|
38
|
+
* @returns {Promise<Buffer>} - Request body buffer
|
|
39
|
+
*/
|
|
40
|
+
async function parseBodyBuffer(request, limit) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let totalLength = 0;
|
|
44
|
+
|
|
45
|
+
request.on('data', (chunk) => {
|
|
46
|
+
totalLength += chunk.length;
|
|
47
|
+
|
|
48
|
+
if (totalLength > limit) {
|
|
49
|
+
request.destroy();
|
|
50
|
+
reject(new Error(`Request body exceeded limit of ${limit} bytes`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
chunks.push(chunk);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
request.on('end', () => {
|
|
58
|
+
resolve(Buffer.concat(chunks));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
request.on('error', (error) => {
|
|
62
|
+
reject(error);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse request body as JSON
|
|
69
|
+
* @param {Object} request - HTTP request object
|
|
70
|
+
* @param {number} limit - Maximum size in bytes
|
|
71
|
+
* @returns {Promise<Object>} - Parsed JSON object
|
|
72
|
+
*/
|
|
73
|
+
async function parseBodyJson(request, limit) {
|
|
74
|
+
const buffer = await parseBodyBuffer(request, limit);
|
|
75
|
+
const text = buffer.toString('utf8');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`Invalid JSON: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse request body as URL-encoded
|
|
86
|
+
* @param {Object} request - HTTP request object
|
|
87
|
+
* @param {number} limit - Maximum size in bytes
|
|
88
|
+
* @returns {Promise<Object>} - Parsed URL-encoded object
|
|
89
|
+
*/
|
|
90
|
+
async function parseBodyUrlEncoded(request, limit) {
|
|
91
|
+
const buffer = await parseBodyBuffer(request, limit);
|
|
92
|
+
const text = buffer.toString('utf8');
|
|
93
|
+
|
|
94
|
+
const result = {};
|
|
95
|
+
const pairs = text.split('&');
|
|
96
|
+
|
|
97
|
+
for (const pair of pairs) {
|
|
98
|
+
const [key, value] = pair.split('=');
|
|
99
|
+
if (key) {
|
|
100
|
+
result[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse request body as text
|
|
109
|
+
* @param {Object} request - HTTP request object
|
|
110
|
+
* @param {number} limit - Maximum size in bytes
|
|
111
|
+
* @returns {Promise<string>} - Parsed text
|
|
112
|
+
*/
|
|
113
|
+
async function parseBodyText(request, limit) {
|
|
114
|
+
const buffer = await parseBodyBuffer(request, limit);
|
|
115
|
+
return buffer.toString('utf8');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create body parser middleware for AetherJS
|
|
120
|
+
* @param {Object} options - Body parser configuration
|
|
121
|
+
* @returns {Function} - Body parser middleware function
|
|
122
|
+
*/
|
|
123
|
+
function createBodyParserMiddleware(options = {}) {
|
|
124
|
+
// Load configuration from environment variables
|
|
125
|
+
const envConfig = {
|
|
126
|
+
jsonLimit: process.env.BODY_LIMIT_JSON,
|
|
127
|
+
urlencodedLimit: process.env.BODY_LIMIT_URLENCODED,
|
|
128
|
+
textLimit: process.env.BODY_LIMIT_TEXT,
|
|
129
|
+
rawLimit: process.env.BODY_LIMIT_RAW,
|
|
130
|
+
enableJson: process.env.BODY_ENABLE_JSON,
|
|
131
|
+
enableUrlencoded: process.env.BODY_ENABLE_URLENCODED,
|
|
132
|
+
enableText: process.env.BODY_ENABLE_TEXT,
|
|
133
|
+
enableRaw: process.env.BODY_ENABLE_RAW
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Default configuration
|
|
137
|
+
const defaults = {
|
|
138
|
+
json: {
|
|
139
|
+
enabled: envConfig.enableJson !== 'false',
|
|
140
|
+
limit: parseSize(envConfig.jsonLimit || '1mb'),
|
|
141
|
+
strict: true,
|
|
142
|
+
reviver: null
|
|
143
|
+
},
|
|
144
|
+
urlencoded: {
|
|
145
|
+
enabled: envConfig.enableUrlencoded !== 'false',
|
|
146
|
+
limit: parseSize(envConfig.urlencodedLimit || '1mb'),
|
|
147
|
+
extended: false,
|
|
148
|
+
parameterLimit: 1000
|
|
149
|
+
},
|
|
150
|
+
text: {
|
|
151
|
+
enabled: envConfig.enableText !== 'false',
|
|
152
|
+
limit: parseSize(envConfig.textLimit || '1mb'),
|
|
153
|
+
defaultCharset: 'utf-8'
|
|
154
|
+
},
|
|
155
|
+
raw: {
|
|
156
|
+
enabled: envConfig.enableRaw !== 'false',
|
|
157
|
+
limit: parseSize(envConfig.rawLimit || '10mb'),
|
|
158
|
+
type: 'application/octet-stream'
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// 💡 修复:安全的嵌套字段深层合并,防止 options 传参直接覆盖 defaults 细节配置
|
|
163
|
+
const config = {
|
|
164
|
+
json: { ...defaults.json, ...options.json },
|
|
165
|
+
urlencoded: { ...defaults.urlencoded, ...options.urlencoded },
|
|
166
|
+
text: { ...defaults.text, ...options.text },
|
|
167
|
+
raw: { ...defaults.raw, ...options.raw }
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// 转换各类型的解析上限
|
|
171
|
+
if (options.json?.limit) config.json.limit = parseSize(options.json.limit);
|
|
172
|
+
if (options.urlencoded?.limit) config.urlencoded.limit = parseSize(options.urlencoded.limit);
|
|
173
|
+
if (options.text?.limit) config.text.limit = parseSize(options.text.limit);
|
|
174
|
+
if (options.raw?.limit) config.raw.limit = parseSize(options.raw.limit);
|
|
175
|
+
|
|
176
|
+
const parsers = new Map();
|
|
177
|
+
|
|
178
|
+
if (config.json.enabled) {
|
|
179
|
+
parsers.set('application/json', async (request) => {
|
|
180
|
+
const data = await parseBodyJson(request, config.json.limit);
|
|
181
|
+
if (config.json.reviver) {
|
|
182
|
+
return JSON.parse(JSON.stringify(data), config.json.reviver);
|
|
183
|
+
}
|
|
184
|
+
return data;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
parsers.set('application/json; charset=utf-8', parsers.get('application/json'));
|
|
188
|
+
parsers.set('application/json; charset=utf8', parsers.get('application/json'));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (config.urlencoded.enabled) {
|
|
192
|
+
parsers.set('application/x-www-form-urlencoded', async (request) => {
|
|
193
|
+
return await parseBodyUrlEncoded(request, config.urlencoded.limit);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (config.text.enabled) {
|
|
198
|
+
parsers.set('text/plain', async (request) => {
|
|
199
|
+
return await parseBodyText(request, config.text.limit);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
parsers.set('text/html', parsers.get('text/plain'));
|
|
203
|
+
parsers.set('text/xml', parsers.get('text/plain'));
|
|
204
|
+
parsers.set('text/css', parsers.get('text/plain'));
|
|
205
|
+
parsers.set('text/javascript', parsers.get('text/plain'));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (config.raw.enabled) {
|
|
209
|
+
parsers.set(config.raw.type, async (request) => {
|
|
210
|
+
return await parseBodyBuffer(request, config.raw.limit);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Body parser middleware function (Fully Aligned to (context, next) standard)
|
|
216
|
+
* @param {AetherContext} context - AetherJS execution context
|
|
217
|
+
* @param {Function} next - Continuation callback
|
|
218
|
+
*/
|
|
219
|
+
return async function bodyParserMiddleware(context, next) {
|
|
220
|
+
// Skip if no body is expected
|
|
221
|
+
if (context.method === 'GET' || context.method === 'HEAD') {
|
|
222
|
+
return typeof next === 'function' ? next() : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const contentType = context.getHeader('content-type') || '';
|
|
226
|
+
const contentLength = parseInt(context.getHeader('content-length')) || 0;
|
|
227
|
+
|
|
228
|
+
// Skip if no content
|
|
229
|
+
if (contentLength === 0) {
|
|
230
|
+
return typeof next === 'function' ? next() : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check content type
|
|
234
|
+
let parser = null;
|
|
235
|
+
for (const [type, parserFunc] of parsers) {
|
|
236
|
+
if (contentType.includes(type)) {
|
|
237
|
+
parser = parserFunc;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// If no parser found and raw is enabled, use raw parser
|
|
243
|
+
if (!parser && config.raw.enabled) {
|
|
244
|
+
parser = parsers.get(config.raw.type);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!parser) {
|
|
248
|
+
// No suitable parser found, continue without parsing
|
|
249
|
+
return typeof next === 'function' ? next() : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
// Parse body
|
|
254
|
+
const body = await parser(context._request);
|
|
255
|
+
|
|
256
|
+
// Store parsed body in context state
|
|
257
|
+
if (contentType.includes('application/json')) {
|
|
258
|
+
context.setState('parsedBody', { json: body });
|
|
259
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
260
|
+
context.setState('parsedBody', { urlencoded: body });
|
|
261
|
+
} else if (contentType.includes('text/')) {
|
|
262
|
+
context.setState('parsedBody', { text: body });
|
|
263
|
+
} else {
|
|
264
|
+
context.setState('parsedBody', { raw: body });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Add convenience methods to context
|
|
268
|
+
context.body = body; // 触发我们在 AetherContext 加的 setter
|
|
269
|
+
context.getBody = () => body;
|
|
270
|
+
|
|
271
|
+
return typeof next === 'function' ? next() : null;
|
|
272
|
+
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// Handle parsing errors
|
|
275
|
+
if (error.message.includes('exceeded limit')) {
|
|
276
|
+
context.setStatus(413).json({
|
|
277
|
+
error: 'Payload Too Large',
|
|
278
|
+
message: error.message
|
|
279
|
+
});
|
|
280
|
+
} else if (error.message.includes('Invalid JSON')) {
|
|
281
|
+
context.setStatus(400).json({
|
|
282
|
+
error: 'Bad Request',
|
|
283
|
+
message: 'Invalid JSON format'
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
context.setStatus(400).json({
|
|
287
|
+
error: 'Bad Request',
|
|
288
|
+
message: error.message
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export default createBodyParserMiddleware;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// compression.js - Response compression middleware for AetherJS
|
|
2
|
+
// Supports gzip, deflate, and brotli compression with zero-copy operations
|
|
3
|
+
|
|
4
|
+
import zlib from "zlib";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse compression types from string
|
|
8
|
+
* @param {string} types - Comma-separated list of content types
|
|
9
|
+
* @returns {Array<string>} - Array of content types
|
|
10
|
+
*/
|
|
11
|
+
function parseCompressionTypes(types) {
|
|
12
|
+
if (!types || typeof types !== "string") {
|
|
13
|
+
return [
|
|
14
|
+
"text/plain",
|
|
15
|
+
"text/html",
|
|
16
|
+
"text/css",
|
|
17
|
+
"application/javascript",
|
|
18
|
+
"application/json",
|
|
19
|
+
"application/xml",
|
|
20
|
+
"text/xml",
|
|
21
|
+
"application/xhtml+xml",
|
|
22
|
+
"text/javascript",
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return types
|
|
27
|
+
.split(",")
|
|
28
|
+
.map((type) => type.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if content type should be compressed
|
|
34
|
+
* @param {string} contentType - Response content type
|
|
35
|
+
* @param {Array<string>} compressibleTypes - List of compressible types
|
|
36
|
+
* @returns {boolean} - Whether to compress
|
|
37
|
+
*/
|
|
38
|
+
function shouldCompress(contentType, compressibleTypes) {
|
|
39
|
+
if (!contentType) return false;
|
|
40
|
+
|
|
41
|
+
for (const type of compressibleTypes) {
|
|
42
|
+
if (contentType.includes(type)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create compression middleware for AetherJS
|
|
52
|
+
* @param {Object} options - Compression configuration
|
|
53
|
+
* @returns {Function} - Compression middleware function
|
|
54
|
+
*/
|
|
55
|
+
function createCompressionMiddleware(options = {}) {
|
|
56
|
+
// Load configuration from environment variables
|
|
57
|
+
const envConfig = {
|
|
58
|
+
enabled: process.env.COMPRESSION_ENABLED,
|
|
59
|
+
threshold: process.env.COMPRESSION_THRESHOLD,
|
|
60
|
+
level: process.env.COMPRESSION_LEVEL,
|
|
61
|
+
memLevel: process.env.COMPRESSION_MEM_LEVEL,
|
|
62
|
+
strategy: process.env.COMPRESSION_STRATEGY,
|
|
63
|
+
chunkSize: process.env.COMPRESSION_CHUNK_SIZE,
|
|
64
|
+
windowBits: process.env.COMPRESSION_WINDOW_BITS,
|
|
65
|
+
gzip: process.env.COMPRESSION_GZIP,
|
|
66
|
+
deflate: process.env.COMPRESSION_DEFLATE,
|
|
67
|
+
brotli: process.env.COMPRESSION_BROTLI,
|
|
68
|
+
types: process.env.COMPRESSION_TYPES,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Default configuration
|
|
72
|
+
const defaults = {
|
|
73
|
+
enabled: envConfig.enabled !== "false",
|
|
74
|
+
threshold: parseInt(envConfig.threshold) || 1024,
|
|
75
|
+
level: parseInt(envConfig.level) || zlib.constants.Z_DEFAULT_COMPRESSION,
|
|
76
|
+
memLevel: parseInt(envConfig.memLevel) || 8,
|
|
77
|
+
strategy: parseInt(envConfig.strategy) || zlib.constants.Z_DEFAULT_STRATEGY,
|
|
78
|
+
chunkSize: parseInt(envConfig.chunkSize) || 16 * 1024,
|
|
79
|
+
windowBits: parseInt(envConfig.windowBits) || 15,
|
|
80
|
+
gzip: envConfig.gzip !== "false",
|
|
81
|
+
deflate: envConfig.deflate === "true",
|
|
82
|
+
brotli:
|
|
83
|
+
envConfig.brotli === "true" &&
|
|
84
|
+
typeof zlib.createBrotliCompress === "function",
|
|
85
|
+
types: parseCompressionTypes(envConfig.types),
|
|
86
|
+
filter: (contentType) =>
|
|
87
|
+
shouldCompress(contentType, parseCompressionTypes(envConfig.types)),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Merge with provided options
|
|
91
|
+
const config = { ...defaults, ...options };
|
|
92
|
+
|
|
93
|
+
// Create compression options
|
|
94
|
+
const gzipOptions = {
|
|
95
|
+
level: config.level,
|
|
96
|
+
memLevel: config.memLevel,
|
|
97
|
+
strategy: config.strategy,
|
|
98
|
+
chunkSize: config.chunkSize,
|
|
99
|
+
windowBits: config.windowBits,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const deflateOptions = { ...gzipOptions };
|
|
103
|
+
const brotliOptions = {
|
|
104
|
+
params: {
|
|
105
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: config.level,
|
|
106
|
+
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
|
|
107
|
+
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: config.chunkSize,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Compress data using specified algorithm
|
|
113
|
+
* @param {Buffer} data - Data to compress
|
|
114
|
+
* @param {string} encoding - Compression algorithm
|
|
115
|
+
* @returns {Promise<Buffer>} - Compressed data
|
|
116
|
+
*/
|
|
117
|
+
async function compressData(data, encoding) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
let compressor;
|
|
120
|
+
|
|
121
|
+
switch (encoding) {
|
|
122
|
+
case "gzip":
|
|
123
|
+
compressor = zlib.createGzip(gzipOptions);
|
|
124
|
+
break;
|
|
125
|
+
case "deflate":
|
|
126
|
+
compressor = zlib.createDeflate(deflateOptions);
|
|
127
|
+
break;
|
|
128
|
+
case "br":
|
|
129
|
+
if (!config.brotli) {
|
|
130
|
+
reject(new Error("Brotli compression not supported"));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
compressor = zlib.createBrotliCompress(brotliOptions);
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
reject(new Error(`Unsupported compression: ${encoding}`));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const chunks = [];
|
|
141
|
+
compressor.on("data", (chunk) => chunks.push(chunk));
|
|
142
|
+
compressor.on("end", () => resolve(Buffer.concat(chunks)));
|
|
143
|
+
compressor.on("error", reject);
|
|
144
|
+
|
|
145
|
+
compressor.write(data);
|
|
146
|
+
compressor.end();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Compression middleware function
|
|
152
|
+
* @param {AetherContext} context - AetherJS execution context
|
|
153
|
+
* @param {Object} signal - Signal object or next function for flow control
|
|
154
|
+
*/
|
|
155
|
+
return async function compressionMiddleware(context, signal) {
|
|
156
|
+
// Safe invoker for compatible pipeline flow control
|
|
157
|
+
const invokeNext = async () => {
|
|
158
|
+
if (signal && typeof signal.next === "function") {
|
|
159
|
+
await signal.next();
|
|
160
|
+
} else if (typeof signal === "function") {
|
|
161
|
+
await signal();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!config.enabled) {
|
|
166
|
+
return await invokeNext();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Store original finalize method
|
|
170
|
+
const originalize = context._finalize;
|
|
171
|
+
|
|
172
|
+
// Override finalize to add compression
|
|
173
|
+
context._finalize = async function () {
|
|
174
|
+
if (this._terminated) return;
|
|
175
|
+
|
|
176
|
+
const body = this._body;
|
|
177
|
+
|
|
178
|
+
// Safe fallback logic for grabbing the outbound content type
|
|
179
|
+
let contentType = "";
|
|
180
|
+
if (typeof this.getHeader === "function") {
|
|
181
|
+
contentType = this.getHeader("content-type") || "";
|
|
182
|
+
} else if (this._headers && typeof this._headers.get === "function") {
|
|
183
|
+
contentType = this._headers.get("content-type") || "";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if compression should be applied
|
|
187
|
+
if (
|
|
188
|
+
!body ||
|
|
189
|
+
(!Buffer.isBuffer(body) && typeof body !== "string") ||
|
|
190
|
+
Buffer.byteLength(body) < config.threshold ||
|
|
191
|
+
!config.filter(contentType)
|
|
192
|
+
) {
|
|
193
|
+
return originalize.call(this);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get accepted encodings
|
|
197
|
+
const acceptEncoding =
|
|
198
|
+
(typeof this.getHeader === "function"
|
|
199
|
+
? this.getHeader("accept-encoding")
|
|
200
|
+
: "") || "";
|
|
201
|
+
let encoding = null;
|
|
202
|
+
|
|
203
|
+
// Determine best compression algorithm
|
|
204
|
+
if (config.gzip && acceptEncoding.includes("gzip")) {
|
|
205
|
+
encoding = "gzip";
|
|
206
|
+
} else if (config.deflate && acceptEncoding.includes("deflate")) {
|
|
207
|
+
encoding = "deflate";
|
|
208
|
+
} else if (config.brotli && acceptEncoding.includes("br")) {
|
|
209
|
+
encoding = "br";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!encoding) {
|
|
213
|
+
return originalize.call(this);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// Convert body to buffer if needed
|
|
218
|
+
const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
219
|
+
|
|
220
|
+
// Compress data
|
|
221
|
+
const compressed = await compressData(bodyBuffer, encoding);
|
|
222
|
+
|
|
223
|
+
// Update response
|
|
224
|
+
this._body = compressed;
|
|
225
|
+
if (typeof this.setHeader === "function") {
|
|
226
|
+
this.setHeader("content-encoding", encoding);
|
|
227
|
+
this.setHeader("vary", "Accept-Encoding");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Call original finalize
|
|
231
|
+
return originalize.call(this);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// Compression failed, use original body
|
|
234
|
+
console.error("Compression error:", error);
|
|
235
|
+
return originalize.call(this);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
await invokeNext();
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export default createCompressionMiddleware;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// cors.js - CORS middleware for AetherJS
|
|
2
|
+
function parseOrigin(origin) {
|
|
3
|
+
if (origin === "*") {
|
|
4
|
+
return (requestOrigin) => "*";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (typeof origin === "string") {
|
|
8
|
+
const origins = origin
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((o) => o.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
if (origins.length === 1) {
|
|
13
|
+
const singleOrigin = origins[0];
|
|
14
|
+
return (requestOrigin) => {
|
|
15
|
+
if (singleOrigin === "*") return "*";
|
|
16
|
+
return requestOrigin === singleOrigin ? requestOrigin : null;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return (requestOrigin) =>
|
|
20
|
+
origins.includes(requestOrigin) ? requestOrigin : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(origin)) {
|
|
24
|
+
return (requestOrigin) =>
|
|
25
|
+
origin.includes(requestOrigin) ? requestOrigin : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof origin === "function") {
|
|
29
|
+
return origin;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return () => null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createCorsMiddleware(options = {}) {
|
|
36
|
+
const envConfig = {
|
|
37
|
+
enabled: process.env.CORS_ENABLED,
|
|
38
|
+
origin: process.env.CORS_ORIGIN,
|
|
39
|
+
methods: process.env.CORS_METHODS,
|
|
40
|
+
allowedHeaders: process.env.CORS_ALLOWED_HEADERS,
|
|
41
|
+
credentials: process.env.CORS_CREDENTIALS,
|
|
42
|
+
maxAge: process.env.CORS_MAX_AGE,
|
|
43
|
+
preflightContinue: process.env.CORS_PREFLIGHT_CONTINUE,
|
|
44
|
+
optionsSuccessStatus: process.env.CORS_OPTIONS_STATUS,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const defaults = {
|
|
48
|
+
enabled: envConfig.enabled !== "false",
|
|
49
|
+
origin: envConfig.origin || "*",
|
|
50
|
+
methods: envConfig.methods
|
|
51
|
+
? envConfig.methods.split(",").map((m) => m.trim())
|
|
52
|
+
: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
53
|
+
allowedHeaders: envConfig.allowedHeaders
|
|
54
|
+
? envConfig.allowedHeaders.split(",").map((h) => h.trim())
|
|
55
|
+
: ["Content-Type", "Authorization"],
|
|
56
|
+
exposedHeaders: [],
|
|
57
|
+
credentials: envConfig.credentials === "true",
|
|
58
|
+
maxAge: envConfig.maxAge ? parseInt(envConfig.maxAge) : 86400,
|
|
59
|
+
preflightContinue: envConfig.preflightContinue === "true",
|
|
60
|
+
optionsSuccessStatus: envConfig.optionsSuccessStatus
|
|
61
|
+
? parseInt(envConfig.optionsSuccessStatus)
|
|
62
|
+
: 204,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const config = { ...defaults, ...options };
|
|
66
|
+
const originValidator = parseOrigin(config.origin);
|
|
67
|
+
const staticHeaders = new Map();
|
|
68
|
+
|
|
69
|
+
if (config.methods && config.methods.length > 0) {
|
|
70
|
+
staticHeaders.set(
|
|
71
|
+
"access-control-allow-methods",
|
|
72
|
+
config.methods.join(", "),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (config.allowedHeaders && config.allowedHeaders.length > 0) {
|
|
76
|
+
staticHeaders.set(
|
|
77
|
+
"access-control-allow-headers",
|
|
78
|
+
config.allowedHeaders.join(", "),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (config.maxAge && config.maxAge > 0) {
|
|
82
|
+
staticHeaders.set("access-control-max-age", config.maxAge.toString());
|
|
83
|
+
}
|
|
84
|
+
if (config.exposedHeaders && config.exposedHeaders.length > 0) {
|
|
85
|
+
staticHeaders.set(
|
|
86
|
+
"access-control-expose-headers",
|
|
87
|
+
config.exposedHeaders.join(", "),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return async function corsMiddleware(context, signal) {
|
|
92
|
+
if (!config.enabled) {
|
|
93
|
+
return signal && signal.next
|
|
94
|
+
? await signal.next()
|
|
95
|
+
: typeof signal === "function"
|
|
96
|
+
? await signal()
|
|
97
|
+
: void 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const requestOrigin = context.getHeader("origin") || "";
|
|
101
|
+
let allowOrigin = originValidator(requestOrigin);
|
|
102
|
+
|
|
103
|
+
// 💡 Critical Fix: According to W3C specs, if credentials are enabled and origin is '*',
|
|
104
|
+
// we must dynamically mirror the incoming request origin instead of returning a literal '*'.
|
|
105
|
+
if (config.credentials && allowOrigin === "*") {
|
|
106
|
+
allowOrigin = requestOrigin || "*";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (allowOrigin && allowOrigin !== null) {
|
|
110
|
+
context.setHeader("access-control-allow-origin", allowOrigin);
|
|
111
|
+
|
|
112
|
+
for (const [header, value] of staticHeaders) {
|
|
113
|
+
context.setHeader(header, value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (config.credentials) {
|
|
117
|
+
context.setHeader("access-control-allow-credentials", "true");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (allowOrigin !== "*") {
|
|
121
|
+
const varyHeader = context.getHeader("vary");
|
|
122
|
+
const varyValues = varyHeader
|
|
123
|
+
? varyHeader.split(",").map((v) => v.trim())
|
|
124
|
+
: [];
|
|
125
|
+
if (!varyValues.includes("Origin")) {
|
|
126
|
+
varyValues.push("Origin");
|
|
127
|
+
context.setHeader("vary", varyValues.join(", "));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle CORS Preflight OPTIONS requests
|
|
133
|
+
if (context.method === "OPTIONS") {
|
|
134
|
+
if (!config.preflightContinue) {
|
|
135
|
+
context.setStatus(config.optionsSuccessStatus);
|
|
136
|
+
context.setHeader("content-length", "0");
|
|
137
|
+
if (typeof context.json === "function") {
|
|
138
|
+
context.json("");
|
|
139
|
+
} else {
|
|
140
|
+
context.raw("");
|
|
141
|
+
}
|
|
142
|
+
return; // Short-circuit the request pipeline instantly, bypassing signal.next()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Backward compatibility: Supports either signal.next() or traditional functional next() dispatching
|
|
147
|
+
if (signal && typeof signal.next === "function") {
|
|
148
|
+
await signal.next();
|
|
149
|
+
} else if (typeof signal === "function") {
|
|
150
|
+
await signal();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default createCorsMiddleware;
|