import Fastify from 'fastify';
import { createServer } from 'node:http';
import wisp from 'wisp-server-node';
import createRammerhead from '../lib/rammerhead/src/server/index.js';
import fastifyHelmet from '@fastify/helmet';
import fastifyStatic from '@fastify/static';
import {
config,
serverUrl,
pages,
externalPages,
getAltPrefix,
} from './routes.mjs';
import { tryReadFile, preloaded404 } from './templates.mjs';
import { fileURLToPath } from 'node:url';
import { existsSync, unlinkSync } from 'node:fs';
console.log(serverUrl);
const shutdown = fileURLToPath(new URL('./.shutdown', import.meta.url));
const rh = createRammerhead();
const rammerheadScopes = [
'/rammerhead.js',
'/hammerhead.js',
'/transport-worker.js',
'/task.js',
'/iframe-task.js',
'/worker-hammerhead.js',
'/messaging',
'/sessionexists',
'/deletesession',
'/newsession',
'/editsession',
'/needpassword',
'/syncLocalStorage',
'/api/shuffleDict',
'/mainport',
].map((pathname) => pathname.replace('/', serverUrl.pathname));
const rammerheadSession = new RegExp(
`^${serverUrl.pathname.replaceAll('.', '\\.')}[a-z0-9]{32}`
),
shouldRouteRh = (req) => {
try {
const url = new URL(req.url, serverUrl);
return (
rammerheadScopes.includes(url.pathname) ||
rammerheadSession.test(url.pathname)
);
} catch (e) {
return false;
}
},
routeRhRequest = (req, res) => {
req.url = req.url.slice(serverUrl.pathname.length - 1);
rh.emit('request', req, res);
},
routeRhUpgrade = (req, socket, head) => {
req.url = req.url.slice(serverUrl.pathname.length - 1);
rh.emit('upgrade', req, socket, head);
};
const serverFactory = (handler) => {
return createServer()
.on('request', (req, res) => {
if (shouldRouteRh(req)) routeRhRequest(req, res);
else handler(req, res);
})
.on('upgrade', (req, socket, head) => {
if (shouldRouteRh(req)) routeRhUpgrade(req, socket, head);
else if (req.url.endsWith(getAltPrefix('wisp', serverUrl.pathname)))
wisp.routeRequest(req, socket, head);
});
};
const app = Fastify({
routerOptions: {
ignoreDuplicateSlashes: true,
ignoreTrailingSlash: true,
},
logger: false,
serverFactory: serverFactory,
});
app.register(fastifyHelmet, {
contentSecurityPolicy: false,
xPoweredBy: false,
});
app.register(fastifyStatic, {
root: fileURLToPath(new URL('../views/dist/pages', import.meta.url)),
prefix: serverUrl.pathname,
decorateReply: false,
});
[
'assets',
'archive',
'uv',
'scram',
'epoxy',
'libcurl',
'baremux',
'chii',
].forEach((prefix) => {
app.register(fastifyStatic, {
root: fileURLToPath(new URL('../views/dist/' + prefix, import.meta.url)),
prefix: getAltPrefix(prefix, serverUrl.pathname),
decorateReply: false,
});
});
app.register(fastifyStatic, {
root: fileURLToPath(
new URL('../views/dist/archive/gfiles/rarch', import.meta.url)
),
prefix: getAltPrefix('serving', serverUrl.pathname),
decorateReply: false,
});
['cores', 'info', 'roms'].forEach((prefix) => {
app.register(fastifyStatic, {
root: fileURLToPath(
new URL('../views/dist/archive/gfiles/rarch/' + prefix, import.meta.url)
),
prefix: getAltPrefix(prefix, serverUrl.pathname),
decorateReply: false,
});
});
app.register(fastifyStatic, {
root: fileURLToPath(
new URL('../views/dist/archive/gfiles/rarch/cores', import.meta.url)
),
prefix: getAltPrefix('uauth', serverUrl.pathname),
decorateReply: false,
});
const supportedTypes = {
default: config.disguiseFiles ? 'image/vnd.microsoft.icon' : 'text/html',
html: 'text/html',
txt: 'text/plain',
xml: 'application/xml',
ico: 'image/vnd.microsoft.icon',
},
disguise = 'ico';
if (config.disguiseFiles) {
const getActualPath = (path) =>
path.slice(0, path.length - 1 - disguise.length),
shouldNotHandle = new RegExp(`\\.(?!html$|${disguise}$)[\\w-]+$`, 'i'),
loaderFile = tryReadFile(
'../views/dist/pages/misc/deobf/loader.html',
import.meta.url,
false
);
let exemptDirs = [
'assets',
'uv',
'scram',
'epoxy',
'libcurl',
'baremux',
'wisp',
'chii',
].map((dir) => getAltPrefix(dir, serverUrl.pathname).slice(1, -1)),
exemptPages = ['login', 'test-shutdown', 'favicon.ico'];
for (const [key, value] of Object.entries(externalPages))
if ('string' === typeof value) exemptPages.push(key);
else exemptDirs.push(key);
for (const path of rammerheadScopes)
if (!shouldNotHandle.test(path)) exemptDirs.push(path.slice(1));
exemptPages = exemptPages.concat(exemptDirs);
if (pages.default === 'login') exemptPages.push('');
app.addHook('preHandler', (req, reply, done) => {
if (req.params.modified) return done();
const reqPath = new URL(req.url, serverUrl).pathname.slice(
serverUrl.pathname.length
);
if (
shouldNotHandle.test(reqPath) ||
exemptDirs.some((dir) => reqPath.indexOf(dir + '/') === 0) ||
exemptPages.includes(reqPath) ||
rammerheadSession.test(serverUrl.pathname + reqPath)
)
return done();
if (!reqPath.endsWith('.' + disguise)) {
reply.type(supportedTypes.html).send(loaderFile);
reply.hijack();
return done();
} else if (!(reqPath in pages) && !reqPath.endsWith('favicon.ico')) {
req.params.modified = true;
req.raw.url = getActualPath(req.raw.url);
if (req.params.path) req.params.path = getActualPath(req.params.path);
if (req.params['*']) req.params['*'] = getActualPath(req.params['*']);
reply.type(supportedTypes[disguise]);
}
return done();
});
}
app.get(serverUrl.pathname + ':path', (req, reply) => {
const reqPath = req.params.path;
if (reqPath === 'favicon.ico') {
reply.send();
return reply.hijack();
}
if (reqPath in externalPages) {
if (req.params.modified)
return reply.code(404).type(supportedTypes.html).send(preloaded404);
let externalRoute = externalPages[reqPath];
if (typeof externalRoute !== 'string')
externalRoute = externalRoute.default;
return reply.redirect(externalRoute);
}
if (reqPath === 'test-shutdown' && existsSync(shutdown)) {
console.log('Holy Unblocker is shutting down.');
app.close();
unlinkSync(shutdown);
process.exitCode = 0;
}
if (reqPath && !(reqPath in pages))
return reply.code(404).type(supportedTypes.default).send(preloaded404);
const fileName = reqPath ? pages[reqPath] : pages[pages.default],
type =
supportedTypes[fileName.slice(fileName.lastIndexOf('.') + 1)] ||
supportedTypes.default;
if (req.params.modified) reply.type(supportedTypes[disguise]);
else reply.type(type);
reply.send(tryReadFile('../views/dist/' + fileName, import.meta.url));
});
app.get(serverUrl.pathname + 'github/:redirect', (req, reply) => {
if (req.params.redirect in externalPages.github)
reply.redirect(externalPages.github[req.params.redirect]);
else reply.code(404).type(supportedTypes.default).send(preloaded404);
});
if (serverUrl.pathname === '/')
app.setNotFoundHandler((req, reply) => {
reply.code(404).type(supportedTypes.default).send(preloaded404);
});
else {
app.get(serverUrl.pathname, (req, reply) => {
reply
.type(supportedTypes.default)
.send(tryReadFile('../views/dist/' + pages.index, import.meta.url));
});
}
app.listen({ port: serverUrl.port, host: serverUrl.hostname });
console.log(`Holy Unblocker is listening on port ${serverUrl.port}.`);
if (config.disguiseFiles)
console.log(
'disguiseFiles is enabled. Visit src/routes.mjs to see the entry point, listed within the pages variable.'
);