index.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. const express = require('express')
  2. const qs = require('querystring')
  3. const urlParser = require('url')
  4. const companion = require('../companion')
  5. const helmet = require('helmet')
  6. const morgan = require('morgan')
  7. const bodyParser = require('body-parser')
  8. const redis = require('../server/redis')
  9. const merge = require('lodash.merge')
  10. // @ts-ignore
  11. const promBundle = require('express-prom-bundle')
  12. const session = require('express-session')
  13. const addRequestId = require('express-request-id')()
  14. const helper = require('./helper')
  15. // @ts-ignore
  16. const { version } = require('../../package.json')
  17. const app = express()
  18. // for server metrics tracking.
  19. const metricsMiddleware = promBundle({ includeMethod: true })
  20. const promClient = metricsMiddleware.promClient
  21. const collectDefaultMetrics = promClient.collectDefaultMetrics
  22. const promInterval = collectDefaultMetrics({ register: promClient.register, timeout: 5000 })
  23. // Add version as a prometheus gauge
  24. const versionGauge = new promClient.Gauge({ name: 'companion_version', help: 'npm version as an integer' })
  25. // @ts-ignore
  26. const numberVersion = version.replace(/\D/g, '') * 1
  27. versionGauge.set(numberVersion)
  28. if (app.get('env') !== 'test') {
  29. clearInterval(promInterval)
  30. }
  31. app.use(addRequestId)
  32. // log server requests.
  33. app.use(morgan('combined'))
  34. morgan.token('url', (req, res) => {
  35. const query = Object.assign({}, req.query)
  36. let hasQuery = false;
  37. ['access_token', 'uppyAuthToken'].forEach((key) => {
  38. if (req.query && req.query[key]) {
  39. // replace logged access token with xxxx character
  40. query[key] = 'x'.repeat(req.query[key].length)
  41. hasQuery = true
  42. }
  43. })
  44. return hasQuery ? `${req.path}?${qs.stringify(query)}` : req.originalUrl || req.url
  45. })
  46. morgan.token('referrer', (req, res) => {
  47. const ref = req.headers.referer || req.headers.referrer
  48. if (typeof ref === 'string') {
  49. // @todo drop the use of url.parse
  50. // when support for node 6 is dropped
  51. // eslint-disable-next-line
  52. const parsed = urlParser.URL ? new urlParser.URL(ref) : urlParser.parse(ref)
  53. const query = qs.parse(parsed.search.replace('?', ''));
  54. ['uppyAuthToken', 'access_token'].forEach(key => {
  55. if (query[key]) {
  56. query[key] = 'x'.repeat(query[key].length)
  57. }
  58. })
  59. const hasQuery = parsed.search
  60. const newURL = `${parsed.href.split('?')[0]}?${qs.stringify(query)}`
  61. return hasQuery ? newURL : parsed.href
  62. }
  63. })
  64. // make app metrics available at '/metrics'.
  65. app.use(metricsMiddleware)
  66. app.use(bodyParser.json())
  67. app.use(bodyParser.urlencoded({ extended: false }))
  68. // Use helmet to secure Express headers
  69. app.use(helmet.frameguard())
  70. app.use(helmet.xssFilter())
  71. app.use(helmet.noSniff())
  72. app.use(helmet.ieNoOpen())
  73. app.disable('x-powered-by')
  74. const companionOptions = helper.getCompanionOptions()
  75. const sessionOptions = {
  76. secret: companionOptions.secret,
  77. resave: true,
  78. saveUninitialized: true
  79. }
  80. if (companionOptions.redisUrl) {
  81. const RedisStore = require('connect-redis')(session)
  82. const redisClient = redis.client(
  83. merge({ url: companionOptions.redisUrl }, companionOptions.redisOptions)
  84. )
  85. sessionOptions.store = new RedisStore({ client: redisClient })
  86. }
  87. if (process.env.COMPANION_COOKIE_DOMAIN) {
  88. sessionOptions.cookie = {
  89. domain: process.env.COMPANION_COOKIE_DOMAIN,
  90. maxAge: 24 * 60 * 60 * 1000 // 1 day
  91. }
  92. }
  93. app.use(session(sessionOptions))
  94. app.use((req, res, next) => {
  95. const protocol = process.env.COMPANION_PROTOCOL || 'http'
  96. // if endpoint urls are specified, then we only allow those endpoints
  97. // otherwise, we allow any client url to access companion.
  98. // here we also enforce that only the protocol allowed by companion is used.
  99. if (process.env.COMPANION_CLIENT_ORIGINS) {
  100. const whitelist = process.env.COMPANION_CLIENT_ORIGINS
  101. .split(',')
  102. .map((url) => helper.hasProtocol(url) ? url : `${protocol}://${url}`)
  103. // @ts-ignore
  104. if (req.headers.origin && whitelist.indexOf(req.headers.origin) > -1) {
  105. res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
  106. // only allow credentials when origin is whitelisted
  107. res.setHeader('Access-Control-Allow-Credentials', 'true')
  108. }
  109. } else {
  110. res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
  111. }
  112. res.setHeader(
  113. 'Access-Control-Allow-Methods',
  114. 'GET, POST, OPTIONS, PUT, PATCH, DELETE'
  115. )
  116. res.setHeader(
  117. 'Access-Control-Allow-Headers',
  118. 'Authorization, Origin, Content-Type, Accept'
  119. )
  120. next()
  121. })
  122. // Routes
  123. app.get('/', (req, res) => {
  124. res.setHeader('Content-Type', 'text/plain')
  125. res.send(helper.buildHelpfulStartupMessage(companionOptions))
  126. })
  127. // initialize companion
  128. helper.validateConfig(companionOptions)
  129. if (process.env.COMPANION_PATH) {
  130. app.use(process.env.COMPANION_PATH, companion.app(companionOptions))
  131. } else {
  132. app.use(companion.app(companionOptions))
  133. }
  134. // WARNING: This route is added in order to validate your app with OneDrive.
  135. // Only set COMPANION_ONEDRIVE_DOMAIN_VALIDATION if you are sure that you are setting the
  136. // correct value for COMPANION_ONEDRIVE_KEY (i.e application ID). If there's a slightest possiblilty
  137. // that you might have mixed the values for COMPANION_ONEDRIVE_KEY and COMPANION_ONEDRIVE_SECRET,
  138. // please do not set a value for COMPANION_ONEDRIVE_DOMAIN_VALIDATION
  139. if (process.env.COMPANION_ONEDRIVE_DOMAIN_VALIDATION === 'true' && process.env.COMPANION_ONEDRIVE_KEY) {
  140. app.get('/.well-known/microsoft-identity-association.json', (req, res) => {
  141. const content = JSON.stringify({
  142. associatedApplications: [
  143. { applicationId: process.env.COMPANION_ONEDRIVE_KEY }
  144. ]
  145. })
  146. res.header('Content-Length', `${Buffer.byteLength(content, 'utf8')}`)
  147. // use writeHead to prevent 'charset' from being appended
  148. // https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-configure-publisher-domain#to-select-a-verified-domain
  149. res.writeHead(200, { 'Content-Type': 'application/json' })
  150. res.write(content)
  151. res.end()
  152. })
  153. }
  154. app.use((req, res, next) => {
  155. return res.status(404).json({ message: 'Not Found' })
  156. })
  157. if (app.get('env') === 'production') {
  158. // @ts-ignore
  159. app.use((err, req, res, next) => {
  160. console.error('\x1b[31m', req.id, err, '\x1b[0m')
  161. res.status(err.status || 500).json({ message: 'Something went wrong', requestId: req.id })
  162. })
  163. } else {
  164. // @ts-ignore
  165. app.use((err, req, res, next) => {
  166. console.error('\x1b[31m', req.id, err, '\x1b[0m')
  167. res.status(err.status || 500).json({ message: err.message, error: err, requestId: req.id })
  168. })
  169. }
  170. module.exports = { app, companionOptions }