import axios from 'axios'; import * as bodyParser from 'body-parser'; import { blue, yellow } from 'colors'; import * as flash from 'connect-flash'; import * as MongoStoreConnect from 'connect-mongo'; import * as express from 'express'; import * as expressFlash from 'express-flash'; import * as session from 'express-session'; import { createServer } from 'https'; import { JSDOM } from 'jsdom'; import * as passport from 'passport'; import * as webpack from 'webpack'; import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; import * as config from '../../webpack.config'; import * as workerConfig from '../../webpack.worker.config'; import { logPort } from './ActionUtilities'; import RouteManager from './RouteManager'; import RouteSubscriber from './RouteSubscriber'; import { publicDirectory, resolvedPorts } from './SocketData'; import { setupDynamicToolsAPI } from './api/dynamicTools'; import { SSL } from './apis/google/CredentialsLoader'; import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager'; import { Database } from './database'; import { WebSocket } from './websocket'; /* RouteSetter is a wrapper around the server that prevents the server from being exposed. */ export type RouteSetter = (server: RouteManager) => void; // export let disconnect: Function; export let resolvedServerUrl: string; const week = 7 * 24 * 60 * 60 * 1000; const secret = '64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc'; const store = process.env.DB === 'MEM' ? new session.MemoryStore() : MongoStoreConnect.create({ mongoUrl: Database.url }); /* Determine if the enviroment is dev mode or release mode. */ function determineEnvironment() { const isRelease = process.env.RELEASE === 'true'; const color = isRelease ? blue : yellow; const label = isRelease ? 'release' : 'development'; console.log(`\nrunning server in ${color(label)} mode`); // // swilkins: I don't think we need to read from ClientUtils.RELEASE anymore. Should be able to invoke process.env.RELEASE // // on the client side, thanks to dotenv in webpack.config.js // let clientUtils = fs.readFileSync('./src/client/util/ClientUtils.ts.temp', 'utf8'); // clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(isRelease))}`; // fs.writeFileSync('./src/client/util/ClientUtils.ts', clientUtils, 'utf8'); return isRelease; } function buildWithMiddleware(server: express.Express) { [ session({ secret, resave: false, cookie: { maxAge: week }, saveUninitialized: true, store, }), flash(), expressFlash(), bodyParser.json({ limit: '10mb' }), bodyParser.urlencoded({ extended: true }), passport.initialize(), passport.session(), (req: express.Request, res: express.Response, next: express.NextFunction) => { res.locals.user = req.user; // console.log('HEADER:' + req.originalUrl + ' path = ' + req.path); if ((req.originalUrl.endsWith('.png') || req.originalUrl.endsWith('.jpg') || (process.env.RELEASE === 'true' && req.originalUrl.endsWith('.js'))) && req.method === 'GET') { const period = 30000; res.set('Cache-control', `public, max-age=${period}`); } else { // for the other requests set strict no caching parameters res.set('Cache-control', `no-store`); } next(); }, ].forEach(next => server.use(next)); return server; } function registerCorsProxy(server: express.Express) { // .replace('', ' ') server.use('/corsproxy', async (req, res) => { try { // Extract URL from either query param or path let targetUrl: string; if (req.query.url) { // Case 1: URL passed as query parameter (/corsproxy?url=...) targetUrl = req.query.url as string; } else { // Case 2: URL passed as path (/corsproxy/http://example.com) const path = req.originalUrl.replace(/^\/corsproxy\/?/, ''); targetUrl = decodeURIComponent(path); // Add protocol if missing (assuming https as default) if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) { targetUrl = `https://${targetUrl}`; } } if (!targetUrl) { res.send(` Error

Failed to load: ${targetUrl}

URL is required

`); // res.status(400).json({ error: 'URL is required' }); return; } // Validate URL format try { new URL(targetUrl); } catch (e) { res.send(` Error

Failed to load: ${targetUrl}

${e}

