@gugananuvem/aws-local-simulator 1.0.34 → 1.0.39
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 +662 -658
- package/package.json +11 -4
- package/src/config/default-config.js +71 -0
- package/src/config/env-loader.js +4 -2
- package/src/middleware/authorization.js +271 -0
- package/src/middleware/cloudtrail-logger.js +333 -0
- package/src/middleware/error-handler.js +138 -0
- package/src/middleware/index.js +79 -0
- package/src/middleware/policy-enforcement.js +407 -0
- package/src/server.js +74 -4
- package/src/services/acm/index.js +66 -0
- package/src/services/acm/models/certificate.js +202 -0
- package/src/services/acm/server.js +224 -0
- package/src/services/acm/simulator.js +293 -0
- package/src/services/alb/index.js +104 -0
- package/src/services/alb/models/loadBalancer.js +88 -0
- package/src/services/alb/models/targetGroup.js +128 -0
- package/src/services/alb/server.js +160 -0
- package/src/services/alb/simulator.js +262 -0
- package/src/services/apigateway/index.js +22 -0
- package/src/services/apigateway/server.js +247 -35
- package/src/services/apigateway/simulator.js +780 -26
- package/src/services/asg/index.js +100 -0
- package/src/services/asg/models/autoScalingGroup.js +110 -0
- package/src/services/asg/models/launchTemplate.js +41 -0
- package/src/services/asg/models/scalingPolicy.js +51 -0
- package/src/services/asg/server.js +236 -0
- package/src/services/asg/simulator.js +410 -0
- package/src/services/aurora/cluster-manager.js +731 -0
- package/src/services/aurora/data-api.js +514 -0
- package/src/services/aurora/db-engine.js +270 -0
- package/src/services/aurora/index.js +159 -0
- package/src/services/aurora/rds-api.js +189 -0
- package/src/services/aurora/server.js +213 -0
- package/src/services/aurora/sql-translator.js +334 -0
- package/src/services/cloudformation/index.js +2 -2
- package/src/services/cloudformation/server.js +1 -0
- package/src/services/cloudfront/cache-engine.js +225 -0
- package/src/services/cloudfront/geo-restrictions.js +377 -0
- package/src/services/cloudfront/index.js +220 -0
- package/src/services/cloudfront/models/behavior.js +158 -0
- package/src/services/cloudfront/models/distribution.js +284 -0
- package/src/services/cloudfront/models/index.js +99 -0
- package/src/services/cloudfront/models/invalidation.js +109 -0
- package/src/services/cloudfront/models/origin.js +155 -0
- package/src/services/cloudfront/origin-router.js +552 -0
- package/src/services/cloudfront/origin-validator.js +398 -0
- package/src/services/cloudfront/server.js +512 -0
- package/src/services/cloudfront/signed-url-generator.js +377 -0
- package/src/services/cloudfront/simulator.js +425 -0
- package/src/services/cloudfront/store.js +326 -0
- package/src/services/cloudshell/env-builder.js +27 -0
- package/src/services/cloudshell/index.js +48 -0
- package/src/services/cloudshell/routes.js +60 -0
- package/src/services/cloudshell/session-manager.js +226 -0
- package/src/services/cloudshell/static/.gitkeep +0 -0
- package/src/services/cloudshell/static/index.html +406 -0
- package/src/services/cloudshell/websocket-handler.js +107 -0
- package/src/services/cloudtrail/index.js +2 -3
- package/src/services/cloudtrail/server.js +68 -0
- package/src/services/cognito/server.js +4 -1
- package/src/services/dynamodb/server.js +4 -1
- package/src/services/dynamodb/simulator.js +2 -1
- package/src/services/ec2/index.js +101 -0
- package/src/services/ec2/instanceServer.js +311 -0
- package/src/services/ec2/models/ami.js +39 -0
- package/src/services/ec2/models/instance.js +98 -0
- package/src/services/ec2/models/networkInterface.js +74 -0
- package/src/services/ec2/models/securityGroup.js +71 -0
- package/src/services/ec2/server.js +350 -0
- package/src/services/ec2/simulator.js +436 -0
- package/src/services/eventbridge/server.js +36 -0
- package/src/services/eventbridge/simulator.js +230 -45
- package/src/services/iam/index.js +65 -0
- package/src/services/iam/middleware.js +99 -0
- package/src/services/iam/server.js +331 -0
- package/src/services/iam/simulator.js +1432 -0
- package/src/services/index.js +28 -0
- package/src/services/kinesis-analytics/index.js +105 -0
- package/src/services/kinesis-analytics/models/application.js +325 -0
- package/src/services/kinesis-analytics/models/index.js +12 -0
- package/src/services/kinesis-analytics/server.js +250 -0
- package/src/services/kinesis-analytics/simulator.js +407 -0
- package/src/services/kinesis-analytics/store.js +176 -0
- package/src/services/kinesis-data-streams/index.js +106 -0
- package/src/services/kinesis-data-streams/models/index.js +12 -0
- package/src/services/kinesis-data-streams/models/record.js +70 -0
- package/src/services/kinesis-data-streams/models/stream.js +280 -0
- package/src/services/kinesis-data-streams/server.js +255 -0
- package/src/services/kinesis-data-streams/simulator.js +514 -0
- package/src/services/kinesis-data-streams/store.js +163 -0
- package/src/services/kinesis-firehose/index.js +106 -0
- package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
- package/src/services/kinesis-firehose/models/index.js +10 -0
- package/src/services/kinesis-firehose/server.js +215 -0
- package/src/services/kinesis-firehose/simulator.js +388 -0
- package/src/services/kinesis-firehose/store.js +163 -0
- package/src/services/kms/server.js +4 -1
- package/src/services/lambda/handler-loader.js +50 -5
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +18 -12
- package/src/services/lambda/simulator.js +73 -17
- package/src/services/opensearch/index.js +105 -0
- package/src/services/opensearch/models/document.js +115 -0
- package/src/services/opensearch/models/index.js +73 -0
- package/src/services/opensearch/server.js +310 -0
- package/src/services/opensearch/simulator.js +605 -0
- package/src/services/opensearch/store.js +162 -0
- package/src/services/organizations/control-tower-simulator.js +321 -0
- package/src/services/organizations/control-tower-store.js +182 -0
- package/src/services/organizations/iam-integration.js +413 -0
- package/src/services/organizations/index.js +172 -0
- package/src/services/organizations/models/index.js +13 -0
- package/src/services/organizations/models/invitation.js +48 -0
- package/src/services/organizations/models/organization.js +566 -0
- package/src/services/organizations/models/role.js +244 -0
- package/src/services/organizations/models/scp.js +131 -0
- package/src/services/organizations/server.js +736 -0
- package/src/services/organizations/simulator.js +593 -0
- package/src/services/organizations/store.js +235 -0
- package/src/services/organizations/vpc-integration.js +453 -0
- package/src/services/route53/index.js +67 -0
- package/src/services/route53/models/healthCheck.js +101 -0
- package/src/services/route53/models/hostedZone.js +37 -0
- package/src/services/route53/models/resourceRecord.js +106 -0
- package/src/services/route53/server.js +841 -0
- package/src/services/route53/simulator.js +350 -0
- package/src/services/s3/index.js +62 -0
- package/src/services/s3/presigned-url-generator.js +337 -0
- package/src/services/s3/server.js +231 -0
- package/src/services/s3/simulator.js +551 -4
- package/src/services/sns/index.js +31 -0
- package/src/services/sns/server.js +9 -0
- package/src/services/sqs/index.js +38 -0
- package/src/services/sqs/server.js +57 -4
- package/src/services/sqs/simulator.js +423 -33
- package/src/services/sts/simulator.js +209 -17
- package/src/services/vpc/README.md +459 -0
- package/src/services/vpc/ec2ResponseFormatter.js +431 -0
- package/src/services/vpc/index.js +90 -0
- package/src/services/vpc/models/internetGateway.js +71 -0
- package/src/services/vpc/models/routeTable.js +160 -0
- package/src/services/vpc/models/subnet.js +48 -0
- package/src/services/vpc/models/vpc.js +37 -0
- package/src/services/vpc/models/vpcEndpoint.js +100 -0
- package/src/services/vpc/models/vpcGateway.js +134 -0
- package/src/services/vpc/server.js +836 -0
- package/src/services/vpc/simulator.js +613 -0
- package/src/utils/policy-cache.js +215 -0
- package/aws-config +0 -154
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 Presigned URL Generator
|
|
3
|
+
* Gera URLs assinadas para upload, download e operações custom
|
|
4
|
+
* Permite acesso temporário a objetos S3 sem credenciais
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
class PresignedUrlGenerator {
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.signatureVersion = 'AWS4-HMAC-SHA256';
|
|
12
|
+
this.algorithm = 'AWS4-HMAC-SHA256';
|
|
13
|
+
this.service = 's3';
|
|
14
|
+
this.region = config.region || 'us-east-1';
|
|
15
|
+
this.s3Endpoint = config.s3Endpoint || 'http://localhost:4566';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Gerar presigned URL para GET (download)
|
|
20
|
+
* @param {string} bucket - Nome do bucket
|
|
21
|
+
* @param {string} key - Chave/path do objeto
|
|
22
|
+
* @param {object} options - Opções
|
|
23
|
+
* @param {number} [options.expiresIn] - Tempo de expiração em segundos (default: 3600)
|
|
24
|
+
* @param {object} [options.credentials] - AWS credentials {accessKeyId, secretAccessKey}
|
|
25
|
+
* @returns {string} Presigned URL
|
|
26
|
+
*/
|
|
27
|
+
generateGetUrl(bucket, key, options = {}) {
|
|
28
|
+
const expiresIn = options.expiresIn || 3600;
|
|
29
|
+
const credentials = options.credentials || this._getDefaultCredentials();
|
|
30
|
+
|
|
31
|
+
return this._generatePresignedUrl('GET', bucket, key, expiresIn, credentials, {});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gerar presigned URL para PUT (upload)
|
|
36
|
+
* @param {string} bucket - Nome do bucket
|
|
37
|
+
* @param {string} key - Chave/path do objeto
|
|
38
|
+
* @param {object} options - Opções
|
|
39
|
+
* @param {number} [options.expiresIn] - Tempo de expiração em segundos (default: 3600)
|
|
40
|
+
* @param {object} [options.credentials] - AWS credentials
|
|
41
|
+
* @param {string} [options.contentType] - Content-Type esperado
|
|
42
|
+
* @param {object} [options.metadata] - Metadados (x-amz-meta-*)
|
|
43
|
+
* @param {string} [options.serverSideEncryption] - Encryption type (AES256, aws:kms)
|
|
44
|
+
* @returns {string} Presigned URL
|
|
45
|
+
*/
|
|
46
|
+
generatePutUrl(bucket, key, options = {}) {
|
|
47
|
+
const expiresIn = options.expiresIn || 3600;
|
|
48
|
+
const credentials = options.credentials || this._getDefaultCredentials();
|
|
49
|
+
const conditions = {};
|
|
50
|
+
|
|
51
|
+
// Adicionar condições para validação
|
|
52
|
+
if (options.contentType) {
|
|
53
|
+
conditions['content-type'] = options.contentType;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.serverSideEncryption) {
|
|
57
|
+
conditions['x-amz-server-side-encryption'] = options.serverSideEncryption;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (options.metadata) {
|
|
61
|
+
Object.entries(options.metadata).forEach(([key, value]) => {
|
|
62
|
+
conditions[`x-amz-meta-${key}`] = value;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return this._generatePresignedUrl('PUT', bucket, key, expiresIn, credentials, conditions);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Gerar presigned URL para DELETE
|
|
71
|
+
* @param {string} bucket - Nome do bucket
|
|
72
|
+
* @param {string} key - Chave/path do objeto
|
|
73
|
+
* @param {object} options - Opções
|
|
74
|
+
* @param {number} [options.expiresIn] - Tempo de expiração em segundos
|
|
75
|
+
* @param {object} [options.credentials] - AWS credentials
|
|
76
|
+
* @returns {string} Presigned URL
|
|
77
|
+
*/
|
|
78
|
+
generateDeleteUrl(bucket, key, options = {}) {
|
|
79
|
+
const expiresIn = options.expiresIn || 3600;
|
|
80
|
+
const credentials = options.credentials || this._getDefaultCredentials();
|
|
81
|
+
|
|
82
|
+
return this._generatePresignedUrl('DELETE', bucket, key, expiresIn, credentials, {});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Gerar presigned URL para HEAD (verificar objeto)
|
|
87
|
+
* @param {string} bucket - Nome do bucket
|
|
88
|
+
* @param {string} key - Chave/path do objeto
|
|
89
|
+
* @param {object} options - Opções
|
|
90
|
+
* @param {number} [options.expiresIn] - Tempo de expiração em segundos
|
|
91
|
+
* @param {object} [options.credentials] - AWS credentials
|
|
92
|
+
* @returns {string} Presigned URL
|
|
93
|
+
*/
|
|
94
|
+
generateHeadUrl(bucket, key, options = {}) {
|
|
95
|
+
const expiresIn = options.expiresIn || 3600;
|
|
96
|
+
const credentials = options.credentials || this._getDefaultCredentials();
|
|
97
|
+
|
|
98
|
+
return this._generatePresignedUrl('HEAD', bucket, key, expiresIn, credentials, {});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validar presigned URL
|
|
103
|
+
* @param {string} url - URL para validar
|
|
104
|
+
* @param {string} method - HTTP method esperado
|
|
105
|
+
* @param {object} [credentials] - Credentials para validação
|
|
106
|
+
* @returns {object} Resultado da validação
|
|
107
|
+
*/
|
|
108
|
+
validatePresignedUrl(url, method = 'GET', credentials = null) {
|
|
109
|
+
try {
|
|
110
|
+
const urlObj = new URL(url);
|
|
111
|
+
|
|
112
|
+
// Extrair parâmetros de query
|
|
113
|
+
const params = Object.fromEntries(urlObj.searchParams);
|
|
114
|
+
|
|
115
|
+
// Verificar assinatura
|
|
116
|
+
if (!params.X-Amz-Signature) {
|
|
117
|
+
return {
|
|
118
|
+
isValid: false,
|
|
119
|
+
error: 'Signature missing'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Verificar expiração
|
|
124
|
+
if (params['X-Amz-Date']) {
|
|
125
|
+
const requestTime = this._parseAmzDate(params['X-Amz-Date']);
|
|
126
|
+
const expiresIn = parseInt(params['X-Amz-Expires']) || 3600;
|
|
127
|
+
const expireTime = new Date(requestTime.getTime() + expiresIn * 1000);
|
|
128
|
+
|
|
129
|
+
if (new Date() > expireTime) {
|
|
130
|
+
return {
|
|
131
|
+
isValid: false,
|
|
132
|
+
error: 'URL expired'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Verificar método
|
|
138
|
+
if (method && method !== 'GET') {
|
|
139
|
+
// A URL assinada funciona para qualquer método que foi assinado
|
|
140
|
+
// Aqui seria verificado qual método foi assinado
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
isValid: true,
|
|
145
|
+
method: params['X-Amz-Algorithm'] ? 'SIGNATURE_V4' : 'UNKNOWN',
|
|
146
|
+
expiresAt: this._parseAmzDate(params['X-Amz-Date'])
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return {
|
|
150
|
+
isValid: false,
|
|
151
|
+
error: error.message
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Gerar policy para upload direto (POST policy)
|
|
158
|
+
* Usado por navegadores para upload direto sem backend
|
|
159
|
+
* @param {string} bucket - Nome do bucket
|
|
160
|
+
* @param {string} key - Chave/path do objeto
|
|
161
|
+
* @param {object} options - Opções
|
|
162
|
+
* @returns {object} {policy, signature, credentials}
|
|
163
|
+
*/
|
|
164
|
+
generatePostPolicy(bucket, key, options = {}) {
|
|
165
|
+
const expiresIn = options.expiresIn || 3600;
|
|
166
|
+
const credentials = options.credentials || this._getDefaultCredentials();
|
|
167
|
+
const contentLengthRange = options.contentLengthRange || [1, 104857600]; // 100MB default
|
|
168
|
+
|
|
169
|
+
// Data no formato ISO8601
|
|
170
|
+
const amzDate = new Date().toISOString().replace(/[:-]/g, '').split('.')[0] + 'Z';
|
|
171
|
+
|
|
172
|
+
// Policy
|
|
173
|
+
const policy = {
|
|
174
|
+
expiration: new Date(Date.now() + expiresIn * 1000).toISOString(),
|
|
175
|
+
conditions: [
|
|
176
|
+
{ bucket: bucket },
|
|
177
|
+
{ key: key },
|
|
178
|
+
['content-length-range', contentLengthRange[0], contentLengthRange[1]],
|
|
179
|
+
{ 'x-amz-credential': `${credentials.accessKeyId}/${this._getCredentialScope(amzDate)}` },
|
|
180
|
+
{ 'x-amz-algorithm': 'AWS4-HMAC-SHA256' },
|
|
181
|
+
{ 'x-amz-date': amzDate }
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Adicionar condições customizadas
|
|
186
|
+
if (options.contentType) {
|
|
187
|
+
policy.conditions.push({ 'content-type': options.contentType });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (options.metadata) {
|
|
191
|
+
Object.entries(options.metadata).forEach(([k, v]) => {
|
|
192
|
+
policy.conditions.push({ [`x-amz-meta-${k}`]: v });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Converter policy para base64
|
|
197
|
+
const policyJson = JSON.stringify(policy);
|
|
198
|
+
const policyB64 = Buffer.from(policyJson).toString('base64');
|
|
199
|
+
|
|
200
|
+
// Gerar assinatura
|
|
201
|
+
const signature = this._signPolicy(policyB64, credentials, amzDate);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
policy: policyB64,
|
|
205
|
+
signature: signature,
|
|
206
|
+
credentials: {
|
|
207
|
+
accessKeyId: credentials.accessKeyId,
|
|
208
|
+
algorithm: 'AWS4-HMAC-SHA256',
|
|
209
|
+
date: amzDate,
|
|
210
|
+
credentialScope: this._getCredentialScope(amzDate)
|
|
211
|
+
},
|
|
212
|
+
url: `${this.s3Endpoint}/${bucket}`,
|
|
213
|
+
policyObject: policy
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Private Methods
|
|
219
|
+
*/
|
|
220
|
+
|
|
221
|
+
_generatePresignedUrl(method, bucket, key, expiresIn, credentials, conditions = {}) {
|
|
222
|
+
const now = new Date();
|
|
223
|
+
const amzDate = now.toISOString().replace(/[:-]/g, '').split('.')[0] + 'Z';
|
|
224
|
+
const dateStamp = amzDate.substring(0, 8);
|
|
225
|
+
|
|
226
|
+
// Construir canonical request
|
|
227
|
+
const host = `${bucket}.s3.amazonaws.com`;
|
|
228
|
+
const canonicalUri = `/${key}`;
|
|
229
|
+
|
|
230
|
+
const canonicalQuerystring = [
|
|
231
|
+
`X-Amz-Algorithm=AWS4-HMAC-SHA256`,
|
|
232
|
+
`X-Amz-Credential=${encodeURIComponent(credentials.accessKeyId)}/${this._getCredentialScope(amzDate)}`,
|
|
233
|
+
`X-Amz-Date=${amzDate}`,
|
|
234
|
+
`X-Amz-Expires=${expiresIn}`,
|
|
235
|
+
`X-Amz-SignedHeaders=host`
|
|
236
|
+
].sort().join('&');
|
|
237
|
+
|
|
238
|
+
const canonicalHeaders = `host:${host}\n`;
|
|
239
|
+
const signedHeaders = 'host';
|
|
240
|
+
|
|
241
|
+
const canonicalRequest = [
|
|
242
|
+
method,
|
|
243
|
+
canonicalUri,
|
|
244
|
+
canonicalQuerystring,
|
|
245
|
+
canonicalHeaders,
|
|
246
|
+
'',
|
|
247
|
+
signedHeaders
|
|
248
|
+
].join('\n');
|
|
249
|
+
|
|
250
|
+
// Criar string para assinar
|
|
251
|
+
const canonicalRequestHash = crypto
|
|
252
|
+
.createHash('sha256')
|
|
253
|
+
.update(canonicalRequest)
|
|
254
|
+
.digest('hex');
|
|
255
|
+
|
|
256
|
+
const stringToSign = [
|
|
257
|
+
'AWS4-HMAC-SHA256',
|
|
258
|
+
amzDate,
|
|
259
|
+
this._getCredentialScope(amzDate),
|
|
260
|
+
canonicalRequestHash
|
|
261
|
+
].join('\n');
|
|
262
|
+
|
|
263
|
+
// Gerar assinatura
|
|
264
|
+
const signature = this._calculateSignature(
|
|
265
|
+
stringToSign,
|
|
266
|
+
credentials.secretAccessKey,
|
|
267
|
+
dateStamp
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Construir presigned URL
|
|
271
|
+
const presignedUrl = `https://${host}${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signature}`;
|
|
272
|
+
|
|
273
|
+
return presignedUrl;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_getCredentialScope(amzDate) {
|
|
277
|
+
const dateStamp = amzDate.substring(0, 8);
|
|
278
|
+
return `${dateStamp}/${this.region}/${this.service}/aws4_request`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_calculateSignature(stringToSign, secretAccessKey, dateStamp) {
|
|
282
|
+
const kDate = crypto
|
|
283
|
+
.createHmac('sha256', `AWS4${secretAccessKey}`)
|
|
284
|
+
.update(dateStamp)
|
|
285
|
+
.digest();
|
|
286
|
+
|
|
287
|
+
const kRegion = crypto
|
|
288
|
+
.createHmac('sha256', kDate)
|
|
289
|
+
.update(this.region)
|
|
290
|
+
.digest();
|
|
291
|
+
|
|
292
|
+
const kService = crypto
|
|
293
|
+
.createHmac('sha256', kRegion)
|
|
294
|
+
.update(this.service)
|
|
295
|
+
.digest();
|
|
296
|
+
|
|
297
|
+
const kSigning = crypto
|
|
298
|
+
.createHmac('sha256', kService)
|
|
299
|
+
.update('aws4_request')
|
|
300
|
+
.digest();
|
|
301
|
+
|
|
302
|
+
const signature = crypto
|
|
303
|
+
.createHmac('sha256', kSigning)
|
|
304
|
+
.update(stringToSign)
|
|
305
|
+
.digest('hex');
|
|
306
|
+
|
|
307
|
+
return signature;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_signPolicy(policyB64, credentials, amzDate) {
|
|
311
|
+
const dateStamp = amzDate.substring(0, 8);
|
|
312
|
+
const signature = this._calculateSignature(policyB64, credentials.secretAccessKey, dateStamp);
|
|
313
|
+
return signature;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_parseAmzDate(amzDate) {
|
|
317
|
+
// Format: 20240115T103000Z
|
|
318
|
+
const year = parseInt(amzDate.substring(0, 4));
|
|
319
|
+
const month = parseInt(amzDate.substring(4, 6)) - 1;
|
|
320
|
+
const day = parseInt(amzDate.substring(6, 8));
|
|
321
|
+
const hour = parseInt(amzDate.substring(9, 11));
|
|
322
|
+
const minute = parseInt(amzDate.substring(11, 13));
|
|
323
|
+
const second = parseInt(amzDate.substring(13, 15));
|
|
324
|
+
|
|
325
|
+
return new Date(Date.UTC(year, month, day, hour, minute, second));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_getDefaultCredentials() {
|
|
329
|
+
// Return default test credentials
|
|
330
|
+
return {
|
|
331
|
+
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
332
|
+
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = PresignedUrlGenerator;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { setupServiceMiddleware } = require('../../middleware');
|
|
1
2
|
/**
|
|
2
3
|
* S3 Server - Servidor HTTP para S3
|
|
3
4
|
*/
|
|
@@ -9,6 +10,8 @@ const logger = require('../../utils/logger');
|
|
|
9
10
|
|
|
10
11
|
class S3Server {
|
|
11
12
|
constructor(port, config) {
|
|
13
|
+
this.organizationsService = null;
|
|
14
|
+
this.stsService = null;
|
|
12
15
|
this.port = port;
|
|
13
16
|
this.config = config;
|
|
14
17
|
this.app = express();
|
|
@@ -66,6 +69,14 @@ class S3Server {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
setupRoutes() {
|
|
72
|
+
// IAM enforcement (applied conditionally via injectDependencies)
|
|
73
|
+
this.app.use((req, res, next) => {
|
|
74
|
+
if (this.iamMiddleware && !req.path.startsWith('/__admin')) {
|
|
75
|
+
return this.iamMiddleware(req, res, next);
|
|
76
|
+
}
|
|
77
|
+
next();
|
|
78
|
+
});
|
|
79
|
+
|
|
69
80
|
// Admin endpoints devem vir antes das rotas S3 para evitar conflito com /:bucket
|
|
70
81
|
this.setupAdminRoutes();
|
|
71
82
|
|
|
@@ -91,6 +102,18 @@ class S3Server {
|
|
|
91
102
|
}
|
|
92
103
|
return res.status(200).send();
|
|
93
104
|
}
|
|
105
|
+
if (req.query.notification !== undefined) {
|
|
106
|
+
let config = {};
|
|
107
|
+
try {
|
|
108
|
+
const bodyStr = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : req.body || '{}';
|
|
109
|
+
config = JSON.parse(bodyStr);
|
|
110
|
+
} catch (_) {}
|
|
111
|
+
const result = this.simulator.putBucketNotificationConfiguration(req.params.bucket, config);
|
|
112
|
+
if (result.error) {
|
|
113
|
+
return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
114
|
+
}
|
|
115
|
+
return res.status(200).send();
|
|
116
|
+
}
|
|
94
117
|
const result = this.simulator.createBucket(req.params.bucket);
|
|
95
118
|
if (result.error) {
|
|
96
119
|
res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
@@ -208,6 +231,13 @@ class S3Server {
|
|
|
208
231
|
}
|
|
209
232
|
return res.set('Content-Type', 'application/xml').send(this._generateWebsiteConfigXml(result.websiteConfiguration));
|
|
210
233
|
}
|
|
234
|
+
if (req.query.notification !== undefined) {
|
|
235
|
+
const result = this.simulator.getBucketNotificationConfiguration(req.params.bucket);
|
|
236
|
+
if (result.error) {
|
|
237
|
+
return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
|
|
238
|
+
}
|
|
239
|
+
return res.set('Content-Type', 'application/xml').send(this._generateNotificationConfigXml(result.notificationConfiguration));
|
|
240
|
+
}
|
|
211
241
|
const bucket = req.params.bucket;
|
|
212
242
|
const prefix = req.query.prefix || '';
|
|
213
243
|
const delimiter = req.query.delimiter;
|
|
@@ -264,6 +294,179 @@ class S3Server {
|
|
|
264
294
|
this.app.get('/__admin/stats', (req, res) => {
|
|
265
295
|
res.json(this.simulator.getStats());
|
|
266
296
|
});
|
|
297
|
+
|
|
298
|
+
// ─── Presigned URL Endpoints ──────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* POST /__admin/presigned-urls/get
|
|
302
|
+
* Generate a presigned GET URL for downloading an object
|
|
303
|
+
* Body: { bucket, key, expiresIn?, credentials? }
|
|
304
|
+
*/
|
|
305
|
+
this.app.post('/__admin/presigned-urls/get', (req, res) => {
|
|
306
|
+
try {
|
|
307
|
+
const { bucket, key, expiresIn, credentials } = req.body || {};
|
|
308
|
+
|
|
309
|
+
if (!bucket || !key) {
|
|
310
|
+
return res.status(400).json({ error: 'bucket and key are required' });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const result = this.simulator.generatePresignedGetUrl(bucket, key, { expiresIn, credentials });
|
|
314
|
+
|
|
315
|
+
if (result.error) {
|
|
316
|
+
return res.status(result.status || 500).json(result.error);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
res.json({
|
|
320
|
+
presignedUrl: result.presignedUrl,
|
|
321
|
+
expiresIn: expiresIn || 3600,
|
|
322
|
+
expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
|
|
323
|
+
});
|
|
324
|
+
} catch (err) {
|
|
325
|
+
logger.error('Error generating presigned GET URL:', err);
|
|
326
|
+
res.status(500).json({ error: err.message });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* POST /__admin/presigned-urls/put
|
|
332
|
+
* Generate a presigned PUT URL for uploading an object
|
|
333
|
+
* Body: { bucket, key, expiresIn?, credentials?, contentType? }
|
|
334
|
+
*/
|
|
335
|
+
this.app.post('/__admin/presigned-urls/put', (req, res) => {
|
|
336
|
+
try {
|
|
337
|
+
const { bucket, key, expiresIn, credentials, contentType } = req.body || {};
|
|
338
|
+
|
|
339
|
+
if (!bucket || !key) {
|
|
340
|
+
return res.status(400).json({ error: 'bucket and key are required' });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const result = this.simulator.generatePresignedPutUrl(bucket, key, { expiresIn, credentials, contentType });
|
|
344
|
+
|
|
345
|
+
if (result.error) {
|
|
346
|
+
return res.status(result.status || 500).json(result.error);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
res.json({
|
|
350
|
+
presignedUrl: result.presignedUrl,
|
|
351
|
+
expiresIn: expiresIn || 3600,
|
|
352
|
+
expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
|
|
353
|
+
});
|
|
354
|
+
} catch (err) {
|
|
355
|
+
logger.error('Error generating presigned PUT URL:', err);
|
|
356
|
+
res.status(500).json({ error: err.message });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* POST /__admin/presigned-urls/delete
|
|
362
|
+
* Generate a presigned DELETE URL for deleting an object
|
|
363
|
+
* Body: { bucket, key, expiresIn?, credentials? }
|
|
364
|
+
*/
|
|
365
|
+
this.app.post('/__admin/presigned-urls/delete', (req, res) => {
|
|
366
|
+
try {
|
|
367
|
+
const { bucket, key, expiresIn, credentials } = req.body || {};
|
|
368
|
+
|
|
369
|
+
if (!bucket || !key) {
|
|
370
|
+
return res.status(400).json({ error: 'bucket and key are required' });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const result = this.simulator.generatePresignedDeleteUrl(bucket, key, { expiresIn, credentials });
|
|
374
|
+
|
|
375
|
+
if (result.error) {
|
|
376
|
+
return res.status(result.status || 500).json(result.error);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
res.json({
|
|
380
|
+
presignedUrl: result.presignedUrl,
|
|
381
|
+
expiresIn: expiresIn || 3600,
|
|
382
|
+
expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
logger.error('Error generating presigned DELETE URL:', err);
|
|
386
|
+
res.status(500).json({ error: err.message });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* POST /__admin/presigned-urls/head
|
|
392
|
+
* Generate a presigned HEAD URL for checking object metadata
|
|
393
|
+
* Body: { bucket, key, expiresIn?, credentials? }
|
|
394
|
+
*/
|
|
395
|
+
this.app.post('/__admin/presigned-urls/head', (req, res) => {
|
|
396
|
+
try {
|
|
397
|
+
const { bucket, key, expiresIn, credentials } = req.body || {};
|
|
398
|
+
|
|
399
|
+
if (!bucket || !key) {
|
|
400
|
+
return res.status(400).json({ error: 'bucket and key are required' });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const result = this.simulator.generatePresignedHeadUrl(bucket, key, { expiresIn, credentials });
|
|
404
|
+
|
|
405
|
+
if (result.error) {
|
|
406
|
+
return res.status(result.status || 500).json(result.error);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
res.json({
|
|
410
|
+
presignedUrl: result.presignedUrl,
|
|
411
|
+
expiresIn: expiresIn || 3600,
|
|
412
|
+
expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
|
|
413
|
+
});
|
|
414
|
+
} catch (err) {
|
|
415
|
+
logger.error('Error generating presigned HEAD URL:', err);
|
|
416
|
+
res.status(500).json({ error: err.message });
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* POST /__admin/presigned-urls/post-policy
|
|
422
|
+
* Generate a presigned POST policy for form uploads
|
|
423
|
+
* Body: { bucket, key, expiresIn?, credentials?, contentType?, conditions? }
|
|
424
|
+
*/
|
|
425
|
+
this.app.post('/__admin/presigned-urls/post-policy', (req, res) => {
|
|
426
|
+
try {
|
|
427
|
+
const { bucket, key, expiresIn, credentials, contentType, conditions } = req.body || {};
|
|
428
|
+
|
|
429
|
+
if (!bucket || !key) {
|
|
430
|
+
return res.status(400).json({ error: 'bucket and key are required' });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const result = this.simulator.generatePresignedPostPolicy(bucket, key, { expiresIn, credentials, contentType, conditions });
|
|
434
|
+
|
|
435
|
+
if (result.error) {
|
|
436
|
+
return res.status(result.status || 500).json(result.error);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
res.json({
|
|
440
|
+
...result,
|
|
441
|
+
expiresIn: expiresIn || 3600,
|
|
442
|
+
expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
|
|
443
|
+
});
|
|
444
|
+
} catch (err) {
|
|
445
|
+
logger.error('Error generating presigned POST policy:', err);
|
|
446
|
+
res.status(500).json({ error: err.message });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* POST /__admin/presigned-urls/validate
|
|
452
|
+
* Validate a presigned URL
|
|
453
|
+
* Body: { url, method?, credentials? }
|
|
454
|
+
*/
|
|
455
|
+
this.app.post('/__admin/presigned-urls/validate', (req, res) => {
|
|
456
|
+
try {
|
|
457
|
+
const { url, method, credentials } = req.body || {};
|
|
458
|
+
|
|
459
|
+
if (!url) {
|
|
460
|
+
return res.status(400).json({ error: 'url is required' });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const result = this.simulator.validatePresignedUrl(url, method || 'GET', credentials);
|
|
464
|
+
res.json(result);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
logger.error('Error validating presigned URL:', err);
|
|
467
|
+
res.status(500).json({ error: err.message });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
267
470
|
}
|
|
268
471
|
|
|
269
472
|
_serveWebsite(req, res, keyPath) {
|
|
@@ -318,6 +521,34 @@ class S3Server {
|
|
|
318
521
|
</WebsiteConfiguration>`;
|
|
319
522
|
}
|
|
320
523
|
|
|
524
|
+
_generateNotificationConfigXml(config) {
|
|
525
|
+
const nc = config || { LambdaFunctionConfigurations: [], QueueConfigurations: [], TopicConfigurations: [] };
|
|
526
|
+
const lambdaXml = (nc.LambdaFunctionConfigurations || []).map(cfg => `
|
|
527
|
+
<CloudFunctionConfiguration>
|
|
528
|
+
<Id>${cfg.Id || ''}</Id>
|
|
529
|
+
${(cfg.Events || []).map(e => `<Event>${e}</Event>`).join('')}
|
|
530
|
+
<CloudFunction>${cfg.LambdaFunctionArn}</CloudFunction>
|
|
531
|
+
${cfg.Filter && cfg.Filter.Key ? `<Filter><S3Key>${(cfg.Filter.Key.FilterRules || []).map(r => `<FilterRule><Name>${r.Name}</Name><Value>${r.Value}</Value></FilterRule>`).join('')}</S3Key></Filter>` : ''}
|
|
532
|
+
</CloudFunctionConfiguration>`).join('');
|
|
533
|
+
const queueXml = (nc.QueueConfigurations || []).map(cfg => `
|
|
534
|
+
<QueueConfiguration>
|
|
535
|
+
<Id>${cfg.Id || ''}</Id>
|
|
536
|
+
${(cfg.Events || []).map(e => `<Event>${e}</Event>`).join('')}
|
|
537
|
+
<Queue>${cfg.QueueArn}</Queue>
|
|
538
|
+
${cfg.Filter && cfg.Filter.Key ? `<Filter><S3Key>${(cfg.Filter.Key.FilterRules || []).map(r => `<FilterRule><Name>${r.Name}</Name><Value>${r.Value}</Value></FilterRule>`).join('')}</S3Key></Filter>` : ''}
|
|
539
|
+
</QueueConfiguration>`).join('');
|
|
540
|
+
const topicXml = (nc.TopicConfigurations || []).map(cfg => `
|
|
541
|
+
<TopicConfiguration>
|
|
542
|
+
<Id>${cfg.Id || ''}</Id>
|
|
543
|
+
${(cfg.Events || []).map(e => `<Event>${e}</Event>`).join('')}
|
|
544
|
+
<Topic>${cfg.TopicArn}</Topic>
|
|
545
|
+
${cfg.Filter && cfg.Filter.Key ? `<Filter><S3Key>${(cfg.Filter.Key.FilterRules || []).map(r => `<FilterRule><Name>${r.Name}</Name><Value>${r.Value}</Value></FilterRule>`).join('')}</S3Key></Filter>` : ''}
|
|
546
|
+
</TopicConfiguration>`).join('');
|
|
547
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
548
|
+
<NotificationConfiguration>${lambdaXml}${queueXml}${topicXml}
|
|
549
|
+
</NotificationConfiguration>`;
|
|
550
|
+
}
|
|
551
|
+
|
|
321
552
|
start() {
|
|
322
553
|
return new Promise((resolve) => {
|
|
323
554
|
this.server = this.app.listen(this.port, () => {
|