slower 2.0.1 → 2.1.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/readme.md.old +180 -0
  3. package/src/slower.js +102 -15
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "path-to-regexp": "^6.2.2"
4
4
  },
5
5
  "name": "slower",
6
- "version": "2.0.1",
6
+ "version": "2.1.0",
7
7
  "main": "index.js",
8
8
  "devDependencies": {},
9
9
  "scripts": {
package/readme.md.old ADDED
@@ -0,0 +1,180 @@
1
+ # Slower
2
+
3
+ Slower is a small web framework, express-like, but simpler and limited.
4
+ It allows for generic route-declaration, fallback pages, and multiple middleware functions.
5
+
6
+ ### API Methods:
7
+
8
+ ```
9
+ app.enableStrictHeaders(): this
10
+ > Enables the use of the set of 'Strict Headers'.
11
+ > These headers increase security levels, and are a good practice to apply.
12
+ > However, using these headers in testing scenarios are not a need,
13
+ and may have buggy or negative effects. So, apply those to simulate scenarios only.
14
+ > Headers configured:
15
+ > Content-Security-Policy
16
+ > Cross-Origin-Opener-Policy
17
+ > Cross-Origin-Resource-Policy
18
+ > Origin-Agent-Cluster
19
+ > Referrer-Policy
20
+ > X-DNS-Prefetch-Control (disabled)
21
+ > X-Download-Options
22
+ > X-Frame-Options
23
+ > X-XSS-Protection (disabled)
24
+ > X-Powered-By (removed)
25
+ > Returns the own object instance, so that methods can be chained.
26
+ ```
27
+ ```
28
+ app.disableStrictHeaders(): this
29
+ > Disables the use of the set of 'Strict Headers'.
30
+ > See 'enableStrictHeaders()' for more information.
31
+ > Returns the own object instance, so that methods can be chained.
32
+ ```
33
+ ```
34
+ app.setRoute(string: path = '/', string: type = 'GET', function: callback): this
35
+ > Creates a new route for path defined in 'path', responding to the HTTP verb defined in 'type' argument.
36
+ > The callback is executed when the route is accessed.
37
+ > Returns the own object instance, so that methods can be chained.
38
+ ```
39
+ ```
40
+ app.setMiddleware(function: callback): this
41
+ > Sets a new middleware function: callback function will be accessed for every server access.
42
+ > Many middlewares can be defined, and will be applied in the order they are defined.
43
+ > Returns the own object instance, so that methods can be chained.
44
+ ```
45
+ ```
46
+ app.setDynamic(string: path, string: file = '', string: mime = '', object: replacementData = null): this
47
+ > Creates a new GET route for path defined in 'path'.
48
+ > This is a custom file-response route, configured for template rendering just before response.
49
+ > Providing an object as 'replacementData' in this format { valueToBeReplaced: valueToReplace },
50
+ allows for template rendering. The value to replace in the file, uses this notation: '<{content}>'.
51
+ > URL reference in filename:
52
+ > For direct references, it is possible to use the token '{%}' to replace the filename for the URL.
53
+ > Ex:
54
+ app.setStatic('/login', './templates/{%}.html', 'text/html');
55
+ This will access the 'login.html' file when the route '/login' is accessed.
56
+ > Example:
57
+ Responding a route for '/custom' with file 'custom.html':
58
+ app.setDynamic('/custom', './templates/custom.html', 'text/html', { smile: ':)' })
59
+ In file './templates/custom.html':
60
+ "<h2> This is a custom thing: <{smile}> </h2>"
61
+ Rendered in browser:
62
+ <h2> This is a custom thing: :) </h2>
63
+ > Returns the own object instance, so that methods can be chained.
64
+ ```
65
+ ```
66
+ app.setStatic(string: path, string: file = '', string: mime = ''): this
67
+ > Creates a new GET route for path defined in 'path', responding with the specified file and MIME type.
68
+ > URL reference in filename:
69
+ > For direct references, it is possible to use the token '{%}' to replace the filename for the URL.
70
+ > Ex:
71
+ app.setStatic('/login', './templates/{%}.html', 'text/html');
72
+ This will access the 'login.html' file when the route '/login' is accessed.
73
+ > Example: A route for '/login' page, responding with 'login.html' file
74
+ setStatic('/login', __dirname+'/public/static/views/login.html', 'text/html');
75
+ > Returns the own object instance, so that methods can be chained.
76
+ ```
77
+ ```
78
+ app.setFallback(function: callback): this
79
+ > Creates a function for fallback state. When no other routes intercept the route, this will be used.
80
+ > Special use for 'page not found' fallback pages or highly customized routes and situations.
81
+ > Returns the own object instance, so that methods can be chained.
82
+ ```
83
+ ```
84
+ app.setFallbackFile (string: file = '', string: mime = '', object: replacementData = null): this
85
+ > Creates a function for fallback state. When no other routes intercept the route, this will be used.
86
+ > Equivalent to setFallback, but responds with a file. Allows for template rendering.
87
+ > See 'setDynamic' for more information about template rendering.
88
+ > Special use for 'page not found' fallback pages, ex: './e404.html'.
89
+ > Returns the own object instance, so that methods can be chained.
90
+ ```
91
+ ```
92
+ app.setAllowedMethods(array: methods = []): this
93
+ > Sets a list of methods to respond to.
94
+ > By using this, it is possible to restrict the application to avoid
95
+ responding to dangerous HTTP verbs, such as 'DELETE'.
96
+ > By default, all methods are allowed (see Slower.constructor.http_methods)
97
+ > Calling this function without parameters is an easy way to block responses to all requests (lock server).
98
+ > Returns the own object instance, so that methods can be chained.
99
+ ```
100
+ ```
101
+ app.start(number|string: port = 8080, string: host = undefined, function: callback = ()=>{}): this
102
+ > Starts the server, at a specific host and port, then calling the callback function.
103
+ > Not defining a specific port or host will start the server at '0.0.0.0:8080'.
104
+ > Returns the own object instance, so that methods can be chained.
105
+ ```
106
+
107
+ Example usage:
108
+ ```
109
+ const Slower = require('slower');
110
+ const port = 8080;
111
+ let app = Slower();
112
+ app.setMiddleware((req, res) => {
113
+ req.time = Date.now();
114
+ console.log(`${req.time} - ${req.method} : ${req.url}`);
115
+ });
116
+ app.setStatic('/favicon.ico', __dirname+'/public/{%}');
117
+ app.setRoute('/', 'GET', (req, res) => {
118
+ res.writeHead(200, { 'Content-Type': 'text/html' });
119
+ res.write('<html><body><p>This is the / page.</p></body></html>');
120
+ res.end();
121
+ });
122
+ app.setRoute('/{*}.css', 'GET', (req, res) => {
123
+ let data = fs.readFileSync(...some.css.file...)
124
+ res.writeHead(200, { 'Content-Type': 'text/css' });
125
+ res.write(data);
126
+ res.end();
127
+ });
128
+ const generateDownloadNumber = () => { Math.round(Math.random() * 10) }
129
+ app.setDynamic('/download/{?}/', './main-download.txt', { DowloadName: generateDownloadNumber });
130
+ // Responds to download routes such as '/download/2/'
131
+ app.setFallback((req, res) => {
132
+ res.writeHead(200, { 'Content-Type': 'text/html' });
133
+ res.write('<html><body><p>This is the fallback page.</p></body></html>');
134
+ res.end();
135
+ });
136
+ // Start app listening on all interfaces (0.0.0.0)
137
+ app.start(port, null, () => {
138
+ console.log(`Running on localhost:${port}`);
139
+ console.log(app);
140
+ });
141
+ ```
142
+ ### API modifications on 'net.Socket' instances:
143
+ - The API modifies every ```net.Socket``` instance BEFORE it is passed
144
+ to ```app.connectionListener```. This means that all events receiving
145
+ a socket will receive the modified socket instead.
146
+ - The modifications adds the following properties to the socket instance:
147
+ ```
148
+ <socket>.session: Object => A container for persistent data appended to sockets
149
+ <socket>.session.port: Number => The local port number
150
+ <socket>.session.rport: Number => The remote port number
151
+ <socket>.session.host: String => The local host interface address
152
+ <socket>.session.rhost: String => The remote host interface address
153
+ ```
154
+ - It is possible to use the ```socket.session``` object to append data that will persist
155
+ during the lifetime of a single connection. Useful for keeping short-life local variables.
156
+
157
+ - In HTTP ```http.IncomingMessage``` instances, the 'socket' instance is found over 'request'.
158
+ So, considering the common callback of ```(req, res)```, the session container will be ```req.session```
159
+
160
+ ### API security headers implementation:
161
+ - It is possible to enforce a higher set of security headers on responses
162
+ without having to set them manually. The API 'enableStrictHeaders' and 'disableStrictHeaders'
163
+ methods do exacly that. The strict headers are disabled by default, as some resources are too strict,
164
+ but it is also possible to enable them all, and then set a middleware to override any header.
165
+ - Headers set by 'enableStrictHeaders':
166
+ ```
167
+ Content-Security-Policy: default-src=none; script-src=self; connect-src=self; img-src=self;
168
+ style-src=self; frame-ancestors=none; form-action=self;
169
+ Cross-Origin-Opener-Policy: same-origin
170
+ Cross-Origin-Resource-Policy: same-site
171
+ Origin-Agent-Cluster: ?1
172
+ Referrer-Policy: no-referrer
173
+ Strict-Transport-Security: max-age=31536000; includeSubDomains // temporarily disabled for maintenance
174
+ X-Content-Type-Options: nosniff // temporarily disabled for maintenance
175
+ X-DNS-Prefetch-Control: off
176
+ X-Download-Options: noopen
177
+ X-Frame-Options: DENY
178
+ X-Powered-By: (This header is removed if a response includes it)
179
+ X-XSS-Protection: 0
180
+ ```
package/src/slower.js CHANGED
@@ -80,13 +80,13 @@ class SlowerRouter {
80
80
  #setRoute (method, path, handler) {
81
81
  if (typeof method !== 'string')
82
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');
83
+ if (typeof path !== 'string' && typeof path !== 'function' && path?.constructor?.name !== 'RegExp')
84
+ throw new Error('<SlowerRouter>.route :: "path" parameter must be of type Function, String or RegExp');
85
85
  if (typeof handler !== 'function')
86
86
  throw new Error('<SlowerRouter>.route :: "handler" parameter must be of type Function');
87
87
  if (!this.layers.get(method))
88
88
  this.layers.set(method, new Map());
89
- if (typeof path === 'string')
89
+ if (typeof path === 'string' || path?.constructor?.name !== 'RegExp')
90
90
  path = match(path, { decode: decodeURIComponent }); // 'path' is a function now
91
91
  this.layers.get(method).set(path, handler);
92
92
  return this;
@@ -134,20 +134,107 @@ class SlowerRouter {
134
134
  * app.static('./public', '/') // Access with 'GET /{filename}'
135
135
  */
136
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
- });
137
+ const absoluteDir = path.resolve(directoryPath);
138
+ for (const file of utils.getFiles(absoluteDir)) {
139
+ // Get only the file name from the absolute file path 'c:\u\a.txt' -> 'a.txt'
140
+ let fileAsURLPath = file.replaceAll(/\\/g, '/').split('/').slice(-1)[0];
141
+ // If there is a mount path, merge it with the file and remove duplicate bars '//'
142
+ if (!mountPath) mountPath = directoryPath.replaceAll(/\\/g, '/').split('/').slice(-1)[0];
143
+ fileAsURLPath = ('/' + mountPath + '/' + fileAsURLPath).replaceAll(/\/+/g,'/');
144
+
145
+
146
+ async function staticfhandle (req, res, next) {
147
+ try {
148
+ const fileStream = createReadStream(file);
149
+ res.setHeader('Content-Type', MIME_TABLE[extension] || MIME_TABLE['default']);
150
+ return await pipeline(fileStream, res);
151
+
152
+ } catch (err) {
153
+ // In case the file does not exist, return a 404 and prevent it from breaking
154
+ res.status(404).end();
155
+ }
156
+ }
157
+
158
+ this.get(fileAsURLPath, staticfhandle);
159
+
160
+ // If the file is an html file, also add a path for using it without the '.html' extension
161
+ // both 'GET /files/somefile.html' and 'GET /files/somefile' would work
162
+ const extension = utils.getFileExtension(file);
163
+ if (extension === 'html')
164
+ this.get(fileAsURLPath.replace('.html', ''), staticfhandle);
148
165
  }
149
166
  return this;
150
167
  }
