@gugananuvem/aws-local-simulator 1.0.31 → 1.0.34
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 +834 -834
- package/aws-config +153 -153
- package/bin/aws-local-simulator.js +63 -63
- package/package.json +3 -2
- package/src/config/config-loader.js +114 -114
- package/src/config/default-config.js +79 -79
- package/src/config/env-loader.js +68 -68
- package/src/index.js +146 -146
- package/src/index.mjs +123 -123
- package/src/server.js +463 -463
- package/src/services/apigateway/index.js +75 -75
- package/src/services/apigateway/server.js +607 -607
- package/src/services/apigateway/simulator.js +1405 -1405
- package/src/services/athena/index.js +75 -75
- package/src/services/athena/server.js +101 -101
- package/src/services/athena/simulador.js +998 -998
- package/src/services/athena/simulator.js +346 -346
- package/src/services/cloudformation/index.js +106 -106
- package/src/services/cloudformation/server.js +417 -417
- package/src/services/cloudformation/simulador.js +1020 -1020
- package/src/services/cloudtrail/index.js +84 -84
- package/src/services/cloudtrail/server.js +235 -235
- package/src/services/cloudtrail/simulador.js +719 -719
- package/src/services/cloudwatch/index.js +84 -84
- package/src/services/cloudwatch/server.js +366 -366
- package/src/services/cloudwatch/simulador.js +1173 -1173
- package/src/services/cognito/index.js +79 -79
- package/src/services/cognito/server.js +297 -297
- package/src/services/cognito/simulator.js +1992 -1761
- package/src/services/config/index.js +96 -96
- package/src/services/config/server.js +215 -215
- package/src/services/config/simulador.js +1260 -1260
- package/src/services/dynamodb/index.js +74 -74
- package/src/services/dynamodb/server.js +139 -139
- package/src/services/dynamodb/simulator.js +1005 -982
- package/src/services/dynamodb/sqlite-store.js +722 -0
- package/src/services/ecs/index.js +65 -65
- package/src/services/ecs/server.js +235 -235
- package/src/services/ecs/simulator.js +844 -844
- package/src/services/eventbridge/index.js +89 -89
- package/src/services/eventbridge/server.js +209 -209
- package/src/services/eventbridge/simulator.js +684 -684
- package/src/services/index.js +45 -45
- package/src/services/kms/index.js +75 -75
- package/src/services/kms/server.js +81 -81
- package/src/services/kms/simulator.js +344 -344
- package/src/services/lambda/handler-loader.js +183 -183
- package/src/services/lambda/index.js +81 -81
- package/src/services/lambda/route-registry.js +274 -274
- package/src/services/lambda/server.js +191 -191
- package/src/services/lambda/simulator.js +364 -364
- package/src/services/parameter-store/index.js +80 -80
- package/src/services/parameter-store/server.js +50 -50
- package/src/services/parameter-store/simulator.js +201 -201
- package/src/services/s3/index.js +73 -73
- package/src/services/s3/server.js +350 -350
- package/src/services/s3/simulator.js +568 -568
- package/src/services/secret-manager/index.js +80 -80
- package/src/services/secret-manager/server.js +51 -51
- package/src/services/secret-manager/simulator.js +182 -182
- package/src/services/sns/index.js +89 -89
- package/src/services/sns/server.js +607 -607
- package/src/services/sns/simulator.js +1482 -1482
- package/src/services/sqs/index.js +98 -98
- package/src/services/sqs/server.js +360 -360
- package/src/services/sqs/simulator.js +509 -509
- package/src/services/sts/index.js +37 -37
- package/src/services/sts/server.js +144 -144
- package/src/services/sts/simulator.js +69 -69
- package/src/services/xray/index.js +83 -83
- package/src/services/xray/server.js +308 -308
- package/src/services/xray/simulador.js +994 -994
- package/src/template/aws-config-template.js +87 -87
- package/src/template/aws-config-template.mjs +90 -90
- package/src/template/config-template.json +203 -203
- package/src/utils/aws-config.js +91 -91
- package/src/utils/cloudtrail-audit.js +129 -129
- package/src/utils/local-store.js +83 -83
- package/src/utils/logger.js +59 -59
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S3 Server - Servidor HTTP para S3
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const express = require('express');
|
|
6
|
-
const cors = require('cors');
|
|
7
|
-
const S3Simulator = require('./simulator');
|
|
8
|
-
const logger = require('../../utils/logger');
|
|
9
|
-
|
|
10
|
-
class S3Server {
|
|
11
|
-
constructor(port, config) {
|
|
12
|
-
this.port = port;
|
|
13
|
-
this.config = config;
|
|
14
|
-
this.app = express();
|
|
15
|
-
this.simulator = null;
|
|
16
|
-
this.server = null;
|
|
17
|
-
this.setupMiddlewares();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
setupMiddlewares() {
|
|
21
|
-
this.app.use(cors());
|
|
22
|
-
// Captura raw body como Buffer para qualquer content-type
|
|
23
|
-
this.app.use(express.raw({ type: () => true, limit: '100mb' }));
|
|
24
|
-
this.app.use(express.text({ limit: '100mb' }));
|
|
25
|
-
|
|
26
|
-
// Suporte a Virtual Host Style addressing (bucket.localhost:4566)
|
|
27
|
-
this.app.use((req, res, next) => {
|
|
28
|
-
const host = req.headers.host || '';
|
|
29
|
-
// Se o host contém múltiplos pontos e não é apenas um IP ou localhost puro
|
|
30
|
-
if (host.includes('.') && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
31
|
-
const parts = host.split('.');
|
|
32
|
-
// O primeiro fragmento é o nome do bucket se houver mais de 2 partes (bucket.localhost:port)
|
|
33
|
-
// ou se a segunda parte for localhost
|
|
34
|
-
if (parts.length >= 2) {
|
|
35
|
-
const bucket = parts[0];
|
|
36
|
-
// Evita processar se for admin ou se já estiver no path style (heurística simples)
|
|
37
|
-
if (bucket !== 'localhost' && !req.path.startsWith('/__admin/')) {
|
|
38
|
-
req.url = `/${bucket}${req.url}`;
|
|
39
|
-
logger.verboso(`S3: Virtual Host Style detected. Rewriting to ${req.url}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
next();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Logging de requisições
|
|
47
|
-
if (logger.currentLogLevel === 'verboso') {
|
|
48
|
-
this.app.use((req, res, next) => {
|
|
49
|
-
const start = Date.now();
|
|
50
|
-
res.on('finish', () => {
|
|
51
|
-
const duration = Date.now() - start;
|
|
52
|
-
logger.verboso(`S3: ${req.method} ${req.path} - ${duration}ms`);
|
|
53
|
-
});
|
|
54
|
-
next();
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
async initialize() {
|
|
61
|
-
if (!this.simulator) {
|
|
62
|
-
this.simulator = new S3Simulator(this.config);
|
|
63
|
-
await this.simulator.initialize();
|
|
64
|
-
}
|
|
65
|
-
this.setupRoutes();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
setupRoutes() {
|
|
69
|
-
// Admin endpoints devem vir antes das rotas S3 para evitar conflito com /:bucket
|
|
70
|
-
this.setupAdminRoutes();
|
|
71
|
-
|
|
72
|
-
// Website static serving: GET /website/{bucket}/[path]
|
|
73
|
-
this.app.get('/website/:bucket', (req, res) => this._serveWebsite(req, res, ''));
|
|
74
|
-
this.app.get('/website/:bucket/*', (req, res) => this._serveWebsite(req, res, req.params[0]));
|
|
75
|
-
|
|
76
|
-
// Listar buckets
|
|
77
|
-
this.app.get('/', (req, res) => {
|
|
78
|
-
const buckets = this.simulator.listBuckets();
|
|
79
|
-
res.set('Content-Type', 'application/xml');
|
|
80
|
-
res.send(this.simulator.generateListBucketsResponse(buckets));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Criar bucket
|
|
84
|
-
this.app.put('/:bucket', (req, res) => {
|
|
85
|
-
if (req.query.website !== undefined) {
|
|
86
|
-
let config = {};
|
|
87
|
-
try { config = JSON.parse(req.body?.toString() || '{}'); } catch (_) {}
|
|
88
|
-
const result = this.simulator.putWebsiteConfig(req.params.bucket, config);
|
|
89
|
-
if (result.error) {
|
|
90
|
-
return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
91
|
-
}
|
|
92
|
-
return res.status(200).send();
|
|
93
|
-
}
|
|
94
|
-
const result = this.simulator.createBucket(req.params.bucket);
|
|
95
|
-
if (result.error) {
|
|
96
|
-
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
97
|
-
} else {
|
|
98
|
-
res.status(200).send();
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Deletar bucket
|
|
103
|
-
this.app.delete('/:bucket', (req, res) => {
|
|
104
|
-
if (req.query.website !== undefined) {
|
|
105
|
-
const result = this.simulator.deleteWebsiteConfig(req.params.bucket);
|
|
106
|
-
if (result.error) {
|
|
107
|
-
return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
108
|
-
}
|
|
109
|
-
return res.status(204).send();
|
|
110
|
-
}
|
|
111
|
-
const result = this.simulator.deleteBucket(req.params.bucket);
|
|
112
|
-
if (result.error) {
|
|
113
|
-
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
114
|
-
} else {
|
|
115
|
-
res.status(204).send();
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Head bucket
|
|
120
|
-
this.app.head('/:bucket', (req, res) => {
|
|
121
|
-
const bucket = this.simulator.getBucket(req.params.bucket);
|
|
122
|
-
if (!bucket) {
|
|
123
|
-
res.status(404).send();
|
|
124
|
-
} else {
|
|
125
|
-
res.status(200).send();
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Upload object
|
|
130
|
-
this.app.put('/:bucket/*', (req, res) => {
|
|
131
|
-
try {
|
|
132
|
-
const bucket = req.params.bucket;
|
|
133
|
-
const key = req.params[0];
|
|
134
|
-
const result = this.simulator.putObject(bucket, key, req.body, req.headers);
|
|
135
|
-
|
|
136
|
-
if (result.error) {
|
|
137
|
-
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
138
|
-
} else {
|
|
139
|
-
res.set('ETag', `"${result.etag}"`);
|
|
140
|
-
res.status(200).send();
|
|
141
|
-
}
|
|
142
|
-
} catch (err) {
|
|
143
|
-
logger.error('S3 putObject error:', err);
|
|
144
|
-
res.status(500).send(this.simulator.generateErrorResponse('InternalError', err.message));
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Get object
|
|
149
|
-
this.app.get('/:bucket/*', (req, res) => {
|
|
150
|
-
const bucket = req.params.bucket;
|
|
151
|
-
const key = req.params[0];
|
|
152
|
-
const result = this.simulator.getObject(bucket, key, req.headers);
|
|
153
|
-
|
|
154
|
-
if (result.error) {
|
|
155
|
-
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
156
|
-
} else {
|
|
157
|
-
res.set('ETag', `"${result.etag}"`);
|
|
158
|
-
res.set('Last-Modified', result.lastModified);
|
|
159
|
-
res.set('Content-Type', result.contentType);
|
|
160
|
-
res.set('Content-Length', result.size);
|
|
161
|
-
|
|
162
|
-
if (result.metadata) {
|
|
163
|
-
Object.entries(result.metadata).forEach(([k, v]) => {
|
|
164
|
-
res.set(`x-amz-meta-${k}`, v);
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
res.send(result.content);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Head object
|
|
173
|
-
this.app.head('/:bucket/*', (req, res) => {
|
|
174
|
-
const bucket = req.params.bucket;
|
|
175
|
-
const key = req.params[0];
|
|
176
|
-
const result = this.simulator.headObject(bucket, key);
|
|
177
|
-
|
|
178
|
-
if (result.error) {
|
|
179
|
-
res.status(result.status).send();
|
|
180
|
-
} else {
|
|
181
|
-
res.set('ETag', `"${result.etag}"`);
|
|
182
|
-
res.set('Last-Modified', result.lastModified);
|
|
183
|
-
res.set('Content-Type', result.contentType);
|
|
184
|
-
res.set('Content-Length', result.size);
|
|
185
|
-
res.status(200).send();
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// Delete object
|
|
190
|
-
this.app.delete('/:bucket/*', (req, res) => {
|
|
191
|
-
const bucket = req.params.bucket;
|
|
192
|
-
const key = req.params[0];
|
|
193
|
-
const result = this.simulator.deleteObject(bucket, key);
|
|
194
|
-
|
|
195
|
-
if (result.error) {
|
|
196
|
-
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
197
|
-
} else {
|
|
198
|
-
res.status(204).send();
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// Website config
|
|
203
|
-
this.app.get('/:bucket', (req, res) => {
|
|
204
|
-
if (req.query.website !== undefined) {
|
|
205
|
-
const result = this.simulator.getWebsiteConfig(req.params.bucket);
|
|
206
|
-
if (result.error) {
|
|
207
|
-
return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
208
|
-
}
|
|
209
|
-
return res.set('Content-Type', 'application/xml').send(this._generateWebsiteConfigXml(result.websiteConfiguration));
|
|
210
|
-
}
|
|
211
|
-
const bucket = req.params.bucket;
|
|
212
|
-
const prefix = req.query.prefix || '';
|
|
213
|
-
const delimiter = req.query.delimiter;
|
|
214
|
-
const maxKeys = parseInt(req.query['max-keys']) || 1000;
|
|
215
|
-
const listType = req.query['list-type'];
|
|
216
|
-
|
|
217
|
-
const result = this.simulator.listObjects(bucket, { prefix, delimiter, maxKeys });
|
|
218
|
-
|
|
219
|
-
if (result.error) {
|
|
220
|
-
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
221
|
-
} else {
|
|
222
|
-
res.set('Content-Type', 'application/xml');
|
|
223
|
-
if (listType === '2') {
|
|
224
|
-
res.send(this.simulator.generateListObjectsV2Response(result));
|
|
225
|
-
} else {
|
|
226
|
-
res.send(this.simulator.generateListObjectsResponse(result));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// Admin endpoints
|
|
232
|
-
// (já registrados no início de setupRoutes)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
setupAdminRoutes() {
|
|
236
|
-
this.app.get('/__admin/buckets', (req, res) => {
|
|
237
|
-
res.json(this.simulator.getBucketsInfo());
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
this.app.get('/__admin/buckets/:bucket', (req, res) => {
|
|
241
|
-
const info = this.simulator.getBucketInfo(req.params.bucket);
|
|
242
|
-
if (info.error) {
|
|
243
|
-
res.status(404).json(info.error);
|
|
244
|
-
} else {
|
|
245
|
-
res.json(info);
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
this.app.get('/__admin/buckets/:bucket/objects', (req, res) => {
|
|
250
|
-
const objects = this.simulator.listAllObjects(req.params.bucket);
|
|
251
|
-
res.json(objects);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
this.app.delete('/__admin/buckets/:bucket', (req, res) => {
|
|
255
|
-
this.simulator.clearBucket(req.params.bucket);
|
|
256
|
-
res.json({ message: `Bucket ${req.params.bucket} cleared` });
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
this.app.delete('/__admin/buckets/:bucket/objects/:key', (req, res) => {
|
|
260
|
-
this.simulator.deleteObject(req.params.bucket, req.params.key);
|
|
261
|
-
res.json({ message: 'Object deleted' });
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
this.app.get('/__admin/stats', (req, res) => {
|
|
265
|
-
res.json(this.simulator.getStats());
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
_serveWebsite(req, res, keyPath) {
|
|
270
|
-
const bucketName = req.params.bucket;
|
|
271
|
-
const bucket = this.simulator.getBucket(bucketName);
|
|
272
|
-
|
|
273
|
-
if (!bucket) {
|
|
274
|
-
return res.status(404).send('<html><body><h1>404 - Bucket not found</h1></body></html>');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (!bucket.website) {
|
|
278
|
-
return res.status(403).send('<html><body><h1>403 - This bucket does not have static website hosting enabled</h1></body></html>');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const websiteConfig = bucket.websiteConfiguration || {};
|
|
282
|
-
const indexDoc = websiteConfig.IndexDocument?.Suffix || 'index.html';
|
|
283
|
-
const errorDoc = websiteConfig.ErrorDocument?.Key || 'error.html';
|
|
284
|
-
|
|
285
|
-
// Resolve a chave: path vazio ou terminando em '/' serve o indexDocument
|
|
286
|
-
let key = keyPath || '';
|
|
287
|
-
if (!key || key.endsWith('/')) {
|
|
288
|
-
key = key + indexDoc;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
let result = this.simulator.getObject(bucketName, key, req.headers);
|
|
292
|
-
|
|
293
|
-
// Se não encontrou e não tem extensão, tenta com indexDoc dentro do path
|
|
294
|
-
if (result.error && !key.includes('.')) {
|
|
295
|
-
result = this.simulator.getObject(bucketName, key + '/' + indexDoc, req.headers);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (result.error) {
|
|
299
|
-
const errorResult = this.simulator.getObject(bucketName, errorDoc, req.headers);
|
|
300
|
-
if (!errorResult.error) {
|
|
301
|
-
res.status(404).set('Content-Type', errorResult.contentType || 'text/html');
|
|
302
|
-
return res.send(errorResult.content);
|
|
303
|
-
}
|
|
304
|
-
return res.status(404).send('<html><body><h1>404 - Not Found</h1></body></html>');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
res.set('Content-Type', result.contentType || 'text/html');
|
|
308
|
-
res.set('ETag', `"${result.etag}"`);
|
|
309
|
-
res.set('Last-Modified', result.lastModified);
|
|
310
|
-
res.send(result.content);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
_generateWebsiteConfigXml(config) {
|
|
314
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
315
|
-
<WebsiteConfiguration>
|
|
316
|
-
<IndexDocument><Suffix>${config.IndexDocument?.Suffix || 'index.html'}</Suffix></IndexDocument>
|
|
317
|
-
<ErrorDocument><Key>${config.ErrorDocument?.Key || 'error.html'}</Key></ErrorDocument>
|
|
318
|
-
</WebsiteConfiguration>`;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
start() {
|
|
322
|
-
return new Promise((resolve) => {
|
|
323
|
-
this.server = this.app.listen(this.port, () => {
|
|
324
|
-
logger.info(`🗄️ S3 rodando em http://localhost:${this.port}`);
|
|
325
|
-
resolve();
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
stop() {
|
|
331
|
-
return new Promise((resolve) => {
|
|
332
|
-
if (this.server) {
|
|
333
|
-
this.server.close(() => resolve());
|
|
334
|
-
} else {
|
|
335
|
-
resolve();
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
getStatus() {
|
|
341
|
-
return {
|
|
342
|
-
running: !!this.server,
|
|
343
|
-
port: this.port,
|
|
344
|
-
endpoint: `http://localhost:${this.port}`,
|
|
345
|
-
bucketsCount: this.simulator?.getBucketsCount() || 0,
|
|
346
|
-
objectsCount: this.simulator?.getTotalObjectsCount() || 0
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
1
|
+
/**
|
|
2
|
+
* S3 Server - Servidor HTTP para S3
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const express = require('express');
|
|
6
|
+
const cors = require('cors');
|
|
7
|
+
const S3Simulator = require('./simulator');
|
|
8
|
+
const logger = require('../../utils/logger');
|
|
9
|
+
|
|
10
|
+
class S3Server {
|
|
11
|
+
constructor(port, config) {
|
|
12
|
+
this.port = port;
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.app = express();
|
|
15
|
+
this.simulator = null;
|
|
16
|
+
this.server = null;
|
|
17
|
+
this.setupMiddlewares();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setupMiddlewares() {
|
|
21
|
+
this.app.use(cors());
|
|
22
|
+
// Captura raw body como Buffer para qualquer content-type
|
|
23
|
+
this.app.use(express.raw({ type: () => true, limit: '100mb' }));
|
|
24
|
+
this.app.use(express.text({ limit: '100mb' }));
|
|
25
|
+
|
|
26
|
+
// Suporte a Virtual Host Style addressing (bucket.localhost:4566)
|
|
27
|
+
this.app.use((req, res, next) => {
|
|
28
|
+
const host = req.headers.host || '';
|
|
29
|
+
// Se o host contém múltiplos pontos e não é apenas um IP ou localhost puro
|
|
30
|
+
if (host.includes('.') && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
31
|
+
const parts = host.split('.');
|
|
32
|
+
// O primeiro fragmento é o nome do bucket se houver mais de 2 partes (bucket.localhost:port)
|
|
33
|
+
// ou se a segunda parte for localhost
|
|
34
|
+
if (parts.length >= 2) {
|
|
35
|
+
const bucket = parts[0];
|
|
36
|
+
// Evita processar se for admin ou se já estiver no path style (heurística simples)
|
|
37
|
+
if (bucket !== 'localhost' && !req.path.startsWith('/__admin/')) {
|
|
38
|
+
req.url = `/${bucket}${req.url}`;
|
|
39
|
+
logger.verboso(`S3: Virtual Host Style detected. Rewriting to ${req.url}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
next();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Logging de requisições
|
|
47
|
+
if (logger.currentLogLevel === 'verboso') {
|
|
48
|
+
this.app.use((req, res, next) => {
|
|
49
|
+
const start = Date.now();
|
|
50
|
+
res.on('finish', () => {
|
|
51
|
+
const duration = Date.now() - start;
|
|
52
|
+
logger.verboso(`S3: ${req.method} ${req.path} - ${duration}ms`);
|
|
53
|
+
});
|
|
54
|
+
next();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async initialize() {
|
|
61
|
+
if (!this.simulator) {
|
|
62
|
+
this.simulator = new S3Simulator(this.config);
|
|
63
|
+
await this.simulator.initialize();
|
|
64
|
+
}
|
|
65
|
+
this.setupRoutes();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setupRoutes() {
|
|
69
|
+
// Admin endpoints devem vir antes das rotas S3 para evitar conflito com /:bucket
|
|
70
|
+
this.setupAdminRoutes();
|
|
71
|
+
|
|
72
|
+
// Website static serving: GET /website/{bucket}/[path]
|
|
73
|
+
this.app.get('/website/:bucket', (req, res) => this._serveWebsite(req, res, ''));
|
|
74
|
+
this.app.get('/website/:bucket/*', (req, res) => this._serveWebsite(req, res, req.params[0]));
|
|
75
|
+
|
|
76
|
+
// Listar buckets
|
|
77
|
+
this.app.get('/', (req, res) => {
|
|
78
|
+
const buckets = this.simulator.listBuckets();
|
|
79
|
+
res.set('Content-Type', 'application/xml');
|
|
80
|
+
res.send(this.simulator.generateListBucketsResponse(buckets));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Criar bucket
|
|
84
|
+
this.app.put('/:bucket', (req, res) => {
|
|
85
|
+
if (req.query.website !== undefined) {
|
|
86
|
+
let config = {};
|
|
87
|
+
try { config = JSON.parse(req.body?.toString() || '{}'); } catch (_) {}
|
|
88
|
+
const result = this.simulator.putWebsiteConfig(req.params.bucket, config);
|
|
89
|
+
if (result.error) {
|
|
90
|
+
return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
91
|
+
}
|
|
92
|
+
return res.status(200).send();
|
|
93
|
+
}
|
|
94
|
+
const result = this.simulator.createBucket(req.params.bucket);
|
|
95
|
+
if (result.error) {
|
|
96
|
+
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
97
|
+
} else {
|
|
98
|
+
res.status(200).send();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Deletar bucket
|
|
103
|
+
this.app.delete('/:bucket', (req, res) => {
|
|
104
|
+
if (req.query.website !== undefined) {
|
|
105
|
+
const result = this.simulator.deleteWebsiteConfig(req.params.bucket);
|
|
106
|
+
if (result.error) {
|
|
107
|
+
return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
108
|
+
}
|
|
109
|
+
return res.status(204).send();
|
|
110
|
+
}
|
|
111
|
+
const result = this.simulator.deleteBucket(req.params.bucket);
|
|
112
|
+
if (result.error) {
|
|
113
|
+
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
114
|
+
} else {
|
|
115
|
+
res.status(204).send();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Head bucket
|
|
120
|
+
this.app.head('/:bucket', (req, res) => {
|
|
121
|
+
const bucket = this.simulator.getBucket(req.params.bucket);
|
|
122
|
+
if (!bucket) {
|
|
123
|
+
res.status(404).send();
|
|
124
|
+
} else {
|
|
125
|
+
res.status(200).send();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Upload object
|
|
130
|
+
this.app.put('/:bucket/*', (req, res) => {
|
|
131
|
+
try {
|
|
132
|
+
const bucket = req.params.bucket;
|
|
133
|
+
const key = req.params[0];
|
|
134
|
+
const result = this.simulator.putObject(bucket, key, req.body, req.headers);
|
|
135
|
+
|
|
136
|
+
if (result.error) {
|
|
137
|
+
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
138
|
+
} else {
|
|
139
|
+
res.set('ETag', `"${result.etag}"`);
|
|
140
|
+
res.status(200).send();
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logger.error('S3 putObject error:', err);
|
|
144
|
+
res.status(500).send(this.simulator.generateErrorResponse('InternalError', err.message));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Get object
|
|
149
|
+
this.app.get('/:bucket/*', (req, res) => {
|
|
150
|
+
const bucket = req.params.bucket;
|
|
151
|
+
const key = req.params[0];
|
|
152
|
+
const result = this.simulator.getObject(bucket, key, req.headers);
|
|
153
|
+
|
|
154
|
+
if (result.error) {
|
|
155
|
+
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
156
|
+
} else {
|
|
157
|
+
res.set('ETag', `"${result.etag}"`);
|
|
158
|
+
res.set('Last-Modified', result.lastModified);
|
|
159
|
+
res.set('Content-Type', result.contentType);
|
|
160
|
+
res.set('Content-Length', result.size);
|
|
161
|
+
|
|
162
|
+
if (result.metadata) {
|
|
163
|
+
Object.entries(result.metadata).forEach(([k, v]) => {
|
|
164
|
+
res.set(`x-amz-meta-${k}`, v);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
res.send(result.content);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Head object
|
|
173
|
+
this.app.head('/:bucket/*', (req, res) => {
|
|
174
|
+
const bucket = req.params.bucket;
|
|
175
|
+
const key = req.params[0];
|
|
176
|
+
const result = this.simulator.headObject(bucket, key);
|
|
177
|
+
|
|
178
|
+
if (result.error) {
|
|
179
|
+
res.status(result.status).send();
|
|
180
|
+
} else {
|
|
181
|
+
res.set('ETag', `"${result.etag}"`);
|
|
182
|
+
res.set('Last-Modified', result.lastModified);
|
|
183
|
+
res.set('Content-Type', result.contentType);
|
|
184
|
+
res.set('Content-Length', result.size);
|
|
185
|
+
res.status(200).send();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Delete object
|
|
190
|
+
this.app.delete('/:bucket/*', (req, res) => {
|
|
191
|
+
const bucket = req.params.bucket;
|
|
192
|
+
const key = req.params[0];
|
|
193
|
+
const result = this.simulator.deleteObject(bucket, key);
|
|
194
|
+
|
|
195
|
+
if (result.error) {
|
|
196
|
+
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
197
|
+
} else {
|
|
198
|
+
res.status(204).send();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Website config
|
|
203
|
+
this.app.get('/:bucket', (req, res) => {
|
|
204
|
+
if (req.query.website !== undefined) {
|
|
205
|
+
const result = this.simulator.getWebsiteConfig(req.params.bucket);
|
|
206
|
+
if (result.error) {
|
|
207
|
+
return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
208
|
+
}
|
|
209
|
+
return res.set('Content-Type', 'application/xml').send(this._generateWebsiteConfigXml(result.websiteConfiguration));
|
|
210
|
+
}
|
|
211
|
+
const bucket = req.params.bucket;
|
|
212
|
+
const prefix = req.query.prefix || '';
|
|
213
|
+
const delimiter = req.query.delimiter;
|
|
214
|
+
const maxKeys = parseInt(req.query['max-keys']) || 1000;
|
|
215
|
+
const listType = req.query['list-type'];
|
|
216
|
+
|
|
217
|
+
const result = this.simulator.listObjects(bucket, { prefix, delimiter, maxKeys });
|
|
218
|
+
|
|
219
|
+
if (result.error) {
|
|
220
|
+
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
221
|
+
} else {
|
|
222
|
+
res.set('Content-Type', 'application/xml');
|
|
223
|
+
if (listType === '2') {
|
|
224
|
+
res.send(this.simulator.generateListObjectsV2Response(result));
|
|
225
|
+
} else {
|
|
226
|
+
res.send(this.simulator.generateListObjectsResponse(result));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Admin endpoints
|
|
232
|
+
// (já registrados no início de setupRoutes)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
setupAdminRoutes() {
|
|
236
|
+
this.app.get('/__admin/buckets', (req, res) => {
|
|
237
|
+
res.json(this.simulator.getBucketsInfo());
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
this.app.get('/__admin/buckets/:bucket', (req, res) => {
|
|
241
|
+
const info = this.simulator.getBucketInfo(req.params.bucket);
|
|
242
|
+
if (info.error) {
|
|
243
|
+
res.status(404).json(info.error);
|
|
244
|
+
} else {
|
|
245
|
+
res.json(info);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
this.app.get('/__admin/buckets/:bucket/objects', (req, res) => {
|
|
250
|
+
const objects = this.simulator.listAllObjects(req.params.bucket);
|
|
251
|
+
res.json(objects);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this.app.delete('/__admin/buckets/:bucket', (req, res) => {
|
|
255
|
+
this.simulator.clearBucket(req.params.bucket);
|
|
256
|
+
res.json({ message: `Bucket ${req.params.bucket} cleared` });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
this.app.delete('/__admin/buckets/:bucket/objects/:key', (req, res) => {
|
|
260
|
+
this.simulator.deleteObject(req.params.bucket, req.params.key);
|
|
261
|
+
res.json({ message: 'Object deleted' });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
this.app.get('/__admin/stats', (req, res) => {
|
|
265
|
+
res.json(this.simulator.getStats());
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_serveWebsite(req, res, keyPath) {
|
|
270
|
+
const bucketName = req.params.bucket;
|
|
271
|
+
const bucket = this.simulator.getBucket(bucketName);
|
|
272
|
+
|
|
273
|
+
if (!bucket) {
|
|
274
|
+
return res.status(404).send('<html><body><h1>404 - Bucket not found</h1></body></html>');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!bucket.website) {
|
|
278
|
+
return res.status(403).send('<html><body><h1>403 - This bucket does not have static website hosting enabled</h1></body></html>');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const websiteConfig = bucket.websiteConfiguration || {};
|
|
282
|
+
const indexDoc = websiteConfig.IndexDocument?.Suffix || 'index.html';
|
|
283
|
+
const errorDoc = websiteConfig.ErrorDocument?.Key || 'error.html';
|
|
284
|
+
|
|
285
|
+
// Resolve a chave: path vazio ou terminando em '/' serve o indexDocument
|
|
286
|
+
let key = keyPath || '';
|
|
287
|
+
if (!key || key.endsWith('/')) {
|
|
288
|
+
key = key + indexDoc;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let result = this.simulator.getObject(bucketName, key, req.headers);
|
|
292
|
+
|
|
293
|
+
// Se não encontrou e não tem extensão, tenta com indexDoc dentro do path
|
|
294
|
+
if (result.error && !key.includes('.')) {
|
|
295
|
+
result = this.simulator.getObject(bucketName, key + '/' + indexDoc, req.headers);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (result.error) {
|
|
299
|
+
const errorResult = this.simulator.getObject(bucketName, errorDoc, req.headers);
|
|
300
|
+
if (!errorResult.error) {
|
|
301
|
+
res.status(404).set('Content-Type', errorResult.contentType || 'text/html');
|
|
302
|
+
return res.send(errorResult.content);
|
|
303
|
+
}
|
|
304
|
+
return res.status(404).send('<html><body><h1>404 - Not Found</h1></body></html>');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
res.set('Content-Type', result.contentType || 'text/html');
|
|
308
|
+
res.set('ETag', `"${result.etag}"`);
|
|
309
|
+
res.set('Last-Modified', result.lastModified);
|
|
310
|
+
res.send(result.content);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_generateWebsiteConfigXml(config) {
|
|
314
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
315
|
+
<WebsiteConfiguration>
|
|
316
|
+
<IndexDocument><Suffix>${config.IndexDocument?.Suffix || 'index.html'}</Suffix></IndexDocument>
|
|
317
|
+
<ErrorDocument><Key>${config.ErrorDocument?.Key || 'error.html'}</Key></ErrorDocument>
|
|
318
|
+
</WebsiteConfiguration>`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
start() {
|
|
322
|
+
return new Promise((resolve) => {
|
|
323
|
+
this.server = this.app.listen(this.port, () => {
|
|
324
|
+
logger.info(`🗄️ S3 rodando em http://localhost:${this.port}`);
|
|
325
|
+
resolve();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
stop() {
|
|
331
|
+
return new Promise((resolve) => {
|
|
332
|
+
if (this.server) {
|
|
333
|
+
this.server.close(() => resolve());
|
|
334
|
+
} else {
|
|
335
|
+
resolve();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
getStatus() {
|
|
341
|
+
return {
|
|
342
|
+
running: !!this.server,
|
|
343
|
+
port: this.port,
|
|
344
|
+
endpoint: `http://localhost:${this.port}`,
|
|
345
|
+
bucketsCount: this.simulator?.getBucketsCount() || 0,
|
|
346
|
+
objectsCount: this.simulator?.getTotalObjectsCount() || 0
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
351
|
module.exports = S3Server;
|