slower 1.1.26 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +52 -1
- package/package.json +7 -6
- package/src/decorators.js +228 -0
- package/src/slower.js +153 -0
- package/src/utils.js +61 -0
- package/TODO +0 -1
- package/lib/router.js +0 -476
- package/lib/utils.js +0 -172
- package/readme.md +0 -204
- /package/{lib → src}/mimetable.json +0 -0
package/lib/router.js
DELETED
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
const http = require('http');
|
|
2
|
-
const https = require('https');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const { clone, noop, slugify, isSparseEqual, last, renderDynamicHTML, setSocketLocals, setSocketSecurityHeaders, setResponseAssessors, isPromise, UUID } = require('./utils');
|
|
5
|
-
const mimetable = require('./mimetable.json');
|
|
6
|
-
|
|
7
|
-
class Route {
|
|
8
|
-
constructor (path, type, callback) {
|
|
9
|
-
this.path = (path.startsWith('/') ? path : '/' + path);
|
|
10
|
-
this.type = SlowerRouter.http_methods.includes(slugify(type).toUpperCase()) ? slugify(type).toUpperCase() : null;
|
|
11
|
-
this.callback = (typeof callback == 'function' ? callback : noop);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
class SlowerRouter {
|
|
16
|
-
static http_methods = [
|
|
17
|
-
'GET', 'POST', 'PUT',
|
|
18
|
-
'HEAD', 'DELETE', 'OPTIONS',
|
|
19
|
-
'TRACE', 'COPY', 'LOCK',
|
|
20
|
-
'MKCOL', 'MOVE', 'PURGE',
|
|
21
|
-
'PROPFIND', 'PROPPATCH', 'UNLOCK',
|
|
22
|
-
'REPORT', 'MKACTIVITY', 'CHECKOUT',
|
|
23
|
-
'MERGE', 'M-SEARCH', 'NOTIFY',
|
|
24
|
-
'SUBSCRIBE', 'UNSUBSCRIBE', 'PATCH',
|
|
25
|
-
'SEARCH', 'CONNECT'
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
static mime_table = mimetable;
|
|
29
|
-
|
|
30
|
-
constructor () {
|
|
31
|
-
this.strictHeaders = false;
|
|
32
|
-
this.routes = [];
|
|
33
|
-
this.middleware = [noop];
|
|
34
|
-
this.fallback = noop;
|
|
35
|
-
this.allowedMethods = clone(SlowerRouter.http_methods);
|
|
36
|
-
this.blockedMethodCallback = noop;
|
|
37
|
-
this.tls = { use: false, key: null, cert: null };
|
|
38
|
-
this.server = null;
|
|
39
|
-
this.connectionpool = {};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
enableStrictHeaders () {
|
|
43
|
-
// This activates the responses with many security-related response headers.
|
|
44
|
-
// It is not enabled by default, as some header are very restrictive
|
|
45
|
-
this.strictHeaders = true;
|
|
46
|
-
return this;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
disableStrictHeaders () {
|
|
50
|
-
// This deactivates the responses with many security-related response headers.
|
|
51
|
-
// It is not enabled by default, as some headers are very restrictive
|
|
52
|
-
this.strictHeaders = false;
|
|
53
|
-
return this;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
setSecureContext (key = null, cert = null) {
|
|
57
|
-
// Activates TLS and HTTPS using a certificate and a key
|
|
58
|
-
if (!key || !cert || !Buffer.isBuffer(key) || !Buffer.isBuffer(cert)) {
|
|
59
|
-
throw new Error ("INVALID SECURE CONTEXT INFORMATION PROVIDED - CHECK KEY AND CERTIFICATE");
|
|
60
|
-
}
|
|
61
|
-
this.tls.use = true;
|
|
62
|
-
this.tls.key = key;
|
|
63
|
-
this.tls.cert = cert;
|
|
64
|
-
return this;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Defines a route for a determined request method
|
|
69
|
-
* @category Router
|
|
70
|
-
* @param {String} path The route that will be defined
|
|
71
|
-
* @param {String} type The method to respond to
|
|
72
|
-
* @param {Function} callback Callback to use when the chosen route is accessed
|
|
73
|
-
* @returns {Object} An Route object
|
|
74
|
-
* @example <caption> Defining a simple GET route:</caption>
|
|
75
|
-
* setRoute('/', 'GET', (req, res) => {
|
|
76
|
-
* console.log(res.url);
|
|
77
|
-
* res.end('received');
|
|
78
|
-
* });
|
|
79
|
-
* // => <Route> { path:..., type:..., callback:... }
|
|
80
|
-
*/
|
|
81
|
-
setRoute = function (path = '/', type = 'GET', callback) {
|
|
82
|
-
let stat = new Route(path, type, callback);
|
|
83
|
-
this.routes.push(stat);
|
|
84
|
-
return this;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Defines a middleware for all requests
|
|
89
|
-
* @category Middleware
|
|
90
|
-
* @param {Function} callback Callback to use when the requests are made
|
|
91
|
-
* @returns {Array} The used middlewares list
|
|
92
|
-
*/
|
|
93
|
-
setMiddleware = function (callback) {
|
|
94
|
-
this.middleware.push((typeof callback == 'function' ? callback : noop));
|
|
95
|
-
return this;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Defines a route for a determined request method
|
|
100
|
-
* @category Router
|
|
101
|
-
* @param {String} path The route that will be defined
|
|
102
|
-
* @param {String} file The file path that will be used to respond to the route
|
|
103
|
-
* @param {String} mime The file's mime type
|
|
104
|
-
* @param {Object} replacementData The replacement data map for the dynamic HTML rendering
|
|
105
|
-
* @returns {Object} An Route object already configured
|
|
106
|
-
*/
|
|
107
|
-
/**
|
|
108
|
-
* Dynamic rendering example:
|
|
109
|
-
* It's a template engine, to render HTML containing template spaces.
|
|
110
|
-
* The charset for replacement is <{content}>
|
|
111
|
-
* @since 1.2.5
|
|
112
|
-
*
|
|
113
|
-
* @param {String} html The HTML code
|
|
114
|
-
* @param {Object} patterns The patterns to replace in the HTML code
|
|
115
|
-
* @return {String} The HTML with the templates replaces
|
|
116
|
-
*
|
|
117
|
-
* @example <caption> Rendering: </caption>
|
|
118
|
-
* var template = 'Hello, my name is <{name}>. I\\'m <{age}> years old.';
|
|
119
|
-
* console.log(TemplateEngine(template, {
|
|
120
|
-
* name: "Krasimir",
|
|
121
|
-
* age: 29
|
|
122
|
-
* }));
|
|
123
|
-
*/
|
|
124
|
-
setDynamic = function (path, file = '', mime = '', replacementData = null) {
|
|
125
|
-
let encoding = (mime === SlowerRouter.mime_table['default'] ? undefined : 'utf-8');
|
|
126
|
-
if (file.includes('{%}')) { file = file.replace(/\{\%\}/gim, path.replace(/\//gim,'')); }
|
|
127
|
-
let stat = new Route(path, 'GET', (req, res) => {
|
|
128
|
-
let data, targetFile, extension, targetMime, targetEncoding;
|
|
129
|
-
if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
|
|
130
|
-
targetFile = file.replace(/\//gim, '\\');
|
|
131
|
-
targetFile = ((targetFile.endsWith('\\')) ? targetFile : targetFile+'\\') + req.url.replace('/', '');
|
|
132
|
-
} else {
|
|
133
|
-
targetFile = ((file == '' || !file) ? req.url : file);
|
|
134
|
-
}
|
|
135
|
-
extension = last(targetFile.split('.'));
|
|
136
|
-
targetMime = mime || (SlowerRouter.mime_table[extension]?.[0] || SlowerRouter.mime_table.default[0]);
|
|
137
|
-
targetEncoding = encoding || (SlowerRouter.mime_table[extension]?.[1] || SlowerRouter.mime_table.default[1]);
|
|
138
|
-
try {
|
|
139
|
-
data = fs.readFileSync(targetFile, targetEncoding);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
data = '';
|
|
142
|
-
}
|
|
143
|
-
try {
|
|
144
|
-
if (replacementData) data = renderDynamicHTML(data, replacementData);
|
|
145
|
-
} catch (err) {}
|
|
146
|
-
res.writeHead(200, { 'Content-Type': targetMime });
|
|
147
|
-
res.write(data);
|
|
148
|
-
res.end();
|
|
149
|
-
});
|
|
150
|
-
this.routes.push(stat);
|
|
151
|
-
return this;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// SERVES FILES FOR SPECIFIC PATHS. WILDCARS ALLOWED. BE CAREFUL - PATH TRAVERSAL MAY BE POSSIBLE
|
|
155
|
-
// Use app.setStatic('/*', __dirname+'/') for serving all local files.
|
|
156
|
-
// or use app.setStatic('/*', __dirname+'/public/') for serving files in a specific folder (public).
|
|
157
|
-
// This is a one-liner for the setRoute function, when the route responds with a simple page or document
|
|
158
|
-
/**
|
|
159
|
-
* Defines a route for a determined request method
|
|
160
|
-
* @category Router
|
|
161
|
-
* @param {String} path The route that will be defined
|
|
162
|
-
* @param {String} file The file path that will be used to respond to the route
|
|
163
|
-
* @param {String} mime The file's mime type
|
|
164
|
-
* @returns {Object} An Route object already configured
|
|
165
|
-
* @example <caption> Defining a simple GET route:</caption>
|
|
166
|
-
* setStatic('/login', __dirname+'/public/static/views/{%}.html', 'text/html');
|
|
167
|
-
*/
|
|
168
|
-
setStatic = function (path, file = '', mime = '') {
|
|
169
|
-
let encoding = (mime === SlowerRouter.mime_table['default'] ? undefined : 'utf-8');
|
|
170
|
-
if (file.includes('{%}')) { file = file.replace(/\{\%\}/gim, path.replace(/\//gim,'')); }
|
|
171
|
-
let stat = new Route(path, 'GET', (req, res) => {
|
|
172
|
-
let data, targetFile, extension, targetMime, targetEncoding;
|
|
173
|
-
if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
|
|
174
|
-
targetFile = file.replace(/\//gim, '\\');
|
|
175
|
-
targetFile = ((targetFile.endsWith('\\')) ? targetFile : targetFile+'\\') + req.url.replace('/', '');
|
|
176
|
-
} else {
|
|
177
|
-
targetFile = ((file == '' || !file) ? req.url : file);
|
|
178
|
-
}
|
|
179
|
-
extension = last(targetFile.split('.'));
|
|
180
|
-
targetMime = mime || (SlowerRouter.mime_table[extension]?.[0] || SlowerRouter.mime_table.default[0]);
|
|
181
|
-
targetEncoding = encoding || (SlowerRouter.mime_table[extension]?.[1] || SlowerRouter.mime_table.default[1]);
|
|
182
|
-
try {
|
|
183
|
-
data = fs.readFileSync(targetFile, targetEncoding);
|
|
184
|
-
} catch (err) {
|
|
185
|
-
data = '';
|
|
186
|
-
}
|
|
187
|
-
res.writeHead(200, { 'Content-Type': targetMime });
|
|
188
|
-
res.write(data);
|
|
189
|
-
res.end();
|
|
190
|
-
});
|
|
191
|
-
this.routes.push(stat);
|
|
192
|
-
return this;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Defines a default response for non-defined routes (custom treated 404 error)
|
|
197
|
-
* @category Router
|
|
198
|
-
* @param {String} callback The function to execute for unhandled routes
|
|
199
|
-
* @returns {undefined}
|
|
200
|
-
*/
|
|
201
|
-
setFallback = function (callback) { this.fallback = callback; return this; }
|
|
202
|
-
|
|
203
|
-
// SERVES FILES FOR SPECIFIC PATHS. WILDCARS ALLOWED. BE CAREFUL - PATH TRAVERSAL MAY BE POSSIBLE
|
|
204
|
-
// Use app.setStatic('/*', __dirname+'/') for serving all local files.
|
|
205
|
-
// or use app.setStatic('/*', __dirname+'/public/') for serving files in a specific folder (public).
|
|
206
|
-
// This is a one-liner for the setRoute function, when the route responds with a simple page or document
|
|
207
|
-
/**
|
|
208
|
-
* Defines a route for a determined request method
|
|
209
|
-
* @category Router
|
|
210
|
-
* @param {String} path The route that will be defined
|
|
211
|
-
* @param {String} file The file path that will be used to respond to the route
|
|
212
|
-
* @param {String} mime The file's mime type
|
|
213
|
-
* @param {Object} replacementData The replacement data map for the dynamic HTML rendering
|
|
214
|
-
* @returns {Object} An Route object already configured
|
|
215
|
-
* @example <caption> Defining a simple GET route:</caption>
|
|
216
|
-
* setStatic('/login', __dirname+'/public/static/views/login.html', 'text/html', 'utf8');
|
|
217
|
-
*/
|
|
218
|
-
/**
|
|
219
|
-
* Dynamic rendering example:
|
|
220
|
-
* It's a template engine, to render HTML containing template spaces.
|
|
221
|
-
* The charset for replacement is <{content}>
|
|
222
|
-
* @since 1.2.5
|
|
223
|
-
*
|
|
224
|
-
* @param {String} html The HTML code
|
|
225
|
-
* @param {Object} patterns The patterns to replace in the HTML code
|
|
226
|
-
* @return {String} The HTML with the templates replaces
|
|
227
|
-
*
|
|
228
|
-
* @example <caption> Rendering: </caption>
|
|
229
|
-
* var template = 'Hello, my name is <{name}>. I\\'m <{age}> years old.';
|
|
230
|
-
* console.log(TemplateEngine(template, {
|
|
231
|
-
* name: "Krasimir",
|
|
232
|
-
* age: 29
|
|
233
|
-
* }));
|
|
234
|
-
*/
|
|
235
|
-
setFallbackFile = function (file = '', mime = '', replacementData = null) {
|
|
236
|
-
this.fallback = function fb (req,res) {
|
|
237
|
-
let encoding = (mime === SlowerRouter.mime_table['default'] ? undefined : 'utf-8');
|
|
238
|
-
let data, targetFile, extension, targetMime, targetEncoding;
|
|
239
|
-
if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
|
|
240
|
-
targetFile = file.replace(/\//gim, '\\');
|
|
241
|
-
targetFile = ((targetFile.endsWith('\\')) ? targetFile : targetFile+'\\') + req.url.replace('/', '');
|
|
242
|
-
} else {
|
|
243
|
-
targetFile = ((file == '' || !file) ? req.url : file);
|
|
244
|
-
}
|
|
245
|
-
extension = last(targetFile.split('.'));
|
|
246
|
-
targetMime = mime || (SlowerRouter.mime_table[extension]?.[0] || SlowerRouter.mime_table.default[0]);
|
|
247
|
-
targetEncoding = encoding || (SlowerRouter.mime_table[extension]?.[1] || SlowerRouter.mime_table.default[1]);
|
|
248
|
-
try {
|
|
249
|
-
data = fs.readFileSync(targetFile, targetEncoding);
|
|
250
|
-
} catch (err) {
|
|
251
|
-
data = '';
|
|
252
|
-
}
|
|
253
|
-
try {
|
|
254
|
-
if (replacementData) data = renderDynamicHTML(data, replacementData);
|
|
255
|
-
} catch (err) {}
|
|
256
|
-
res.writeHead(200, { 'Content-Type': targetMime });
|
|
257
|
-
res.write(data);
|
|
258
|
-
res.end();
|
|
259
|
-
}
|
|
260
|
-
return this;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Sets the methods that the application will respond to. The rest is simply discarded with empty responses.
|
|
264
|
-
// To configure a deeper level of error handling, or to serve customized 'METHOD_NOT_ALLOWED' errors,
|
|
265
|
-
// use 'setRoute' with params ('/*', {{method_name}}, (req,res) => { and serve the error file here });
|
|
266
|
-
// INFO: Middlewares are still triggered when a blocked method request comes.
|
|
267
|
-
/**
|
|
268
|
-
* Defines a route for a determined request method
|
|
269
|
-
* @category Router
|
|
270
|
-
* @param {Array} methods The methods that are allowed by the application. Methods that do not conform with standards are ignored.
|
|
271
|
-
* @returns {Object} The AllowedMethods Array object.
|
|
272
|
-
* @example <caption> Allowing only GET and POST:</caption>
|
|
273
|
-
* app.setAllowedMethods(['GET', 'POST']);
|
|
274
|
-
*/
|
|
275
|
-
setAllowedMethods = function (methods = []) {
|
|
276
|
-
this.allowedMethods = [];
|
|
277
|
-
// If not specified, all methods are allowed
|
|
278
|
-
if (methods.length == 0 || methods == '*') {
|
|
279
|
-
return this.allowedMethods = JSON.parse(JSON.stringify(SlowerRouter.http_methods));
|
|
280
|
-
}
|
|
281
|
-
for (let i = 0; i < methods.length; i++) {
|
|
282
|
-
if (!SlowerRouter.http_methods.includes(methods[i])) continue;
|
|
283
|
-
this.allowedMethods.push(methods[i]);
|
|
284
|
-
}
|
|
285
|
-
// Default callback for blocked methods respond with code 200 and an empty response
|
|
286
|
-
this.blockedMethodCallback = function (req,res) {
|
|
287
|
-
res.writeHead(405); // sends the meme error 418 'i am a teapot'
|
|
288
|
-
res.end();
|
|
289
|
-
};
|
|
290
|
-
return this;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/*
|
|
294
|
-
Route Objects looks like:
|
|
295
|
-
|
|
296
|
-
{
|
|
297
|
-
type: "route",
|
|
298
|
-
path: "/",
|
|
299
|
-
method: "GET",
|
|
300
|
-
handle: someFunction
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
{
|
|
304
|
-
type: "middleware",
|
|
305
|
-
handle: someFunction
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
{
|
|
309
|
-
type: "fallback",
|
|
310
|
-
handle: someFunction
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
{
|
|
314
|
-
type: "static",
|
|
315
|
-
path: "/style.css",
|
|
316
|
-
file: "./www/assets/style.css"
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
{
|
|
320
|
-
type: "dynamic",
|
|
321
|
-
path: "/client.html",
|
|
322
|
-
file: "./www/templates/client.html",
|
|
323
|
-
data: {
|
|
324
|
-
"CLIENTID": someRenderingFunction
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
{
|
|
329
|
-
type: "fallbackfile",
|
|
330
|
-
file: "./www/pages/404.html"
|
|
331
|
-
data: {
|
|
332
|
-
"CUSTOM_ERROR_MESSAGE": () => { return err.message },
|
|
333
|
-
"CUSTOM_ERROR_CODE": someErrorCodeFetchingFunction
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
*/
|
|
338
|
-
// Converts a series of objectRoutes into actual internal routes
|
|
339
|
-
parse = function (routeObjectList) {
|
|
340
|
-
for (let entry of routeObjectList) {
|
|
341
|
-
const { type, path, method, file, data, handle } = entry;
|
|
342
|
-
switch (type) {
|
|
343
|
-
case 'route': this.setRoute(path, method, handle); break;
|
|
344
|
-
case 'middleware': this.setMiddleware(handle); break;
|
|
345
|
-
case 'fallback': this.setFallback(handle); break;
|
|
346
|
-
case 'static': this.setStatic(path, file, undefined); break;
|
|
347
|
-
case 'dynamic': this.setDynamic(path, file, undefined, data); break;
|
|
348
|
-
case 'fallbackfile': this.setFallbackFile(file, undefined, data); break;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return this;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Starts the server - listening at <host>:<port>
|
|
355
|
-
/**
|
|
356
|
-
* @category Router
|
|
357
|
-
* @param {Number} port The port number the server will listen to.
|
|
358
|
-
* @param {String} host The host's network interface address the server will listen into (use a falsy value or '0.0.0.0' for listening on all).
|
|
359
|
-
* @returns {Promise<http.Server>} The server instance
|
|
360
|
-
*/
|
|
361
|
-
start = async function (port = 8080, host = undefined) {
|
|
362
|
-
let routes = this.routes;
|
|
363
|
-
let middle = this.middleware;
|
|
364
|
-
let fallback = this.fallback;
|
|
365
|
-
let allowedMethods = this.allowedMethods;
|
|
366
|
-
let blockedMethodCallback = this.blockedMethodCallback;
|
|
367
|
-
|
|
368
|
-
if (this.tls.use && this.tls.cert && this.tls.key) {
|
|
369
|
-
this.server = https.createServer({ key:this.tls.key, cert: this.tls.cert }, mainServerHandler);
|
|
370
|
-
|
|
371
|
-
} else {
|
|
372
|
-
this.server = http.createServer({}, mainServerHandler);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
this.server.on('connection', (socket) => {
|
|
376
|
-
const id = UUID();
|
|
377
|
-
this.connectionpool[id] = socket;
|
|
378
|
-
socket.on('close', () => delete this.connectionpool[id]);
|
|
379
|
-
socket.on('error', () => delete this.connectionpool[id]);
|
|
380
|
-
socket.on('timeout', () => { this.connectionpool[id].destroy(); delete this.connectionpool[id]; });
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
async function mainServerHandler (req, res) {
|
|
384
|
-
try {
|
|
385
|
-
// Sets local req.session object
|
|
386
|
-
setSocketLocals(req);
|
|
387
|
-
setResponseAssessors(res);
|
|
388
|
-
// Sets security headers in req.headers
|
|
389
|
-
// This is set before custom callbacks are applies
|
|
390
|
-
// so it is possible to override all of the headers
|
|
391
|
-
if (!!this.strictHeaders) setSocketSecurityHeaders(req);
|
|
392
|
-
// Runs all middlewares
|
|
393
|
-
for (let i = 0; i < middle.length; i++) {
|
|
394
|
-
const rr = middle[i](req, res);
|
|
395
|
-
if (isPromise(rr)) await rr;
|
|
396
|
-
if (res.writableEnded) return; // if res.end() is caller early in a middleware
|
|
397
|
-
}
|
|
398
|
-
// Only respond to allowed methods with callbacks, else, use the default empty response.
|
|
399
|
-
if (allowedMethods.includes(req.method)) {
|
|
400
|
-
for (let i = 0; i < routes.length; i++) {
|
|
401
|
-
let route = routes[i];
|
|
402
|
-
if (
|
|
403
|
-
route.type === req.method &&
|
|
404
|
-
(
|
|
405
|
-
route.path === req.url ||
|
|
406
|
-
isSparseEqual(route.path, req.url)
|
|
407
|
-
)
|
|
408
|
-
) {
|
|
409
|
-
i = routes.length;
|
|
410
|
-
const rr = route.callback(req, res);
|
|
411
|
-
if (isPromise(rr)) await rr;
|
|
412
|
-
// if (res.writableEnded) return; // if res.end() was called
|
|
413
|
-
// else res.end(); // if res.end() was not called
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
const rr = fallback(req,res);
|
|
418
|
-
if (isPromise(rr)) await rr;
|
|
419
|
-
if (!res.writableEnded) res.end();
|
|
420
|
-
} else {
|
|
421
|
-
blockedMethodCallback(req, res);
|
|
422
|
-
if (!res.writableEnded) res.end();
|
|
423
|
-
}
|
|
424
|
-
} catch(err) {
|
|
425
|
-
console.log(err);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
this.port = port;
|
|
430
|
-
this.host = host;
|
|
431
|
-
if (!host) host = undefined; // Turn falsy values into undefined, for default behaviour
|
|
432
|
-
return new Promise((resolve, reject) => this.server.listen(port, host, () => resolve(this)));
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
*
|
|
437
|
-
* @param {Object} options
|
|
438
|
-
* @returns
|
|
439
|
-
* Options include:
|
|
440
|
-
* forceDisconnection: boolean (default: false) - If enabled, force sockets to disconnect after a 2 seconds timeout.
|
|
441
|
-
* instantClose: boolean (default: false) - Only relevant if 'forceDisconnection' is enabled. If enabled, force sockets to disconnect immediately.
|
|
442
|
-
* silent: boolean (default: true) - If set to false, show the disconnection status messages.
|
|
443
|
-
*/
|
|
444
|
-
close = async function ({ forceDisconnection = false, instantClose = false, silent = true } = {forceDisconnection: false, instantClose: false, silent: true}) {
|
|
445
|
-
return new Promise (async (resolve, reject) => {
|
|
446
|
-
this.server.on('close', () => resolve());
|
|
447
|
-
this.server.on('error', (err) => reject(err));
|
|
448
|
-
|
|
449
|
-
// After this, no more sockets can connect, but the already connected ones, remain
|
|
450
|
-
this.server.close();
|
|
451
|
-
if (!silent) console.log('<> Shutting down server...');
|
|
452
|
-
|
|
453
|
-
if (forceDisconnection) {
|
|
454
|
-
// wait 1 second before continuing
|
|
455
|
-
if (!instantClose) await new Promise ((resolve1, reject1) => setTimeout(resolve1, 1000));
|
|
456
|
-
|
|
457
|
-
// If there are sockets still connected (waiting for timeout),
|
|
458
|
-
// wait until all disconnected or force all to disconnect after 2 seconds
|
|
459
|
-
if (!silent) {
|
|
460
|
-
const activesockets = await new Promise((resolve2, reject2) => this.server.getConnections((err,count) => resolve2(count)));
|
|
461
|
-
console.log(`<> Awaiting disconnection of active sockets [${activesockets}]`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// wait 1 second and destroy the ones still connected
|
|
465
|
-
if (!instantClose) await new Promise ((resolve1, reject1) => setTimeout(resolve1, 1000));
|
|
466
|
-
for (let sock in this.connectionpool) {
|
|
467
|
-
this.connectionpool[sock].destroy();
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const Slower = function () { return new SlowerRouter(); }
|
|
475
|
-
|
|
476
|
-
module.exports = Slower;
|
package/lib/utils.js
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
const noop = function () {};
|
|
2
|
-
|
|
3
|
-
const last = (array) => { return array[array.length-1]; }
|
|
4
|
-
|
|
5
|
-
const slugify = (string, replacement = '-', replaceSpaces = true) => {
|
|
6
|
-
return (string
|
|
7
|
-
.replace(/<(?:.|\n)*?>/gm, '')
|
|
8
|
-
.replace(/[!\"#$%&'\(\)\*\+,\/:;<=>\?\@\[\\\]\^`\{\|\}~]/g, '')
|
|
9
|
-
.replace((replaceSpaces ? /(\s|\.)/g : /(\.)/g), replacement)
|
|
10
|
-
.replace(/—/g, replacement)
|
|
11
|
-
.replace(/-{2,}/g, replacement));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Compares two strings in 'sparse' mode. Using the wildcards '{*}' and '{?}' to match strings.
|
|
16
|
-
* Use '{*}' for any number of (any) characters, and {?}' for one (any) character.
|
|
17
|
-
*
|
|
18
|
-
* @since 1.2.7
|
|
19
|
-
*
|
|
20
|
-
* @param {String} str1 The first string to compare
|
|
21
|
-
* @param {String} str2 The second string to compare
|
|
22
|
-
* @return {Boolean} If the strings are sparsely equal or not
|
|
23
|
-
* @example <caption> Comparing simple strings: </caption>
|
|
24
|
-
* isSparseEqual("hello", "hello")
|
|
25
|
-
* // => true
|
|
26
|
-
* isSparseEqual("hello", "wello")
|
|
27
|
-
* // => false
|
|
28
|
-
* @example <caption> Comparing complex strings: </caption>
|
|
29
|
-
* isSparseEqual("{?}ello", "hello")
|
|
30
|
-
* // => true
|
|
31
|
-
* isSparseEqual("h*", "hello")
|
|
32
|
-
* // => true
|
|
33
|
-
* isSparseEqual("h{*}e", "hello")
|
|
34
|
-
* // => false
|
|
35
|
-
* isSparseEqual("h{*}e", "helle")
|
|
36
|
-
* // => true
|
|
37
|
-
*/
|
|
38
|
-
const isSparseEqual = (str1 = '', str2 = '') => {
|
|
39
|
-
const string1 = str1.replace(/{\?}/g, '.').replace(/{\*}/g, '.*');
|
|
40
|
-
const string2 = str2.replace(/{\?}/g, '.').replace(/{\*}/g, '.*');
|
|
41
|
-
const regex = new RegExp(`^${string1}$`);
|
|
42
|
-
return regex.test(string2);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* It's a template engine, to render HTML containing template spaces.
|
|
47
|
-
* The charset for replacement is {{content}}
|
|
48
|
-
* @since 1.2.5
|
|
49
|
-
*
|
|
50
|
-
* @param {String} html The HTML code
|
|
51
|
-
* @param {Object} patterns The patterns to replace in the HTML code
|
|
52
|
-
* @return {String} The HTML with the templates replaces
|
|
53
|
-
*
|
|
54
|
-
* @example <caption> Rendering: </caption>
|
|
55
|
-
* var template = 'Hello, my name is {{name}}. I\\'m {{age}} years old.';
|
|
56
|
-
* console.log(TemplateEngine(template, {
|
|
57
|
-
* name: "Krasimir",
|
|
58
|
-
* age: 29
|
|
59
|
-
* }));
|
|
60
|
-
*/
|
|
61
|
-
const renderDynamicHTML = (html, patterns) => {
|
|
62
|
-
let template = html;
|
|
63
|
-
for (let item in patterns) {
|
|
64
|
-
template = template.replace(
|
|
65
|
-
new RegExp('{{'+item+'}}', 'gim'),
|
|
66
|
-
patterns[item]
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return template;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const setSocketLocals = (reqSocket) => {
|
|
73
|
-
reqSocket.session = {};
|
|
74
|
-
reqSocket.session.port = reqSocket.socket.localPort;
|
|
75
|
-
reqSocket.session.rport = reqSocket.socket.remotePort;
|
|
76
|
-
reqSocket.session.host = (
|
|
77
|
-
reqSocket.socket.localAddress.startsWith('::') ?
|
|
78
|
-
reqSocket.socket.localAddress.substring(
|
|
79
|
-
reqSocket.socket.localAddress.indexOf(':',2)+1
|
|
80
|
-
) : reqSocket.socket.localAddress);
|
|
81
|
-
reqSocket.session.rhost = (
|
|
82
|
-
reqSocket.socket.remoteAddress.startsWith('::') ?
|
|
83
|
-
reqSocket.socket.remoteAddress.substring(
|
|
84
|
-
reqSocket.socket.remoteAddress.indexOf(':',2)+1
|
|
85
|
-
) : reqSocket.socket.remoteAddress);
|
|
86
|
-
return reqSocket;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const setResponseAssessors = (resSocket) => {
|
|
90
|
-
resSocket.sendText = (data, code = 200) => {
|
|
91
|
-
resSocket.writeHead(code, { 'Content-Type': 'text/plain' });
|
|
92
|
-
resSocket.write(data);
|
|
93
|
-
resSocket.end();
|
|
94
|
-
};
|
|
95
|
-
resSocket.sendJSON = (data, code = 200) => {
|
|
96
|
-
resSocket.writeHead(code, { 'Content-Type': 'application/json' });
|
|
97
|
-
resSocket.write(typeof data === 'string' ? data : JSON.stringify(data));
|
|
98
|
-
resSocket.end();
|
|
99
|
-
};
|
|
100
|
-
resSocket.sendHTML = (data, code = 200) => {
|
|
101
|
-
resSocket.writeHead(code, { 'Content-Type': 'text/html' });
|
|
102
|
-
resSocket.write(data);
|
|
103
|
-
resSocket.end();
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const setSocketSecurityHeaders = (req) => {
|
|
108
|
-
// This should be set in a regular website:
|
|
109
|
-
// Forces the use of HTTPS for a long time, including subDomains - and prevent MitM attacks
|
|
110
|
-
// Does not work in servers that don't allow HTTPS (like this one)
|
|
111
|
-
// req.setHeader('Strict-Transport-Security', ['max-age=31536000', 'includeSubDomains']); // Only works on HTTPS
|
|
112
|
-
|
|
113
|
-
// This blocks requests with MIME type different from style/css and */script
|
|
114
|
-
// TODO test this - probably gonna break the server, as MIMEs are not overriden here
|
|
115
|
-
// req.setHeader('X-Content-Type-Options', 'nosniff');
|
|
116
|
-
|
|
117
|
-
// This deines the main security policy - Usually, when using a website,
|
|
118
|
-
// this should be highly customized, but, for simple sites, it can be leaved like that:
|
|
119
|
-
req.setHeader('Content-Security-Policy', [
|
|
120
|
-
'default-src=none',
|
|
121
|
-
'script-src=self',
|
|
122
|
-
'connect-src=self',
|
|
123
|
-
'img-src=self',
|
|
124
|
-
'style-src=self',
|
|
125
|
-
'frame-ancestors=none',
|
|
126
|
-
'form-action=self',
|
|
127
|
-
// 'upgrade-insecure-requests' // Only works on HTTPS
|
|
128
|
-
]);
|
|
129
|
-
// Isolates the browsing context exclusively to same-origin documents.
|
|
130
|
-
// Cross-origin documents are not loaded in the same browsing context.
|
|
131
|
-
req.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
132
|
-
// The HTTP Cross-Origin-Resource-Policy response header conveys a desire
|
|
133
|
-
// that the browser blocks no-cors cross-origin/cross-site requests to the given resource.
|
|
134
|
-
req.setHeader('Cross-Origin-Resource-Policy', 'same-site');
|
|
135
|
-
// A new HTTP response header that instructs the browser
|
|
136
|
-
// to prevent synchronous scripting access between same-site cross-origin pages.
|
|
137
|
-
req.setHeader('Origin-Agent-Cluster', '?1');
|
|
138
|
-
// Blocks information about the website when sending
|
|
139
|
-
// local requests or redirections to other sites
|
|
140
|
-
req.setHeader('Referrer-Policy', 'no-referrer');
|
|
141
|
-
// Enabling this makes all URLs in a page (even the cross domain ones)
|
|
142
|
-
// to be prefetched - This is dangerouns in terms of DNS queries
|
|
143
|
-
req.setHeader('X-DNS-Prefetch-Control', 'off');
|
|
144
|
-
// A legacy header, just for IE, and is highly dangerous
|
|
145
|
-
// Not setting this as disabled can cause malicious
|
|
146
|
-
// HTML+JS code to be loaded in the wrong context
|
|
147
|
-
req.setHeader('X-Download-Options', 'noopen');
|
|
148
|
-
// Blocks attempts to display the website as an IFrame
|
|
149
|
-
// If another website tries to display this website as a frame,
|
|
150
|
-
// this header will block it
|
|
151
|
-
req.setHeader('X-Frame-Options', 'DENY');
|
|
152
|
-
// Set the unnecessary XSS-Protection legacy header to disabled
|
|
153
|
-
// This header increases the number of vulnerabilities, and is used only in IE
|
|
154
|
-
req.setHeader('X-XSS-Protection', 0);
|
|
155
|
-
// Remove X-Powered-By - This header allows attackers to
|
|
156
|
-
// gather information about the application engine
|
|
157
|
-
if (req.hasHeader('X-Powered-By')) req.removeHeader('X-Powered-By');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const toBool = [() => true, () => false];
|
|
161
|
-
|
|
162
|
-
const clone = (object) => JSON.parse(JSON.stringify(object));
|
|
163
|
-
|
|
164
|
-
const isPromise = obj => {
|
|
165
|
-
return obj !== null &&
|
|
166
|
-
(typeof obj === 'object' || typeof obj === 'function') &&
|
|
167
|
-
typeof obj.then === 'function';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const UUID = () => '################################'.replace(new RegExp(`[#]`, 'gim'), () => Math.random().toString(16)[6]);
|
|
171
|
-
|
|
172
|
-
module.exports = { clone, noop, slugify, isSparseEqual, toBool, last, renderDynamicHTML, setSocketLocals, setSocketSecurityHeaders, setResponseAssessors, isPromise, UUID };
|