You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

194 lines
5.2 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. 'use strict'
  2. const path = require('path')
  3. const http = require('http')
  4. const url = require('url')
  5. const Koa = require('koa')
  6. const convert = require('koa-convert')
  7. const cors = require('kcors')
  8. const _ = require('koa-route')
  9. const pathToRegexp = require('path-to-regexp')
  10. /**
  11. * @module local-web-server
  12. */
  13. module.exports = localWebServer
  14. /**
  15. * Returns a Koa application
  16. *
  17. * @param [options] {object} - options
  18. * @param [options.static] {object} - koajs/static config
  19. * @param [options.static.root] {string} - root directory
  20. * @param [options.static.options] {string} - options
  21. * @param [options.serveIndex] {object} - koa-serve-index config
  22. * @param [options.serveIndex.path] {string} - root directory
  23. * @param [options.serveIndex.options] {string} - options
  24. * @param [options.forbid] {string[]} - a list of forbidden routes.
  25. *
  26. * @alias module:local-web-server
  27. * @example
  28. * const localWebServer = require('local-web-server')
  29. * localWebServer().listen(8000)
  30. */
  31. function localWebServer (options) {
  32. options = Object.assign({
  33. static: {},
  34. serveIndex: {},
  35. spa: null,
  36. log: {},
  37. compress: false,
  38. mime: {},
  39. forbid: [],
  40. rewrite: []
  41. }, options)
  42. const log = options.log
  43. log.options = log.options || {}
  44. const app = new Koa()
  45. const _use = app.use
  46. app.use = x => _use.call(app, convert(x))
  47. /* CORS: allow from any origin */
  48. app.use(cors())
  49. /* rewrite rules */
  50. if (options.rewrite && options.rewrite.length) {
  51. options.rewrite.forEach(route => {
  52. if (route.to) {
  53. if (url.parse(route.to).host) {
  54. app.use(_.all(route.from, proxyRequest(route)))
  55. } else {
  56. const rewrite = require('koa-rewrite')
  57. app.use(rewrite(route.from, route.to))
  58. }
  59. }
  60. })
  61. }
  62. /* path blacklist */
  63. if (options.forbid.length) {
  64. app.use(blacklist(options.forbid))
  65. }
  66. /* Cache */
  67. if (!options['no-cache']) {
  68. const conditional = require('koa-conditional-get')
  69. const etag = require('koa-etag')
  70. app.use(conditional())
  71. app.use(etag())
  72. }
  73. /* mime-type overrides */
  74. if (options.mime) {
  75. app.use((ctx, next) => {
  76. return next().then(() => {
  77. const reqPathExtension = path.extname(ctx.path).slice(1)
  78. Object.keys(options.mime).forEach(mimeType => {
  79. const extsToOverride = options.mime[mimeType]
  80. if (extsToOverride.indexOf(reqPathExtension) > -1) ctx.type = mimeType
  81. })
  82. })
  83. })
  84. }
  85. /* compress response */
  86. if (options.compress) {
  87. const compress = require('koa-compress')
  88. app.use(compress())
  89. }
  90. /* Logging */
  91. if (log.format !== 'none') {
  92. const morgan = require('koa-morgan')
  93. if (!log.format) {
  94. const streamLogStats = require('stream-log-stats')
  95. log.options.stream = streamLogStats({ refreshRate: 500 })
  96. app.use(morgan.middleware('common', log.options))
  97. } else if (log.format === 'logstalgia') {
  98. morgan.token('date', logstalgiaDate)
  99. app.use(morgan.middleware('combined', log.options))
  100. } else {
  101. app.use(morgan.middleware(log.format, log.options))
  102. }
  103. }
  104. /* serve static files */
  105. if (options.static.root) {
  106. const serve = require('koa-static')
  107. app.use(serve(options.static.root, options.static.options))
  108. }
  109. /* serve directory index */
  110. if (options.serveIndex.path) {
  111. const serveIndex = require('koa-serve-index')
  112. app.use(serveIndex(options.serveIndex.path, options.serveIndex.options))
  113. }
  114. /* for any URL not matched by static (e.g. `/search`), serve the SPA */
  115. if (options.spa) {
  116. const send = require('koa-send')
  117. app.use(_.all('*', function * () {
  118. yield send(this, options.spa, { root: options.static.root || process.cwd() })
  119. }))
  120. }
  121. return app
  122. }
  123. function logstalgiaDate () {
  124. var d = new Date()
  125. return (`${d.getDate()}/${d.getUTCMonth()}/${d.getFullYear()}:${d.toTimeString()}`).replace('GMT', '').replace(' (BST)', '')
  126. }
  127. function proxyRequest (route) {
  128. const httpProxy = require('http-proxy')
  129. const proxy = httpProxy.createProxyServer({
  130. changeOrigin: true
  131. })
  132. return function * proxyMiddleware () {
  133. const next = arguments[arguments.length - 1]
  134. const keys = []
  135. route.re = pathToRegexp(route.from, keys)
  136. route.new = this.path.replace(route.re, route.to)
  137. keys.forEach((key, index) => {
  138. const re = RegExp(`:${key.name}`, 'g')
  139. route.new = route.new
  140. .replace(re, arguments[index] || '')
  141. })
  142. /* test no keys remain in the new path */
  143. keys.length = 0
  144. pathToRegexp(route.new, keys)
  145. if (keys.length) {
  146. this.throw(500, `[PROXY] Invalid target URL: ${route.new}`)
  147. return next()
  148. }
  149. this.response = false
  150. proxy.once('error', err => {
  151. this.throw(500, `[PROXY] ${err.message}: ${route.new}`)
  152. })
  153. proxy.once('proxyReq', function (proxyReq) {
  154. proxyReq.path = url.parse(route.new).path;
  155. })
  156. proxy.web(this.req, this.res, { target: route.new })
  157. }
  158. }
  159. function blacklist (forbid) {
  160. return function blacklist (ctx, next) {
  161. if (forbid.some(expression => pathToRegexp(expression).test(ctx.path))) {
  162. ctx.throw(403, http.STATUS_CODES[403])
  163. } else {
  164. return next()
  165. }
  166. }
  167. }
  168. process.on('unhandledRejection', (reason, p) => {
  169. throw reason
  170. })