mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-17 16:50:22 +00:00
303 lines
9.3 KiB
TypeScript
303 lines
9.3 KiB
TypeScript
import corsImport from 'cors'
|
|
import type { NextFunction, Request, RequestHandler, Response } from 'express'
|
|
import promBundle from 'express-prom-bundle'
|
|
|
|
import packageJson from '../../package.json' with { type: 'json' }
|
|
import type { CompanionRuntimeOptions } from '../types/companion-options.js'
|
|
import type { ProviderUserSession } from '../types/express.js'
|
|
import * as tokenService from './helpers/jwt.js'
|
|
import { getCookieName } 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: RequestHandler = (req, res, next) => {
|
|
if (!req.session) {
|
|
logger.debug(
|
|
'No session attached to req object. Exiting dispatcher.',
|
|
undefined,
|
|
req.id,
|
|
)
|
|
return res.sendStatus(400)
|
|
}
|
|
|
|
if (!req.companion.provider) {
|
|
logger.debug(
|
|
'No provider/provider-handler found. Exiting dispatcher.',
|
|
undefined,
|
|
req.id,
|
|
)
|
|
return res.sendStatus(400)
|
|
}
|
|
|
|
return next()
|
|
}
|
|
|
|
const isOAuthProviderReq = (req: Request) =>
|
|
isOAuthProvider(req.companion.providerClass?.oauthProvider)
|
|
const isSimpleAuthProviderReq = (req: Request) =>
|
|
!!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: RequestHandler = (req, res, next) => {
|
|
if (!isOAuthProviderReq(req)) {
|
|
logger.debug('Provider does not support OAuth.', undefined, req.id)
|
|
return res.sendStatus(400)
|
|
}
|
|
|
|
return next()
|
|
}
|
|
|
|
export const hasSimpleAuthProvider: RequestHandler = (req, res, next) => {
|
|
if (!isSimpleAuthProviderReq(req)) {
|
|
logger.debug('Provider does not support simple auth.', undefined, req.id)
|
|
return res.sendStatus(400)
|
|
}
|
|
|
|
return next()
|
|
}
|
|
|
|
export const hasBody: RequestHandler = (req, res, next) => {
|
|
if (!req.body) {
|
|
logger.debug(
|
|
'No body attached to req object. Exiting dispatcher.',
|
|
undefined,
|
|
req.id,
|
|
)
|
|
return res.sendStatus(400)
|
|
}
|
|
|
|
return next()
|
|
}
|
|
|
|
export const hasSearchQuery: RequestHandler = (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: RequestHandler = (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 { secret } = req.companion.options
|
|
const providerName = req.params['providerName']
|
|
if (typeof providerName !== 'string' || providerName.length === 0) {
|
|
res.sendStatus(400)
|
|
return
|
|
}
|
|
try {
|
|
const payload = tokenService.verifyEncryptedAuthToken<
|
|
Record<string, ProviderUserSession>
|
|
>(token, secret, providerName)
|
|
req.companion.providerUserSession = payload[providerName]
|
|
} catch (err) {
|
|
logger.error(
|
|
err instanceof Error ? err.message : String(err),
|
|
'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['providerName']
|
|
const providerOption =
|
|
typeof providerName === 'string'
|
|
? providerOptions?.[providerName]
|
|
: undefined
|
|
const key = providerOption?.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: RequestHandler = (req, res, next) => {
|
|
const providerName = req.params['providerName']
|
|
if (typeof providerName !== 'string' || providerName.length === 0) {
|
|
next()
|
|
return
|
|
}
|
|
const { secret } = req.companion.options
|
|
if (req.companion.authToken != null) {
|
|
try {
|
|
const payload = tokenService.verifyEncryptedAuthToken<
|
|
Record<string, ProviderUserSession>
|
|
>(req.companion.authToken, secret, providerName)
|
|
req.companion.providerUserSession = payload[providerName]
|
|
} catch (err) {
|
|
logger.error(
|
|
err instanceof Error ? err.message : String(err),
|
|
'token.gentle.verify.error',
|
|
req.id,
|
|
)
|
|
}
|
|
}
|
|
next()
|
|
}
|
|
|
|
export const cookieAuthToken: RequestHandler = (req, res, next) => {
|
|
const oauthProvider = req.companion.providerClass?.oauthProvider
|
|
if (oauthProvider == null || oauthProvider.length === 0) {
|
|
return next()
|
|
}
|
|
req.companion.authToken = req.cookies[getCookieName(oauthProvider)]
|
|
return next()
|
|
}
|
|
|
|
export const cors =
|
|
(options: {
|
|
corsOrigins?: CompanionRuntimeOptions['corsOrigins']
|
|
sendSelfEndpoint?: CompanionRuntimeOptions['sendSelfEndpoint']
|
|
}): RequestHandler =>
|
|
(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()),
|
|
)
|
|
|
|
const sendSelfEndpoint = options['sendSelfEndpoint']
|
|
if (sendSelfEndpoint != null) {
|
|
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,
|
|
methods: Array.from(allowMethodsSet),
|
|
allowedHeaders: Array.from(allowHeadersSet).join(','),
|
|
exposedHeaders: Array.from(exposeHeadersSet).join(','),
|
|
...(origin !== undefined && { origin }),
|
|
})(req, res, next)
|
|
}
|
|
|
|
function hasPromClient(mw: unknown): mw is RequestHandler & {
|
|
promClient: {
|
|
collectDefaultMetrics: (opts: unknown) => void
|
|
Gauge: new (opts: unknown) => { set: (n: number) => void }
|
|
register: unknown
|
|
}
|
|
} {
|
|
return !!mw && typeof mw === 'function' && 'promClient' in mw
|
|
}
|
|
|
|
export const metrics = ({
|
|
path = undefined,
|
|
}: {
|
|
path?: string
|
|
} = {}): RequestHandler => {
|
|
const metricsMiddleware = promBundle({
|
|
includeMethod: true,
|
|
...(path && { metricsPath: `${path}/metrics` }),
|
|
})
|
|
if (!hasPromClient(metricsMiddleware)) return metricsMiddleware
|
|
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
|
|
}
|
|
|
|
export const getCompanionMiddleware = (
|
|
options: CompanionRuntimeOptions,
|
|
): RequestHandler => {
|
|
const middleware = (req: Request, _res: Response, next: NextFunction) => {
|
|
const s3Client = getS3Client(options, false)
|
|
const s3ClientCreatePresignedPost = getS3Client(options, true)
|
|
const authToken =
|
|
req.header('uppy-auth-token') || req.query['uppyAuthToken']
|
|
|
|
req.companion = {
|
|
options,
|
|
buildURL: getURLBuilder(options),
|
|
...(s3Client && { s3Client }),
|
|
...(s3ClientCreatePresignedPost && { s3ClientCreatePresignedPost }),
|
|
...(typeof authToken === 'string' && { authToken }),
|
|
}
|
|
next()
|
|
}
|
|
|
|
return middleware
|
|
}
|