Lloyd Brookes
9 years ago
13 changed files with 817 additions and 1114 deletions
-
446README.md
-
9doc/blacklist.md
-
59doc/https.md
-
69doc/logging.md
-
12doc/mime-types.md
-
241doc/mock-response.md
-
37doc/rewrite.md
-
12doc/spa.md
-
28doc/stored-config.md
-
416jsdoc2md/README.hbs
-
291lib/default-stack.js
-
17lib/local-web-server.js
-
294lib/middleware-stack.js
@ -0,0 +1,9 @@ |
|||||
|
## Access Control |
||||
|
|
||||
|
By default, access to all files is allowed (including dot files). Use `--forbid` to establish a blacklist: |
||||
|
```sh |
||||
|
$ ws --forbid '*.json' '*.yml' |
||||
|
serving at http://localhost:8000 |
||||
|
``` |
||||
|
|
||||
|
[Example](https://github.com/75lb/local-web-server/tree/master/example/forbid). |
@ -0,0 +1,59 @@ |
|||||
|
## HTTPS Server |
||||
|
|
||||
|
Some modern techs (ServiceWorker, any `MediaDevices.getUserMedia()` request etc.) *must* be served from a secure origin (HTTPS). To launch an HTTPS server, supply a `--key` and `--cert` to local-web-server, for example: |
||||
|
|
||||
|
``` |
||||
|
$ ws --key localhost.key --cert localhost.crt |
||||
|
``` |
||||
|
|
||||
|
If you don't have a key and certificate it's trivial to create them. You do not need third-party verification (Verisign etc.) for development purposes. To get the green padlock in the browser, the certificate.. |
||||
|
|
||||
|
* must have a `Common Name` value matching the FQDN of the server |
||||
|
* must be verified by a Certificate Authority (but we can overrule this - see below) |
||||
|
|
||||
|
First create a certificate: |
||||
|
|
||||
|
1. Install openssl. |
||||
|
|
||||
|
`$ brew install openssl` |
||||
|
|
||||
|
2. Generate a RSA private key. |
||||
|
|
||||
|
`$ openssl genrsa -des3 -passout pass:x -out ws.pass.key 2048` |
||||
|
|
||||
|
3. Create RSA key. |
||||
|
|
||||
|
``` |
||||
|
$ openssl rsa -passin pass:x -in ws.pass.key -out ws.key |
||||
|
``` |
||||
|
|
||||
|
4. Create certificate request. The command below will ask a series of questions about the certificate owner. The most imporant answer to give is for `Common Name`, you can accept the default values for the others. **Important**: you **must** input your server's correct FQDN (`dev-server.local`, `laptop.home` etc.) into the `Common Name` field. The cert is only valid for the domain specified here. You can find out your computers host name by running the command `hostname`. For example, mine is `mba3.home`. |
||||
|
|
||||
|
`$ openssl req -new -key ws.key -out ws.csr` |
||||
|
|
||||
|
5. Generate self-signed certificate. |
||||
|
|
||||
|
`$ openssl x509 -req -days 365 -in ws.csr -signkey ws.key -out ws.crt` |
||||
|
|
||||
|
6. Clean up files we're finished with |
||||
|
|
||||
|
`$ rm ws.pass.key ws.csr` |
||||
|
|
||||
|
7. Launch HTTPS server. In iTerm, control-click the first URL (with the hostname matching `Common Name`) to launch your browser. |
||||
|
|
||||
|
``` |
||||
|
$ ws --key ws.key --cert ws.crt |
||||
|
serving at https://mba3.home:8010, https://127.0.0.1:8010, https://192.168.1.203:8010 |
||||
|
``` |
||||
|
|
||||
|
Chrome and Firefox will still complain your certificate has not been verified by a Certificate Authority. Firefox will offer you an `Add an exception` option, allowing you to ignore the warning and manually mark the certificate as trusted. In Chrome on Mac, you can manually trust the certificate another way: |
||||
|
|
||||
|
1. Open Keychain |
||||
|
2. Click File -> Import. Select the `.crt` file you created. |
||||
|
3. In the `Certificates` category, double-click the cert you imported. |
||||
|
4. In the `trust` section, underneath `when using this certificate`, select `Always Trust`. |
||||
|
|
||||
|
Now you have a valid, trusted certificate for development. |
||||
|
|
||||
|
### Built-in certificate |
||||
|
As a quick win, you can run `ws` with the `https` flag. This will launch an HTTPS server using a [built-in certificate](https://github.com/75lb/local-web-server/tree/master/ssl) registered to the domain 127.0.0.1. |
@ -0,0 +1,69 @@ |
|||||
|
# Logging |
||||
|
By default, local-web-server outputs a simple, dynamic statistics view. To see traditional web server logs, use `--log-format`: |
||||
|
|
||||
|
```sh |
||||
|
$ ws --log-format combined |
||||
|
serving at http://localhost:8000 |
||||
|
::1 - - [16/Nov/2015:11:16:52 +0000] "GET / HTTP/1.1" 200 12290 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2562.0 Safari/537.36" |
||||
|
``` |
||||
|
|
||||
|
The format value supplied is passed directly to [morgan](https://github.com/expressjs/morgan). The exception is `--log-format none` which disables all output. |
||||
|
|
||||
|
# Visualisation |
||||
|
|
||||
|
## Goaccess |
||||
|
To get live statistics in [goaccess](http://goaccess.io/), first create this config file at `~/.goaccessrc`: |
||||
|
|
||||
|
``` |
||||
|
time-format %T |
||||
|
date-format %d/%b/%Y |
||||
|
log-format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" |
||||
|
``` |
||||
|
|
||||
|
Then, start the server, outputting `combined` format logs to disk: |
||||
|
|
||||
|
```sh |
||||
|
$ ws -f combined > web.log |
||||
|
``` |
||||
|
|
||||
|
In a separate tab, point goaccess at `web.log` and it will display statistics in real time: |
||||
|
|
||||
|
``` |
||||
|
$ goaccess -p ~/.goaccessrc -f web.log |
||||
|
``` |
||||
|
|
||||
|
## Logstalgia |
||||
|
local-web-server is compatible with [logstalgia](http://code.google.com/p/logstalgia/). |
||||
|
|
||||
|
### Install Logstalgia |
||||
|
On MacOSX, install with [homebrew](http://brew.sh): |
||||
|
```sh |
||||
|
$ brew install logstalgia |
||||
|
``` |
||||
|
|
||||
|
Alternatively, [download a release for your system from github](https://github.com/acaudwell/Logstalgia/releases/latest). |
||||
|
|
||||
|
Then pipe the `logstalgia` output format directly into logstalgia for real-time visualisation: |
||||
|
```sh |
||||
|
$ ws -f logstalgia | logstalgia - |
||||
|
``` |
||||
|
|
||||
|
![local-web-server with logstalgia](https://raw.githubusercontent.com/75lb/local-web-server/master/doc/img/logstagia.gif) |
||||
|
|
||||
|
## glTail |
||||
|
To use with [glTail](http://www.fudgie.org), write your log to disk using the "default" format: |
||||
|
```sh |
||||
|
$ ws -f default > web.log |
||||
|
``` |
||||
|
|
||||
|
Then specify this file in your glTail config: |
||||
|
|
||||
|
```yaml |
||||
|
servers: |
||||
|
dev: |
||||
|
host: localhost |
||||
|
source: local |
||||
|
files: /Users/Lloyd/Documents/MySite/web.log |
||||
|
parser: apache |
||||
|
color: 0.2, 0.2, 1.0, 1.0 |
||||
|
``` |
@ -0,0 +1,12 @@ |
|||||
|
## mime-types |
||||
|
You can set additional mime-type/extension mappings, or override the defaults by setting a `mime` value in the stored config. This value is passed directly to [mime.define()](https://github.com/broofa/node-mime#mimedefine). Example: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"mime": { |
||||
|
"text/plain": [ "php", "pl" ] |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
[Example](https://github.com/75lb/local-web-server/tree/master/example/mime-override). |
@ -0,0 +1,241 @@ |
|||||
|
## Mock Responses |
||||
|
|
||||
|
Mocks give you full control over the response headers and body returned to the client. They can be used to return anything from a simple html string to a resourceful REST API. Typically, they're used to mock services but can be used for anything. |
||||
|
|
||||
|
In the config, define an array called `mocks`. Each mock definition maps a <code>[route](http://expressjs.com/guide/routing.html#route-paths)</code> to a `response`. A simple home page: |
||||
|
```json |
||||
|
{ |
||||
|
"mocks": [ |
||||
|
{ |
||||
|
"route": "/", |
||||
|
"response": { |
||||
|
"body": "<h1>Welcome to the Mock Responses example</h1>" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Under the hood, the property values from the `response` object are written onto the underlying [koa response object](https://github.com/koajs/koa/blob/master/docs/api/response.md). You can set any valid koa response properies, for example [type](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsetype-1): |
||||
|
```json |
||||
|
{ |
||||
|
"mocks": [ |
||||
|
{ |
||||
|
"route": "/", |
||||
|
"response": { |
||||
|
"type": "text/plain", |
||||
|
"body": "<h1>Welcome to the Mock Responses example</h1>" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Conditional Response |
||||
|
|
||||
|
To define a conditional response, set a `request` object on the mock definition. The `request` value acts as a query - the response defined will only be returned if each property of the `request` query matches. For example, return an XML response *only* if the request headers include `accept: application/xml`, else return 404 Not Found. |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"mocks": [ |
||||
|
{ |
||||
|
"route": "/two", |
||||
|
"request": { "accepts": "xml" }, |
||||
|
"response": { |
||||
|
"body": "<result id='2' name='whatever' />" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Multiple Potential Responses |
||||
|
|
||||
|
To specify multiple potential responses, set an array of mock definitions to the `responses` property. The first response with a matching request query will be sent. In this example, the client will get one of two responses depending on the request method: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"mocks": [ |
||||
|
{ |
||||
|
"route": "/three", |
||||
|
"responses": [ |
||||
|
{ |
||||
|
"request": { "method": "GET" }, |
||||
|
"response": { |
||||
|
"body": "<h1>Mock response for 'GET' request on /three</h1>" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"request": { "method": "POST" }, |
||||
|
"response": { |
||||
|
"status": 400, |
||||
|
"body": { "message": "That method is not allowed." } |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Dynamic Response |
||||
|
|
||||
|
The examples above all returned static data. To define a dynamic response, create a mock module. Specify its path in the `module` property: |
||||
|
```json |
||||
|
{ |
||||
|
"mocks": [ |
||||
|
{ |
||||
|
"route": "/four", |
||||
|
"module": "/mocks/stream-self.js" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Here's what the `stream-self` module looks like. The module should export a mock definition (an object, or array of objects, each with a `response` and optional `request`). In this example, the module simply streams itself to the response but you could set `body` to *any* [valid value](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsebody-1). |
||||
|
```js |
||||
|
const fs = require('fs') |
||||
|
|
||||
|
module.exports = { |
||||
|
response: { |
||||
|
body: fs.createReadStream(__filename) |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Response function |
||||
|
|
||||
|
For more power, define the response as a function. It will receive the [koa context](https://github.com/koajs/koa/blob/master/docs/api/context.md) as its first argument. Now you have full programmatic control over the response returned. |
||||
|
```js |
||||
|
module.exports = { |
||||
|
response: function (ctx) { |
||||
|
ctx.body = '<h1>I can do anything i want.</h1>' |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
If the route contains tokens, their values are passed to the response. For example, with this mock... |
||||
|
```json |
||||
|
{ |
||||
|
"mocks": [ |
||||
|
{ |
||||
|
"route": "/players/:id", |
||||
|
"module": "/mocks/players.js" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
...the `id` value is passed to the `response` function. For example, a path of `/players/10?name=Lionel` would pass `10` to the response function. Additional, the value `Lionel` would be available on `ctx.query.name`: |
||||
|
```js |
||||
|
module.exports = { |
||||
|
response: function (ctx, id) { |
||||
|
ctx.body = `<h1>id: ${id}, name: ${ctx.query.name}</h1>` |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### RESTful Resource example |
||||
|
|
||||
|
Here's an example of a REST collection (users). We'll create two routes, one for actions on the resource collection, one for individual resource actions. |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"mocks": [ |
||||
|
{ "route": "/users", "module": "/mocks/users.js" }, |
||||
|
{ "route": "/users/:id", "module": "/mocks/user.js" } |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Define a module (`users.json`) defining seed data: |
||||
|
|
||||
|
```json |
||||
|
[ |
||||
|
{ "id": 1, "name": "Lloyd", "age": 40, "nationality": "English" }, |
||||
|
{ "id": 2, "name": "Mona", "age": 34, "nationality": "Palestinian" }, |
||||
|
{ "id": 3, "name": "Francesco", "age": 24, "nationality": "Italian" } |
||||
|
] |
||||
|
``` |
||||
|
|
||||
|
The collection module: |
||||
|
|
||||
|
```js |
||||
|
const users = require('./users.json') |
||||
|
|
||||
|
/* responses for /users */ |
||||
|
const mockResponses = [ |
||||
|
/* Respond with 400 Bad Request for PUT and DELETE - inappropriate on a collection */ |
||||
|
{ request: { method: 'PUT' }, response: { status: 400 } }, |
||||
|
{ request: { method: 'DELETE' }, response: { status: 400 } }, |
||||
|
{ |
||||
|
/* for GET requests return a subset of data, optionally filtered on 'minAge' and 'nationality' */ |
||||
|
request: { method: 'GET' }, |
||||
|
response: function (ctx) { |
||||
|
ctx.body = users.filter(user => { |
||||
|
const meetsMinAge = (user.age || 1000) >= (Number(ctx.query.minAge) || 0) |
||||
|
const requiredNationality = user.nationality === (ctx.query.nationality || user.nationality) |
||||
|
return meetsMinAge && requiredNationality |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
/* for POST requests, create a new user and return the path to the new resource */ |
||||
|
request: { method: 'POST' }, |
||||
|
response: function (ctx) { |
||||
|
const newUser = ctx.request.body |
||||
|
users.push(newUser) |
||||
|
newUser.id = users.length |
||||
|
ctx.status = 201 |
||||
|
ctx.response.set('Location', `/users/${newUser.id}`) |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
module.exports = mockResponses |
||||
|
``` |
||||
|
|
||||
|
The individual resource module: |
||||
|
|
||||
|
```js |
||||
|
const users = require('./users.json') |
||||
|
|
||||
|
/* responses for /users/:id */ |
||||
|
const mockResponses = [ |
||||
|
/* don't support POST here */ |
||||
|
{ request: { method: 'POST' }, response: { status: 400 } }, |
||||
|
|
||||
|
/* for GET requests, return a particular user */ |
||||
|
{ |
||||
|
request: { method: 'GET' }, |
||||
|
response: function (ctx, id) { |
||||
|
ctx.body = users.find(user => user.id === Number(id)) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
/* for PUT requests, update the record */ |
||||
|
{ |
||||
|
request: { method: 'PUT' }, |
||||
|
response: function (ctx, id) { |
||||
|
const updatedUser = ctx.request.body |
||||
|
const existingUserIndex = users.findIndex(user => user.id === Number(id)) |
||||
|
users.splice(existingUserIndex, 1, updatedUser) |
||||
|
ctx.status = 200 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
/* DELETE request: remove the record */ |
||||
|
{ |
||||
|
request: { method: 'DELETE' }, |
||||
|
response: function (ctx, id) { |
||||
|
const existingUserIndex = users.findIndex(user => user.id === Number(id)) |
||||
|
users.splice(existingUserIndex, 1) |
||||
|
ctx.status = 200 |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
module.exports = mockResponses |
||||
|
``` |
||||
|
|
||||
|
[Example](https://github.com/75lb/local-web-server/tree/master/example/mock). |
@ -0,0 +1,37 @@ |
|||||
|
## URL rewriting |
||||
|
|
||||
|
Your application requested `/css/style.css` but it's stored at `/build/css/style.css`. To avoid a 404 you need a rewrite rule: |
||||
|
|
||||
|
```sh |
||||
|
$ ws --rewrite '/css/style.css -> /build/css/style.css' |
||||
|
``` |
||||
|
|
||||
|
Or, more generally (matching any stylesheet under `/css`): |
||||
|
|
||||
|
```sh |
||||
|
$ ws --rewrite '/css/:stylesheet -> /build/css/:stylesheet' |
||||
|
``` |
||||
|
|
||||
|
With a deep CSS directory structure it may be easier to mount the entire contents of `/build/css` to the `/css` path: |
||||
|
|
||||
|
```sh |
||||
|
$ ws --rewrite '/css/* -> /build/css/$1' |
||||
|
``` |
||||
|
|
||||
|
this rewrites `/css/a` as `/build/css/a`, `/css/a/b/c` as `/build/css/a/b/c` etc. |
||||
|
|
||||
|
### Proxied requests |
||||
|
|
||||
|
If the `to` URL contains a remote host, local-web-server will act as a proxy - fetching and responding with the remote resource. |
||||
|
|
||||
|
Mount the npm registry locally: |
||||
|
```sh |
||||
|
$ ws --rewrite '/npm/* -> http://registry.npmjs.org/$1' |
||||
|
``` |
||||
|
|
||||
|
Map local requests for repo data to the Github API: |
||||
|
```sh |
||||
|
$ ws --rewrite '/:user/repos/:name -> https://api.github.com/repos/:user/:name' |
||||
|
``` |
||||
|
|
||||
|
[Example](https://github.com/75lb/local-web-server/tree/master/example/rewrite). |
@ -0,0 +1,12 @@ |
|||||
|
## Single Page Application |
||||
|
|
||||
|
You're building a web app with client-side routing, so mark `index.html` as the SPA. |
||||
|
```sh |
||||
|
$ ws --spa index.html |
||||
|
``` |
||||
|
|
||||
|
By default, typical SPA paths (e.g. `/user/1`, `/login`) would return `404 Not Found` as a file does not exist with that path. By marking `index.html` as the SPA you create this rule: |
||||
|
|
||||
|
*If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* |
||||
|
|
||||
|
[Example](https://github.com/75lb/local-web-server/tree/master/example/spa). |
@ -0,0 +1,28 @@ |
|||||
|
## Stored config |
||||
|
|
||||
|
Use the same options every time? Persist then to `package.json`: |
||||
|
```json |
||||
|
{ |
||||
|
"name": "example", |
||||
|
"version": "1.0.0", |
||||
|
"local-web-server": { |
||||
|
"port": 8100, |
||||
|
"forbid": "*.json" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
or `.local-web-server.json` |
||||
|
```json |
||||
|
{ |
||||
|
"port": 8100, |
||||
|
"forbid": "*.json" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
local-web-server will merge and use all config found, searching from the current directory upward. In the case both `package.json` and `.local-web-server.json` config is found in the same directory, `.local-web-server.json` will take precedence. Options set on the command line take precedence over all. |
||||
|
|
||||
|
To inspect stored config, run: |
||||
|
```sh |
||||
|
$ ws --config |
||||
|
``` |
@ -0,0 +1,291 @@ |
|||||
|
'use strict' |
||||
|
const arrayify = require('array-back') |
||||
|
const path = require('path') |
||||
|
const url = require('url') |
||||
|
const debug = require('./debug') |
||||
|
const mw = require('./middleware') |
||||
|
const t = require('typical') |
||||
|
const compose = require('koa-compose') |
||||
|
const flatten = require('reduce-flatten') |
||||
|
const MiddlewareStack = require('./middleware-stack') |
||||
|
|
||||
|
class DefaultStack extends MiddlewareStack { |
||||
|
/** |
||||
|
* allow from any origin |
||||
|
*/ |
||||
|
addCors () { |
||||
|
this.push({ middleware: require('kcors') }) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* pretty print JSON */ |
||||
|
addJson () { |
||||
|
this.push({ middleware: require('koa-json') }) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* rewrite rules */ |
||||
|
addRewrite (rewriteRules) { |
||||
|
this.push({ |
||||
|
optionDefinitions: { |
||||
|
name: 'rewrite', alias: 'r', type: String, multiple: true, |
||||
|
typeLabel: '[underline]{expression} ...', |
||||
|
description: "A list of URL rewrite rules. For each rule, separate the 'from' and 'to' routes with '->'. Whitespace surrounded the routes is ignored. E.g. '/from -> /to'." |
||||
|
}, |
||||
|
middleware: function (cliOptions) { |
||||
|
const options = parseRewriteRules(arrayify(cliOptions.rewrite || rewriteRules)) |
||||
|
if (options.length) { |
||||
|
return options.map(route => { |
||||
|
if (route.to) { |
||||
|
/* `to` address is remote if the url specifies a host */ |
||||
|
if (url.parse(route.to).host) { |
||||
|
const _ = require('koa-route') |
||||
|
debug('proxy rewrite', `${route.from} -> ${route.to}`) |
||||
|
return _.all(route.from, mw.proxyRequest(route)) |
||||
|
} else { |
||||
|
const rewrite = require('koa-rewrite') |
||||
|
const rmw = rewrite(route.from, route.to) |
||||
|
rmw._name = 'rewrite' |
||||
|
return rmw |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* must come after rewrite. |
||||
|
See https://github.com/nodejitsu/node-http-proxy/issues/180. */
|
||||
|
addBodyParser () { |
||||
|
this.push({ middleware: require('koa-bodyparser') }) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* path blacklist */ |
||||
|
addBlacklist (forbidList) { |
||||
|
this.push({ |
||||
|
optionDefinitions: { |
||||
|
name: 'forbid', alias: 'b', type: String, |
||||
|
multiple: true, typeLabel: '[underline]{path} ...', |
||||
|
description: 'A list of forbidden routes.' |
||||
|
}, |
||||
|
middleware: function (cliOptions) { |
||||
|
forbidList = arrayify(cliOptions.forbid || forbidList) |
||||
|
if (forbidList.length) { |
||||
|
const pathToRegexp = require('path-to-regexp') |
||||
|
debug('forbid', forbidList.join(', ')) |
||||
|
return function blacklist (ctx, next) { |
||||
|
if (forbidList.some(expression => pathToRegexp(expression).test(ctx.path))) { |
||||
|
ctx.status = 403 |
||||
|
} else { |
||||
|
return next() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* cache */ |
||||
|
addCache () { |
||||
|
this.push({ |
||||
|
optionDefinitions: { |
||||
|
name: 'no-cache', alias: 'n', type: Boolean, |
||||
|
description: 'Disable etag-based caching - forces loading from disk each request.' |
||||
|
}, |
||||
|
middleware: function (cliOptions) { |
||||
|
const noCache = cliOptions['no-cache'] |
||||
|
if (!noCache) { |
||||
|
return [ |
||||
|
require('koa-conditional-get')(), |
||||
|
require('koa-etag')() |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* mime-type overrides */ |
||||
|
addMimeOverride (mime) { |
||||
|
this.push({ |
||||
|
middleware: function (cliOptions) { |
||||
|
mime = cliOptions.mime || mime |
||||
|
if (mime) { |
||||
|
debug('mime override', JSON.stringify(mime)) |
||||
|
return mw.mime(mime) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* compress response */ |
||||
|
addCompression (compress) { |
||||
|
this.push({ |
||||
|
optionDefinitions: { |
||||
|
name: 'compress', alias: 'c', type: Boolean, |
||||
|
description: 'Serve gzip-compressed resources, where applicable.' |
||||
|
}, |
||||
|
middleware: function (cliOptions) { |
||||
|
compress = t.isDefined(cliOptions.compress) |
||||
|
? cliOptions.compress |
||||
|
: compress |
||||
|
if (compress) { |
||||
|
debug('compression', 'enabled') |
||||
|
return require('koa-compress')() |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* Logging */ |
||||
|
addLogging (format, options) { |
||||
|
options = options || {} |
||||
|
this.push({ |
||||
|
optionDefinitions: { |
||||
|
name: 'log-format', |
||||
|
alias: 'f', |
||||
|
type: String, |
||||
|
description: "If a format is supplied an access log is written to stdout. If not, a dynamic statistics view is displayed. Use a preset ('none', 'dev','combined', 'short', 'tiny' or 'logstalgia') or supply a custom format (e.g. ':method -> :url')." |
||||
|
}, |
||||
|
middleware: function (cliOptions) { |
||||
|
format = cliOptions['log-format'] || format |
||||
|
|
||||
|
if (cliOptions.verbose && !format) { |
||||
|
format = 'none' |
||||
|
} |
||||
|
|
||||
|
if (format !== 'none') { |
||||
|
const morgan = require('koa-morgan') |
||||
|
|
||||
|
if (!format) { |
||||
|
const streamLogStats = require('stream-log-stats') |
||||
|
options.stream = streamLogStats({ refreshRate: 500 }) |
||||
|
return morgan('common', options) |
||||
|
} else if (format === 'logstalgia') { |
||||
|
morgan.token('date', () => { |
||||
|
var d = new Date() |
||||
|
return (`${d.getDate()}/${d.getUTCMonth()}/${d.getFullYear()}:${d.toTimeString()}`).replace('GMT', '').replace(' (BST)', '') |
||||
|
}) |
||||
|
return morgan('combined', options) |
||||
|
} else { |
||||
|
return morgan(format, options) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* Mock Responses */ |
||||
|
addMockResponses (mocks) { |
||||
|
this.push({ |
||||
|
middleware: function (cliOptions) { |
||||
|
mocks = arrayify(cliOptions.mocks || mocks) |
||||
|
return mocks.map(mock => { |
||||
|
if (mock.module) { |
||||
|
const modulePath = path.resolve(path.join(cliOptions.directory, mock.module)) |
||||
|
mock.responses = require(modulePath) |
||||
|
} |
||||
|
|
||||
|
if (mock.responses) { |
||||
|
return mw.mockResponses(mock.route, mock.responses) |
||||
|
} else if (mock.response) { |
||||
|
mock.target = { |
||||
|
request: mock.request, |
||||
|
response: mock.response |
||||
|
} |
||||
|
return mw.mockResponses(mock.route, mock.target) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* for any URL not matched by static (e.g. `/search`), serve the SPA */ |
||||
|
addSpa (spa, assetTest) { |
||||
|
this.push({ |
||||
|
optionDefinitions: { |
||||
|
name: 'spa', alias: 's', type: String, typeLabel: '[underline]{file}', |
||||
|
description: 'Path to a Single Page App, e.g. app.html.' |
||||
|
}, |
||||
|
middleware: function (cliOptions) { |
||||
|
spa = cliOptions.spa || spa || 'index.html' |
||||
|
assetTest = new RegExp(cliOptions['spa-asset-test'] || assetTest || '\\.') |
||||
|
if (spa) { |
||||
|
const send = require('koa-send') |
||||
|
const _ = require('koa-route') |
||||
|
debug('SPA', spa) |
||||
|
return _.get('*', function spaMw (ctx, route, next) { |
||||
|
const root = path.resolve(cliOptions.directory || process.cwd()) |
||||
|
if (ctx.accepts('text/html') && !assetTest.test(route)) { |
||||
|
debug(`SPA request. Route: ${route}, isAsset: ${assetTest.test(route)}`) |
||||
|
return send(ctx, spa, { root: root }).then(next) |
||||
|
} else { |
||||
|
return send(ctx, route, { root: root }).then(next) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* serve static files */ |
||||
|
addStatic (root, options) { |
||||
|
this.push({ |
||||
|
optionDefinitions: { |
||||
|
name: 'directory', alias: 'd', type: String, typeLabel: '[underline]{path}', |
||||
|
description: 'Root directory, defaults to the current directory.' |
||||
|
}, |
||||
|
middleware: function (cliOptions) { |
||||
|
/* update global cliOptions */ |
||||
|
cliOptions.directory = cliOptions.directory || root || process.cwd() |
||||
|
options = Object.assign({ hidden: true }, options) |
||||
|
if (cliOptions.directory) { |
||||
|
const serve = require('koa-static') |
||||
|
return serve(cliOptions.directory, options) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/* serve directory index */ |
||||
|
addIndex (path, options) { |
||||
|
this.push({ |
||||
|
middleware: function (cliOptions) { |
||||
|
path = cliOptions.directory || path || process.cwd() |
||||
|
options = Object.assign({ icons: true, hidden: true }, options) |
||||
|
if (path) { |
||||
|
const serveIndex = require('koa-serve-index') |
||||
|
return serveIndex(path, options) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return this |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = DefaultStack |
||||
|
|
||||
|
function parseRewriteRules (rules) { |
||||
|
return rules && rules.map(rule => { |
||||
|
if (t.isString(rule)) { |
||||
|
const matches = rule.match(/(\S*)\s*->\s*(\S*)/) |
||||
|
if (!(matches && matches.length >= 3)) throw new Error('Invalid rule: ' + rule) |
||||
|
return { |
||||
|
from: matches[1], |
||||
|
to: matches[2] |
||||
|
} |
||||
|
} else { |
||||
|
return rule |
||||
|
} |
||||
|
}) |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue