slower 1.1.27 → 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 CHANGED
@@ -1 +1,52 @@
1
- module.exports = require('./lib/router');
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": "1.1.27",
4
- "description": "A package for simple HTTP server routing.",
6
+ "version": "2.0.0",
5
7
  "main": "index.js",
6
- "directories": {
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