@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,841 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route53 Server
|
|
3
|
+
* REST API para gerenciamento de DNS
|
|
4
|
+
* AWS CLI compatible com respostas em XML
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const logger = require('../../utils/logger');
|
|
9
|
+
|
|
10
|
+
class Route53Server {
|
|
11
|
+
constructor(port, config) {
|
|
12
|
+
this.port = port;
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.app = express();
|
|
15
|
+
this.setupMiddlewares();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setupMiddlewares() {
|
|
19
|
+
// ===== CORS Middleware =====
|
|
20
|
+
this.app.use((req, res, next) => {
|
|
21
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
22
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
23
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
24
|
+
res.header('Access-Control-Max-Age', '86400');
|
|
25
|
+
|
|
26
|
+
// Handle preflight requests
|
|
27
|
+
if (req.method === 'OPTIONS') {
|
|
28
|
+
return res.sendStatus(200);
|
|
29
|
+
}
|
|
30
|
+
next();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Capture ALL request bodies as raw text regardless of Content-Type
|
|
34
|
+
this.app.use(express.raw({ type: () => true, limit: '10mb' }));
|
|
35
|
+
|
|
36
|
+
// Copy body to rawBody for compatibility, then log
|
|
37
|
+
this.app.use((req, res, next) => {
|
|
38
|
+
// Normalise rawBody: handle both Buffer and string
|
|
39
|
+
let data = '';
|
|
40
|
+
if (Buffer.isBuffer(req.body)) {
|
|
41
|
+
data = req.body.toString('utf8');
|
|
42
|
+
} else if (typeof req.body === 'string') {
|
|
43
|
+
data = req.body;
|
|
44
|
+
}
|
|
45
|
+
req.rawBody = data;
|
|
46
|
+
|
|
47
|
+
console.log(`\n[Route53 RAW] ${req.method} ${req.path} ${Object.keys(req.query).length ? '?' + new URLSearchParams(req.query).toString() : ''}`);
|
|
48
|
+
console.log(`[Route53 RAW] Headers:`);
|
|
49
|
+
console.log(` Content-Type: ${req.get('content-type')}`);
|
|
50
|
+
console.log(` Content-Length: ${req.get('content-length')}`);
|
|
51
|
+
console.log(` User-Agent: ${req.get('user-agent')}`);
|
|
52
|
+
console.log(` Authorization: ${req.get('authorization') ? req.get('authorization').substring(0, 30) + '...' : 'undefined'}`);
|
|
53
|
+
console.log(`[Route53 RAW] Body length: ${data.length}`);
|
|
54
|
+
console.log(`[Route53 RAW] Body (first 800 chars): ${data.substring(0, 800)}`);
|
|
55
|
+
console.log(`[Route53 RAW] Body (last 200 chars): ${data.substring(Math.max(0, data.length - 200))}`);
|
|
56
|
+
|
|
57
|
+
const contentType = req.get('content-type') || '';
|
|
58
|
+
if (data && contentType.includes('application/json')) {
|
|
59
|
+
try {
|
|
60
|
+
req.body = JSON.parse(data);
|
|
61
|
+
console.log(`[Route53 RAW] Parsed as JSON`);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(`[Route53 RAW] JSON Parse Error: ${e.message}`);
|
|
64
|
+
req.body = data;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
console.log(`[Route53 RAW] Using raw text body (not JSON)`);
|
|
68
|
+
req.body = data;
|
|
69
|
+
}
|
|
70
|
+
next();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.app.use((req, res, next) => {
|
|
74
|
+
if (req.path.includes('/2013-04-01/hostedzone')) {
|
|
75
|
+
logger.info(`[Route53] ${req.method} ${req.path}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Por padrão retorna XML (para AWS CLI)
|
|
79
|
+
if (req.get('accept')?.includes('application/json') || req.path.includes('/json/')) {
|
|
80
|
+
res.set('Content-Type', 'application/json');
|
|
81
|
+
} else {
|
|
82
|
+
res.set('Content-Type', 'application/xml');
|
|
83
|
+
}
|
|
84
|
+
next();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Helper para gerar resposta XML
|
|
89
|
+
xmlResponse(xml) {
|
|
90
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${xml}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Helper para gerar resposta de erro no formato correto do AWS Route53
|
|
94
|
+
errorResponse(statusCode, errorType, message) {
|
|
95
|
+
const xml = `<ErrorResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
96
|
+
<Error>
|
|
97
|
+
<Type>${errorType}</Type>
|
|
98
|
+
<Code>${errorType}</Code>
|
|
99
|
+
<Message>${message}</Message>
|
|
100
|
+
</Error>
|
|
101
|
+
<RequestId>req-${Date.now()}</RequestId>
|
|
102
|
+
</ErrorResponse>`;
|
|
103
|
+
return { statusCode, xml };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Parser de XML simplificado
|
|
107
|
+
parseXML(xmlString) {
|
|
108
|
+
const result = {};
|
|
109
|
+
|
|
110
|
+
// Extrair Name
|
|
111
|
+
const nameMatch = xmlString.match(/<Name>([^<]+)<\/Name>/);
|
|
112
|
+
if (nameMatch) result.Name = nameMatch[1].trim();
|
|
113
|
+
|
|
114
|
+
// Extrair CallerReference
|
|
115
|
+
const refMatch = xmlString.match(/<CallerReference>([^<]+)<\/CallerReference>/);
|
|
116
|
+
if (refMatch) result.CallerReference = refMatch[1].trim();
|
|
117
|
+
|
|
118
|
+
// Extrair Type
|
|
119
|
+
const typeMatch = xmlString.match(/<Type>([^<]+)<\/Type>/);
|
|
120
|
+
if (typeMatch) result.Type = typeMatch[1].trim();
|
|
121
|
+
|
|
122
|
+
// Extrair TTL
|
|
123
|
+
const ttlMatch = xmlString.match(/<TTL>(\d+)<\/TTL>/);
|
|
124
|
+
if (ttlMatch) result.TTL = parseInt(ttlMatch[1]);
|
|
125
|
+
|
|
126
|
+
// Extrair Values/ResourceRecords
|
|
127
|
+
const valueMatches = xmlString.match(/<Value>([^<]+)<\/Value>/g);
|
|
128
|
+
if (valueMatches) {
|
|
129
|
+
result.ResourceRecords = valueMatches.map(v => {
|
|
130
|
+
const val = v.replace(/<\/?Value>/g, '').trim();
|
|
131
|
+
return { Value: val };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
parseChangeBatchXML(xmlString) {
|
|
139
|
+
const changes = [];
|
|
140
|
+
|
|
141
|
+
// Find all Change blocks
|
|
142
|
+
const changeMatches = xmlString.match(/<Change>[\s\S]*?<\/Change>/g) || [];
|
|
143
|
+
|
|
144
|
+
for (const changeXml of changeMatches) {
|
|
145
|
+
const change = {};
|
|
146
|
+
|
|
147
|
+
// Extract Action
|
|
148
|
+
const actionMatch = changeXml.match(/<Action>([^<]+)<\/Action>/);
|
|
149
|
+
if (actionMatch) change.Action = actionMatch[1].trim();
|
|
150
|
+
|
|
151
|
+
// Extract ResourceRecordSet
|
|
152
|
+
const resourceRecordSetMatch = changeXml.match(/<ResourceRecordSet>([\s\S]*?)<\/ResourceRecordSet>/);
|
|
153
|
+
if (resourceRecordSetMatch) {
|
|
154
|
+
const rrsXml = resourceRecordSetMatch[1];
|
|
155
|
+
const rrs = {};
|
|
156
|
+
|
|
157
|
+
// Extract Name
|
|
158
|
+
const nameMatch = rrsXml.match(/<Name>([^<]+)<\/Name>/);
|
|
159
|
+
if (nameMatch) rrs.Name = nameMatch[1].trim();
|
|
160
|
+
|
|
161
|
+
// Extract Type
|
|
162
|
+
const typeMatch = rrsXml.match(/<Type>([^<]+)<\/Type>/);
|
|
163
|
+
if (typeMatch) rrs.Type = typeMatch[1].trim();
|
|
164
|
+
|
|
165
|
+
// Extract TTL
|
|
166
|
+
const ttlMatch = rrsXml.match(/<TTL>(\d+)<\/TTL>/);
|
|
167
|
+
if (ttlMatch) rrs.TTL = parseInt(ttlMatch[1]);
|
|
168
|
+
|
|
169
|
+
// Extract ResourceRecords
|
|
170
|
+
const valueMatches = rrsXml.match(/<Value>([^<]+)<\/Value>/g) || [];
|
|
171
|
+
rrs.ResourceRecords = valueMatches.map(v => {
|
|
172
|
+
const val = v.replace(/<\/?Value>/g, '').trim();
|
|
173
|
+
return { Value: val };
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
change.ResourceRecordSet = rrs;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (change.Action && change.ResourceRecordSet) {
|
|
180
|
+
changes.push(change);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { Changes: changes };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async initialize(simulator) {
|
|
188
|
+
this.simulator = simulator;
|
|
189
|
+
this.setupRoutes();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setupRoutes() {
|
|
193
|
+
// === ROTAS JSON (para dashboard React) ===
|
|
194
|
+
this.app.get('/json/hosted-zones', (req, res) => this.listHostedZonesJson(req, res));
|
|
195
|
+
this.app.get('/json/hosted-zone/:id/records', (req, res) => this.listResourceRecordsJson(req, res));
|
|
196
|
+
this.app.post('/json/hosted-zone', (req, res) => this.createHostedZoneJson(req, res));
|
|
197
|
+
this.app.delete('/json/hosted-zone/:id', (req, res) => this.deleteHostedZoneJson(req, res));
|
|
198
|
+
|
|
199
|
+
// === ROTAS XML (para AWS CLI) ===
|
|
200
|
+
// Hosted Zones
|
|
201
|
+
this.app.post('/2013-04-01/hostedzone', (req, res) => this.createHostedZone(req, res));
|
|
202
|
+
this.app.get('/2013-04-01/hostedzone', (req, res) => this.listHostedZones(req, res));
|
|
203
|
+
this.app.get('/2013-04-01/hostedzone/:id', (req, res) => this.getHostedZone(req, res));
|
|
204
|
+
this.app.delete('/2013-04-01/hostedzone/:id', (req, res) => this.deleteHostedZone(req, res));
|
|
205
|
+
|
|
206
|
+
// Resource Records
|
|
207
|
+
// Use both POST methods - one for batch changes, one for single record
|
|
208
|
+
this.app.post('/2013-04-01/hostedzone/:id/rrset', (req, res) => {
|
|
209
|
+
// AWS CLI sends XML for change-resource-record-sets; always route to changeResourceRecordSets
|
|
210
|
+
// which handles both XML (ChangeResourceRecordSetsRequest with Changes array)
|
|
211
|
+
// and JSON batch payloads.
|
|
212
|
+
const body = req.body || '';
|
|
213
|
+
if (typeof body === 'string' && body.trim().startsWith('<')) {
|
|
214
|
+
// XML body — always a ChangeBatch operation
|
|
215
|
+
this.changeResourceRecordSets(req, res);
|
|
216
|
+
} else if (typeof body === 'object' && body !== null && Array.isArray(body.Changes)) {
|
|
217
|
+
this.changeResourceRecordSets(req, res);
|
|
218
|
+
} else if (typeof body === 'string') {
|
|
219
|
+
// Try JSON
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(body);
|
|
222
|
+
if (parsed.Changes && Array.isArray(parsed.Changes)) {
|
|
223
|
+
req.body = parsed;
|
|
224
|
+
this.changeResourceRecordSets(req, res);
|
|
225
|
+
} else {
|
|
226
|
+
this.createResourceRecord(req, res);
|
|
227
|
+
}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// Not JSON either — fall through to single-record create
|
|
230
|
+
this.createResourceRecord(req, res);
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
this.createResourceRecord(req, res);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
this.app.get('/2013-04-01/hostedzone/:id/rrset', (req, res) => this.listResourceRecords(req, res));
|
|
237
|
+
this.app.delete('/2013-04-01/hostedzone/:id/rrset/:name/:type', (req, res) => this.deleteResourceRecord(req, res));
|
|
238
|
+
|
|
239
|
+
// Health Checks
|
|
240
|
+
this.app.post('/2013-04-01/healthcheck', (req, res) => this.createHealthCheck(req, res));
|
|
241
|
+
this.app.get('/2013-04-01/healthcheck', (req, res) => this.listHealthChecks(req, res));
|
|
242
|
+
this.app.get('/2013-04-01/healthcheck/:id', (req, res) => this.getHealthCheck(req, res));
|
|
243
|
+
this.app.delete('/2013-04-01/healthcheck/:id', (req, res) => this.deleteHealthCheck(req, res));
|
|
244
|
+
|
|
245
|
+
// DNS Resolution
|
|
246
|
+
this.app.get('/2013-04-01/resolve/:domain/:type', (req, res) => this.resolveDNS(req, res));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async createHostedZone(req, res) {
|
|
250
|
+
try {
|
|
251
|
+
logger.info('[Route53] Raw body:', req.rawBody ? req.rawBody.substring(0, 300) : 'empty');
|
|
252
|
+
|
|
253
|
+
let body = {};
|
|
254
|
+
|
|
255
|
+
// Parsear body (XML ou JSON)
|
|
256
|
+
if (typeof req.body === 'string' && req.body.trim().startsWith('<')) {
|
|
257
|
+
// É XML
|
|
258
|
+
body = this.parseXML(req.body);
|
|
259
|
+
} else if (typeof req.body === 'string') {
|
|
260
|
+
// Tentar JSON
|
|
261
|
+
try {
|
|
262
|
+
body = JSON.parse(req.body);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
logger.error('[Route53] Erro ao parsear body');
|
|
265
|
+
}
|
|
266
|
+
} else if (typeof req.body === 'object') {
|
|
267
|
+
body = req.body;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
logger.info('[Route53] Parsed body:', body);
|
|
271
|
+
|
|
272
|
+
const { Name, CallerReference } = body;
|
|
273
|
+
|
|
274
|
+
if (!Name) {
|
|
275
|
+
const { statusCode, xml } = this.errorResponse(400, 'MissingParameter', 'Name is required');
|
|
276
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const zone = this.simulator.createHostedZone(Name, {
|
|
280
|
+
CallerReference: CallerReference || Date.now().toString(),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const zoneData = zone.toJSON();
|
|
284
|
+
const xml = `<CreateHostedZoneResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
285
|
+
<HostedZone>
|
|
286
|
+
<Id>${zoneData.Id}</Id>
|
|
287
|
+
<Name>${zoneData.Name}</Name>
|
|
288
|
+
<CallerReference>${zoneData.CallerReference}</CallerReference>
|
|
289
|
+
<Config>
|
|
290
|
+
<PrivateZone>false</PrivateZone>
|
|
291
|
+
</Config>
|
|
292
|
+
<ResourceRecordSetCount>0</ResourceRecordSetCount>
|
|
293
|
+
</HostedZone>
|
|
294
|
+
<ChangeInfo>
|
|
295
|
+
<Id>/change/${zoneData.Id.replace(/^\/hostedzone\//, "")}</Id>
|
|
296
|
+
<Status>PENDING</Status>
|
|
297
|
+
<SubmittedAt>${new Date().toISOString()}</SubmittedAt>
|
|
298
|
+
</ChangeInfo>
|
|
299
|
+
<DelegationSet>
|
|
300
|
+
<NameServers>
|
|
301
|
+
<NameServer>ns-1.example.com</NameServer>
|
|
302
|
+
<NameServer>ns-2.example.com</NameServer>
|
|
303
|
+
<NameServer>ns-3.example.com</NameServer>
|
|
304
|
+
<NameServer>ns-4.example.com</NameServer>
|
|
305
|
+
</NameServers>
|
|
306
|
+
</DelegationSet>
|
|
307
|
+
</CreateHostedZoneResponse>`;
|
|
308
|
+
|
|
309
|
+
res.status(201).send(this.xmlResponse(xml));
|
|
310
|
+
} catch (error) {
|
|
311
|
+
logger.error('[Route53] CreateHostedZone error:', error);
|
|
312
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
313
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async listHostedZones(req, res) {
|
|
318
|
+
try {
|
|
319
|
+
const zones = this.simulator.listHostedZones();
|
|
320
|
+
|
|
321
|
+
const zonesXml = zones.map(z => {
|
|
322
|
+
const zone = z.toJSON();
|
|
323
|
+
return `
|
|
324
|
+
<HostedZone>
|
|
325
|
+
<Id>${zone.Id}</Id>
|
|
326
|
+
<Name>${zone.Name}</Name>
|
|
327
|
+
<CallerReference>${zone.CallerReference}</CallerReference>
|
|
328
|
+
<Config>
|
|
329
|
+
<PrivateZone>false</PrivateZone>
|
|
330
|
+
</Config>
|
|
331
|
+
<ResourceRecordSetCount>${zone.ResourceRecordSetCount || 0}</ResourceRecordSetCount>
|
|
332
|
+
</HostedZone>`;
|
|
333
|
+
}).join('');
|
|
334
|
+
|
|
335
|
+
const xml = `<ListHostedZonesResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
336
|
+
<HostedZones>${zonesXml}
|
|
337
|
+
</HostedZones>
|
|
338
|
+
<IsTruncated>false</IsTruncated>
|
|
339
|
+
<MaxItems>100</MaxItems>
|
|
340
|
+
</ListHostedZonesResponse>`;
|
|
341
|
+
|
|
342
|
+
res.send(this.xmlResponse(xml));
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
345
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async getHostedZone(req, res) {
|
|
350
|
+
try {
|
|
351
|
+
const zone = this.simulator.getHostedZone(req.params.id);
|
|
352
|
+
if (!zone) {
|
|
353
|
+
const { statusCode, xml } = this.errorResponse(404, 'NoSuchHostedZone', `Hosted Zone not found with ID: ${req.params.id}`);
|
|
354
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const zoneData = zone.toJSON();
|
|
358
|
+
const xml = `<GetHostedZoneResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
359
|
+
<HostedZone>
|
|
360
|
+
<Id>${zoneData.Id}</Id>
|
|
361
|
+
<Name>${zoneData.Name}</Name>
|
|
362
|
+
<CallerReference>${zoneData.CallerReference}</CallerReference>
|
|
363
|
+
<Config>
|
|
364
|
+
<PrivateZone>false</PrivateZone>
|
|
365
|
+
</Config>
|
|
366
|
+
<ResourceRecordSetCount>${zoneData.ResourceRecordSetCount || 0}</ResourceRecordSetCount>
|
|
367
|
+
</HostedZone>
|
|
368
|
+
<DelegationSet>
|
|
369
|
+
<NameServers>
|
|
370
|
+
<NameServer>ns-1.example.com</NameServer>
|
|
371
|
+
<NameServer>ns-2.example.com</NameServer>
|
|
372
|
+
<NameServer>ns-3.example.com</NameServer>
|
|
373
|
+
<NameServer>ns-4.example.com</NameServer>
|
|
374
|
+
</NameServers>
|
|
375
|
+
</DelegationSet>
|
|
376
|
+
</GetHostedZoneResponse>`;
|
|
377
|
+
|
|
378
|
+
res.send(this.xmlResponse(xml));
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
381
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async deleteHostedZone(req, res) {
|
|
386
|
+
try {
|
|
387
|
+
this.simulator.deleteHostedZone(req.params.id);
|
|
388
|
+
|
|
389
|
+
const xml = `<DeleteHostedZoneResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
390
|
+
<ChangeInfo>
|
|
391
|
+
<Id>/change/${req.params.id}</Id>
|
|
392
|
+
<Status>PENDING</Status>
|
|
393
|
+
<SubmittedAt>${new Date().toISOString()}</SubmittedAt>
|
|
394
|
+
</ChangeInfo>
|
|
395
|
+
</DeleteHostedZoneResponse>`;
|
|
396
|
+
|
|
397
|
+
res.send(this.xmlResponse(xml));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
const errorType = error.message && error.message.includes('not found') ? 'NoSuchHostedZone' : 'InternalError';
|
|
400
|
+
const { statusCode, xml } = this.errorResponse(
|
|
401
|
+
errorType === 'NoSuchHostedZone' ? 404 : 400,
|
|
402
|
+
errorType,
|
|
403
|
+
error.message
|
|
404
|
+
);
|
|
405
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async createResourceRecord(req, res) {
|
|
410
|
+
try {
|
|
411
|
+
let body = {};
|
|
412
|
+
|
|
413
|
+
if (typeof req.body === 'string' && req.body.trim().startsWith('<')) {
|
|
414
|
+
body = this.parseXML(req.body);
|
|
415
|
+
} else if (typeof req.body === 'string') {
|
|
416
|
+
try {
|
|
417
|
+
body = JSON.parse(req.body);
|
|
418
|
+
} catch (e) {}
|
|
419
|
+
} else {
|
|
420
|
+
body = req.body || {};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const { id } = req.params;
|
|
424
|
+
const { Name, Type, TTL, ResourceRecords } = body;
|
|
425
|
+
|
|
426
|
+
if (!Name || !Type || !ResourceRecords || ResourceRecords.length === 0) {
|
|
427
|
+
const { statusCode, xml } = this.errorResponse(400, 'MissingParameter', 'Name, Type, and ResourceRecords are required');
|
|
428
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const values = ResourceRecords.map(r => typeof r === 'string' ? r : r.Value);
|
|
432
|
+
const record = this.simulator.createResourceRecord(id, Name, Type, values, { TTL: TTL || 300 });
|
|
433
|
+
|
|
434
|
+
const xml = `<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
435
|
+
<ChangeInfo>
|
|
436
|
+
<Id>/change/${id}</Id>
|
|
437
|
+
<Status>PENDING</Status>
|
|
438
|
+
<SubmittedAt>${new Date().toISOString()}</SubmittedAt>
|
|
439
|
+
</ChangeInfo>
|
|
440
|
+
</ChangeResourceRecordSetsResponse>`;
|
|
441
|
+
|
|
442
|
+
res.status(201).send(this.xmlResponse(xml));
|
|
443
|
+
} catch (error) {
|
|
444
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
445
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async changeResourceRecordSets(req, res) {
|
|
450
|
+
try {
|
|
451
|
+
const { id } = req.params;
|
|
452
|
+
|
|
453
|
+
// Validate zone exists before processing changes
|
|
454
|
+
const zone = this.simulator.getHostedZone(id);
|
|
455
|
+
if (!zone) {
|
|
456
|
+
const { statusCode, xml } = this.errorResponse(404, 'NoSuchHostedZone', `No hosted zone found with ID: ${id}`);
|
|
457
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let changeBatch = {};
|
|
461
|
+
|
|
462
|
+
console.log(`\n[changeResourceRecordSets] Processing request for zone: ${id}`);
|
|
463
|
+
console.log(`[changeResourceRecordSets] req.body type: ${typeof req.body}`);
|
|
464
|
+
console.log(`[changeResourceRecordSets] req.body: ${JSON.stringify(req.body).substring(0, 300)}`);
|
|
465
|
+
|
|
466
|
+
// Parse XML or JSON
|
|
467
|
+
if (typeof req.body === 'string' && req.body.trim().startsWith('<')) {
|
|
468
|
+
console.log('[changeResourceRecordSets] Parsing as XML');
|
|
469
|
+
changeBatch = this.parseChangeBatchXML(req.body);
|
|
470
|
+
} else if (typeof req.body === 'string') {
|
|
471
|
+
console.log('[changeResourceRecordSets] Parsing as JSON string');
|
|
472
|
+
try {
|
|
473
|
+
changeBatch = JSON.parse(req.body);
|
|
474
|
+
} catch (e) {
|
|
475
|
+
console.error(`[changeResourceRecordSets] JSON Parse Error: ${e.message}`);
|
|
476
|
+
console.error(`[changeResourceRecordSets] Body: ${req.body.substring(0, 500)}`);
|
|
477
|
+
const { statusCode, xml } = this.errorResponse(400, 'InvalidInput', `Invalid request body: ${e.message}`);
|
|
478
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
479
|
+
}
|
|
480
|
+
} else if (typeof req.body === 'object') {
|
|
481
|
+
console.log('[changeResourceRecordSets] Using object body');
|
|
482
|
+
changeBatch = req.body || {};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
console.log(`[changeResourceRecordSets] Parsed changeBatch: ${JSON.stringify(changeBatch).substring(0, 300)}`);
|
|
486
|
+
|
|
487
|
+
const { Changes } = changeBatch;
|
|
488
|
+
if (!Changes || !Array.isArray(Changes)) {
|
|
489
|
+
console.error(`[changeResourceRecordSets] No Changes array found`);
|
|
490
|
+
const { statusCode, xml } = this.errorResponse(400, 'InvalidInput', 'ChangeBatch with Changes array is required');
|
|
491
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
console.log(`[changeResourceRecordSets] Found ${Changes.length} changes to process`);
|
|
495
|
+
|
|
496
|
+
// Process each change
|
|
497
|
+
for (let i = 0; i < Changes.length; i++) {
|
|
498
|
+
const change = Changes[i];
|
|
499
|
+
console.log(`[changeResourceRecordSets] Processing change ${i}: ${JSON.stringify(change).substring(0, 200)}`);
|
|
500
|
+
|
|
501
|
+
const { Action, ResourceRecordSet } = change;
|
|
502
|
+
const { Name, Type, TTL, ResourceRecords } = ResourceRecordSet || {};
|
|
503
|
+
|
|
504
|
+
console.log(`[changeResourceRecordSets] Action: ${Action}, Name: ${Name}, Type: ${Type}`);
|
|
505
|
+
|
|
506
|
+
if (!Action || !ResourceRecordSet || !Name || !Type || !ResourceRecords) {
|
|
507
|
+
console.error(`[changeResourceRecordSets] Invalid change - Missing required fields`);
|
|
508
|
+
const { statusCode, xml } = this.errorResponse(400, 'InvalidInput', 'Each Change must have Action, Name, Type, and ResourceRecords');
|
|
509
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (Action === 'CREATE') {
|
|
513
|
+
console.log(`[changeResourceRecordSets] Creating record: ${Name}`);
|
|
514
|
+
const values = ResourceRecords.map(r => typeof r === 'string' ? r : r.Value);
|
|
515
|
+
this.simulator.createResourceRecord(id, Name, Type, values, { TTL: TTL || 300 });
|
|
516
|
+
console.log(`[changeResourceRecordSets] Record created successfully`);
|
|
517
|
+
} else if (Action === 'DELETE') {
|
|
518
|
+
console.log(`[changeResourceRecordSets] Deleting record: ${Name}`);
|
|
519
|
+
this.simulator.deleteResourceRecord(id, Name, Type);
|
|
520
|
+
} else if (Action === 'UPSERT') {
|
|
521
|
+
console.log(`[changeResourceRecordSets] Upserting record: ${Name}`);
|
|
522
|
+
try {
|
|
523
|
+
this.simulator.deleteResourceRecord(id, Name, Type);
|
|
524
|
+
} catch (e) {
|
|
525
|
+
// Record might not exist, that's ok
|
|
526
|
+
}
|
|
527
|
+
const values = ResourceRecords.map(r => typeof r === 'string' ? r : r.Value);
|
|
528
|
+
this.simulator.createResourceRecord(id, Name, Type, values, { TTL: TTL || 300 });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const xml = `<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
533
|
+
<ChangeInfo>
|
|
534
|
+
<Id>/change/${id}</Id>
|
|
535
|
+
<Status>PENDING</Status>
|
|
536
|
+
<SubmittedAt>${new Date().toISOString()}</SubmittedAt>
|
|
537
|
+
</ChangeInfo>
|
|
538
|
+
</ChangeResourceRecordSetsResponse>`;
|
|
539
|
+
|
|
540
|
+
console.log(`[changeResourceRecordSets] ✅ Success - sending response`);
|
|
541
|
+
res.status(200).send(this.xmlResponse(xml));
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.error(`[changeResourceRecordSets] ❌ Error:`, error);
|
|
544
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
545
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async listResourceRecords(req, res) {
|
|
550
|
+
try {
|
|
551
|
+
const { id } = req.params;
|
|
552
|
+
|
|
553
|
+
// Validate zone exists before processing
|
|
554
|
+
const zone = this.simulator.getHostedZone(id);
|
|
555
|
+
if (!zone) {
|
|
556
|
+
const { statusCode, xml } = this.errorResponse(404, 'NoSuchHostedZone', `No hosted zone found with ID: ${id}`);
|
|
557
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const records = this.simulator.listResourceRecords(id);
|
|
561
|
+
|
|
562
|
+
const recordsXml = records.map(r => {
|
|
563
|
+
const record = r.toJSON();
|
|
564
|
+
const resourceRecords = record.ResourceRecords.map(rr =>
|
|
565
|
+
`<ResourceRecord><Value>${rr.Value}</Value></ResourceRecord>`
|
|
566
|
+
).join('');
|
|
567
|
+
|
|
568
|
+
return `
|
|
569
|
+
<ResourceRecordSet>
|
|
570
|
+
<Name>${record.Name}</Name>
|
|
571
|
+
<Type>${record.Type}</Type>
|
|
572
|
+
<TTL>${record.TTL}</TTL>
|
|
573
|
+
<ResourceRecords>${resourceRecords}</ResourceRecords>
|
|
574
|
+
</ResourceRecordSet>`;
|
|
575
|
+
}).join('');
|
|
576
|
+
|
|
577
|
+
const xml = `<ListResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
578
|
+
<ResourceRecordSets>${recordsXml}
|
|
579
|
+
</ResourceRecordSets>
|
|
580
|
+
<IsTruncated>false</IsTruncated>
|
|
581
|
+
<MaxItems>100</MaxItems>
|
|
582
|
+
</ListResourceRecordSetsResponse>`;
|
|
583
|
+
|
|
584
|
+
res.send(this.xmlResponse(xml));
|
|
585
|
+
} catch (error) {
|
|
586
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
587
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async deleteResourceRecord(req, res) {
|
|
592
|
+
try {
|
|
593
|
+
const { id, name, type } = req.params;
|
|
594
|
+
this.simulator.deleteResourceRecord(id, decodeURIComponent(name), type);
|
|
595
|
+
|
|
596
|
+
const xml = `<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
597
|
+
<ChangeInfo>
|
|
598
|
+
<Id>/change/${id}</Id>
|
|
599
|
+
<Status>PENDING</Status>
|
|
600
|
+
<SubmittedAt>${new Date().toISOString()}</SubmittedAt>
|
|
601
|
+
</ChangeInfo>
|
|
602
|
+
</ChangeResourceRecordSetsResponse>`;
|
|
603
|
+
|
|
604
|
+
res.send(this.xmlResponse(xml));
|
|
605
|
+
} catch (error) {
|
|
606
|
+
const errorType = error.message && error.message.includes('not found') ? 'NoSuchResourceRecordSet' : 'InternalError';
|
|
607
|
+
const { statusCode, xml } = this.errorResponse(
|
|
608
|
+
errorType === 'NoSuchResourceRecordSet' ? 404 : 400,
|
|
609
|
+
errorType,
|
|
610
|
+
error.message
|
|
611
|
+
);
|
|
612
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async createHealthCheck(req, res) {
|
|
617
|
+
try {
|
|
618
|
+
let body = {};
|
|
619
|
+
|
|
620
|
+
if (typeof req.body === 'string' && req.body.trim().startsWith('<')) {
|
|
621
|
+
body = this.parseXML(req.body);
|
|
622
|
+
} else if (typeof req.body === 'string') {
|
|
623
|
+
try {
|
|
624
|
+
body = JSON.parse(req.body);
|
|
625
|
+
} catch (e) {}
|
|
626
|
+
} else {
|
|
627
|
+
body = req.body || {};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const { HealthCheckConfig } = body;
|
|
631
|
+
|
|
632
|
+
if (!HealthCheckConfig) {
|
|
633
|
+
const { statusCode, xml } = this.errorResponse(400, 'MissingParameter', 'HealthCheckConfig is required');
|
|
634
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const healthCheck = this.simulator.createHealthCheck(HealthCheckConfig);
|
|
638
|
+
const hcData = healthCheck.toJSON();
|
|
639
|
+
|
|
640
|
+
const xml = `<CreateHealthCheckResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
641
|
+
<HealthCheck>
|
|
642
|
+
<Id>${hcData.Id}</Id>
|
|
643
|
+
<HealthCheckConfig>
|
|
644
|
+
<Type>${hcData.Type}</Type>
|
|
645
|
+
</HealthCheckConfig>
|
|
646
|
+
</HealthCheck>
|
|
647
|
+
<HealthCheckVersion>1</HealthCheckVersion>
|
|
648
|
+
</CreateHealthCheckResponse>`;
|
|
649
|
+
|
|
650
|
+
res.status(201).send(this.xmlResponse(xml));
|
|
651
|
+
} catch (error) {
|
|
652
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
653
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async listHealthChecks(req, res) {
|
|
658
|
+
try {
|
|
659
|
+
const healthChecks = this.simulator.listHealthChecks();
|
|
660
|
+
|
|
661
|
+
const hcsXml = healthChecks.map(hc => {
|
|
662
|
+
const hcData = hc.toJSON();
|
|
663
|
+
return `
|
|
664
|
+
<HealthCheck>
|
|
665
|
+
<Id>${hcData.Id}</Id>
|
|
666
|
+
<HealthCheckConfig>
|
|
667
|
+
<Type>${hcData.Type}</Type>
|
|
668
|
+
</HealthCheckConfig>
|
|
669
|
+
</HealthCheck>`;
|
|
670
|
+
}).join('');
|
|
671
|
+
|
|
672
|
+
const xml = `<ListHealthChecksResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
673
|
+
<HealthChecks>${hcsXml}
|
|
674
|
+
</HealthChecks>
|
|
675
|
+
<IsTruncated>false</IsTruncated>
|
|
676
|
+
<MaxItems>100</MaxItems>
|
|
677
|
+
</ListHealthChecksResponse>`;
|
|
678
|
+
|
|
679
|
+
res.send(this.xmlResponse(xml));
|
|
680
|
+
} catch (error) {
|
|
681
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
682
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async getHealthCheck(req, res) {
|
|
687
|
+
try {
|
|
688
|
+
const healthCheck = this.simulator.getHealthCheck(req.params.id);
|
|
689
|
+
|
|
690
|
+
if (!healthCheck) {
|
|
691
|
+
const { statusCode, xml } = this.errorResponse(404, 'NoSuchHealthCheck', `Health Check not found with ID: ${req.params.id}`);
|
|
692
|
+
return res.status(statusCode).send(this.xmlResponse(xml));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const hcData = healthCheck.toJSON();
|
|
696
|
+
const xml = `<GetHealthCheckResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
697
|
+
<HealthCheck>
|
|
698
|
+
<Id>${hcData.Id}</Id>
|
|
699
|
+
<HealthCheckConfig>
|
|
700
|
+
<Type>${hcData.Type}</Type>
|
|
701
|
+
</HealthCheckConfig>
|
|
702
|
+
</HealthCheck>
|
|
703
|
+
<HealthCheckVersion>1</HealthCheckVersion>
|
|
704
|
+
</GetHealthCheckResponse>`;
|
|
705
|
+
|
|
706
|
+
res.send(this.xmlResponse(xml));
|
|
707
|
+
} catch (error) {
|
|
708
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
709
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async deleteHealthCheck(req, res) {
|
|
714
|
+
try {
|
|
715
|
+
this.simulator.deleteHealthCheck(req.params.id);
|
|
716
|
+
|
|
717
|
+
const xml = `<DeleteHealthCheckResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
718
|
+
<Success>true</Success>
|
|
719
|
+
</DeleteHealthCheckResponse>`;
|
|
720
|
+
|
|
721
|
+
res.send(this.xmlResponse(xml));
|
|
722
|
+
} catch (error) {
|
|
723
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
724
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async resolveDNS(req, res) {
|
|
729
|
+
try {
|
|
730
|
+
const { domain, type } = req.params;
|
|
731
|
+
const queryType = req.query.queryType || 'simple';
|
|
732
|
+
|
|
733
|
+
const result = this.simulator.resolveDNS(domain, type, queryType);
|
|
734
|
+
|
|
735
|
+
const recordsXml = result.records.map(r =>
|
|
736
|
+
`<Record>
|
|
737
|
+
<Name>${r.Name}</Name>
|
|
738
|
+
<Type>${r.Type}</Type>
|
|
739
|
+
<TTL>${r.TTL}</TTL>
|
|
740
|
+
<Values>${r.ResourceRecords.map(rr => `<Value>${rr.Value}</Value>`).join('')}</Values>
|
|
741
|
+
</Record>`
|
|
742
|
+
).join('');
|
|
743
|
+
|
|
744
|
+
const xml = `<ResolveDNSResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
745
|
+
<Domain>${result.domain}</Domain>
|
|
746
|
+
<Type>${result.type}</Type>
|
|
747
|
+
<Records>${recordsXml}</Records>
|
|
748
|
+
<ResolvedAt>${result.resolvedAt}</ResolvedAt>
|
|
749
|
+
</ResolveDNSResponse>`;
|
|
750
|
+
|
|
751
|
+
res.send(this.xmlResponse(xml));
|
|
752
|
+
} catch (error) {
|
|
753
|
+
const { statusCode, xml } = this.errorResponse(400, 'InternalError', error.message);
|
|
754
|
+
res.status(statusCode).send(this.xmlResponse(xml));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async start() {
|
|
759
|
+
return new Promise((resolve) => {
|
|
760
|
+
this.server = this.app.listen(this.port, () => {
|
|
761
|
+
logger.info(`✅ Route53 Server running on port ${this.port}`);
|
|
762
|
+
resolve();
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ========== JSON ENDPOINTS (para Dashboard React) ==========
|
|
768
|
+
|
|
769
|
+
async listHostedZonesJson(req, res) {
|
|
770
|
+
try {
|
|
771
|
+
const zones = this.simulator.listHostedZones();
|
|
772
|
+
const data = zones.map(z => z.toJSON());
|
|
773
|
+
res.json({ HostedZones: data, IsTruncated: false, MaxItems: '100' });
|
|
774
|
+
} catch (error) {
|
|
775
|
+
res.status(400).json({ error: error.message });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async listResourceRecordsJson(req, res) {
|
|
780
|
+
try {
|
|
781
|
+
const { id } = req.params;
|
|
782
|
+
const records = this.simulator.listResourceRecords(id);
|
|
783
|
+
const data = records.map(r => r.toJSON());
|
|
784
|
+
res.json({ ResourceRecordSets: data, IsTruncated: false, MaxItems: '100' });
|
|
785
|
+
} catch (error) {
|
|
786
|
+
res.status(400).json({ error: error.message });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async createHostedZoneJson(req, res) {
|
|
791
|
+
try {
|
|
792
|
+
const { name, callerReference, isPrivate } = req.body;
|
|
793
|
+
|
|
794
|
+
if (!name) {
|
|
795
|
+
return res.status(400).json({ error: 'Name is required' });
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const zone = this.simulator.createHostedZone(name, {
|
|
799
|
+
CallerReference: callerReference || Date.now().toString(),
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
res.status(201).json({
|
|
803
|
+
HostedZone: zone.toJSON(),
|
|
804
|
+
ChangeInfo: {
|
|
805
|
+
Id: `/change/${zone.Id}`,
|
|
806
|
+
Status: 'PENDING',
|
|
807
|
+
SubmittedAt: new Date().toISOString(),
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
} catch (error) {
|
|
811
|
+
res.status(400).json({ error: error.message });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async deleteHostedZoneJson(req, res) {
|
|
816
|
+
try {
|
|
817
|
+
this.simulator.deleteHostedZone(req.params.id);
|
|
818
|
+
res.json({
|
|
819
|
+
ChangeInfo: {
|
|
820
|
+
Id: `/change/${req.params.id}`,
|
|
821
|
+
Status: 'PENDING',
|
|
822
|
+
SubmittedAt: new Date().toISOString(),
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
} catch (error) {
|
|
826
|
+
res.status(400).json({ error: error.message });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async stop() {
|
|
831
|
+
return new Promise((resolve) => {
|
|
832
|
+
if (this.server) {
|
|
833
|
+
this.server.close(resolve);
|
|
834
|
+
} else {
|
|
835
|
+
resolve();
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
module.exports = Route53Server;
|