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/index.js
CHANGED
|
@@ -1 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
const Slower = require('./src/slower');
|
|
2
|
+
module.exports = Slower;
|
|
3
|
+
|
|
4
|
+
// TODO __
|
|
5
|
+
// const setSocketSecurityHeaders = (req) => {
|
|
6
|
+
// // This should be set in a regular website:
|
|
7
|
+
// // Forces the use of HTTPS for a long time, including subDomains - and prevent MitM attacks
|
|
8
|
+
// // Does not work in servers that don't allow HTTPS (like this one)
|
|
9
|
+
// // req.setHeader('Strict-Transport-Security', ['max-age=31536000', 'includeSubDomains']); // Only works on HTTPS
|
|
10
|
+
// // This blocks requests with MIME type different from style/css and */script
|
|
11
|
+
// // This denies the main security policy - Usually, when using a website,
|
|
12
|
+
// // this should be highly customized, but, for simple sites, it can be left like that:
|
|
13
|
+
// req.setHeader('Content-Security-Policy', [
|
|
14
|
+
// 'default-src=none',
|
|
15
|
+
// 'script-src=self',
|
|
16
|
+
// 'connect-src=self',
|
|
17
|
+
// 'img-src=self',
|
|
18
|
+
// 'style-src=self',
|
|
19
|
+
// 'frame-ancestors=none',
|
|
20
|
+
// 'form-action=self',
|
|
21
|
+
// // 'upgrade-insecure-requests' // Only works on HTTPS
|
|
22
|
+
// ]);
|
|
23
|
+
// // Isolates the browsing context exclusively to same-origin documents.
|
|
24
|
+
// // Cross-origin documents are not loaded in the same browsing context.
|
|
25
|
+
// req.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
26
|
+
// // The HTTP Cross-Origin-Resource-Policy response header conveys a desire
|
|
27
|
+
// // that the browser blocks no-cors cross-origin/cross-site requests to the given resource.
|
|
28
|
+
// req.setHeader('Cross-Origin-Resource-Policy', 'same-site');
|
|
29
|
+
// // A new HTTP response header that instructs the browser
|
|
30
|
+
// // to prevent synchronous scripting access between same-site cross-origin pages.
|
|
31
|
+
// req.setHeader('Origin-Agent-Cluster', '?1');
|
|
32
|
+
// // Blocks information about the website when sending
|
|
33
|
+
// // local requests or redirections to other sites
|
|
34
|
+
// req.setHeader('Referrer-Policy', 'no-referrer');
|
|
35
|
+
// // Enabling this makes all URLs in a page (even the cross domain ones)
|
|
36
|
+
// // to be prefetched - This is dangerouns in terms of DNS queries
|
|
37
|
+
// req.setHeader('X-DNS-Prefetch-Control', 'off');
|
|
38
|
+
// // A legacy header, just for IE, and is highly dangerous
|
|
39
|
+
// // Not setting this as disabled can cause malicious
|
|
40
|
+
// // HTML+JS code to be loaded in the wrong context
|
|
41
|
+
// req.setHeader('X-Download-Options', 'noopen');
|
|
42
|
+
// // Blocks attempts to display the website as an IFrame
|
|
43
|
+
// // If another website tries to display this website as a frame,
|
|
44
|
+
// // this header will block it
|
|
45
|
+
// req.setHeader('X-Frame-Options', 'DENY');
|
|
46
|
+
// // Set the unnecessary XSS-Protection legacy header to disabled
|
|
47
|
+
// // This header increases the number of vulnerabilities, and is used only in IE
|
|
48
|
+
// req.setHeader('X-XSS-Protection', 0);
|
|
49
|
+
// // Remove X-Powered-By - This header allows attackers to
|
|
50
|
+
// // gather information about the application engine
|
|
51
|
+
// if (req.hasHeader('X-Powered-By')) req.removeHeader('X-Powered-By');
|
|
52
|
+
// }
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"path-to-regexp": "^6.2.2"
|
|
4
|
+
},
|
|
2
5
|
"name": "slower",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A package for simple HTTP server routing.",
|
|
6
|
+
"version": "2.0.0",
|
|
5
7
|
"main": "index.js",
|
|
6
|
-
"
|
|
7
|
-
"lib": "lib"
|
|
8
|
-
},
|
|
8
|
+
"devDependencies": {},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [],
|
|
13
13
|
"author": "Tomás Luchesi <no.mad.devtech@gmail.com>",
|
|
14
|
-
"license": "MIT"
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"description": ""
|
|
15
16
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const MIME_TABLE = require('./mimetable.json');
|
|
3
|
+
const utils = require('./utils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Receives and configures the HTTP Response object
|
|
7
|
+
* @param {http.ServerResponse} response
|
|
8
|
+
* @returns {http.ServerResponse}
|
|
9
|
+
* @exposes .status()
|
|
10
|
+
* @exposes .send()
|
|
11
|
+
* @exposes .json()
|
|
12
|
+
* @exposes .file()
|
|
13
|
+
*/
|
|
14
|
+
function setupResponse (response) {
|
|
15
|
+
/**
|
|
16
|
+
* Sets the response status code
|
|
17
|
+
* @chainable
|
|
18
|
+
* @param {number} statusCode The response status code
|
|
19
|
+
* @returns {http.ServerResponse}
|
|
20
|
+
*/
|
|
21
|
+
response.status = function (statusCode) {
|
|
22
|
+
response.statusCode = statusCode;
|
|
23
|
+
return response;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sets a response header to a specified value
|
|
28
|
+
* @chainable
|
|
29
|
+
* @overload
|
|
30
|
+
* @param {string} header
|
|
31
|
+
* @param {string} value
|
|
32
|
+
* @returns {http.ServerResponse}
|
|
33
|
+
* @info Pass in an object of "header":"value" entries
|
|
34
|
+
* to set multiple headers at once
|
|
35
|
+
* @example
|
|
36
|
+
* res.set('Content-Length', 1000)
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Sets multiple response headers to specified values
|
|
40
|
+
* @chainable
|
|
41
|
+
* @overload
|
|
42
|
+
* @param {object} headers
|
|
43
|
+
* @returns {http.ServerResponse}
|
|
44
|
+
* @example
|
|
45
|
+
* res.set({ 'Content-Length':1000, 'Content-Type':'text/plain' })
|
|
46
|
+
*/
|
|
47
|
+
response.set = function (header, value) {
|
|
48
|
+
if (typeof header === 'string')
|
|
49
|
+
response.setHeader(header, value);
|
|
50
|
+
else
|
|
51
|
+
for (let prop of header)
|
|
52
|
+
response.setHeader(prop, header[prop]);
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An alias for 'res.getHeader()'.
|
|
58
|
+
* @param {string} header
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
response.get = function(header) {
|
|
62
|
+
return response.getHeader(header);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sets the Content-Type header with a specific MIME type.
|
|
67
|
+
* @chainable
|
|
68
|
+
* @param {string} mime The MIME type to set (can be: complete, like "text/html" or from a file extension, like "html")
|
|
69
|
+
* @returns {http.ServerResponse}
|
|
70
|
+
* @info And if no type is specified, binary type is used (application/octet-stream)
|
|
71
|
+
*/
|
|
72
|
+
response.type = function (mime) {
|
|
73
|
+
let mimetype = MIME_TABLE[mime] || mime || MIME_TABLE[extension] || MIME_TABLE['default'];
|
|
74
|
+
response.setHeader('Content-type', mimetype);
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sends a string or buffer as JSON and ends the response
|
|
80
|
+
* @param {string|Buffer} data
|
|
81
|
+
* @returns {undefined}
|
|
82
|
+
*/
|
|
83
|
+
response.json = function json (data) {
|
|
84
|
+
if (response.statusCode === undefined) response.status(200);
|
|
85
|
+
response.setHeader('Content-type', 'application/json');
|
|
86
|
+
response.write(JSON.stringify(data));
|
|
87
|
+
response.end();
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sends a string or buffer as HTML data and ends the response
|
|
93
|
+
* @param {string|Buffer} data
|
|
94
|
+
* @returns {undefined}
|
|
95
|
+
*/
|
|
96
|
+
response.send = function (data) {
|
|
97
|
+
if (response.statusCode === undefined) response.status(200);
|
|
98
|
+
if (!response.getHeader('Content-Type'))
|
|
99
|
+
response.setHeader('Content-Type', 'text/html');
|
|
100
|
+
response.write(data);
|
|
101
|
+
response.end();
|
|
102
|
+
return undefined;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sends a file and ends the response
|
|
107
|
+
* @param {String} filename The name of the file to send
|
|
108
|
+
* @returns {undefined}
|
|
109
|
+
*/
|
|
110
|
+
response.file = function (filename) {
|
|
111
|
+
if (response.statusCode === undefined) response.status(200);
|
|
112
|
+
if (!response.getHeader('Content-Type')) {
|
|
113
|
+
let extension = (filename||'').split('.').slice(-1)[0];
|
|
114
|
+
response.setHeader('Content-Type', MIME_TABLE[extension] || MIME_TABLE['default']);
|
|
115
|
+
}
|
|
116
|
+
const stream = fs.createReadStream(filename);
|
|
117
|
+
stream.pipe(response);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Renders an HTML document and sends it to client
|
|
123
|
+
* @param {string} filename The file to use as rendering View
|
|
124
|
+
* @param {object} locals The properties to replace in the View
|
|
125
|
+
* @info The locals object may contain either strings or functions or objects with 'toString' method.
|
|
126
|
+
* @returns {undefined}
|
|
127
|
+
* @example
|
|
128
|
+
* // In 'home.html:
|
|
129
|
+
* <h2>{{user}}</h2> has <h2>{{count}}</h2> items
|
|
130
|
+
* // In server.js: (generates a new random item for each request)
|
|
131
|
+
* res.render('./views/home.html', { user: 'Mike', count: () => randomInt() });
|
|
132
|
+
*/
|
|
133
|
+
response.render = function (filename, locals = {}) {
|
|
134
|
+
let html = fs.readFileSync(filename, 'utf-8');
|
|
135
|
+
for (let item in locals) {
|
|
136
|
+
html = html.replace(
|
|
137
|
+
new RegExp('{{'+item+'}}', 'gim'),
|
|
138
|
+
typeof locals[item] === 'function' ?
|
|
139
|
+
locals[item]() : locals[item]?.toString() || ''
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
response.setHeader('Content-type', 'text/html');
|
|
143
|
+
response.write(html);
|
|
144
|
+
response.end();
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sets the 'Location' header and redirects to a given path, URL, or link.
|
|
150
|
+
* @param {string} location An URL, link, path, or the string 'back' to set the target
|
|
151
|
+
* @returns {http.undefined}
|
|
152
|
+
* @info If location is not passed, it is assumed to be '/'
|
|
153
|
+
* @info If location is set to the string 'back' it redirects to the link in the request 'Referrer' header
|
|
154
|
+
* @example
|
|
155
|
+
* // Redirecting out of the site to the clients previous URL:
|
|
156
|
+
* res.status(300).redirect('back');
|
|
157
|
+
* // Redirecting to other page in current domain:
|
|
158
|
+
* res.status(300).redirect('/other/page');
|
|
159
|
+
* // Redirecting to other URL:
|
|
160
|
+
* res.status(300).redirect('http://example.com');
|
|
161
|
+
*/
|
|
162
|
+
response.redirect = function (location) {
|
|
163
|
+
response.setHeader('Location', location === 'back' ? request.getHeader('Referrer') : location || '/');
|
|
164
|
+
response.end();
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
return response;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Receives and configures the HTTP Request object
|
|
174
|
+
* @param {http.IncomingMessage} request
|
|
175
|
+
* @returns {http.IncomingMessage}
|
|
176
|
+
* @exposes req.body
|
|
177
|
+
* @exposes req.urlParts
|
|
178
|
+
* @exposes req.query
|
|
179
|
+
* @exposes req.session = { port, rport, host, rhost }
|
|
180
|
+
*/
|
|
181
|
+
function setupRequest (request) {
|
|
182
|
+
/**
|
|
183
|
+
* @property
|
|
184
|
+
* Holds the request body data as a buffer
|
|
185
|
+
*/
|
|
186
|
+
return new Promise (resolve => {
|
|
187
|
+
// Set classical socket locals
|
|
188
|
+
request.session = {
|
|
189
|
+
port: request.socket.localPort,
|
|
190
|
+
rport: request.socket.remotePort,
|
|
191
|
+
host: utils.normalizeAddress(request.socket.localAddress),
|
|
192
|
+
rhost: utils.normalizeAddress(request.socket.remoteAddress)
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* An alias for 'req.getHeader()'.
|
|
198
|
+
* @param {string} header
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
request.get = function(header) {
|
|
202
|
+
return request.getHeader(header);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add req.params placeholder, it is added in the main router, not here
|
|
206
|
+
request.params = undefined;
|
|
207
|
+
|
|
208
|
+
// Add a req.query object, containing the pairs of key=value queries (if any)
|
|
209
|
+
request.query = utils.getURLQueryString(request.url);
|
|
210
|
+
|
|
211
|
+
request.urlParts = request.url.split('/').filter(Boolean);
|
|
212
|
+
request.body = [];
|
|
213
|
+
request.on('timeout', () => reject(new Error('Timeout')));
|
|
214
|
+
request.on('data', chunk => request.body.push(chunk));
|
|
215
|
+
request.on('error', (err) => reject(err));
|
|
216
|
+
request.on('end', () => {
|
|
217
|
+
let tmp = Buffer.concat(request.body);
|
|
218
|
+
request.body = {
|
|
219
|
+
buffer: tmp,
|
|
220
|
+
text: () => tmp.toString(),
|
|
221
|
+
json: () => JSON.parse(tmp)
|
|
222
|
+
}
|
|
223
|
+
return resolve(request);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = { setupRequest, setupResponse };
|
package/src/slower.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
const https = require('node:https');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { createReadStream } = require('node:fs');
|
|
6
|
+
const { pipeline } = require('node:stream/promises');
|
|
7
|
+
|
|
8
|
+
const { match } = require('path-to-regexp');
|
|
9
|
+
|
|
10
|
+
const { setupRequest, setupResponse } = require('./decorators');
|
|
11
|
+
const utils = require('./utils');
|
|
12
|
+
|
|
13
|
+
const MIME_TABLE = require('./mimetable.json');
|
|
14
|
+
const HTTP_VERBS = http.METHODS.map(v => v.toLowerCase());
|
|
15
|
+
|
|
16
|
+
class SlowerRouter {
|
|
17
|
+
#server;
|
|
18
|
+
|
|
19
|
+
// You can create it with HTTPS options here
|
|
20
|
+
/**
|
|
21
|
+
* Use HTTPS server instead of HTTP.
|
|
22
|
+
* Pass in all regular HTTPS options as parameters.
|
|
23
|
+
* 'key' and 'cert' options are required for HTTPS.
|
|
24
|
+
* @param {object} options
|
|
25
|
+
* @returns {http.Server|https.Server}
|
|
26
|
+
* @example
|
|
27
|
+
* SlowerRouter({https:true, key:'...', cert:'...'}); // Create HTTPS server
|
|
28
|
+
* SlowerRouter(); // Create regular HTTP
|
|
29
|
+
*/
|
|
30
|
+
constructor (options = {}) {
|
|
31
|
+
this.METHODS = HTTP_VERBS;
|
|
32
|
+
this.layers = new Map();
|
|
33
|
+
|
|
34
|
+
// Create basic route shortcuts
|
|
35
|
+
// get(), post(), put(), delete()
|
|
36
|
+
for (let verb of HTTP_VERBS) {
|
|
37
|
+
this.layers.set(verb, new Map());
|
|
38
|
+
this[verb] = function (path, callback) {
|
|
39
|
+
return this.#setRoute(verb, path, callback);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.https)
|
|
44
|
+
this.#server = https.createServer(options);
|
|
45
|
+
else
|
|
46
|
+
this.#server = http.createServer(options);
|
|
47
|
+
|
|
48
|
+
this.#server.on('request', this.#requestHandlerWrapper(this));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#requestHandlerWrapper () {
|
|
52
|
+
// Save the 'this' scope
|
|
53
|
+
// Inside the requestHandler function, 'this' corresponds to the http.Server instance
|
|
54
|
+
const self = this;
|
|
55
|
+
|
|
56
|
+
return (async function requestHandler (req, res) {
|
|
57
|
+
// Get all routes that match the URL and join with middlewares to cycle
|
|
58
|
+
let foundRoutes = utils.getMatchingRoute(req.url, req.method, self.layers);
|
|
59
|
+
let layers = foundRoutes;
|
|
60
|
+
|
|
61
|
+
// Set properties on request and response objects
|
|
62
|
+
req = await setupRequest(req);
|
|
63
|
+
res = await setupResponse(res);
|
|
64
|
+
|
|
65
|
+
// Cycle throught all middlewares and proper routes and call with 'next()' as third argument
|
|
66
|
+
;(async function cycleMatching (routes) {
|
|
67
|
+
if (routes.length === 0) return;
|
|
68
|
+
let route = routes[0];
|
|
69
|
+
if (route.params) req.params = route.params;
|
|
70
|
+
if (route.callback) route = route.callback;
|
|
71
|
+
route(req, res, async () => cycleMatching(routes.slice(1)));
|
|
72
|
+
})(layers);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
listen (...v) { return this.#server.listen(...v); }
|
|
77
|
+
close (callback) { return this.#server.close(callback); }
|
|
78
|
+
|
|
79
|
+
// Add any type of route
|
|
80
|
+
#setRoute (method, path, handler) {
|
|
81
|
+
if (typeof method !== 'string')
|
|
82
|
+
throw new Error('<SlowerRouter>.route :: "method" parameter must be of type String');
|
|
83
|
+
if (typeof path !== 'string' && path?.constructor?.name !== 'RegExp')
|
|
84
|
+
throw new Error('<SlowerRouter>.route :: "path" parameter must be of type String or RegExp');
|
|
85
|
+
if (typeof handler !== 'function')
|
|
86
|
+
throw new Error('<SlowerRouter>.route :: "handler" parameter must be of type Function');
|
|
87
|
+
if (!this.layers.get(method))
|
|
88
|
+
this.layers.set(method, new Map());
|
|
89
|
+
if (typeof path === 'string')
|
|
90
|
+
path = match(path, { decode: decodeURIComponent }); // 'path' is a function now
|
|
91
|
+
this.layers.get(method).set(path, handler);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add middleware
|
|
96
|
+
/**
|
|
97
|
+
* Create a middleware for all HTTP methods, for a specific path
|
|
98
|
+
* @overload
|
|
99
|
+
* @param {String} path
|
|
100
|
+
* @param {Function} handler
|
|
101
|
+
* @returns {SlowerRouter}
|
|
102
|
+
*/
|
|
103
|
+
/**
|
|
104
|
+
* Create a global middleware (all paths and all HTTP methods)
|
|
105
|
+
* @overload
|
|
106
|
+
* @param {Function} handler
|
|
107
|
+
* @returns {SlowerRouter}
|
|
108
|
+
*/
|
|
109
|
+
all (path, handler) {
|
|
110
|
+
if (typeof path === 'string')
|
|
111
|
+
for (let verb of HTTP_VERBS) this.#setRoute(verb, path, handler);
|
|
112
|
+
else if (typeof path !== 'function')
|
|
113
|
+
throw new Error('<SlowerRouter>.use :: "handler" parameter must be of type Function');
|
|
114
|
+
else for (let verb of HTTP_VERBS) this.#setRoute(verb, '/(.{0,})', path);
|
|
115
|
+
// this.middlewares.add(handlerA);
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
// Just a more comprehensive call to app.all for defining middlewares
|
|
119
|
+
use (...b) { this.all(...b); return this; };
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Serve static files from a directory
|
|
123
|
+
* @param {String} directoryPath The directory to serve
|
|
124
|
+
* @param {String} mountPath A path to use as mounting point
|
|
125
|
+
* @returns {SlowerRouter}
|
|
126
|
+
* @example
|
|
127
|
+
* // Using a mounting poing:
|
|
128
|
+
* app.static('./public', '/files') // Access with 'GET /files/{filename}'
|
|
129
|
+
*
|
|
130
|
+
* // Not using a mounting point:
|
|
131
|
+
* app.static('./public') // Access with 'GET /public/{filename}'
|
|
132
|
+
*
|
|
133
|
+
* // Using root ('/') as mounting point:
|
|
134
|
+
* app.static('./public', '/') // Access with 'GET /{filename}'
|
|
135
|
+
*/
|
|
136
|
+
static (directoryPath, mountPath = '') {
|
|
137
|
+
let folderRelative = directoryPath.replace('./', '');
|
|
138
|
+
for (const file of utils.getFiles(directoryPath)) {
|
|
139
|
+
let pathWithoutBase = '/' + file.replace(folderRelative, '').replaceAll('\\', '/');
|
|
140
|
+
if (mountPath) pathWithoutBase = mountPath + '/' + pathWithoutBase.slice(1).split('/').slice(1).join('/');
|
|
141
|
+
pathWithoutBase = pathWithoutBase.replaceAll('//', '/');
|
|
142
|
+
this.get(pathWithoutBase, async (req, res, next) => {
|
|
143
|
+
const relativePath = path.join(__dirname, '../../', file); // TODO: Test this as module, and maybe replace '../../' with '../'
|
|
144
|
+
const fileStream = createReadStream(relativePath);
|
|
145
|
+
res.setHeader('Content-Type', MIME_TABLE[utils.getFileExtension(file)] || MIME_TABLE['default']);
|
|
146
|
+
return await pipeline(fileStream, res);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = Slower = options => new SlowerRouter(options);
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// https://dev.to/thiagomr/como-funciona-o-express-js-criando-um-http-server-express-like-do-zero-sem-frameworks-125p
|
|
4
|
+
|
|
5
|
+
// https://dev.to/wesleymreng7/creating-your-own-expressjs-from-scratch-part-1-basics-methods-and-routing-a8
|
|
6
|
+
// https://dev.to/wesleymreng7/creating-your-own-expressjs-from-scratch-part-2-middlewares-and-controllers-2fbc
|
|
7
|
+
// https://dev.to/wesleymreng7/creating-your-own-expressjs-from-scratch-part-3-treating-request-and-response-objects-4ecf
|
|
8
|
+
// https://dev.to/wesleymreng7/creating-your-own-expressjs-from-scratch-part-4-modular-router-and-global-middlewares-560m
|
|
9
|
+
// https://dev.to/wesleymreng7/creating-your-own-expressjs-from-scratch-part-5-serving-static-files-3e11
|
|
10
|
+
// https://dev.to/wesleymreng7/creating-your-own-expressjs-from-scratch-part-6-creating-a-body-parser-middleware-15e7
|
|
11
|
+
|
|
12
|
+
// https://thecodebarbarian.com/write-your-own-express-from-scratch
|
|
13
|
+
|
|
14
|
+
const { statSync, readdirSync } = require('node:fs')
|
|
15
|
+
const { join } = require('node:path');
|
|
16
|
+
const { parse } = require('node:querystring');
|
|
17
|
+
|
|
18
|
+
function* getFiles(folder) {
|
|
19
|
+
const files = readdirSync(folder);
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const absolutePath = join(folder, file)
|
|
22
|
+
if (statSync(absolutePath).isDirectory()) {
|
|
23
|
+
yield* getFiles(absolutePath)
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
yield absolutePath.replaceAll('..\\','')
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const getMatchingRoute = (url, method, layers) => {
|
|
32
|
+
method = method.toLowerCase();
|
|
33
|
+
let list = [];
|
|
34
|
+
// Get only the layers from the proper HTTP verb
|
|
35
|
+
let routes = layers.get(method) || new Map();
|
|
36
|
+
// Iterate through all routes, and get the one that match
|
|
37
|
+
for (let [ pathFn, callback ] of routes) {
|
|
38
|
+
let params = pathFn(getURLPathBody(url));
|
|
39
|
+
// Return the matching route
|
|
40
|
+
if (!!params) {
|
|
41
|
+
// add to list
|
|
42
|
+
let built = ({ callback });
|
|
43
|
+
if (params.params && params.params['0'] === undefined)
|
|
44
|
+
built['params'] = params.params;
|
|
45
|
+
list.push(built);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return list;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const normalizeAddress = addr => addr.startsWith('::') ? addr : addr.substring(addr.indexOf(':',2)+1);
|
|
52
|
+
|
|
53
|
+
const getFileExtension = (fname) => fname.split('.').filter(Boolean).slice(-1)[0];
|
|
54
|
+
|
|
55
|
+
// /page?foo=bar&abc=123 -> /page
|
|
56
|
+
const getURLPathBody = (urlPath = '') => urlPath.split('?')[0] || '';
|
|
57
|
+
|
|
58
|
+
// /page?foo=bar&abc=123 -> { foo: 'bar', abc: '123' }
|
|
59
|
+
const getURLQueryString = (urlPath = '') => parse((urlPath.split('?')[1] || '').split('#')[0]);
|
|
60
|
+
|
|
61
|
+
module.exports = { getMatchingRoute, getFiles, normalizeAddress, getFileExtension, getURLPathBody, getURLQueryString };
|
package/TODO
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
o metodo setStatic quebra quando se usa ambos {*} e {%} ou {?} com {*}, pois ele faz a substituição do nome do arquivo por {*}^em vez de fazer pelo nome do URL
|