UNPKG

@uppy/companion

Version:

OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:

261 lines (231 loc) 7.64 kB
import corsImport from 'cors' import promBundle from 'express-prom-bundle' import packageJson from '../../package.json' with { type: 'json' } import * as tokenService from './helpers/jwt.js' import { getURLBuilder } from './helpers/utils.js' import * as logger from './logger.js' import { isOAuthProvider } from './provider/Provider.js' import getS3Client from './s3-client.js' export const hasSessionAndProvider = (req, res, next) => { if (!req.session) { logger.debug( 'No session attached to req object. Exiting dispatcher.', null, req.id, ) return res.sendStatus(400) } if (!req.companion.provider) { logger.debug( 'No provider/provider-handler found. Exiting dispatcher.', null, req.id, ) return res.sendStatus(400) } return next() } const isOAuthProviderReq = (req) => isOAuthProvider(req.companion.providerClass.oauthProvider) const isSimpleAuthProviderReq = (req) => !!req.companion.providerClass.hasSimpleAuth /** * Middleware can be used to verify that the current request is to an OAuth provider * This is because not all requests are supported by non-oauth providers (formerly known as SearchProviders) */ export const hasOAuthProvider = (req, res, next) => { if (!isOAuthProviderReq(req)) { logger.debug('Provider does not support OAuth.', null, req.id) return res.sendStatus(400) } return next() } export const hasSimpleAuthProvider = (req, res, next) => { if (!isSimpleAuthProviderReq(req)) { logger.debug('Provider does not support simple auth.', null, req.id) return res.sendStatus(400) } return next() } export const hasBody = (req, res, next) => { if (!req.body) { logger.debug( 'No body attached to req object. Exiting dispatcher.', null, req.id, ) return res.sendStatus(400) } return next() } export const hasSearchQuery = (req, res, next) => { if (typeof req.query.q !== 'string') { logger.debug( 'search request has no search query', 'search.query.check', req.id, ) return res.sendStatus(400) } return next() } export const verifyToken = (req, res, next) => { if (isOAuthProviderReq(req) || isSimpleAuthProviderReq(req)) { // For OAuth / simple auth provider, we find the encrypted auth token from the header: const token = req.companion.authToken if (token == null) { logger.info('cannot auth token', 'token.verify.unset', req.id) res.sendStatus(401) return } const { providerName } = req.params try { const payload = tokenService.verifyEncryptedAuthToken( token, req.companion.options.secret, providerName, ) req.companion.providerUserSession = payload[providerName] } catch (err) { logger.error(err.message, 'token.verify.error', req.id) res.sendStatus(401) return } next() return } // for non auth providers, we just load the static key from options if (!isOAuthProviderReq(req)) { const { providerOptions } = req.companion.options const { providerName } = req.params const key = providerOptions[providerName]?.key if (!key) { logger.info( `unconfigured credentials for ${providerName}`, 'non.oauth.token.load.unset', req.id, ) res.sendStatus(501) return } req.companion.providerUserSession = { accessToken: key, } next() } } // does not fail if token is invalid export const gentleVerifyToken = (req, res, next) => { const { providerName } = req.params if (req.companion.authToken) { try { const payload = tokenService.verifyEncryptedAuthToken( req.companion.authToken, req.companion.options.secret, providerName, ) req.companion.providerUserSession = payload[providerName] } catch (err) { logger.error(err.message, 'token.gentle.verify.error', req.id) } } next() } export const cookieAuthToken = (req, res, next) => { req.companion.authToken = req.cookies[`uppyAuthToken--${req.companion.providerClass.oauthProvider}`] return next() } export const cors = (options = {}) => (req, res, next) => { // HTTP headers are not case sensitive, and express always handles them in lower case, so that's why we lower case them. // I believe that HTTP verbs are case sensitive, and should be uppercase. const existingExposeHeaders = res.get('Access-Control-Expose-Headers') const exposeHeadersSet = new Set( existingExposeHeaders ?.split(',') ?.map((method) => method.trim().toLowerCase()), ) if (options.sendSelfEndpoint) exposeHeadersSet.add('i-am') // Needed for basic operation: https://github.com/transloadit/uppy/issues/3021 const allowedHeaders = [ 'uppy-auth-token', 'uppy-credentials-params', 'authorization', 'origin', 'content-type', 'accept', ] const existingAllowHeaders = res.get('Access-Control-Allow-Headers') const allowHeadersSet = new Set( existingAllowHeaders ? existingAllowHeaders .split(',') .map((method) => method.trim().toLowerCase()) .concat(allowedHeaders) : allowedHeaders, ) const existingAllowMethods = res.get('Access-Control-Allow-Methods') const allowMethodsSet = new Set( existingAllowMethods ?.split(',') ?.map((method) => method.trim().toUpperCase()), ) // Needed for basic operation: allowMethodsSet.add('GET').add('POST').add('OPTIONS').add('DELETE') // If endpoint urls are specified, then we only allow those endpoints. // Otherwise, we allow any client url to access companion. // Must be set to at least true (origin "*" with "credentials: true" will cause error in many browsers) // https://github.com/expressjs/cors/issues/119 // allowedOrigins can also be any type supported by https://github.com/expressjs/cors#configuration-options const { corsOrigins: origin = true } = options // Because we need to merge with existing headers, we need to call cors inside our own middleware return corsImport({ credentials: true, origin, methods: Array.from(allowMethodsSet), allowedHeaders: Array.from(allowHeadersSet).join(','), exposedHeaders: Array.from(exposeHeadersSet).join(','), })(req, res, next) } export const metrics = ({ path = undefined } = {}) => { const metricsMiddleware = promBundle({ includeMethod: true, metricsPath: path ? `${path}/metrics` : undefined, }) // @ts-ignore Not in the typings, but it does exist const { promClient } = metricsMiddleware const { collectDefaultMetrics } = promClient collectDefaultMetrics({ register: promClient.register }) // Add version as a prometheus gauge const versionGauge = new promClient.Gauge({ name: 'companion_version', help: 'npm version as an integer', }) const numberVersion = Number(packageJson.version.replace(/\D/g, '')) versionGauge.set(numberVersion) return metricsMiddleware } /** * * @param {object} options */ export const getCompanionMiddleware = (options) => { /** * @param {object} req * @param {object} res * @param {Function} next */ const middleware = (req, res, next) => { req.companion = { options, s3Client: getS3Client(options, false), s3ClientCreatePresignedPost: getS3Client(options, true), authToken: req.header('uppy-auth-token') || req.query.uppyAuthToken, buildURL: getURLBuilder(options), } next() } return middleware }