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.

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