companion.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. const express = require('express')
  2. const Grant = require('grant').default.express()
  3. const merge = require('lodash.merge')
  4. const cookieParser = require('cookie-parser')
  5. const interceptor = require('express-interceptor')
  6. const { randomUUID } = require('node:crypto')
  7. const grantConfig = require('./config/grant')()
  8. const providerManager = require('./server/provider')
  9. const controllers = require('./server/controllers')
  10. const s3 = require('./server/controllers/s3')
  11. const url = require('./server/controllers/url')
  12. const createEmitter = require('./server/emitter')
  13. const redis = require('./server/redis')
  14. const jobs = require('./server/jobs')
  15. const logger = require('./server/logger')
  16. const middlewares = require('./server/middlewares')
  17. const { getMaskableSecrets, defaultOptions, validateConfig } = require('./config/companion')
  18. const { ProviderApiError, ProviderAuthError } = require('./server/provider/error')
  19. const { getCredentialsOverrideMiddleware } = require('./server/provider/credentials')
  20. // @ts-ignore
  21. const { version } = require('../package.json')
  22. function setLoggerProcessName ({ loggerProcessName }) {
  23. if (loggerProcessName != null) logger.setProcessName(loggerProcessName)
  24. }
  25. // intercepts grantJS' default response error when something goes
  26. // wrong during oauth process.
  27. const interceptGrantErrorResponse = interceptor((req, res) => {
  28. return {
  29. isInterceptable: () => {
  30. // match grant.js' callback url
  31. return /^\/connect\/\w+\/callback/.test(req.path)
  32. },
  33. intercept: (body, send) => {
  34. const unwantedBody = 'error=Grant%3A%20missing%20session%20or%20misconfigured%20provider'
  35. if (body === unwantedBody) {
  36. logger.error(`grant.js responded with error: ${body}`, 'grant.oauth.error', req.id)
  37. res.set('Content-Type', 'text/plain')
  38. const reqHint = req.id ? `Request ID: ${req.id}` : ''
  39. send([
  40. 'Companion was unable to complete the OAuth process :(',
  41. 'Error: User session is missing or the Provider was misconfigured',
  42. reqHint,
  43. ].join('\n'))
  44. } else {
  45. send(body)
  46. }
  47. },
  48. }
  49. })
  50. // make the errors available publicly for custom providers
  51. module.exports.errors = { ProviderApiError, ProviderAuthError }
  52. module.exports.socket = require('./server/socket')
  53. module.exports.setLoggerProcessName = setLoggerProcessName
  54. /**
  55. * Entry point into initializing the Companion app.
  56. *
  57. * @param {object} optionsArg
  58. * @returns {{ app: import('express').Express, emitter: any }}}
  59. */
  60. module.exports.app = (optionsArg = {}) => {
  61. setLoggerProcessName(optionsArg)
  62. validateConfig(optionsArg)
  63. const options = merge({}, defaultOptions, optionsArg)
  64. const providers = providerManager.getDefaultProviders()
  65. providerManager.addProviderOptions(options, grantConfig)
  66. const { customProviders } = options
  67. if (customProviders) {
  68. providerManager.addCustomProviders(customProviders, providers, grantConfig)
  69. }
  70. // mask provider secrets from log messages
  71. logger.setMaskables(getMaskableSecrets(options))
  72. // create singleton redis client
  73. if (options.redisUrl) {
  74. redis.client(options)
  75. }
  76. const emitter = createEmitter(options.redisUrl, options.redisPubSubScope)
  77. const app = express()
  78. if (options.metrics) {
  79. app.use(middlewares.metrics({ path: options.server.path }))
  80. }
  81. app.use(cookieParser()) // server tokens are added to cookies
  82. app.use(interceptGrantErrorResponse)
  83. // override provider credentials at request time
  84. // Making `POST` request to the `/connect/:provider/:override?` route requires a form body parser middleware:
  85. // See https://github.com/simov/grant#dynamic-http
  86. app.use('/connect/:authProvider/:override?', express.urlencoded({ extended: false }), getCredentialsOverrideMiddleware(providers, options))
  87. app.use(Grant(grantConfig))
  88. app.use((req, res, next) => {
  89. if (options.sendSelfEndpoint) {
  90. const { protocol } = options.server
  91. res.header('i-am', `${protocol}://${options.sendSelfEndpoint}`)
  92. }
  93. next()
  94. })
  95. app.use(middlewares.cors(options))
  96. // add uppy options to the request object so it can be accessed by subsequent handlers.
  97. app.use('*', middlewares.getCompanionMiddleware(options))
  98. app.use('/s3', s3(options.s3))
  99. app.use('/url', url())
  100. app.post('/:providerName/preauth', express.json(), express.urlencoded({ extended: false }), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.preauth)
  101. app.get('/:providerName/connect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.connect)
  102. app.get('/:providerName/redirect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.redirect)
  103. app.get('/:providerName/callback', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.callback)
  104. app.post('/:providerName/deauthorization/callback', express.json(), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.deauthorizationCallback)
  105. app.get('/:providerName/logout', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.gentleVerifyToken, controllers.logout)
  106. app.get('/:providerName/send-token', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.verifyToken, controllers.sendToken)
  107. app.get('/:providerName/list/:id?', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list)
  108. // backwards compat:
  109. app.get('/search/:providerName/list', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list)
  110. app.post('/:providerName/get/:id', express.json(), middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get)
  111. // backwards compat:
  112. app.post('/search/:providerName/get/:id', express.json(), middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get)
  113. app.get('/:providerName/thumbnail/:id', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.cookieAuthToken, middlewares.verifyToken, controllers.thumbnail)
  114. app.param('providerName', providerManager.getProviderMiddleware(providers))
  115. if (app.get('env') !== 'test') {
  116. jobs.startCleanUpJob(options.filePath)
  117. }
  118. const processId = randomUUID()
  119. jobs.startPeriodicPingJob({
  120. urls: options.periodicPingUrls,
  121. interval: options.periodicPingInterval,
  122. count: options.periodicPingCount,
  123. staticPayload: options.periodicPingStaticPayload,
  124. version,
  125. processId,
  126. })
  127. return { app, emitter }
  128. }