index.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. const express = require('express')
  2. const qs = require('querystring')
  3. const helmet = require('helmet')
  4. const morgan = require('morgan')
  5. const bodyParser = require('body-parser')
  6. const { URL } = require('url')
  7. const merge = require('lodash.merge')
  8. const session = require('express-session')
  9. const addRequestId = require('express-request-id')()
  10. const logger = require('../server/logger')
  11. const redis = require('../server/redis')
  12. const companion = require('../companion')
  13. const helper = require('./helper')
  14. const middlewares = require('../server/middlewares')
  15. /**
  16. * Configures an Express app for running Companion standalone
  17. *
  18. * @returns {object}
  19. */
  20. module.exports = function server (inputCompanionOptions = {}) {
  21. const app = express()
  22. // Query string keys whose values should not end up in logging output.
  23. const sensitiveKeys = new Set(['access_token', 'uppyAuthToken'])
  24. /**
  25. * Obscure the contents of query string keys listed in `sensitiveKeys`.
  26. *
  27. * Returns a copy of the object with unknown types removed and sensitive values replaced by ***.
  28. *
  29. * The input type is more broad that it needs to be, this way typescript can help us guarantee that we're dealing with all
  30. * possible inputs :)
  31. *
  32. * @param {Record<string, any>} rawQuery
  33. * @returns {{
  34. * query: Record<string, any>,
  35. * censored: boolean
  36. * }}
  37. */
  38. function censorQuery (rawQuery) {
  39. /** @type {Record<string, any>} */
  40. const query = {}
  41. let censored = false
  42. Object.keys(rawQuery).forEach((key) => {
  43. if (typeof rawQuery[key] !== 'string') {
  44. return
  45. }
  46. if (sensitiveKeys.has(key)) {
  47. // replace logged access token
  48. query[key] = '********'
  49. censored = true
  50. } else {
  51. query[key] = rawQuery[key]
  52. }
  53. })
  54. return { query, censored }
  55. }
  56. app.use(addRequestId)
  57. // log server requests.
  58. app.use(morgan('combined'))
  59. morgan.token('url', (req, res) => {
  60. const { query, censored } = censorQuery(req.query)
  61. return censored ? `${req.path}?${qs.stringify(query)}` : req.originalUrl || req.url
  62. })
  63. morgan.token('referrer', (req, res) => {
  64. const ref = req.headers.referer || req.headers.referrer
  65. if (typeof ref === 'string') {
  66. let parsed
  67. try {
  68. parsed = new URL(ref)
  69. } catch (_) {
  70. return ref
  71. }
  72. const rawQuery = qs.parse(parsed.search.replace('?', ''))
  73. const { query, censored } = censorQuery(rawQuery)
  74. return censored ? `${parsed.href.split('?')[0]}?${qs.stringify(query)}` : parsed.href
  75. }
  76. })
  77. // for server metrics tracking.
  78. // make app metrics available at '/metrics'.
  79. // TODO for the next major version: use instead companion option "metrics": true and remove this code
  80. // eslint-disable-next-line max-len
  81. // See discussion: https://github.com/transloadit/uppy/pull/2854/files/64be97205e4012818abfcc8b0b8b7fe09de91729#diff-68f5e3eb307c1c9d1fd02224fd7888e2f74718744e1b6e35d929fcab1cc50ed1
  82. if (process.env.COMPANION_HIDE_METRICS !== 'true') {
  83. app.use(middlewares.metrics())
  84. }
  85. app.use(bodyParser.json())
  86. app.use(bodyParser.urlencoded({ extended: false }))
  87. // Use helmet to secure Express headers
  88. app.use(helmet.frameguard())
  89. app.use(helmet.xssFilter())
  90. app.use(helmet.noSniff())
  91. app.use(helmet.ieNoOpen())
  92. app.disable('x-powered-by')
  93. let corsOrigins
  94. if (process.env.COMPANION_CLIENT_ORIGINS) {
  95. corsOrigins = process.env.COMPANION_CLIENT_ORIGINS
  96. .split(',')
  97. .map((url) => (helper.hasProtocol(url) ? url : `${process.env.COMPANION_PROTOCOL || 'http'}://${url}`))
  98. } else if (process.env.COMPANION_CLIENT_ORIGINS_REGEX) {
  99. corsOrigins = new RegExp(process.env.COMPANION_CLIENT_ORIGINS_REGEX)
  100. }
  101. const moreCompanionOptions = { ...inputCompanionOptions, corsOrigins }
  102. const companionOptions = helper.getCompanionOptions(moreCompanionOptions)
  103. const sessionOptions = {
  104. secret: companionOptions.secret,
  105. resave: true,
  106. saveUninitialized: true,
  107. }
  108. if (companionOptions.redisUrl) {
  109. const RedisStore = require('connect-redis')(session)
  110. const redisClient = redis.client(
  111. merge({ url: companionOptions.redisUrl }, companionOptions.redisOptions)
  112. )
  113. sessionOptions.store = new RedisStore({ client: redisClient })
  114. }
  115. if (process.env.COMPANION_COOKIE_DOMAIN) {
  116. sessionOptions.cookie = {
  117. domain: process.env.COMPANION_COOKIE_DOMAIN,
  118. maxAge: 24 * 60 * 60 * 1000, // 1 day
  119. }
  120. }
  121. app.use(session(sessionOptions))
  122. app.use((req, res, next) => {
  123. res.setHeader(
  124. 'Access-Control-Allow-Headers',
  125. 'Authorization, Origin, Content-Type, Accept'
  126. )
  127. next()
  128. })
  129. // Routes
  130. if (process.env.COMPANION_HIDE_WELCOME !== 'true') {
  131. app.get('/', (req, res) => {
  132. res.setHeader('Content-Type', 'text/plain')
  133. res.send(helper.buildHelpfulStartupMessage(companionOptions))
  134. })
  135. }
  136. let companionApp
  137. try {
  138. // initialize companion
  139. companionApp = companion.app(companionOptions)
  140. } catch (error) {
  141. // eslint-disable-next-line no-console
  142. console.error('\x1b[31m', error.message, '\x1b[0m')
  143. process.exit(1)
  144. }
  145. // add companion to server middleware
  146. if (process.env.COMPANION_PATH) {
  147. app.use(process.env.COMPANION_PATH, companionApp)
  148. } else {
  149. app.use(companionApp)
  150. }
  151. // WARNING: This route is added in order to validate your app with OneDrive.
  152. // Only set COMPANION_ONEDRIVE_DOMAIN_VALIDATION if you are sure that you are setting the
  153. // correct value for COMPANION_ONEDRIVE_KEY (i.e application ID). If there's a slightest possiblilty
  154. // that you might have mixed the values for COMPANION_ONEDRIVE_KEY and COMPANION_ONEDRIVE_SECRET,
  155. // please DO NOT set any value for COMPANION_ONEDRIVE_DOMAIN_VALIDATION
  156. if (process.env.COMPANION_ONEDRIVE_DOMAIN_VALIDATION === 'true' && process.env.COMPANION_ONEDRIVE_KEY) {
  157. app.get('/.well-known/microsoft-identity-association.json', (req, res) => {
  158. const content = JSON.stringify({
  159. associatedApplications: [
  160. { applicationId: process.env.COMPANION_ONEDRIVE_KEY },
  161. ],
  162. })
  163. res.header('Content-Length', `${Buffer.byteLength(content, 'utf8')}`)
  164. // use writeHead to prevent 'charset' from being appended
  165. // eslint-disable-next-line max-len
  166. // https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-configure-publisher-domain#to-select-a-verified-domain
  167. res.writeHead(200, { 'Content-Type': 'application/json' })
  168. res.write(content)
  169. res.end()
  170. })
  171. }
  172. app.use((req, res, next) => {
  173. return res.status(404).json({ message: 'Not Found' })
  174. })
  175. // @ts-ignore
  176. app.use((err, req, res, next) => {
  177. const logStackTrace = true
  178. if (app.get('env') === 'production') {
  179. // if the error is a URIError from the requested URL we only log the error message
  180. // to avoid uneccessary error alerts
  181. if (err.status === 400 && err instanceof URIError) {
  182. logger.error(err.message, 'root.error', req.id)
  183. } else {
  184. logger.error(err, 'root.error', req.id, logStackTrace)
  185. }
  186. res.status(err.status || 500).json({ message: 'Something went wrong', requestId: req.id })
  187. } else {
  188. logger.error(err, 'root.error', req.id, logStackTrace)
  189. res.status(err.status || 500).json({ message: err.message, error: err, requestId: req.id })
  190. }
  191. })
  192. return { app, companionOptions }
  193. }