index.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. const express = require('express')
  2. const qs = require('querystring')
  3. const companion = require('../companion')
  4. const helmet = require('helmet')
  5. const morgan = require('morgan')
  6. const bodyParser = require('body-parser')
  7. const redis = require('../server/redis')
  8. const logger = require('../server/logger')
  9. const { parseURL } = require('../server/helpers/utils')
  10. const merge = require('lodash.merge')
  11. // @ts-ignore
  12. const promBundle = require('express-prom-bundle')
  13. const session = require('express-session')
  14. const addRequestId = require('express-request-id')()
  15. const helper = require('./helper')
  16. // @ts-ignore
  17. const { version } = require('../../package.json')
  18. const app = express()
  19. // for server metrics tracking.
  20. const metricsMiddleware = promBundle({ includeMethod: true })
  21. const promClient = metricsMiddleware.promClient
  22. const collectDefaultMetrics = promClient.collectDefaultMetrics
  23. const promInterval = collectDefaultMetrics({ register: promClient.register, timeout: 5000 })
  24. // Add version as a prometheus gauge
  25. const versionGauge = new promClient.Gauge({ name: 'companion_version', help: 'npm version as an integer' })
  26. // @ts-ignore
  27. const numberVersion = version.replace(/\D/g, '') * 1
  28. versionGauge.set(numberVersion)
  29. if (app.get('env') !== 'test') {
  30. clearInterval(promInterval)
  31. }
  32. // Query string keys whose values should not end up in logging output.
  33. const sensitiveKeys = new Set(['access_token', 'uppyAuthToken'])
  34. /**
  35. * Obscure the contents of query string keys listed in `sensitiveKeys`.
  36. *
  37. * Returns a copy of the object with unknown types removed and sensitive values replaced by ***.
  38. *
  39. * The input type is more broad that it needs to be, this way typescript can help us guarantee that we're dealing with all possible inputs :)
  40. *
  41. * @param {{ [key: string]: any }} rawQuery
  42. * @returns {{
  43. * query: { [key: string]: string },
  44. * censored: boolean
  45. * }}
  46. */
  47. function censorQuery (rawQuery) {
  48. /** @type {{ [key: string]: string }} */
  49. const query = {}
  50. let censored = false
  51. Object.keys(rawQuery).forEach((key) => {
  52. if (typeof rawQuery[key] !== 'string') {
  53. return
  54. }
  55. if (sensitiveKeys.has(key)) {
  56. // replace logged access token
  57. query[key] = '********'
  58. censored = true
  59. } else {
  60. query[key] = rawQuery[key]
  61. }
  62. })
  63. return { query, censored }
  64. }
  65. app.use(addRequestId)
  66. // log server requests.
  67. app.use(morgan('combined'))
  68. morgan.token('url', (req, res) => {
  69. const { query, censored } = censorQuery(req.query)
  70. return censored ? `${req.path}?${qs.stringify(query)}` : req.originalUrl || req.url
  71. })
  72. morgan.token('referrer', (req, res) => {
  73. const ref = req.headers.referer || req.headers.referrer
  74. if (typeof ref === 'string') {
  75. const parsed = parseURL(ref)
  76. const rawQuery = qs.parse(parsed.search.replace('?', ''))
  77. const { query, censored } = censorQuery(rawQuery)
  78. return censored ? `${parsed.href.split('?')[0]}?${qs.stringify(query)}` : parsed.href
  79. }
  80. })
  81. // make app metrics available at '/metrics'.
  82. app.use(metricsMiddleware)
  83. app.use(bodyParser.json())
  84. app.use(bodyParser.urlencoded({ extended: false }))
  85. // Use helmet to secure Express headers
  86. app.use(helmet.frameguard())
  87. app.use(helmet.xssFilter())
  88. app.use(helmet.noSniff())
  89. app.use(helmet.ieNoOpen())
  90. app.disable('x-powered-by')
  91. const companionOptions = helper.getCompanionOptions()
  92. const sessionOptions = {
  93. secret: companionOptions.secret,
  94. resave: true,
  95. saveUninitialized: true
  96. }
  97. if (companionOptions.redisUrl) {
  98. const RedisStore = require('connect-redis')(session)
  99. const redisClient = redis.client(
  100. merge({ url: companionOptions.redisUrl }, companionOptions.redisOptions)
  101. )
  102. sessionOptions.store = new RedisStore({ client: redisClient })
  103. }
  104. if (process.env.COMPANION_COOKIE_DOMAIN) {
  105. sessionOptions.cookie = {
  106. domain: process.env.COMPANION_COOKIE_DOMAIN,
  107. maxAge: 24 * 60 * 60 * 1000 // 1 day
  108. }
  109. }
  110. app.use(session(sessionOptions))
  111. app.use((req, res, next) => {
  112. const protocol = process.env.COMPANION_PROTOCOL || 'http'
  113. // if endpoint urls are specified, then we only allow those endpoints
  114. // otherwise, we allow any client url to access companion.
  115. // here we also enforce that only the protocol allowed by companion is used.
  116. if (process.env.COMPANION_CLIENT_ORIGINS) {
  117. const whitelist = process.env.COMPANION_CLIENT_ORIGINS
  118. .split(',')
  119. .map((url) => helper.hasProtocol(url) ? url : `${protocol}://${url}`)
  120. // @ts-ignore
  121. if (req.headers.origin && whitelist.indexOf(req.headers.origin) > -1) {
  122. res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
  123. // only allow credentials when origin is whitelisted
  124. res.setHeader('Access-Control-Allow-Credentials', 'true')
  125. }
  126. } else {
  127. res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
  128. }
  129. res.setHeader(
  130. 'Access-Control-Allow-Methods',
  131. 'GET, POST, OPTIONS, PUT, PATCH, DELETE'
  132. )
  133. res.setHeader(
  134. 'Access-Control-Allow-Headers',
  135. 'Authorization, Origin, Content-Type, Accept'
  136. )
  137. next()
  138. })
  139. // Routes
  140. app.get('/', (req, res) => {
  141. res.setHeader('Content-Type', 'text/plain')
  142. res.send(helper.buildHelpfulStartupMessage(companionOptions))
  143. })
  144. // initialize companion
  145. helper.validateConfig(companionOptions)
  146. if (process.env.COMPANION_PATH) {
  147. app.use(process.env.COMPANION_PATH, companion.app(companionOptions))
  148. } else {
  149. app.use(companion.app(companionOptions))
  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 a 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. // https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-configure-publisher-domain#to-select-a-verified-domain
  166. res.writeHead(200, { 'Content-Type': 'application/json' })
  167. res.write(content)
  168. res.end()
  169. })
  170. }
  171. app.use((req, res, next) => {
  172. return res.status(404).json({ message: 'Not Found' })
  173. })
  174. // @ts-ignore
  175. app.use((err, req, res, next) => {
  176. const logStackTrace = true
  177. if (app.get('env') === 'production') {
  178. // if the error is a URIError from the requested URL we only log the error message
  179. // to avoid uneccessary error alerts
  180. if (err.status === 400 && err instanceof URIError) {
  181. logger.error(err.message, 'root.error', req.id)
  182. } else {
  183. logger.error(err, 'root.error', req.id, logStackTrace)
  184. }
  185. res.status(err.status || 500).json({ message: 'Something went wrong', requestId: req.id })
  186. } else {
  187. logger.error(err, 'root.error', req.id, logStackTrace)
  188. res.status(err.status || 500).json({ message: err.message, error: err, requestId: req.id })
  189. }
  190. })
  191. module.exports = { app, companionOptions }