1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
|
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 * 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 { logPort } from './ActionUtilities';
import RouteManager from './RouteManager';
import RouteSubscriber from './RouteSubscriber';
import { publicDirectory, resolvedPorts } from './SocketData';
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';
import axios from 'axios';
import { JSDOM } from 'jsdom';
import { setupDynamicToolsAPI } from './api/dynamicTools';
/* 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('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
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(`<html><body bgcolor="red" link="006666" alink="8B4513" vlink="006666">
<title>Error</title>
<div align="center"><h1>Failed to load: ${targetUrl} </h1></div>
<p>URL is required</p>
</body></html>`);
// res.status(400).json({ error: 'URL is required' });
return;
}
// Validate URL format
try {
new URL(targetUrl);
} catch (e) {
res.send(`<html><body bgcolor="red" link="006666" alink="8B4513" vlink="006666">
<title>Error</title>
<div align="center"><h1>Failed to load: ${targetUrl} </h1></div>
<p>${e}</p>
</body></html>`);
//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 compiler = webpack(config as webpack.Configuration);
// 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.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<void>(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<Error>(resolve => server.close(resolve));
return isRelease;
}
|