From 509cea9de2ee42e6137550eb4155ba8acb096046 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 19 Nov 2015 19:27:36 +0000 Subject: [PATCH] refactor --- lib/local-web-server.js | 145 ++++++------------------------------------------ lib/middleware.js | 120 +++++++++++++++++++++++++++++++++++++++ test/test.js | 36 ++++++------ 3 files changed, 155 insertions(+), 146 deletions(-) create mode 100644 lib/middleware.js diff --git a/lib/local-web-server.js b/lib/local-web-server.js index c1aafe3..a565af7 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,9 +1,6 @@ 'use strict' const path = require('path') -const http = require('http') const url = require('url') -const arrayify = require('array-back') -let debug, pathToRegexp /** * @module local-web-server @@ -15,10 +12,10 @@ module.exports = localWebServer * * @param [options] {object} - options * @param [options.static] {object} - koa-static config - * @param [options.static.root] {string} - root directory + * @param [options.static.root=.] {string} - root directory * @param [options.static.options] {string} - [options](https://github.com/koajs/static#options) * @param [options.serveIndex] {object} - koa-serve-index config - * @param [options.serveIndex.path] {string} - root directory + * @param [options.serveIndex.path=.] {string} - root directory * @param [options.serveIndex.options] {string} - [options](https://github.com/expressjs/serve-index#options) * @param [options.forbid] {string[]} - A list of forbidden routes, each route being an [express route-path](http://expressjs.com/guide/routing.html#route-paths). * @param [options.spa] {string} - specify an SPA file to catch requests for everything but static assets. @@ -53,14 +50,14 @@ function localWebServer (options) { process.env.DEBUG = '*' } + const debug = require('debug')('local-web-server') const Koa = require('koa') const convert = require('koa-convert') const cors = require('kcors') const _ = require('koa-route') const json = require('koa-json') - pathToRegexp = require('path-to-regexp') - debug = require('debug')('local-web-server') const bodyParser = require('koa-bodyparser') + const mw = require('./middleware') const log = options.log log.options = log.options || {} @@ -69,14 +66,7 @@ function localWebServer (options) { const _use = app.use app.use = x => _use.call(app, convert(x)) - function verbose (category, message) { - if (options.verbose) { - debug(category, message) - } - } - app._verbose = verbose - - if (options.verbose && !log.format) { + if (options.verbose && !log.format) { log.format = 'none' } @@ -94,13 +84,13 @@ function localWebServer (options) { options.rewrite.forEach(route => { if (route.to) { if (url.parse(route.to).host) { - verbose('proxy rewrite', `${route.from} -> ${route.to}`) - app.use(_.all(route.from, proxyRequest(route, app))) + debug('proxy rewrite', `${route.from} -> ${route.to}`) + app.use(_.all(route.from, mw.proxyRequest(route, app))) } else { const rewrite = require('koa-rewrite') - const mw = rewrite(route.from, route.to) - mw._name = 'rewrite' - app.use(mw) + const rmw = rewrite(route.from, route.to) + rmw._name = 'rewrite' + app.use(rmw) } } }) @@ -108,8 +98,8 @@ function localWebServer (options) { /* path blacklist */ if (options.forbid.length) { - verbose('forbid', options.forbid.join(', ')) - app.use(blacklist(options.forbid)) + debug('forbid', options.forbid.join(', ')) + app.use(mw.blacklist(options.forbid)) } /* Cache */ @@ -122,22 +112,14 @@ function localWebServer (options) { /* mime-type overrides */ if (options.mime) { - verbose('mime override', JSON.stringify(options.mime)) - app.use((ctx, next) => { - return next().then(() => { - const reqPathExtension = path.extname(ctx.path).slice(1) - Object.keys(options.mime).forEach(mimeType => { - const extsToOverride = options.mime[mimeType] - if (extsToOverride.indexOf(reqPathExtension) > -1) ctx.type = mimeType - }) - }) - }) + debug('mime override', JSON.stringify(options.mime)) + app.use(mw.mime(options.mime)) } /* compress response */ if (options.compress) { const compress = require('koa-compress') - verbose('compression', 'enabled') + debug('compression', 'enabled') app.use(compress()) } @@ -158,7 +140,7 @@ function localWebServer (options) { } /* Mock Responses */ - app.use(mockResponses({ root: options.static.root, verbose: verbose })) + app.use(mw.mockResponses({ root: options.static.root })) /* serve static files */ if (options.static.root) { @@ -175,7 +157,7 @@ function localWebServer (options) { /* for any URL not matched by static (e.g. `/search`), serve the SPA */ if (options.spa) { const send = require('koa-send') - verbose('SPA', options.spa) + debug('SPA', options.spa) app.use(_.all('*', function * () { yield send(this, options.spa, { root: path.resolve(options.static.root) || process.cwd() }) })) @@ -188,99 +170,6 @@ function logstalgiaDate () { return (`${d.getDate()}/${d.getUTCMonth()}/${d.getFullYear()}:${d.toTimeString()}`).replace('GMT', '').replace(' (BST)', '') } -function proxyRequest (route, app) { - const httpProxy = require('http-proxy') - const proxy = httpProxy.createProxyServer({ - changeOrigin: true - }) - - return function * proxyMiddleware () { - const next = arguments[arguments.length - 1] - const keys = [] - route.re = pathToRegexp(route.from, keys) - route.new = this.url.replace(route.re, route.to) - - keys.forEach((key, index) => { - const re = RegExp(`:${key.name}`, 'g') - route.new = route.new - .replace(re, arguments[index] || '') - }) - - /* test no keys remain in the new path */ - keys.length = 0 - pathToRegexp(url.parse(route.new).path, keys) - if (keys.length) { - this.throw(500, `[PROXY] Invalid target URL: ${route.new}`) - return next() - } - - this.response = false - app._verbose('proxy request', `from: ${this.path}, to: ${url.parse(route.new).href}`) - - proxy.once('error', err => { - this.throw(500, `[PROXY] ${err.message}: ${route.new}`) - }) - proxy.once('proxyReq', function (proxyReq) { - proxyReq.path = url.parse(route.new).path - }) - proxy.web(this.req, this.res, { target: route.new }) - } -} - -function blacklist (forbid) { - return function blacklist (ctx, next) { - if (forbid.some(expression => pathToRegexp(expression).test(ctx.path))) { - ctx.throw(403, http.STATUS_CODES[403]) - } else { - return next() - } - } -} - -function mockResponses (options) { - options = options || { root: process.cwd() } - return function mockResponses (ctx, next) { - if (/\.mock.js$/.test(ctx.path)) { - const mocks = arrayify(require(path.join(options.root, ctx.path))) - const testValue = require('test-value') - const t = require('typical') - - /* find a mock with compatible method and accepts */ - let mock = mocks.find(mock => { - return testValue(mock, { - request: { - method: [ ctx.method, undefined ], - accepts: type => ctx.accepts(type) - } - }) - }) - - /* else take the first mock without a request (no request means 'all requests') */ - if (!mock) { - mock = mocks.find(mock => !mock.request) - } - - const mockedReponse = {} - /* resolve any functions on the mock */ - Object.keys(mock.response).forEach(key => { - if (t.isFunction(mock.response[key])) { - mockedReponse[key] = mock.response[key](ctx) - } else { - mockedReponse[key] = mock.response[key] - } - }) - - if (mock) { - Object.assign(ctx.response, mockedReponse) - // options.verbose('mocked response', JSON.stringify(mockedReponse)) - // options.verbose('actual response', JSON.stringify(ctx.response)) - } - } else { - return next() - } - } -} - process.on('unhandledRejection', (reason, p) => { throw reason }) diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 0000000..8a4de5a --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,120 @@ +'use strict' +const path = require('path') +const http = require('http') +const url = require('url') +const arrayify = require('array-back') +const pathToRegexp = require('path-to-regexp') +const debug = require('debug')('local-web-server') + +/** + * @module middleware + */ +exports.proxyRequest = proxyRequest +exports.blacklist = blacklist +exports.mockResponses = mockResponses +exports.mime = mime + +function proxyRequest (route, app) { + const httpProxy = require('http-proxy') + const proxy = httpProxy.createProxyServer({ + changeOrigin: true + }) + + return function * proxyMiddleware () { + const next = arguments[arguments.length - 1] + const keys = [] + route.re = pathToRegexp(route.from, keys) + route.new = this.url.replace(route.re, route.to) + + keys.forEach((key, index) => { + const re = RegExp(`:${key.name}`, 'g') + route.new = route.new + .replace(re, arguments[index] || '') + }) + + /* test no keys remain in the new path */ + keys.length = 0 + pathToRegexp(url.parse(route.new).path, keys) + if (keys.length) { + this.throw(500, `[PROXY] Invalid target URL: ${route.new}`) + return next() + } + + this.response = false + debug('proxy request', `from: ${this.path}, to: ${url.parse(route.new).href}`) + + proxy.once('error', err => { + this.throw(500, `[PROXY] ${err.message}: ${route.new}`) + }) + proxy.once('proxyReq', function (proxyReq) { + proxyReq.path = url.parse(route.new).path + }) + proxy.web(this.req, this.res, { target: route.new }) + } +} + +function blacklist (forbid) { + return function blacklist (ctx, next) { + if (forbid.some(expression => pathToRegexp(expression).test(ctx.path))) { + ctx.throw(403, http.STATUS_CODES[403]) + } else { + return next() + } + } +} + +function mockResponses (options) { + options = options || { root: process.cwd() } + return function mockResponses (ctx, next) { + if (/\.mock.js$/.test(ctx.path)) { + const mocks = arrayify(require(path.join(options.root, ctx.path))) + const testValue = require('test-value') + const t = require('typical') + + /* find a mock with compatible method and accepts */ + let mock = mocks.find(mock => { + return testValue(mock, { + request: { + method: [ ctx.method, undefined ], + accepts: type => ctx.accepts(type) + } + }) + }) + + /* else take the first mock without a request (no request means 'all requests') */ + if (!mock) { + mock = mocks.find(mock => !mock.request) + } + + const mockedReponse = {} + /* resolve any functions on the mock */ + Object.keys(mock.response).forEach(key => { + if (t.isFunction(mock.response[key])) { + mockedReponse[key] = mock.response[key](ctx) + } else { + mockedReponse[key] = mock.response[key] + } + }) + + if (mock) { + Object.assign(ctx.response, mockedReponse) + // debug('mocked response', JSON.stringify(mockedReponse)) + // debug('actual response', JSON.stringify(ctx.response)) + } + } else { + return next() + } + } +} + +function mime (mimeTypes) { + return function mime (ctx, next) { + return next().then(() => { + const reqPathExtension = path.extname(ctx.path).slice(1) + Object.keys(mimeTypes).forEach(mimeType => { + const extsToOverride = mimeTypes[mimeType] + if (extsToOverride.indexOf(reqPathExtension) > -1) ctx.type = mimeType + }) + }) + } +} diff --git a/test/test.js b/test/test.js index 9bf1f2e..a7cb228 100644 --- a/test/test.js +++ b/test/test.js @@ -50,7 +50,7 @@ test('serve-index', function (t) { }}) }) -test('single page app', function(t){ +test('single page app', function (t) { t.plan(4) const app = localWebServer({ log: { format: 'none' }, @@ -89,7 +89,7 @@ test('log: common', function (t) { launchServer(app) }) -test('compress', function(t){ +test('compress', function (t) { t.plan(1) const app = localWebServer({ compress: true, @@ -108,12 +108,12 @@ test('compress', function(t){ ) }) -test('mime', function(t){ +test('mime', function (t) { t.plan(2) const app = localWebServer({ log: { format: 'none' }, static: { root: __dirname + '/fixture' }, - mime: { 'text/plain': [ 'php' ]} + mime: { 'text/plain': [ 'php' ] } }) launchServer(app, { path: '/something.php', onSuccess: response => { t.strictEqual(response.res.statusCode, 200) @@ -140,24 +140,24 @@ test('forbid', function (t) { }) }) -test('rewrite: local', function(t){ +test('rewrite: local', function (t) { t.plan(1) const app = localWebServer({ log: { format: 'none' }, static: { root: __dirname + '/fixture/rewrite' }, - rewrite: [ { from: '/two.html', to: '/one.html'} ] + rewrite: [ { from: '/two.html', to: '/one.html' } ] }) launchServer(app, { path: '/two.html', onSuccess: response => { t.strictEqual(response.data, 'one\n') }}) }) -test('rewrite: proxy', function(t){ +test('rewrite: proxy', function (t) { t.plan(2) const app = localWebServer({ log: { format: 'none' }, static: { root: __dirname + '/fixture/rewrite' }, - rewrite: [ { from: '/test/*', to: 'http://registry.npmjs.org/$1'} ] + rewrite: [ { from: '/test/*', to: 'http://registry.npmjs.org/$1' } ] }) launchServer(app, { path: '/test/', onSuccess: response => { t.strictEqual(response.res.statusCode, 200) @@ -165,7 +165,7 @@ test('rewrite: proxy', function(t){ }}) }) -test('rewrite: proxy with port', function(t){ +test('rewrite: proxy with port', function (t) { t.plan(2) const one = localWebServer({ log: { format: 'none' }, @@ -174,18 +174,18 @@ test('rewrite: proxy with port', function(t){ const two = localWebServer({ log: { format: 'none' }, static: { root: __dirname + '/fixture/spa' }, - rewrite: [ { from: '/test/*', to: 'http://localhost:9000/$1'} ] + rewrite: [ { from: '/test/*', to: 'http://localhost:9000/$1' } ] }) const server1 = http.createServer(one.callback()) const server2 = http.createServer(two.callback()) server1.listen(9000, () => { - server2.listen(8100, () => { - request('http://localhost:8100/test/file.txt').then(response => { - t.strictEqual(response.res.statusCode, 200) - t.ok(/one/.test(response.data)) - server1.close() - server2.close() - }) - }) + server2.listen(8100, () => { + request('http://localhost:8100/test/file.txt').then(response => { + t.strictEqual(response.res.statusCode, 200) + t.ok(/one/.test(response.data)) + server1.close() + server2.close() + }) + }) }) })