Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
QuiteAFancyEmerald
GitHub Repository: QuiteAFancyEmerald/Holy-Unblocker
Path: blob/master/src/server.mjs
11035 views
1
import Fastify from 'fastify';
2
import { createServer } from 'node:http';
3
import { server as wisp, logging } from "@mercuryworkshop/wisp-js/server";
4
import createRammerhead from '../lib/rammerhead/src/server/index.js';
5
import fastifyHelmet from '@fastify/helmet';
6
import fastifyStatic from '@fastify/static';
7
import {
8
config,
9
serverUrl,
10
pages,
11
externalPages,
12
getAltPrefix,
13
} from './routes.mjs';
14
import { tryReadFile, preloaded404 } from './templates.mjs';
15
import { fileURLToPath } from 'node:url';
16
import { existsSync, unlinkSync } from 'node:fs';
17
18
/* Record the server's location as a URL object, including its host and port.
19
* The host can be modified at /src/config.json, whereas the ports can be modified
20
* at /ecosystem.config.js.
21
*/
22
console.log(serverUrl);
23
24
// Wisp Configuration: Refer to the documentation at https://www.npmjs.com/package/@mercuryworkshop/wisp-js
25
26
logging.set_level(logging.NONE);
27
wisp.options.allow_udp_streams = false;
28
wisp.options.allow_loopback_ips = true;
29
30
// For security reasons only allow these ports. Any additional regional proxies or default sandboxed Tor ports should be added here.
31
wisp.options.port_whitelist = [
32
80,
33
443,
34
9050,
35
7000,
36
7001
37
];
38
39
// The server will check for the existence of this file when a shutdown is requested.
40
// The shutdown script in run-command.js will temporarily produce this file.
41
const shutdown = fileURLToPath(new URL('./.shutdown', import.meta.url));
42
43
const rh = createRammerhead();
44
const rammerheadScopes = [
45
'/rammerhead.js',
46
'/hammerhead.js',
47
'/transport-worker.js',
48
'/task.js',
49
'/iframe-task.js',
50
'/worker-hammerhead.js',
51
'/messaging',
52
'/sessionexists',
53
'/deletesession',
54
'/newsession',
55
'/editsession',
56
'/needpassword',
57
'/syncLocalStorage',
58
'/api/shuffleDict',
59
'/mainport',
60
].map((pathname) => pathname.replace('/', serverUrl.pathname));
61
62
const rammerheadSession = new RegExp(
63
`^${serverUrl.pathname.replaceAll('.', '\\.')}[a-z0-9]{32}`
64
),
65
shouldRouteRh = (req) => {
66
try {
67
const url = new URL(req.url, serverUrl);
68
return (
69
rammerheadScopes.includes(url.pathname) ||
70
rammerheadSession.test(url.pathname)
71
);
72
} catch (e) {
73
return false;
74
}
75
},
76
routeRhRequest = (req, res) => {
77
req.url = req.url.slice(serverUrl.pathname.length - 1);
78
rh.emit('request', req, res);
79
},
80
routeRhUpgrade = (req, socket, head) => {
81
req.url = req.url.slice(serverUrl.pathname.length - 1);
82
rh.emit('upgrade', req, socket, head);
83
};
84
85
// Create a server factory for Rammerhead and Wisp
86
const serverFactory = (handler) => {
87
return createServer()
88
.on('request', (req, res) => {
89
if (shouldRouteRh(req)) routeRhRequest(req, res);
90
else handler(req, res);
91
})
92
.on('upgrade', (req, socket, head) => {
93
if (shouldRouteRh(req)) routeRhUpgrade(req, socket, head);
94
else if (req.url.endsWith(getAltPrefix('wisp', serverUrl.pathname)))
95
wisp.routeRequest(req, socket, head);
96
});
97
};
98
99
// Set logger to true for logs.
100
const app = Fastify({
101
routerOptions: {
102
ignoreDuplicateSlashes: true,
103
ignoreTrailingSlash: true,
104
},
105
logger: false,
106
serverFactory: serverFactory,
107
});
108
109
// Apply Helmet middleware for security.
110
app.register(fastifyHelmet, {
111
contentSecurityPolicy: false, // Disable CSP
112
xPoweredBy: false,
113
});
114
115
// Assign server file paths to different paths, for serving content on the website.
116
app.register(fastifyStatic, {
117
root: fileURLToPath(new URL('../views/dist/pages', import.meta.url)),
118
prefix: serverUrl.pathname,
119
decorateReply: false,
120
});
121
122
// All entries in the dist folder are created with source rewrites.
123
// Minified scripts are also served here, if minification is enabled.
124
[
125
'assets',
126
'archive',
127
'uv',
128
'scram',
129
'epoxy',
130
'libcurl',
131
'baremux',
132
'chii',
133
].forEach((prefix) => {
134
app.register(fastifyStatic, {
135
root: fileURLToPath(new URL('../views/dist/' + prefix, import.meta.url)),
136
prefix: getAltPrefix(prefix, serverUrl.pathname),
137
decorateReply: false,
138
});
139
});
140
141
app.register(fastifyStatic, {
142
root: fileURLToPath(
143
new URL('../views/dist/archive/gfiles/rarch', import.meta.url)
144
),
145
prefix: getAltPrefix('serving', serverUrl.pathname),
146
decorateReply: false,
147
});
148
149
// You should NEVER commit roms, due to piracy concerns.
150
['cores', 'info', 'roms'].forEach((prefix) => {
151
app.register(fastifyStatic, {
152
root: fileURLToPath(
153
new URL('../views/dist/archive/gfiles/rarch/' + prefix, import.meta.url)
154
),
155
prefix: getAltPrefix(prefix, serverUrl.pathname),
156
decorateReply: false,
157
});
158
});
159
160
app.register(fastifyStatic, {
161
root: fileURLToPath(
162
new URL('../views/dist/archive/gfiles/rarch/cores', import.meta.url)
163
),
164
prefix: getAltPrefix('uauth', serverUrl.pathname),
165
decorateReply: false,
166
});
167
168
/* If you are trying to add pages or assets in the root folder and
169
* NOT entire folders, check ./src/routes.mjs and add it manually.
170
*
171
* All website files are stored in the /views directory.
172
* This takes one of those files and displays it for a site visitor.
173
* Paths like /browsing are converted into paths like /views/dist/pages/surf.html
174
* back here. Which path converts to what is defined in routes.mjs.
175
*/
176
177
const supportedTypes = {
178
default: config.disguiseFiles ? 'image/vnd.microsoft.icon' : 'text/html',
179
html: 'text/html',
180
txt: 'text/plain',
181
xml: 'application/xml',
182
ico: 'image/vnd.microsoft.icon',
183
},
184
disguise = 'ico';
185
186
if (config.disguiseFiles) {
187
const getActualPath = (path) =>
188
path.slice(0, path.length - 1 - disguise.length),
189
shouldNotHandle = new RegExp(`\\.(?!html$|${disguise}$)[\\w-]+$`, 'i'),
190
loaderFile = tryReadFile(
191
'../views/dist/pages/misc/deobf/loader.html',
192
import.meta.url,
193
false
194
);
195
let exemptDirs = [
196
'assets',
197
'uv',
198
'scram',
199
'epoxy',
200
'libcurl',
201
'baremux',
202
'wisp',
203
'chii',
204
].map((dir) => getAltPrefix(dir, serverUrl.pathname).slice(1, -1)),
205
exemptPages = ['login', 'test-shutdown', 'favicon.ico'];
206
for (const [key, value] of Object.entries(externalPages))
207
if ('string' === typeof value) exemptPages.push(key);
208
else exemptDirs.push(key);
209
for (const path of rammerheadScopes)
210
if (!shouldNotHandle.test(path)) exemptDirs.push(path.slice(1));
211
exemptPages = exemptPages.concat(exemptDirs);
212
if (pages.default === 'login') exemptPages.push('');
213
214
app.addHook('preHandler', (req, reply, done) => {
215
if (req.params.modified) return done();
216
const reqPath = new URL(req.url, serverUrl).pathname.slice(
217
serverUrl.pathname.length
218
);
219
if (
220
shouldNotHandle.test(reqPath) ||
221
exemptDirs.some((dir) => reqPath.indexOf(dir + '/') === 0) ||
222
exemptPages.includes(reqPath) ||
223
rammerheadSession.test(serverUrl.pathname + reqPath)
224
)
225
return done();
226
227
if (!reqPath.endsWith('.' + disguise)) {
228
reply.type(supportedTypes.html).send(loaderFile);
229
reply.hijack();
230
return done();
231
} else if (!(reqPath in pages) && !reqPath.endsWith('favicon.ico')) {
232
req.params.modified = true;
233
req.raw.url = getActualPath(req.raw.url);
234
if (req.params.path) req.params.path = getActualPath(req.params.path);
235
if (req.params['*']) req.params['*'] = getActualPath(req.params['*']);
236
reply.type(supportedTypes[disguise]);
237
reply.header('Access-Control-Allow-Origin', 'null');
238
}
239
return done();
240
});
241
}
242
243
app.get(serverUrl.pathname + ':path', (req, reply) => {
244
// Testing for future features that need cookies to deliver alternate source files.
245
/*
246
if (req.raw.rawHeaders.includes('Cookie'))
247
console.log(
248
'cookie:',
249
req.raw.rawHeaders[req.raw.rawHeaders.indexOf('Cookie') + 1]
250
);
251
*/
252
253
const reqPath = req.params.path;
254
255
// Ignore browsers' automatic requests to favicon.ico, since it does not exist.
256
// This approach is needed for certain pages to not have an icon.
257
if (reqPath === 'favicon.ico') {
258
reply.send();
259
return reply.hijack();
260
}
261
262
if (reqPath in externalPages) {
263
if (req.params.modified)
264
return reply.code(404).type(supportedTypes.html).send(preloaded404);
265
let externalRoute = externalPages[reqPath];
266
if (typeof externalRoute !== 'string')
267
externalRoute = externalRoute.default;
268
return reply.redirect(externalRoute);
269
}
270
271
// If a GET request is sent to /test-shutdown and a script-generated shutdown file
272
// is present, gracefully shut the server down.
273
if (reqPath === 'test-shutdown' && existsSync(shutdown)) {
274
console.log('Holy Unblocker is shutting down.');
275
app.close();
276
unlinkSync(shutdown);
277
process.exitCode = 0;
278
}
279
280
// Return the error page if the query is not found in routes.mjs.
281
if (reqPath && !(reqPath in pages))
282
return reply.code(404).type(supportedTypes.default).send(preloaded404);
283
284
// Serve the default page if the path is the default path.
285
const fileName = reqPath ? pages[reqPath] : pages[pages.default],
286
type =
287
supportedTypes[fileName.slice(fileName.lastIndexOf('.') + 1)] ||
288
supportedTypes.default;
289
290
if (req.params.modified) reply.type(supportedTypes[disguise]);
291
else reply.type(type);
292
reply.send(tryReadFile('../views/dist/' + fileName, import.meta.url));
293
});
294
295
app.get(serverUrl.pathname + 'github/:redirect', (req, reply) => {
296
if (req.params.redirect in externalPages.github)
297
reply.redirect(externalPages.github[req.params.redirect]);
298
else reply.code(404).type(supportedTypes.default).send(preloaded404);
299
});
300
301
if (serverUrl.pathname === '/')
302
// Set an error page for invalid paths outside the query string system.
303
// If the server URL has a prefix, then avoid doing this for stealth reasons.
304
app.setNotFoundHandler((req, reply) => {
305
reply.code(404).type(supportedTypes.default).send(preloaded404);
306
});
307
else {
308
// Apply the following patch(es) if the server URL has a prefix.
309
310
// Patch to fix serving index.html.
311
app.get(serverUrl.pathname, (req, reply) => {
312
reply
313
.type(supportedTypes.default)
314
.send(tryReadFile('../views/dist/' + pages.index, import.meta.url));
315
});
316
}
317
318
app.listen({ port: serverUrl.port, host: serverUrl.hostname });
319
console.log(`Holy Unblocker is listening on port ${serverUrl.port}.`);
320
console.log(`When hosting with a reverse proxy please ensure you are using NGINX only.\nCaddy and Apache are not supported and have security risks due to wisp-js and loopbacks.\nPorts are whitelisted and security is maintained with NGINX only.`);
321
if (config.disguiseFiles)
322
console.log(
323
'disguiseFiles is enabled. Visit src/routes.mjs to see the entry point, listed within the pages variable.'
324
);
325
326