slower 2.1.9 β 2.2.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/README.md +386 -0
- package/examples/router.js +45 -0
- package/examples/subrouter.js +29 -0
- package/examples/third-party-middlewares.js +88 -0
- package/package.json +1 -5
- package/src/slower.js +1 -3
- package/src/utils.js +23 -5
- package/readme.md.old +0 -180
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Slower
|
|
2
|
+
|
|
3
|
+
A minimal, dependency-free HTTP framework for Node.js.
|
|
4
|
+
Slower provides a small subset of Express.jsβstyle routing and middleware with a simpler internal model.
|
|
5
|
+
|
|
6
|
+
Don't want to download 200 megabytes of modules from Express? Then try Slower π.
|
|
7
|
+
|
|
8
|
+
It's just a couple files and a few KB.
|
|
9
|
+
|
|
10
|
+
This might not be the most robust library, but it is small and lightweight, and it does the job. (Just don't use this in production, please.)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
#### A word about this:
|
|
15
|
+
|
|
16
|
+
Sorry for the (really) poorly written documentation, I need time to re-write it.
|
|
17
|
+
If you would like to help, just email me (email on package.json), and I will be more than happy to talk to you.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- No external dependencies
|
|
24
|
+
- Express-like API and functionality (`get`, `post`, `use`, `all`, etc.)
|
|
25
|
+
- Static file serving
|
|
26
|
+
- Sub-routers and route grouping
|
|
27
|
+
- SMALL!
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install slower
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
(Or just download this source code and put on a 'lib/slower' folder, it's what I usually do).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Basic Usage
|
|
42
|
+
|
|
43
|
+
Check `examples/` for some samples on how to use this library.
|
|
44
|
+
|
|
45
|
+
The basic is: if works on Express, probably works here too (apart from some minor modifications, there is very little difference in the two public APIs)
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const slower = require('slower');
|
|
49
|
+
|
|
50
|
+
const app = slower();
|
|
51
|
+
|
|
52
|
+
app.get('/', (req, res) => {
|
|
53
|
+
res.send('Hello world');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
app.listen(3000);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Template URLs:
|
|
60
|
+
|
|
61
|
+
The same basic rules that express.js applied are available here, since JS internalized the URLPattern API, and this library uses the pattern matching from URLPattern.
|
|
62
|
+
|
|
63
|
+
If you have questions, check this guide on URLPattern:
|
|
64
|
+
[https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API#pattern_syntax](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API#pattern_syntax)
|
|
65
|
+
|
|
66
|
+
### Routing
|
|
67
|
+
|
|
68
|
+
#### HTTP Methods
|
|
69
|
+
|
|
70
|
+
All standard HTTP verbs are supported:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
app.get('/users', handler);
|
|
74
|
+
app.post('/users', handler);
|
|
75
|
+
app.put('/users/:id', handler);
|
|
76
|
+
app.delete('/users/:id', handler);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Route Parameters
|
|
80
|
+
|
|
81
|
+
Routes use URLPattern internally and support named parameters:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
app.get('/users/:id', (req, res) => {
|
|
85
|
+
res.json({ id: req.params.id });
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Multiple Handlers / Middleware
|
|
90
|
+
|
|
91
|
+
Handlers are executed sequentially and receive next():
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
app.get('/secure', authMiddleware, (req, res) => {
|
|
95
|
+
res.send('Authorized');
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Middleware
|
|
100
|
+
|
|
101
|
+
`use()`
|
|
102
|
+
Attach middleware for all HTTP methods:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
app.use((req, res, next) => {
|
|
106
|
+
console.log(req.method, req.url);
|
|
107
|
+
next();
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`all()`
|
|
112
|
+
|
|
113
|
+
Apply handlers to specific paths or methods:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
app.all('/health', (req, res) => {
|
|
117
|
+
res.send('OK');
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Request Object (req)
|
|
122
|
+
|
|
123
|
+
- req.params β route parameters
|
|
124
|
+
- req.query β parsed query string
|
|
125
|
+
- req.body
|
|
126
|
+
- buffer
|
|
127
|
+
- text()
|
|
128
|
+
- json()
|
|
129
|
+
- req.ip β remote IP address
|
|
130
|
+
- req.session
|
|
131
|
+
- host
|
|
132
|
+
- port
|
|
133
|
+
- rhost - remote connected host
|
|
134
|
+
- rport - remote connected port
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
app.post('/data', async (req, res) => {
|
|
140
|
+
const body = req.body.json();
|
|
141
|
+
res.json(body);
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Response Object (res)
|
|
146
|
+
|
|
147
|
+
- res.status(code)
|
|
148
|
+
- res.set(name, value)
|
|
149
|
+
- res.get(name)
|
|
150
|
+
- res.type(mimeOrExtension)
|
|
151
|
+
- res.send(body)
|
|
152
|
+
- res.json(object)
|
|
153
|
+
- res.file(path)
|
|
154
|
+
- res.render(templatePath, data)
|
|
155
|
+
- res.redirect(path)
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
res.status(201).json({ success: true });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Static Files
|
|
164
|
+
|
|
165
|
+
Serve a directory of static files:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
app.static('public');
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Or mount it at a specific path:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
app.static('public', '/assets');
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
HTML files are also served without the .html extension automatically.
|
|
178
|
+
|
|
179
|
+
### Routers
|
|
180
|
+
|
|
181
|
+
#### Sub-Router
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const router = slower.Router();
|
|
185
|
+
|
|
186
|
+
router.get('/users', handler);
|
|
187
|
+
router.post('/users', handler);
|
|
188
|
+
|
|
189
|
+
app.useRouter(router, '/api');
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### Route Groups
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
app.route('/users').get(listUsers).post(createUser);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### HTTPS Support
|
|
199
|
+
|
|
200
|
+
Pass options directly to the underlying server:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
const fs = require('fs');
|
|
204
|
+
|
|
205
|
+
const app = slower({
|
|
206
|
+
https: true,
|
|
207
|
+
key: fs.readFileSync('key.pem'),
|
|
208
|
+
cert: fs.readFileSync('cert.pem'),
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Fallback page
|
|
213
|
+
|
|
214
|
+
If no route matches, it returns a default 404 HTML response:
|
|
215
|
+
|
|
216
|
+
`Cannot {METHOD} /{path}`
|
|
217
|
+
|
|
218
|
+
Custom fallback behavior can be implemented using middleware.
|
|
219
|
+
|
|
220
|
+
## Method documentation
|
|
221
|
+
|
|
222
|
+
### Main Export
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
slower(options?: object) : <SlowerRouter>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Creates a new Slower application instance.
|
|
229
|
+
|
|
230
|
+
- `options` are forwarded to `http.createServer` or `https.createServer`
|
|
231
|
+
- If `options.https === true`, HTTPS is used
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### Class: \<SlowerRouter\>
|
|
236
|
+
|
|
237
|
+
Main application router and HTTP server wrapper.
|
|
238
|
+
|
|
239
|
+
#### Server Control
|
|
240
|
+
|
|
241
|
+
Start the HTTP/HTTPS server:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
<SlowerRouter>.listen(...args) : http.Server
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Stops the server:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
<SlowerRouter>.close(callback?: function) : void
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Routing Methods
|
|
254
|
+
|
|
255
|
+
The following methods share the same signature:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
<SlowerRouter>.get(path, ...handlers) : <SlowerRouter>
|
|
259
|
+
|
|
260
|
+
<SlowerRouter>.post(path, ...handlers) : <SlowerRouter>
|
|
261
|
+
|
|
262
|
+
<SlowerRouter>.put(path, ...handlers) : <SlowerRouter>
|
|
263
|
+
|
|
264
|
+
<SlowerRouter>.delete(path, ...handlers) : <SlowerRouter>
|
|
265
|
+
|
|
266
|
+
<SlowerRouter>.patch(path, ...handlers) : <SlowerRouter>
|
|
267
|
+
|
|
268
|
+
<SlowerRouter>.options(path, ...handlers) : <SlowerRouter>
|
|
269
|
+
|
|
270
|
+
<SlowerRouter>.head(path, ...handlers) : <SlowerRouter>
|
|
271
|
+
|
|
272
|
+
<SlowerRouter>.trace(path, ...handlers) : <SlowerRouter>
|
|
273
|
+
|
|
274
|
+
<SlowerRouter>.connect(path, ...handlers) : <SlowerRouter>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- `path` may be a string, RegExp, or middleware function
|
|
278
|
+
- Handlers are called sequentially with `(req, res, next)`
|
|
279
|
+
|
|
280
|
+
#### Middleware
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
<SlowerRouter>.use(...handlers: function\[\]) : <SlowerRouter>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Registers global middleware for all HTTP methods.
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
<SlowerRouter>.all(pathOrMethod, ...handlers) : <SlowerRouter>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Registers handlers across multiple HTTP methods depending on the first argument.
|
|
293
|
+
|
|
294
|
+
#### Static Files
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
<SlowerRouter>.static(directoryPath: string, mountPath?: string) : <SlowerRouter>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Serves static files from a directory.
|
|
301
|
+
|
|
302
|
+
#### Routers
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
<SlowerRouter>.useRouter(router: <SlowerSubRouter>, mountPath?: string) : <SlowerRouter>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Mounts a sub-router at the specified path.
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
<SlowerRouter>.route(path: string | RegExp) : <SlowerSubRouter>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Creates a route group bound to a single path.
|
|
315
|
+
|
|
316
|
+
### Class: <\SlowerSubRouter\>
|
|
317
|
+
|
|
318
|
+
Router container used for grouping routes before mounting.
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
<SlowerSubRouter>.get(path, ...handlers) : <SlowerSubRouter>
|
|
322
|
+
|
|
323
|
+
<SlowerSubRouter>.post(path, ...handlers) : <SlowerSubRouter>
|
|
324
|
+
|
|
325
|
+
<SlowerSubRouter>.put(path, ...handlers) : <SlowerSubRouter>
|
|
326
|
+
|
|
327
|
+
<SlowerSubRouter>.delete(path, ...handlers) : <SlowerSubRouter>
|
|
328
|
+
|
|
329
|
+
<SlowerSubRouter>.patch(path, ...handlers) : <SlowerSubRouter>
|
|
330
|
+
|
|
331
|
+
<SlowerSubRouter>.use(...handlers) : <SlowerSubRouter>
|
|
332
|
+
|
|
333
|
+
<SlowerSubRouter>.all(pathOrMethod, ...handlers) : <SlowerSubRouter>
|
|
334
|
+
|
|
335
|
+
<SlowerSubRouter>.static(directoryPath, mountPath?) : <SlowerSubRouter>
|
|
336
|
+
|
|
337
|
+
<SlowerSubRouter>.route(path) : <SlowerMicroRouter>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Request Extensions
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
req.get(headerName: string) : string | undefined
|
|
344
|
+
|
|
345
|
+
req.body.buffer : Buffer
|
|
346
|
+
|
|
347
|
+
req.body.text() : string
|
|
348
|
+
|
|
349
|
+
req.body.json() : any
|
|
350
|
+
|
|
351
|
+
req.query : object
|
|
352
|
+
|
|
353
|
+
req.params : object | undefined
|
|
354
|
+
|
|
355
|
+
req.ip : string
|
|
356
|
+
|
|
357
|
+
req.session : object
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Response Extensions
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
res.status(code: number) : res
|
|
364
|
+
|
|
365
|
+
res.set(name: string | object, value?: string) : res
|
|
366
|
+
|
|
367
|
+
res.get(name: string) : string | undefined
|
|
368
|
+
|
|
369
|
+
res.type(type: string) : res
|
|
370
|
+
|
|
371
|
+
res.send(body: string | Buffer) : void
|
|
372
|
+
|
|
373
|
+
res.json(data: any) : void
|
|
374
|
+
|
|
375
|
+
res.file(path: string) : void
|
|
376
|
+
|
|
377
|
+
res.render(templatePath: string, data?: object) : void
|
|
378
|
+
|
|
379
|
+
res.redirect(path: string) : void
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**License:** MIT
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
This is MIT-licensed. Just use it.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const middlewares = require('./third-party-middlewares');
|
|
2
|
+
const Slower = require('../index');
|
|
3
|
+
|
|
4
|
+
const app = Slower();
|
|
5
|
+
|
|
6
|
+
app.use(middlewares.requestTime); // Set timestamp: req.moment
|
|
7
|
+
app.use(middlewares.hashRequest); // Set request ID: req.id
|
|
8
|
+
app.use(middlewares.triageRequest); // Set request validity: req.invalid
|
|
9
|
+
app.use(middlewares.consoleLogger); // Logs to console (according to req.invalid)
|
|
10
|
+
app.use(middlewares.blockMethods); // Blocks request (according to req.invalid)
|
|
11
|
+
app.use(middlewares.nocache); // Prevents cache creation
|
|
12
|
+
|
|
13
|
+
app.get(
|
|
14
|
+
'/rea',
|
|
15
|
+
(req, res, next) => console.log('redirected') || next(),
|
|
16
|
+
(req, res) => res.status(301).redirect('/admin')
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
app.get('/admin', (req, res) =>
|
|
20
|
+
res
|
|
21
|
+
.status(200)
|
|
22
|
+
.send(
|
|
23
|
+
`<h2>${JSON.stringify(req.params)}<br/>${JSON.stringify(req.query)}</h2>`
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
app.get('/admins/:id', (req, res) => {
|
|
28
|
+
res.status(200).send(
|
|
29
|
+
`<h2>${JSON.stringify(req.params)}<br/>${JSON.stringify(req.query)}<br/></h2>`
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
app.get('/headers', (req, res) => res.status(200).json(req.headers));
|
|
34
|
+
|
|
35
|
+
app.get('/docs/:docname', (req, res) =>
|
|
36
|
+
res.file(`./${req.params.docname}.html`)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
app.all((req, res) => {
|
|
40
|
+
res.status(404).send('<h2>Error 404 - Page Not Found<h2/>');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
app.listen(80, '127.0.0.1', () =>
|
|
44
|
+
console.log('Listening on http://127.0.0.1:80/')
|
|
45
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const Slower = require('../index');
|
|
2
|
+
|
|
3
|
+
(async () => {
|
|
4
|
+
const appr = Slower.Router();
|
|
5
|
+
appr.get('/a', (req, res) => res.status(200).end('a'));
|
|
6
|
+
|
|
7
|
+
const appf = Slower.Router();
|
|
8
|
+
appf.get('/c', (req, res) => res.status(200).end('c'));
|
|
9
|
+
appf.get('/x/:id', (req, res) => res.status(200).end('x=' + req.params.id));
|
|
10
|
+
appf.all('/d', (req, res) => res.status(200).end('d'));
|
|
11
|
+
|
|
12
|
+
const app = Slower();
|
|
13
|
+
app.useRouter(appr);
|
|
14
|
+
app.useRouter(appf, '/api');
|
|
15
|
+
app.get('/b', (req, res) => res.status(200).end('b'));
|
|
16
|
+
app.all((req, res) => res.status(404).end());
|
|
17
|
+
app.listen('8000', () => console.log('app started at 8000'));
|
|
18
|
+
})();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* TESTING ENDPOINTS:
|
|
22
|
+
*
|
|
23
|
+
* /a Should print 'a'
|
|
24
|
+
* /b Should print 'b'
|
|
25
|
+
* /api/c Should print 'c'
|
|
26
|
+
* /api/d Should print 'd'
|
|
27
|
+
* /api/x/:id Should print 'x={ID}'
|
|
28
|
+
*
|
|
29
|
+
*/
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server middleware to force the client to NOT save any data as cache.
|
|
3
|
+
* @info This is very useful for development purposes, as browser cache for
|
|
4
|
+
* redirections and scripts is very annoying
|
|
5
|
+
* @param {http.IncomingMessage} req
|
|
6
|
+
* @param {http.ServerResponse} res
|
|
7
|
+
* @param {Function} next
|
|
8
|
+
* @return {undefined}
|
|
9
|
+
*/
|
|
10
|
+
function nocache (req, res, next) {
|
|
11
|
+
res.set('Cache-Control', 'no-cache');
|
|
12
|
+
next();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Add a timestamp to the request
|
|
16
|
+
function requestTime (req, res, next) {
|
|
17
|
+
req.moment = Date.now();
|
|
18
|
+
next();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Add a hash to the request
|
|
22
|
+
function murmurHash (key, seed = 0) {
|
|
23
|
+
const C = { M:0x5bd1e995, R:0x18 };
|
|
24
|
+
const im32M = (a) => {
|
|
25
|
+
const c = { o: v => v & 0xffff, i: v => (v >>> 16) & 0xffff };
|
|
26
|
+
return c.o(a)*59797 + (((c.i(a)*59797 + c.o(a)*23505) << 16) >>> 0);
|
|
27
|
+
};
|
|
28
|
+
let len = key.length, h = seed ^ len, i = 0, k = 0;
|
|
29
|
+
while (len >= 4) {
|
|
30
|
+
k = key[i++] | (key[i++] << 8) | (key[i++] << 16) | (key[i++] << 24);
|
|
31
|
+
k = im32M(k);
|
|
32
|
+
k ^= k >>> C.R;
|
|
33
|
+
k = im32M(k);
|
|
34
|
+
h = im32M(h) ^ k;
|
|
35
|
+
len -= 4;
|
|
36
|
+
};
|
|
37
|
+
switch (len) {
|
|
38
|
+
case 3: h ^= key[i + 2] << 16;
|
|
39
|
+
case 2: h ^= key[i + 1] << 8;
|
|
40
|
+
case 1: h ^= key[i];
|
|
41
|
+
h = im32M(h);
|
|
42
|
+
};
|
|
43
|
+
h ^= h >>> 13;
|
|
44
|
+
h = im32M(h);
|
|
45
|
+
h ^= h >>> 15;
|
|
46
|
+
return h >>> 0;
|
|
47
|
+
}
|
|
48
|
+
function hashRequest (req, res, next) {
|
|
49
|
+
req.hash = (murmurHash(Date.now().toString() + JSON.stringify(req.session) + req.url)).toString().padStart(10, '0');
|
|
50
|
+
next();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Set an 'invalid' property if the request is of a proper method
|
|
54
|
+
// This is used in the 'blockMethods' and 'consoleLogger' middlewares
|
|
55
|
+
function triageRequest (req, res, next) {
|
|
56
|
+
const allowedMethods = ['GET','POST','PUT','DELETE'];
|
|
57
|
+
if (!allowedMethods.includes(req.method)) req.invalid = true;
|
|
58
|
+
next();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Logs request to console
|
|
62
|
+
const padStringEnd = (str, maxLen) => str.padEnd(maxLen, ' ');
|
|
63
|
+
const colorizeMethod = (req) => (req.invalid ? '\x1b[31m' : '\x1b[32m') + req.method + '\x1b[0m';
|
|
64
|
+
function consoleLogger (req, res, next) {
|
|
65
|
+
console.log(
|
|
66
|
+
' ' +
|
|
67
|
+
`Hash:${req.hash} ` +
|
|
68
|
+
`Time:${new Date(req.moment).toISOString()} ` +
|
|
69
|
+
`${padStringEnd(req.session.rhost + ':' + req.session.rport + '->' + req.session.host + ':' + req.session.port, 32)} `+
|
|
70
|
+
`${padStringEnd(colorizeMethod(req), 20)} ${req.url}`
|
|
71
|
+
);
|
|
72
|
+
next();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Blocks all requests that are not GET, POST, PUT, or DELETE ones
|
|
76
|
+
function blockMethods (req, res, next) {
|
|
77
|
+
if (req.invalid) res.status(405).end();
|
|
78
|
+
else next();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
nocache,
|
|
83
|
+
requestTime,
|
|
84
|
+
hashRequest,
|
|
85
|
+
consoleLogger,
|
|
86
|
+
blockMethods,
|
|
87
|
+
triageRequest
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slower",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"main": "index.js",
|
|
5
|
-
"devDependencies": {},
|
|
6
5
|
"scripts": {
|
|
7
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
7
|
},
|
|
9
|
-
"dependencies": {
|
|
10
|
-
"path-to-regexp": "^6.2.2"
|
|
11
|
-
},
|
|
12
8
|
"keywords": [],
|
|
13
9
|
"author": "TomΓ‘s Luchesi <no.mad.devtech@gmail.com>",
|
|
14
10
|
"license": "MIT",
|
package/src/slower.js
CHANGED
|
@@ -5,8 +5,6 @@ const fs = require('node:fs');
|
|
|
5
5
|
const { createReadStream } = require('node:fs');
|
|
6
6
|
const { pipeline } = require('node:stream/promises');
|
|
7
7
|
|
|
8
|
-
const { match } = require('path-to-regexp');
|
|
9
|
-
|
|
10
8
|
const { setupRequest, setupResponse } = require('./decorators');
|
|
11
9
|
const utils = require('./utils');
|
|
12
10
|
|
|
@@ -119,7 +117,7 @@ class SlowerRouter {
|
|
|
119
117
|
);
|
|
120
118
|
if (!this.layers.get(method)) this.layers.set(method, new Map());
|
|
121
119
|
if (typeof path === 'string' || path?.constructor?.name !== 'RegExp')
|
|
122
|
-
path =
|
|
120
|
+
path = utils.buildURLPatternFunction(path); // 'path' is a function now
|
|
123
121
|
this.layers.get(method).set(path, handler);
|
|
124
122
|
return this;
|
|
125
123
|
}
|
package/src/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { statSync, readdirSync } = require('node:fs');
|
|
2
2
|
const { join } = require('node:path');
|
|
3
3
|
const { parse } = require('node:querystring');
|
|
4
|
+
const { URLPattern } = require('node:url');
|
|
4
5
|
|
|
5
6
|
function* getFiles(folder) {
|
|
6
7
|
const files = readdirSync(folder);
|
|
@@ -14,6 +15,20 @@ function* getFiles(folder) {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// This is added to all URLPattern objects, to avoid having it complaining about the
|
|
19
|
+
// absolute URL. As this IP is invalid, it is virtually impossible to receive a request with
|
|
20
|
+
// that origin URL.
|
|
21
|
+
const URL_PATTERN_PREFIX = 'https://0';
|
|
22
|
+
// Wrap the native URLPattern function and add the custom local prefix
|
|
23
|
+
const buildURLPatternFunction = template_url => {
|
|
24
|
+
const matcher = new URLPattern(template_url, URL_PATTERN_PREFIX);
|
|
25
|
+
return function (url) {
|
|
26
|
+
return matcher.test(URL_PATTERN_PREFIX + url) ?
|
|
27
|
+
matcher.exec(URL_PATTERN_PREFIX + url)
|
|
28
|
+
: null;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
17
32
|
const getMatchingRoute = (url, method, layers) => {
|
|
18
33
|
method = method.toLowerCase();
|
|
19
34
|
let list = [];
|
|
@@ -21,13 +36,14 @@ const getMatchingRoute = (url, method, layers) => {
|
|
|
21
36
|
let routes = layers.get(method) || new Map();
|
|
22
37
|
// Iterate through all routes, and get the one that match
|
|
23
38
|
for (let [pathFn, callback] of routes) {
|
|
24
|
-
let
|
|
39
|
+
let parsed = pathFn(getURLPathBody(url));
|
|
25
40
|
// Return the matching route
|
|
26
|
-
if (
|
|
41
|
+
if (parsed) {
|
|
27
42
|
// add to list
|
|
28
|
-
let built = { callback };
|
|
29
|
-
if
|
|
30
|
-
|
|
43
|
+
let built = { callback, params: {} };
|
|
44
|
+
// Only if the first key in the group is not undefined and not '0'
|
|
45
|
+
if (parsed?.pathname?.groups && !parsed?.pathname?.groups?.['0'])
|
|
46
|
+
built['params'] = parsed.pathname.groups;
|
|
31
47
|
list.push(built);
|
|
32
48
|
}
|
|
33
49
|
}
|
|
@@ -56,7 +72,9 @@ const noLayersFoundFallback = (req, res) =>
|
|
|
56
72
|
req.url
|
|
57
73
|
}</pre></body></html>`
|
|
58
74
|
);
|
|
75
|
+
|
|
59
76
|
module.exports = {
|
|
77
|
+
buildURLPatternFunction,
|
|
60
78
|
getMatchingRoute,
|
|
61
79
|
getFiles,
|
|
62
80
|
normalizeAddress,
|
package/readme.md.old
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
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
|
-
```
|