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:

197 lines (196 loc) 7.88 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; };