`); //res.status(400).json({ error: 'Invalid URL format' }); return; } const isBinary = /\.(gif|png|jpe?g|bmp|webp|ico|pdf|zip|mp3|mp4|wav|ogg)$/i.test(targetUrl as string); const responseType = isBinary ? 'arraybuffer' : 'text'; const response = await axios.get(targetUrl as string, { headers: { 'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0' }, responseType: responseType, }); const baseUrl = new URL(targetUrl as string); if (response.headers['content-type']?.includes('text/html')) { const dom = new JSDOM(response.data); const document = dom.window.document; // Process all elements with href/src const elements = document.querySelectorAll('[href],[src]'); elements.forEach(elem => { const attrs = []; if (elem.hasAttribute('href')) attrs.push('href'); if (elem.hasAttribute('src')) attrs.push('src'); attrs.forEach(attr => { const originalUrl = elem.getAttribute(attr); if (!originalUrl || originalUrl.startsWith('http://') || originalUrl.startsWith('https://') || originalUrl.startsWith('data:') || /^[a-z]+:/.test(originalUrl)) { return; } const resolvedUrl = new URL(originalUrl, baseUrl).toString(); elem.setAttribute(attr, resolvedUrl); }); }); // Handle base tag const baseTags = document.querySelectorAll('base'); baseTags.forEach(tag => tag.remove()); const newBase = document.createElement('base'); newBase.setAttribute('href', `${baseUrl}/`); document.head.insertBefore(newBase, document.head.firstChild); response.data = dom.serialize(); } res.set({ 'Access-Control-Allow-Origin': '*', 'Content-Type': response.headers['content-type'], }).send(response.data); } catch (error: unknown) { res.status(500).json({ error: 'Proxy error', details: (error as { message: string }).message }); } }); } function registerAuthenticationRoutes(server: express.Express) { server.get('/signup', getSignup); server.post('/signup', postSignup); server.get('/login', getLogin); server.post('/login', postLogin); server.get('/logout', getLogout); server.get('/forgotPassword', getForgot); server.post('/forgotPassword', postForgot); const reset = new RouteSubscriber('resetPassword').add('token').build; server.get(reset, getReset); server.post(reset, postReset); } export default async function InitializeServer(routeSetter: RouteSetter) { const isRelease = determineEnvironment(); const app = buildWithMiddleware(express()); const workerCompiler = webpack(workerConfig as webpack.Configuration); const compiler = webpack(config as webpack.Configuration); if (!compiler) throw new Error('Webpack compiler is not defined. Please check your webpack configuration.'); if (!workerCompiler) throw new Error('Webpack worker compiler is not defined. Please check your webpack worker configuration.'); // print out contents of virtual output filesystem used in development // compiler.outputFileSystem?.readdir?.(config.output.path, (err, files) => (err ? console.error('Error reading virtual output path:', err) : console.log('Files in virtual output path:', files))); // Default route app.get('/', (req, res) => { res.redirect(req.user ? '/home' : '/login'); //res.send('This is the default route.'); }); // route table managed by express. routes are tested sequentially against each of these map rules. when a match is found, the handler is called to process the request app.use(wdm(compiler, { publicPath: config.output.publicPath })); app.use(whm(compiler)); app.use(wdm(workerCompiler, { publicPath: workerConfig.output.publicPath })); app.use(whm(workerCompiler)); app.get(/^\/+$/, (req, res) => res.redirect(req.user ? '/home' : '/login')); // target urls that consist of one or more '/'s with nothing in between app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader('Access-Control-Allow-Origin', '*') })); // all urls that start with dash's public directory: /files/ (e.g., /files/images, /files/audio, etc) // app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) })); registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc) registerCorsProxy(app); // this adds a /corsproxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies // Set up the dynamic tools API setupDynamicToolsAPI(app); isRelease && !SSL.Loaded && SSL.exit(); routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc) isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort)); const server = isRelease ? createServer(SSL.Credentials, app) : app; await new Promise(resolve => { server.listen(resolvedPorts.server, resolve); }); logPort('server', resolvedPorts.server); resolvedServerUrl = `${isRelease && process.env.serverName ? `https://${process.env.serverName}.com` : 'http://localhost'}:${resolvedPorts.server}`; // initialize the web socket (bidirectional communication: if a user changes // a field on one client, that change must be broadcast to all other clients) await WebSocket.initialize(isRelease, SSL.Credentials); // disconnect = async () => new Promise(resolve => server.close(resolve)); return isRelease; }