123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- const express = require('express')
- const Grant = require('grant').default.express()
- const merge = require('lodash/merge')
- const cookieParser = require('cookie-parser')
- const interceptor = require('express-interceptor')
- const { randomUUID } = require('node:crypto')
- const grantConfig = require('./config/grant')()
- const providerManager = require('./server/provider')
- const controllers = require('./server/controllers')
- const s3 = require('./server/controllers/s3')
- const url = require('./server/controllers/url')
- const googlePicker = require('./server/controllers/googlePicker')
- const createEmitter = require('./server/emitter')
- const redis = require('./server/redis')
- const jobs = require('./server/jobs')
- const logger = require('./server/logger')
- const middlewares = require('./server/middlewares')
- const { getMaskableSecrets, defaultOptions, validateConfig } = require('./config/companion')
- const { ProviderApiError, ProviderUserError, ProviderAuthError } = require('./server/provider/error')
- const { getCredentialsOverrideMiddleware } = require('./server/provider/credentials')
- // @ts-ignore
- const { version } = require('../package.json')
- const { isOAuthProvider } = require('./server/provider/Provider')
- function setLoggerProcessName({ loggerProcessName }) {
- if (loggerProcessName != null) logger.setProcessName(loggerProcessName)
- }
- // intercepts grantJS' default response error when something goes
- // wrong during oauth process.
- const interceptGrantErrorResponse = interceptor((req, res) => {
- return {
- isInterceptable: () => {
- // match grant.js' callback url
- return /^\/connect\/\w+\/callback/.test(req.path)
- },
- intercept: (body, send) => {
- const unwantedBody = 'error=Grant%3A%20missing%20session%20or%20misconfigured%20provider'
- if (body === unwantedBody) {
- logger.error(`grant.js responded with error: ${body}`, 'grant.oauth.error', req.id)
- res.set('Content-Type', 'text/plain')
- const reqHint = req.id ? `Request ID: ${req.id}` : ''
- send([
- 'Companion was unable to complete the OAuth process :(',
- 'Error: User session is missing or the Provider was misconfigured',
- reqHint,
- ].join('\n'))
- } else {
- send(body)
- }
- },
- }
- })
- // make the errors available publicly for custom providers
- module.exports.errors = { ProviderApiError, ProviderUserError, ProviderAuthError }
- module.exports.socket = require('./server/socket')
- module.exports.setLoggerProcessName = setLoggerProcessName
- /**
- * Entry point into initializing the Companion app.
- *
- * @param {object} optionsArg
- * @returns {{ app: import('express').Express, emitter: any }}}
- */
- module.exports.app = (optionsArg = {}) => {
- setLoggerProcessName(optionsArg)
- validateConfig(optionsArg)
- const options = merge({}, defaultOptions, optionsArg)
- const providers = providerManager.getDefaultProviders()
- const { customProviders } = options
- if (customProviders) {
- providerManager.addCustomProviders(customProviders, providers, grantConfig)
- }
- const getOauthProvider = (providerName) => providers[providerName]?.oauthProvider
- providerManager.addProviderOptions(options, grantConfig, getOauthProvider)
- // mask provider secrets from log messages
- logger.setMaskables(getMaskableSecrets(options))
- // create singleton redis client if corresponding options are set
- const redisClient = redis.client(options)
- const emitter = createEmitter(redisClient, options.redisPubSubScope)
- const app = express()
- if (options.metrics) {
- app.use(middlewares.metrics({ path: options.server.path }))
- }
- app.use(cookieParser()) // server tokens are added to cookies
- app.use(interceptGrantErrorResponse)
- // override provider credentials at request time
- // Making `POST` request to the `/connect/:provider/:override?` route requires a form body parser middleware:
- // See https://github.com/simov/grant#dynamic-http
- app.use('/connect/:oauthProvider/:override?', express.urlencoded({ extended: false }), getCredentialsOverrideMiddleware(providers, options))
- app.use(Grant(grantConfig))
- app.use((req, res, next) => {
- if (options.sendSelfEndpoint) {
- const { protocol } = options.server
- res.header('i-am', `${protocol}://${options.sendSelfEndpoint}`)
- }
- next()
- })
- app.use(middlewares.cors(options))
- // add uppy options to the request object so it can be accessed by subsequent handlers.
- app.use('*', middlewares.getCompanionMiddleware(options))
- app.use('/s3', s3(options.s3))
- if (options.enableUrlEndpoint) app.use('/url', url())
- if (options.enableGooglePickerEndpoint) app.use('/google-picker', googlePicker())
- app.post('/:providerName/preauth', express.json(), express.urlencoded({ extended: false }), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.preauth)
- app.get('/:providerName/connect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.connect)
- app.get('/:providerName/redirect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.redirect)
- app.get('/:providerName/callback', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.callback)
- app.post('/:providerName/refresh-token', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.verifyToken, controllers.refreshToken)
- app.post('/:providerName/deauthorization/callback', express.json(), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.deauthorizationCallback)
- app.get('/:providerName/logout', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.gentleVerifyToken, controllers.logout)
- app.get('/:providerName/send-token', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.verifyToken, controllers.sendToken)
- app.post('/:providerName/simple-auth', express.json(), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasSimpleAuthProvider, controllers.simpleAuth)
- app.get('/:providerName/list/:id?', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list)
- // backwards compat:
- app.get('/search/:providerName/list', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list)
- app.post('/:providerName/get/:id', express.json(), middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get)
- // backwards compat:
- app.post('/search/:providerName/get/:id', express.json(), middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get)
- app.get('/:providerName/thumbnail/:id', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.cookieAuthToken, middlewares.verifyToken, controllers.thumbnail)
- // Used for testing dynamic credentials only, normally this would run on a separate server.
- if (options.testDynamicOauthCredentials) {
- app.post('/:providerName/test-dynamic-oauth-credentials', (req, res) => {
- if (req.query.secret !== options.testDynamicOauthCredentialsSecret) throw new Error('Invalid secret')
- const { providerName } = req.params
- logger.info(`Returning dynamic OAuth2 credentials for ${providerName}`)
- // for simplicity, we just return the normal credentials for the provider, but in a real-world scenario,
- // we would query based on parameters
- const { key, secret } = options.providerOptions[providerName] ?? { __proto__: null }
- function getRedirectUri() {
- const oauthProvider = getOauthProvider(providerName)
- if (!isOAuthProvider(oauthProvider)) return undefined
- return grantConfig[oauthProvider]?.redirect_uri
- }
- res.send({
- credentials: {
- key,
- secret,
- redirect_uri: getRedirectUri(),
- },
- })
- })
- }
- app.param('providerName', providerManager.getProviderMiddleware(providers, grantConfig))
- if (app.get('env') !== 'test') {
- jobs.startCleanUpJob(options.filePath)
- }
- const processId = randomUUID()
- jobs.startPeriodicPingJob({
- urls: options.periodicPingUrls,
- interval: options.periodicPingInterval,
- count: options.periodicPingCount,
- staticPayload: options.periodicPingStaticPayload,
- version,
- processId,
- })
- return { app, emitter }
- }
|