168
+
169
+ /**
170
+ * Import and use another SlowerRouter, mounted at a specific path
171
+ * @param {string} mountPath Mount the router at a specific path
172
+ * @param {SlowerRouter} SlowerRouterInstance
173
+ */
174
+ useRouter (SlowerSubRouterInstance, mountPath = '/') {
175
+ const mount = p => mountPath ? (mountPath + '/' + p).replace(/\/{2,}/g, '/') : p;
176
+ const layers = SlowerSubRouterInstance.layers;
177
+ for (let layer of layers) {
178
+ if (!!layer.static) {
179
+ this.static(layer.directoryPath, mount(layer.mountPath));
180
+ continue;
181
+ }
182
+ console.log(layer)
183
+ this.#setRoute(layer.method, mount(layer.path), layer.handler);
184
+ }
185
+ return this;
186
+ }
187
+ }
188
+
189
+ class SlowerSubRouter {
190
+ constructor () {
191
+ this.METHODS = HTTP_VERBS;
192
+ this.layers = new Array();
193
+
194
+ // Create basic route shortcuts
195
+ // get(), post(), put(), delete()
196
+ for (let verb of HTTP_VERBS) {
197
+ this[verb] = function (path, callback) {
198
+ return this.#setRoute(verb, path, callback);
199
+ };
200
+ }
201
+ }
202
+
203
+ #setRoute (method, path, handler) {
204
+ if (typeof method !== 'string')
205
+ throw new Error('<SlowerSubRouter>.route :: "method" parameter must be of type String');
206
+ if (typeof path !== 'string' && path?.constructor?.name !== 'RegExp')
207
+ throw new Error('<SlowerSubRouter>.route :: "path" parameter must be of type Function, String or RegExp');
208
+ if (typeof handler !== 'function')
209
+ throw new Error('<SlowerSubRouter>.route :: "handler" parameter must be of type Function');
210
+ this.layers.push({ method, path, handler });
211
+ return this;
212
+ }
213
+
214
+ all (path, handler) {
215
+ if (typeof path === 'string')
216
+ for (let verb of HTTP_VERBS) this.#setRoute(verb, path, handler);
217
+ else if (typeof path !== 'function')
218
+ throw new Error('<SlowerSubRouter>.use :: "handler" parameter must be of type Function');
219
+ else for (let verb of HTTP_VERBS) this.#setRoute(verb, '/(.{0,})', path);
220
+ return this;
221
+ }
222
+ // Just a more comprehensive call to app.all for defining middlewares
223
+ use (...b) { this.all(...b); return this; };
224
+
225
+ static (directoryPath, mountPath = '') {
226
+ this.layers.push({ static: true, directoryPath, mountPath });
227
+ return this;
228
+ }
229
+ }
230
+
231
+ // Create basic main routing creation
232
+ function Slower (options) {
233
+ return new SlowerRouter(options);
234
+ }
235
+ // Add the subrouter class
236
+ Slower.Router = function () {
237
+ return new SlowerSubRouter();
151
238
  }
152
239
 
153
- module.exports = Slower = options => new SlowerRouter(options);
240
+ module.exports = Slower;