companion.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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 googlePicker = require('./server/controllers/googlePicker')
  13. const createEmitter = require('./server/emitter')
  14. const redis = require('./server/redis')
  15. const jobs = require('./server/jobs')
  16. const logger = require('./server/logger')
  17. const middlewares = require('./server/middlewares')
  18. const { getMaskableSecrets, defaultOptions, validateConfig } = require('./config/companion')
  19. const { ProviderApiError, ProviderUserError, ProviderAuthError } = require('./server/provider/error')
  20. const { getCredentialsOverrideMiddleware } = require('./server/provider/credentials')
  21. // @ts-ignore
  22. const { version } = require('../package.json')
  23. const { isOAuthProvider } = require('./server/provider/Provider')
  24. function setLoggerProcessName({ loggerProcessName }) {
  25. if (loggerProcessName != null) logger.setProcessName(loggerProcessName)
  26. }
  27. // intercepts grantJS' default response error when something goes
  28. // wrong during oauth process.
  29. const interceptGrantErrorResponse = interceptor((req, res) => {
  30. return {
  31. isInterceptable: () => {
  32. // match grant.js' callback url
  33. return /^\/connect\/\w+\/callback/.test(req.path)
  34. },
  35. intercept: (body, send) => {
  36. const unwantedBody = 'error=Grant%3A%20missing%20session%20or%20misconfigured%20provider'
  37. if (body === unwantedBody) {
  38. logger.error(`grant.js responded with error: ${body}`, 'grant.oauth.error', req.id)
  39. res.set('Content-Type', 'text/plain')
  40. const reqHint = req.id ? `Request ID: ${req.id}` : ''
  41. send([
  42. 'Companion was unable to complete the OAuth process :(',
  43. 'Error: User session is missing or the Provider was misconfigured',
  44. reqHint,
  45. ].join('\n'))
  46. } else {
  47. send(body)
  48. }
  49. },
  50. }
  51. })
  52. // make the errors available publicly for custom providers
  53. module.exports.errors = { ProviderApiError, ProviderUserError, ProviderAuthError }
  54. module.exports.socket = require('./server/socket')
  55. module.exports.setLoggerProcessName = setLoggerProcessName
  56. /**
  57. * Entry point into initializing the Companion app.
  58. *
  59. * @param {object} optionsArg
  60. * @returns {{ app: import('express').Express, emitter: any }}}
  61. */
  62. module.exports.app = (optionsArg = {}) => {
  63. setLoggerProcessName(optionsArg)
  64. validateConfig(optionsArg)
  65. const options = merge({}, defaultOptions, optionsArg)
  66. const providers = providerManager.getDefaultProviders()
  67. const { customProviders } = options
  68. if (customProviders) {
  69. providerManager.addCustomProviders(customProviders, providers, grantConfig)
  70. }
  71. const getOauthProvider = (providerName) => providers[providerName]?.oauthProvider
  72. providerManager.addProviderOptions(options, grantConfig, getOauthProvider)
  73. // mask provider secrets from log messages
  74. logger.setMaskables(getMaskableSecrets(options))
  75. // create singleton redis client if corresponding options are set
  76. const redisClient = redis.client(options)
  77. const emitter = createEmitter(redisClient, options.redisPubSubScope)
  78. const app = express()
  79. if (options.metrics) {
  80. app.use(middlewares.metrics({ path: options.server.path }))
  81. }
  82. app.use(cookieParser()) // server tokens are added to cookies
  83. app.use(interceptGrantErrorResponse)
  84. // override provider credentials at request time
  85. // Making `POST` request to the `/connect/:provider/:override?` route requires a form body parser middleware:
  86. // See https://github.com/simov/grant#dynamic-http
  87. app.use('/connect/:oauthProvider/:override?', express.urlencoded({ extended: false }), getCredentialsOverrideMiddleware(providers, options))
  88. app.use(Grant(grantConfig))
  89. app.use((req, res, next) => {
  90. if (options.sendSelfEndpoint) {
  91. const { protocol } = options.server
  92. res.header('i-am', `${protocol}://${options.sendSelfEndpoint}`)
  93. }
  94. next()
  95. })
  96. app.use(middlewares.cors(options))
  97. // add uppy options to the request object so it can be accessed by subsequent handlers.
  98. app.use('*', middlewares.getCompanionMiddleware(options))
  99. app.use('/s3', s3(options.s3))
  100. if (options.enableUrlEndpoint) app.use('/url', url())
  101. if (options.enableGooglePickerEndpoint) app.use('/google-picker', googlePicker())
  102. app.post('/:providerName/preauth', express.json(), express.urlencoded({ extended: false }), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.preauth)
  103. app.get('/:providerName/connect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.connect)
  104. app.get('/:providerName/redirect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.redirect)
  105. app.get('/:providerName/callback', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.callback)
  106. app.post('/:providerName/refresh-token', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.verifyToken, controllers.refreshToken)
  107. app.post('/:providerName/deauthorization/callback', express.json(), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.deauthorizationCallback)
  108. app.get('/:providerName/logout', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.gentleVerifyToken, controllers.logout)
  109. app.get('/:providerName/send-token', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.verifyToken, controllers.sendToken)
  110. app.post('/:providerName/simple-auth', express.json(), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasSimpleAuthProvider, controllers.simpleAuth)
  111. app.get('/:providerName/list/:id?', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list)
  112. // backwards compat:
  113. app.get('/search/:providerName/list', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list)
  114. app.post('/:providerName/get/:id', express.json(), middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get)
  115. // backwards compat:
  116. app.post('/search/:providerName/get/:id', express.json(), middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get)
  117. app.get('/:providerName/thumbnail/:id', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.cookieAuthToken, middlewares.verifyToken, controllers.thumbnail)
  118. // Used for testing dynamic credentials only, normally this would run on a separate server.
  119. if (options.testDynamicOauthCredentials) {
  120. app.post('/:providerName/test-dynamic-oauth-credentials', (req, res) => {
  121. if (req.query.secret !== options.testDynamicOauthCredentialsSecret) throw new Error('Invalid secret')
  122. const { providerName } = req.params
  123. logger.info(`Returning dynamic OAuth2 credentials for ${providerName}`)
  124. // for simplicity, we just return the normal credentials for the provider, but in a real-world scenario,
  125. // we would query based on parameters
  126. const { key, secret } = options.providerOptions[providerName] ?? { __proto__: null }
  127. function getRedirectUri() {
  128. const oauthProvider = getOauthProvider(providerName)
  129. if (!isOAuthProvider(oauthProvider)) return undefined
  130. return grantConfig[oauthProvider]?.redirect_uri
  131. }
  132. res.send({
  133. credentials: {
  134. key,
  135. secret,
  136. redirect_uri: getRedirectUri(),
  137. },
  138. })
  139. })
  140. }
  141. app.param('providerName', providerManager.getProviderMiddleware(providers, grantConfig))
  142. if (app.get('env') !== 'test') {
  143. jobs.startCleanUpJob(options.filePath)
  144. }
  145. const processId = randomUUID()
  146. jobs.startPeriodicPingJob({
  147. urls: options.periodicPingUrls,
  148. interval: options.periodicPingInterval,
  149. count: options.periodicPingCount,
  150. staticPayload: options.periodicPingStaticPayload,
  151. version,
  152. processId,
  153. })
  154. return { app, emitter }
  155. }