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