UNPKG

16.9 kBMarkdownView Raw
1# **RE**serve
2
3<table border="0" cellpadding="2" cellspacing="0">
4 <tr>
5 <td valign="top">
6 <strong>RE</strong>
7 </td>
8 <td>
9 <i>duced</i></br />
10 <i>levant</i></br />
11 <i>verse proxy</i><br />
12 <i>gexp-based</i><br />
13 <i>useable</i><br />
14 <strong>serve</strong>
15 </td>
16 </tr>
17</table>
18
19[![Travis-CI](https://travis-ci.org/ArnaudBuchholz/reserve.svg?branch=master)](https://travis-ci.org/ArnaudBuchholz/reserve#)
20[![Coverage Status](https://coveralls.io/repos/github/ArnaudBuchholz/reserve/badge.svg?branch=master)](https://coveralls.io/github/ArnaudBuchholz/reserve?branch=master)
21[![Maintainability](https://api.codeclimate.com/v1/badges/49e3adbc8f31ae2febf3/maintainability)](https://codeclimate.com/github/ArnaudBuchholz/reserve/maintainability)
22[![Package Quality](https://npm.packagequality.com/shield/reserve.svg)](https://packagequality.com/#?package=reserve)
23[![Known Vulnerabilities](https://snyk.io/test/github/ArnaudBuchholz/reserve/badge.svg?targetFile=package.json)](https://snyk.io/test/github/ArnaudBuchholz/reserve?targetFile=package.json)
24[![dependencies Status](https://david-dm.org/ArnaudBuchholz/reserve/status.svg)](https://david-dm.org/ArnaudBuchholz/reserve)
25[![devDependencies Status](https://david-dm.org/ArnaudBuchholz/reserve/dev-status.svg)](https://david-dm.org/ArnaudBuchholz/reserve?type=dev)
26[![reserve](https://badge.fury.io/js/reserve.svg)](https://www.npmjs.org/package/reserve)
27[![reserve](http://img.shields.io/npm/dm/reserve.svg)](https://www.npmjs.org/package/reserve)
28[![install size](https://packagephobia.now.sh/badge?p=reserve)](https://packagephobia.now.sh/result?p=reserve)
29[![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArnaudBuchholz%2Freserve.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Freserve?ref=badge_shield)
31
32A **lightweight** web server statically **configurable** with regular expressions.
33It can also be **embedded** and **extended**.
34
35# Rational
36
37Initially started to build a local **development environment** where static files are served and resources can be fetched from remote repositories, this **tool** is **versatile** and can support different scenarios :
38- A simple web server
39- A reverse proxy to an existing server
40- A server that aggregates several sources
41- ...
42
43By defining **an array of mappings**, one can decide how the server will process the requests. Each mapping associates a **matching** criteria defined with a
44[regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) to a **handler** that will answer the request.
45
46The configuration syntax favors **simplicity** without dropping flexibility.
47
48For instance, the definition of a server that **exposes files** of the current directory but **forbids access** to the directory `private` consists in :
49
50```json
51{
52 "port": 8080,
53 "mappings": [{
54 "match": "^/private/.*",
55 "status": 403
56 }, {
57 "match": "^/(.*)",
58 "file": "./$1"
59 }]
60}
61```
62
63# What's New ?
64
65|Version|content|
66|---|---|
67|1.0.0|Initial version|
68|1.0.5|`watch` option in **custom** handler|
69|1.1.1|[`require('reserve/mock')`](#mocking)|
70||[`colors`](https://www.npmjs.com/package/colors) and [`mime`](https://www.npmjs.com/package/mime) are no more dependencies|
71|1.1.2|Performance testing, `--silent`|
72||`case-sensitive` option in **file** handler|
73|1.1.3|Default hostname changed to `undefined`|
74|1.1.4|Enables external handlers in `json` configuration through [require](https://nodejs.org/api/modules.html#modules_require_id)|
75|1.1.5|Fixes relative path use of external handlers in `json` configuration|
76|1.1.6|Improves response mocking (`flushHeaders()` & `headersSent`)|
77|1.1.7|Compatibility with Node.js >= 12.9|
78||Improves response mocking|
79
80# Usage
81
82* Install the package with `npm install reserve` *(you decide if you want to save it as development dependency or not)*
83* You may create a start script in `package.json` :
84
85```json
86{
87 "scripts": {
88 "start": "reserve"
89 }
90}
91```
92
93* By default, it will look for a file named `reserve.json` in the current working directory
94* A configuration file name can be specified using `--config <file name>`, for instance :
95
96```json
97{
98 "scripts": {
99 "start": "reserve",
100 "start-dev": "reserve --config reserve-dev.json"
101 }
102}
103```
104
105# Embedding
106
107It is possible to implement the server in any application using the `reserve/serve` module :
108
109```javascript
110const path = require('path')
111const reserve = require('reserve/serve')
112reserve({
113 port: 8080,
114 mappings: [{
115 match: /^\/(.*)/,
116 file: path.join(__dirname, '$1')
117 }]
118})
119 .on('ready', ({ url }) => {
120 console.log(`Server running at ${url}`)
121 })
122```
123
124The resulting object implements the [EventEmitter](https://nodejs.org/api/events.html) class and throw the following events with parameters :
125
126| Event | Parameter (object containing members) | Description |
127|---|---|---|
128| **ready** | `url` *(String, example : `'http://0.0.0.0:8080/'`)*| The server is listening and ready to receive requests, hostname is replaced with `0.0.0.0` when **unspecified**.
129| **incoming** | `method` *(String, example : `'GET'`)*, `url` *(String)*, `start` *(Date)* | New request received, these parameters are also transmitted to **error**, **redirecting** and **redirected** events |
130| **error** | `reason` *(Any)* | Error reason, contains **incoming** parameters if related to a request |
131| **redirecting** | `type` *(Handler type, example : `'status'`)*, `redirect` *(String or Number, example : `404`)* | Processing redirection to handler, gives handler type and redirection value. <br />*For instance, when a request will be served by the [file handler](#file), this event is generated once. But if the requested resource does not exist, the request will be redirected to the [status](#status) 404 triggering again this event.* |
132| **redirected** | `end` *(Date)*, `timeSpent` *(Number of ms)*, `statusCode` *(Number)* | Request is fully processed. `timeSpent` is evaluated by comparing `start` and `end` (i.e. not using high resolution timers) and provided for information only. |
133
134The package also gives access to the configuration reader :
135
136```javascript
137const path = require('path')
138const { read } = require('reserve/configuration')
139const reserve = require('reserve/serve')
140read('reserve.json')
141 .then(configuration => {
142 reserve(configuration)
143 .on('ready', ({ url }) => {
144 console.log(`Server running at ${url}`)
145 })
146 })
147```
148
149And a default log output *(verbose mode will dump all redirections)* :
150
151```javascript
152const path = require('path')
153const { read } = require('reserve/configuration')
154const log = require('reserve/log')
155const reserve = require('reserve/serve')
156read('reserve.json')
157 .then(configuration => {
158 log(reserve(configuration), /*verbose: */ true)
159 })
160```
161
162NOTE: log is using [`colors`](https://www.npmjs.com/package/colors) **if installed**.
163
164# Configuration
165
166## hostname *(optional)*
167
168Used to set the `host` parameter when calling http(s) server's [listen](https://nodejs.org/api/net.html#net_server_listen).
169
170Default is `undefined`.
171
172## port *(optional)*
173
174Used to set the `port` parameter when calling http(s) server's [listen](https://nodejs.org/api/net.html#net_server_listen).
175
176Default is `5000`.
177
178## ssl *(optional)*
179
180This object provides certificate information to build an https server. You might be interested by the article [An Express HTTPS server with a self-signed certificate](https://flaviocopes.com/express-https-self-signed-certificate/).
181
182The object must contain :
183* `cert` : a relative or absolute path to the certificate file
184* `key` : a relative or absolute path to the key file
185
186If relative, the configuration file directory or the current working directory (when embedding) is considered.
187
188## handlers
189
190An object associating an handler prefix to an handler object.
191If the property value is a string, the handler is obtained using [require](https://nodejs.org/api/modules.html#modules_require_id).
192
193The handler object is defined by:
194* **redirect** an asynchronous function that will be called with an object containing:
195 - **mapping** the mapping being executed
196 - **match** the regular expression [exec result](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec)
197 - **redirect** the value associated with the handler prefix in the mapping. Capturing groups are substituted.
198 - **request** [http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)
199 - **response** [http.ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse)
200
201**NOTE** : it is not possible to change the associations of the default prefixes (`custom`, `file`, `status`, `url` ...).
202
203## mappings
204
205An array of mappings that is evaluated in the order of declaration.
206* Several mappings may apply to the same request
207* Evaluation stops when the request is **finalized** *(see the note below)*
208* When a handler triggers a redirection, the array of mappings is reevaluated
209
210**NOTE** : REserve hooks the [`response.end`](https://nodejs.org/api/http.html#http_response_end_data_encoding_callback) API to detect when the response is finalized.
211
212Each mapping must contain :
213* `match` : a string (converted to a regular expression) or a regular expression that will be applied to the request URL
214* the handler key (`custom`, `file`, `status`, `url` ...) which value may contain capturing groups *(see [handlers](#handlers))*
215* `cwd` *(optional)* : the current working directory to consider for relative path, defaulted to the configuration file directory or the current working directory (when embedding)
216
217**NOTE** : when using `custom` in a [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) file, since functions can't be used in this format, the expected value is a string referencing the relative or absolute module to load. If relative, the `cwd` member is considered.
218
219For instance :
220
221* `reserve.json` :
222
223```json
224{
225 "port": 8080,
226 "mappings": [{
227 "match": ".*",
228 "custom": "./cors"
229 }, {
230 "match": "^/(.*)",
231 "file": "./$1"
232 }]
233}
234```
235
236* `cors.js` :
237
238```javascript
239module.exports = async (request, response) => response.setHeader('Access-Control-Allow-Origin', '*')
240```
241
242
243## extend
244
245*Only for JSON configuration*
246
247A relative or absolute path to another configuration file to extend.
248If relative, the current configuration file directory is considered.
249
250The current settings overwrite the ones coming from the extended configuration file.
251
252Extended `mappings` are imported at the end of the resulting array, making the current ones being evaluated first. This way, it is possible to override the extended mappings.
253
254# Handlers
255
256## file
257
258Answers the request using **file system**.
259
260Example :
261```json
262{
263 "match": "^/(.*)",
264 "file": "./$1"
265}
266```
267
268* Only supports [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET)
269* Capturing groups can be used as substitution parameters
270* Absolute or relative to the handler's `cwd` member *(see [mappings](#mappings))*
271* Directory access is internally redirected to the inner `index.html` file *(if any)* or `404` status
272* File access returns `404` status if missing or can't be read
273* Mime type computation is based on [`mime`](https://www.npmjs.com/package/mime) **if installed**. Otherwise a limited subset of mime types is used:
274
275|Extension|mime type|
276|---|---|
277|bin|application/octet-stream|
278|css|text/css|
279|gif|image/gif|
280|html|text/html|
281|htm|text/html|
282|jpeg|image/jpeg|
283|jpg|image/jpeg|
284|js|application/javascript|
285|pdf|application/pdf|
286|png|image/png|
287|svg|image/svg+xml|
288|text|text/plain|
289|txt|text/plain|
290|xml|application/xml|
291
292| option | type | default | description |
293|---|---|---|---|
294| `case-sensitive` | Boolean | `false` | *(for Windows)* when `true`, the file path is tested case sensitively. Since it has an impact on **performances**, use carefully. |
295
296## url
297
298Answers the request by **forwarding** it to a different URL.
299
300Example :
301```json
302{
303 "match": "^/proxy/(https?)/(.*)",
304 "url": "$1://$2",
305 "unsecure-cookies": true
306}
307```
308
309* Supports [all HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
310* Capturing groups can be used as substitution parameters
311* Redirects to any URL (http or https)
312
313**NOTE** : It must redirect to an absolute URL
314
315| option | type | default | description |
316|---|---|---|---|
317| `unsecure-cookies` | Boolean | `false` | when `true`, the secured cookies are converted to unsecure ones. Hence, the browser will keep them even if not running on https |
318
319## custom
320
321Enables 'simple' **custom** handlers.
322
323Examples :
324```javascript
325{
326 match: /.*/,
327 custom: async (request, response) => response.setHeader('Access-Control-Allow-Origin', '*')
328}
329```
330
331Or using an external module :
332
333```javascript
334{
335 match: /.*/,
336 custom: './cors.js'
337}
338```
339
340with `cors.js` :
341```javascript
342module.exports = async (request, response) => response.setHeader('Access-Control-Allow-Origin', '*')
343```
344
345External modules are loaded with Node.js [require](https://nodejs.org/api/modules.html#modules_require_id) API.
346
347`custom` must point to a [function](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Functions)
348* That takes at least two parameters : [`request`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) and [`response`](https://nodejs.org/api/http.html#http_class_http_serverresponse)
349* Capturing groups' values are passed as additional parameters.
350* This function must return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
351* If the promise is resolved to a value (i.e. not `undefined`), an internal redirection occurs i.e. the request is going over the mappings again (*infinite loops are not prevented*).
352* If the `response` is not **finalized** after executing the function *(i.e. [`response.end`](https://nodejs.org/api/http.html#http_response_end_data_encoding_callback) was not called)*, the `request` is going over the remaining mappings
353
354| option | type | default | description |
355|---|---|---|---|
356| `watch` | Boolean | `false` | when `true` and using a local module *(does not work with `node_modules`)* the file's modified time is checked before executing the handler. When changed, the module is reloaded |
357
358## status
359
360**Ends** the request with a given status.
361
362Example :
363```json
364{
365 "match": "^/private/.*",
366 "status": 403
367}
368```
369
370* Supports [all HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
371* Accepts only Numbers
372* Used when an internal redirection to a Number occurs
373* Capturing groups are ignored
374* End the response with the given status and, if defined, with a textual message :
375
376| status | message |
377|---|---|
378| 403 | Forbidden |
379| 404 | Not found |
380| 405 | Method Not Allowed |
381| 500 | Internal Server Error |
382
383# Mocking
384
385Since version 1.1.0, the package includes the helper `reserve/mock` to build tests. This method receives a configuration (like `reserve/serve`) and returns a promise resolving to an [EventEmitter](https://nodejs.org/api/events.html) augmented with a `request` method :
386
387```javascript
388function request (method, url, headers = {}, body = '') {
389 return Promise.resolve(mockedResponse)
390}
391```
392
393Call the `request` method to simulate an incoming request, it returns a promise resolving to a mocked response exposing the following members :
394
395| Member | Type | Description |
396|---|---|---|
397| **headers** | Object | Response headers
398| **statusCode** | Number | Status code
399| **finished** | Boolean | `true`
400| **toString()** | String | Gives the response body
401
402Example :
403
404```javascript
405require('reserve/mock')({
406 port: 8080,
407 mappings: [{
408 match: /^\/(.*)/,
409 file: path.join(__dirname, '$1')
410 }]
411})
412 .then(mocked => mocked.request('GET', '/'))
413 .then(response => {
414 assert(response.statusCode === 200)
415 assert(response.toString() === '<html />')
416 })
417```
418
419You may provide mocked handlers *(based on their [actual implementation](https://github.com/ArnaudBuchholz/reserve/tree/master/handlers))*:
420
421```javascript
422require('reserve/mock')({
423 port: 8080,
424 mappings: [{
425 match: /^\/(.*)/,
426 file: path.join(__dirname, '$1')
427 }]
428}, {
429 file: {
430 redirect: async ({ request, mapping, redirect, response }) => {
431 if (redirect === '/') {
432 response.writeHead(201, {
433 'Content-Type': 'text/plain',
434 'Content-Length': 6
435 })
436 response.end('MOCKED')
437 } else {
438 return 500
439 }
440 }
441 }
442})
443 .then(mocked => mocked.request('GET', '/'))
444 .then(response => {
445 assert(response.statusCode === 201)
446 assert(response.toString() === 'MOCKED')
447 })
448```