mockapi-msi 2.0.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/modules/core.js CHANGED
@@ -1,111 +1,381 @@
1
- const parser = require('./urlParser');
2
- const http = require('http');
3
- const constants = require('./constants');
4
- const handlerLoader = require('./configurationParser');
5
-
6
- class Core {
7
-
8
- _logger = null;
9
- _port = 0;
10
- _enableCors = false;
11
- _endpointList = [];
12
- _logLevel = ""
13
- _modulesProxy = null;
14
- _configurations = null;
15
- _data = null;
16
-
17
- constructor(logger, configurations, modulesProxy) {
18
- this._configurations = configurations;
19
- this._logger = logger;
20
- this._port = configurations.port;
21
- this._enableCors = configurations.enableCors;
22
- this._endpointList = configurations.endpoints;
23
- this._modulesProxy = modulesProxy;
24
-
25
- this._data = configurations.data || { };
26
- handlerLoader.loadHandlersFromConfiguration(this._data);
27
- }
28
-
29
- run() {
30
- const self = this;
31
-
32
- http.createServer((request, response) => {
33
- let bodyPayload = [];
34
-
35
- request.on('data', (chunk) => {
36
- bodyPayload.push(chunk);
37
- }).on('end', () => {
38
- bodyPayload = Buffer.concat(bodyPayload).toString();
39
-
40
- const urlInformation = parser.parse(request.url);
41
-
42
- self._logger.info(`Requesting: ${urlInformation.base} - Verb: ${request.method}`);
43
-
44
- if (bodyPayload !== '') {
45
- self._logger.info(`Incoming body: ${bodyPayload}`);
46
- }
47
-
48
- let actionFound = false;
49
- let responseBody = null;
50
- let responseStatus = null;
51
- let contentType = null;
52
-
53
- for (const endpointUrl in self._endpointList) {
54
- if (Object.hasOwnProperty.call(self._endpointList, endpointUrl)) {
55
- const endpoint = self._endpointList[endpointUrl];
56
- const requestMethod = request.method.toLowerCase();
57
-
58
- if (
59
- endpointUrl === urlInformation.base &&
60
- (endpoint.verb === "any" || endpoint.verb.toLowerCase() === requestMethod)
61
- ) {
62
-
63
- if (endpoint.data !== undefined && self._data[endpoint.data] === undefined) {
64
- self._logger.error("No matching data variable for this request");
65
- break;
66
- }
67
-
68
- actionFound = true;
69
- contentType = endpoint.responseContentType;
70
-
71
- try {
72
- const processData = endpoint.data === undefined ?
73
- "" :
74
- self._data[endpoint.data].dataHandler(urlInformation);
75
-
76
- responseBody = endpoint.handler !== undefined ?
77
- self._modulesProxy.execute(endpoint.handler, {
78
- method: requestMethod,
79
- url: endpointUrl,
80
- body: bodyPayload
81
- }, processData) : processData;
82
-
83
- responseStatus = endpoint.responseStatus;
84
- } catch(ex) {
85
- self._logger.error(`${ex.message}`);
86
-
87
- responseStatus = ex.httpStatusCode;
88
- responseBody = ex.message;
89
- }
90
-
91
- break;
92
- }
93
- }
94
- }
95
-
96
- response.statusCode = responseStatus ||
97
- (!actionFound ?
98
- constants.HTTP_STATUS_CODES.NOT_FOUND :
99
- constants.HTTP_STATUS_CODES.OK);
100
-
101
- response.setHeader('Content-Type', contentType || constants.DEFAULT_CONTENT_TYPE);
102
-
103
- self._enableCors && response.setHeader('Access-Control-Allow-Origin', '*');
104
-
105
- response.end(responseBody);
106
- });
107
- }).listen(self._port);
108
- }
109
- }
110
-
111
- module.exports = Core;
1
+ const parser = require('./urlParser');
2
+ const http = require('http');
3
+ const https = require('https');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const constants = require('./constants');
7
+ const handlerLoader = require('./configurationParser');
8
+ const openApi = require('./openApi');
9
+
10
+ const MIME_TYPES = {
11
+ '.html': 'text/html',
12
+ '.css': 'text/css',
13
+ '.js': 'application/javascript',
14
+ '.json': 'application/json',
15
+ '.png': 'image/png',
16
+ '.jpg': 'image/jpeg',
17
+ '.jpeg': 'image/jpeg',
18
+ '.gif': 'image/gif',
19
+ '.svg': 'image/svg+xml',
20
+ '.ico': 'image/x-icon',
21
+ '.txt': 'text/plain',
22
+ '.xml': 'application/xml',
23
+ '.pdf': 'application/pdf'
24
+ };
25
+
26
+ class Core {
27
+
28
+ _logger = null;
29
+ _port = 0;
30
+ _cors = null;
31
+ _endpointList = [];
32
+ _logLevel = ""
33
+ _modulesProxy = null;
34
+ _configurations = null;
35
+ _data = null;
36
+ _staticPath = null;
37
+ _connections = new Set();
38
+ _tls = null;
39
+ _openApi = null;
40
+ _openApiSpec = null;
41
+
42
+ constructor(logger, configurations, modulesProxy) {
43
+ this._configurations = configurations;
44
+ this._logger = logger;
45
+ this._port = configurations.port;
46
+ this._cors = this._parseCors(configurations.enableCors);
47
+ this._endpointList = configurations.endpoints;
48
+ this._modulesProxy = modulesProxy;
49
+ this._staticPath = configurations.staticPath || null;
50
+ this._tls = this._parseTls(configurations.tls);
51
+ this._openApi = this._parseOpenApi(configurations.openApi);
52
+
53
+ this._data = configurations.data || { };
54
+ handlerLoader.loadHandlersFromConfiguration(this._data);
55
+ this._refreshOpenApiSpec();
56
+ }
57
+
58
+ _parseTls(tlsConfig) {
59
+ if (!tlsConfig || !tlsConfig.cert || !tlsConfig.key) return null;
60
+
61
+ try {
62
+ return {
63
+ cert: fs.readFileSync(tlsConfig.cert),
64
+ key: fs.readFileSync(tlsConfig.key)
65
+ };
66
+ } catch (error) {
67
+ this._logger.error(`Failed to load TLS certificates: ${error.message}`);
68
+ return null;
69
+ }
70
+ }
71
+
72
+ _parseCors(corsConfig) {
73
+ if (!corsConfig) return null;
74
+
75
+ if (corsConfig === true) {
76
+ return { origins: '*', methods: '*', headers: '*' };
77
+ }
78
+
79
+ return {
80
+ origins: corsConfig.origins || '*',
81
+ methods: corsConfig.methods || '*',
82
+ headers: corsConfig.headers || '*'
83
+ };
84
+ }
85
+
86
+ _normalizeRoutePath(routePath, fallbackPath) {
87
+ if (typeof routePath !== 'string' || routePath.trim() === '') return fallbackPath;
88
+
89
+ let normalized = routePath.trim();
90
+
91
+ if (!normalized.startsWith('/')) {
92
+ normalized = `/${normalized}`;
93
+ }
94
+
95
+ if (normalized.length > 1 && normalized.endsWith('/')) {
96
+ normalized = normalized.slice(0, -1);
97
+ }
98
+
99
+ return normalized;
100
+ }
101
+
102
+ _parseOpenApi(openApiConfig) {
103
+ const defaultConfig = {
104
+ enabled: true,
105
+ docsPath: '/docs',
106
+ specPath: '/openapi.json',
107
+ title: 'MockAPI',
108
+ version: '1.0.0',
109
+ description: 'OpenAPI definition generated from .mockapi-config.'
110
+ };
111
+
112
+ if (openApiConfig === false) {
113
+ return {
114
+ ...defaultConfig,
115
+ enabled: false
116
+ };
117
+ }
118
+
119
+ if (openApiConfig === true || openApiConfig === undefined || openApiConfig === null) {
120
+ return defaultConfig;
121
+ }
122
+
123
+ if (typeof openApiConfig !== 'object') {
124
+ return defaultConfig;
125
+ }
126
+
127
+ const infoConfig = openApiConfig.info || {};
128
+
129
+ return {
130
+ enabled: openApiConfig.enabled !== false,
131
+ docsPath: this._normalizeRoutePath(openApiConfig.docsPath, defaultConfig.docsPath),
132
+ specPath: this._normalizeRoutePath(openApiConfig.specPath, defaultConfig.specPath),
133
+ title: infoConfig.title || openApiConfig.title || defaultConfig.title,
134
+ version: infoConfig.version || openApiConfig.version || defaultConfig.version,
135
+ description: infoConfig.description || openApiConfig.description || defaultConfig.description
136
+ };
137
+ }
138
+
139
+ _refreshOpenApiSpec() {
140
+ this._openApiSpec = openApi.buildSpec(this._openApi, this._endpointList);
141
+ }
142
+
143
+ _applyCorsHeaders(request, response) {
144
+ if (!this._cors) return false;
145
+
146
+ const origin = request.headers['origin'] || '*';
147
+ const allowedOrigin = this._cors.origins === '*'
148
+ ? '*'
149
+ : (this._cors.origins.includes(origin) ? origin : null);
150
+
151
+ if (!allowedOrigin) return false;
152
+
153
+ response.setHeader('Access-Control-Allow-Origin', allowedOrigin);
154
+
155
+ const methods = this._cors.methods === '*'
156
+ ? 'GET, POST, PUT, DELETE, PATCH, OPTIONS'
157
+ : (Array.isArray(this._cors.methods) ? this._cors.methods.join(', ') : this._cors.methods);
158
+ response.setHeader('Access-Control-Allow-Methods', methods);
159
+
160
+ const headers = this._cors.headers === '*'
161
+ ? 'Content-Type, Authorization, X-Requested-With'
162
+ : (Array.isArray(this._cors.headers) ? this._cors.headers.join(', ') : this._cors.headers);
163
+ response.setHeader('Access-Control-Allow-Headers', headers);
164
+
165
+ return true;
166
+ }
167
+
168
+ _serveStaticFile(request, response, urlPath) {
169
+ const safePath = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, '');
170
+ const filePath = path.join(this._staticPath, safePath);
171
+ const resolvedPath = path.resolve(filePath);
172
+ const resolvedStatic = path.resolve(this._staticPath);
173
+
174
+ if (!resolvedPath.startsWith(resolvedStatic)) {
175
+ response.statusCode = constants.HTTP_STATUS_CODES.FORBIDDEN;
176
+ response.end('Forbidden');
177
+ return;
178
+ }
179
+
180
+ if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
181
+ return false;
182
+ }
183
+
184
+ const ext = path.extname(resolvedPath).toLowerCase();
185
+ const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
186
+
187
+ this._logger.info(`Serving static file: ${resolvedPath}`);
188
+
189
+ response.statusCode = constants.HTTP_STATUS_CODES.OK;
190
+ response.setHeader('Content-Type', mimeType);
191
+ this._applyCorsHeaders(request, response);
192
+
193
+ const stream = fs.createReadStream(resolvedPath);
194
+ stream.pipe(response);
195
+
196
+ return true;
197
+ }
198
+
199
+ _handleOpenApiRequest(request, response, pathName) {
200
+ if (!this._openApi || this._openApi.enabled === false || request.method !== 'GET') {
201
+ return false;
202
+ }
203
+
204
+ if (pathName === this._openApi.specPath) {
205
+ response.statusCode = constants.HTTP_STATUS_CODES.OK;
206
+ response.setHeader('Content-Type', 'application/json');
207
+ this._applyCorsHeaders(request, response);
208
+ response.end(JSON.stringify(this._openApiSpec, null, 2));
209
+ return true;
210
+ }
211
+
212
+ if (pathName === this._openApi.docsPath || pathName === `${this._openApi.docsPath}/`) {
213
+ response.statusCode = constants.HTTP_STATUS_CODES.OK;
214
+ response.setHeader('Content-Type', 'text/html; charset=utf-8');
215
+ this._applyCorsHeaders(request, response);
216
+ response.end(openApi.buildDocsPage(this._openApi));
217
+ return true;
218
+ }
219
+
220
+ return false;
221
+ }
222
+
223
+ run() {
224
+ const self = this;
225
+
226
+ const requestHandler = (request, response) => {
227
+
228
+ // Handle CORS preflight requests
229
+ if (request.method === 'OPTIONS' && self._cors) {
230
+ self._applyCorsHeaders(request, response);
231
+ response.statusCode = constants.HTTP_STATUS_CODES.NO_CONTENT;
232
+ response.end();
233
+ return;
234
+ }
235
+
236
+ const urlInformation = parser.parse(request.url);
237
+
238
+ if (self._handleOpenApiRequest(request, response, urlInformation.pathname)) {
239
+ return;
240
+ }
241
+
242
+ let bodyPayload = [];
243
+
244
+ request.on('data', (chunk) => {
245
+ bodyPayload.push(chunk);
246
+ }).on('end', () => {
247
+ bodyPayload = Buffer.concat(bodyPayload).toString();
248
+
249
+ self._logger.info(`Requesting: ${urlInformation.base} - Verb: ${request.method}`);
250
+
251
+ if (bodyPayload !== '') {
252
+ self._logger.info(`Incoming body: ${bodyPayload}`);
253
+ }
254
+
255
+ let actionFound = false;
256
+ let responseBody = null;
257
+ let responseStatus = null;
258
+ let contentType = null;
259
+ let delay = 0;
260
+
261
+ for (const endpointUrl in self._endpointList) {
262
+ if (Object.hasOwnProperty.call(self._endpointList, endpointUrl)) {
263
+ const endpoint = self._endpointList[endpointUrl];
264
+ const requestMethod = request.method.toLowerCase();
265
+
266
+ const pathResult = parser.matchPath(endpointUrl, urlInformation.base);
267
+
268
+ if (
269
+ pathResult.match &&
270
+ (endpoint.verb === "any" || endpoint.verb.toLowerCase() === requestMethod)
271
+ ) {
272
+
273
+ if (endpoint.data !== undefined && self._data[endpoint.data] === undefined) {
274
+ self._logger.error("No matching data variable for this request");
275
+ break;
276
+ }
277
+
278
+ // Merge path params and query params into urlInformation
279
+ urlInformation.params = pathResult.params;
280
+ urlInformation.query = Object.fromEntries(urlInformation.search);
281
+
282
+ actionFound = true;
283
+ contentType = endpoint.responseContentType;
284
+ delay = endpoint.delay || 0;
285
+
286
+ try {
287
+ const processData = endpoint.data === undefined ?
288
+ "" :
289
+ self._data[endpoint.data].dataHandler(urlInformation);
290
+
291
+ responseBody = endpoint.handler !== undefined ?
292
+ self._modulesProxy.execute(endpoint.handler, {
293
+ method: requestMethod,
294
+ url: endpointUrl,
295
+ body: bodyPayload,
296
+ params: urlInformation.params,
297
+ query: urlInformation.query
298
+ }, processData) : processData;
299
+
300
+ responseStatus = endpoint.responseStatus;
301
+ } catch(ex) {
302
+ self._logger.error(`${ex.message}`);
303
+
304
+ responseStatus = ex.httpStatusCode;
305
+ responseBody = ex.message;
306
+ }
307
+
308
+ break;
309
+ }
310
+ }
311
+ }
312
+
313
+ const sendResponse = () => {
314
+ // Try static file serving before returning 404
315
+ if (!actionFound && self._staticPath) {
316
+ const served = self._serveStaticFile(request, response, urlInformation.pathname);
317
+ if (served !== false) return;
318
+ }
319
+
320
+ response.statusCode = responseStatus ||
321
+ (!actionFound ?
322
+ constants.HTTP_STATUS_CODES.NOT_FOUND :
323
+ constants.HTTP_STATUS_CODES.OK);
324
+
325
+ response.setHeader('Content-Type', contentType || constants.DEFAULT_CONTENT_TYPE);
326
+
327
+ self._applyCorsHeaders(request, response);
328
+
329
+ response.end(responseBody);
330
+ };
331
+
332
+ if (delay > 0) {
333
+ self._logger.info(`Delaying response by ${delay}ms`);
334
+ setTimeout(sendResponse, delay);
335
+ } else {
336
+ sendResponse();
337
+ }
338
+ });
339
+ };
340
+
341
+ this._server = this._tls
342
+ ? https.createServer(this._tls, requestHandler)
343
+ : http.createServer(requestHandler);
344
+
345
+ this._server.listen(self._port);
346
+
347
+ this._server.on('connection', (socket) => {
348
+ self._connections.add(socket);
349
+ socket.on('close', () => self._connections.delete(socket));
350
+ });
351
+ }
352
+
353
+ reload(configurations) {
354
+ this._configurations = configurations;
355
+ this._port = configurations.port;
356
+ this._cors = this._parseCors(configurations.enableCors);
357
+ this._endpointList = configurations.endpoints;
358
+ this._staticPath = configurations.staticPath || null;
359
+ this._openApi = this._parseOpenApi(configurations.openApi);
360
+
361
+ this._data = configurations.data || {};
362
+ handlerLoader.loadHandlersFromConfiguration(this._data);
363
+ this._refreshOpenApiSpec();
364
+
365
+ this._logger.info('Configuration reloaded');
366
+ }
367
+
368
+ stop(callback) {
369
+ if (this._server) {
370
+ this._server.close(callback);
371
+ for (const socket of this._connections) {
372
+ socket.destroy();
373
+ }
374
+ this._connections.clear();
375
+ } else if (callback) {
376
+ callback();
377
+ }
378
+ }
379
+ }
380
+
381
+ module.exports = Core;