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:

189 lines (188 loc) 7.46 kB
"use strict"; const express = require('express'); const qs = require('node:querystring'); const { randomUUID } = require('node:crypto'); const helmet = require('helmet'); const morgan = require('morgan'); const { URL } = require('node:url'); const session = require('express-session'); const RedisStore = require('connect-redis').default; const logger = require('../server/logger'); const redis = require('../server/redis'); const companion = require('../companion'); const { getCompanionOptions, generateSecret, buildHelpfulStartupMessage, } = require('./helper'); /** * Configures an Express app for running Companion standalone * * @returns {object} */ module.exports = function server(inputCompanionOptions) { const companionOptions = getCompanionOptions(inputCompanionOptions); companion.setLoggerProcessName(companionOptions); if (!companionOptions.secret) companionOptions.secret = generateSecret('secret'); if (!companionOptions.preAuthSecret) companionOptions.preAuthSecret = generateSecret('preAuthSecret'); const app = express(); const router = express.Router(); if (companionOptions.server.path) { app.use(companionOptions.server.path, router); } else { app.use(router); } // Query string keys whose values should not end up in logging output. const sensitiveKeys = new Set(['access_token', 'uppyAuthToken']); /** * Obscure the contents of query string keys listed in `sensitiveKeys`. * * Returns a copy of the object with unknown types removed and sensitive values replaced by ***. * * The input type is more broad that it needs to be, this way typescript can help us guarantee that we're dealing with all * possible inputs :) * * @param {Record<string, any>} rawQuery * @returns {{ * query: Record<string, any>, * censored: boolean * }} */ function censorQuery(rawQuery) { /** @type {Record<string, any>} */ const query = {}; let censored = false; Object.keys(rawQuery).forEach((key) => { if (typeof rawQuery[key] !== 'string') { return; } if (sensitiveKeys.has(key)) { // replace logged access token query[key] = '********'; censored = true; } else { query[key] = rawQuery[key]; } }); return { query, censored }; } router.use((request, response, next) => { const headerName = 'X-Request-Id'; const oldValue = request.get(headerName); response.set(headerName, oldValue ?? randomUUID()); next(); }); // log server requests. router.use(morgan('combined')); morgan.token('url', (req) => { const { query, censored } = censorQuery(req.query); return censored ? `${req.path}?${qs.stringify(query)}` : req.originalUrl || req.url; }); morgan.token('referrer', (req) => { const ref = req.headers.referer || req.headers.referrer; if (typeof ref === 'string') { let parsed; try { parsed = new URL(ref); } catch (_) { return ref; } const rawQuery = qs.parse(parsed.search.replace('?', '')); const { query, censored } = censorQuery(rawQuery); return censored ? `${parsed.href.split('?')[0]}?${qs.stringify(query)}` : parsed.href; } return undefined; }); // Use helmet to secure Express headers router.use(helmet.frameguard()); router.use(helmet.xssFilter()); router.use(helmet.noSniff()); router.use(helmet.ieNoOpen()); app.disable('x-powered-by'); const sessionOptions = { secret: companionOptions.secret, resave: true, saveUninitialized: true, }; const redisClient = redis.client(companionOptions); if (redisClient) { sessionOptions.store = new RedisStore({ client: redisClient, prefix: process.env.COMPANION_REDIS_EXPRESS_SESSION_PREFIX || 'companion-session:', }); } if (process.env.COMPANION_COOKIE_DOMAIN) { sessionOptions.cookie = { domain: process.env.COMPANION_COOKIE_DOMAIN, maxAge: 24 * 60 * 60 * 1000, // 1 day }; } // Session is used for grant redirects, so that we don't need to expose secret tokens in URLs // See https://github.com/transloadit/uppy/pull/1668 // https://github.com/transloadit/uppy/issues/3538#issuecomment-1069232909 // https://github.com/simov/grant#callback-session router.use(session(sessionOptions)); // Routes if (process.env.COMPANION_HIDE_WELCOME !== 'true') { router.get('/', (req, res) => { res.setHeader('Content-Type', 'text/plain'); res.send(buildHelpfulStartupMessage(companionOptions)); }); } // initialize companion const { app: companionApp } = companion.app(companionOptions); // add companion to server middleware router.use(companionApp); // WARNING: This route is added in order to validate your app with OneDrive. // Only set COMPANION_ONEDRIVE_DOMAIN_VALIDATION if you are sure that you are setting the // correct value for COMPANION_ONEDRIVE_KEY (i.e application ID). If there's a slightest possiblilty // that you might have mixed the values for COMPANION_ONEDRIVE_KEY and COMPANION_ONEDRIVE_SECRET, // please DO NOT set any value for COMPANION_ONEDRIVE_DOMAIN_VALIDATION if (process.env.COMPANION_ONEDRIVE_DOMAIN_VALIDATION === 'true' && process.env.COMPANION_ONEDRIVE_KEY) { router.get('/.well-known/microsoft-identity-association.json', (req, res) => { const content = JSON.stringify({ associatedApplications: [ { applicationId: process.env.COMPANION_ONEDRIVE_KEY }, ], }); res.header('Content-Length', `${Buffer.byteLength(content, 'utf8')}`); // use writeHead to prevent 'charset' from being appended // https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-configure-publisher-domain#to-select-a-verified-domain res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(content); res.end(); }); } app.use((req, res) => { return res.status(404).json({ message: 'Not Found' }); }); app.use((err, req, res, next) => { if (app.get('env') === 'production') { // if the error is a URIError from the requested URL we only log the error message // to avoid uneccessary error alerts if (err.status === 400 && err.name === 'URIError') { logger.error(err.message, 'root.error', req.id); } else { logger.error(err, 'root.error', req.id); } res .status(500) .json({ message: 'Something went wrong', requestId: req.id }); } else { logger.error(err, 'root.error', req.id); res .status(500) .json({ message: err.message, error: err, requestId: req.id }); } }); return { app, companionOptions }; };