Minetrack/lib/server.js
2023-12-30 23:03:54 +00:00

140 lines
3.5 KiB
JavaScript

const http = require("http");
const format = require("util").format;
const WebSocket = require("ws");
const finalHttpHandler = require("finalhandler");
const serveStatic = require("serve-static");
const logger = require("./logger");
const HASHED_FAVICON_URL_REGEX = /hashedfavicon_([a-z0-9]{32}).png/g;
function getRemoteAddr(req) {
return (
req.headers["cf-connecting-ip"] ||
req.headers["x-forwarded-for"] ||
req.connection.remoteAddress
);
}
class Server {
static getHashedFaviconUrl(hash) {
// Format must be compatible with HASHED_FAVICON_URL_REGEX
return format("/hashedfavicon_%s.png", hash);
}
constructor(app) {
this._app = app;
this.createHttpServer();
this.createWebSocketServer();
}
createHttpServer() {
const distServeStatic = serveStatic("dist/");
const faviconsServeStatic = serveStatic("favicons/");
this._http = http.createServer((req, res) => {
logger.log("info", "%s requested: %s", getRemoteAddr(req), req.url);
// Test the URL against a regex for hashed favicon URLs
// Require only 1 match ([0]) and test its first captured group ([1])
// Any invalid value or hit miss will pass into static handlers below
const faviconHash = [...req.url.matchAll(HASHED_FAVICON_URL_REGEX)];
if (
faviconHash.length === 1 &&
this.handleFaviconRequest(res, faviconHash[0][1])
) {
return;
}
// Attempt to handle req using distServeStatic, otherwise fail over to faviconServeStatic
// If faviconServeStatic fails, pass to finalHttpHandler to terminate
distServeStatic(req, res, () => {
faviconsServeStatic(req, res, finalHttpHandler(req, res));
});
});
}
handleFaviconRequest = (res, faviconHash) => {
for (const serverRegistration of this._app.serverRegistrations) {
if (
serverRegistration.faviconHash &&
serverRegistration.faviconHash === faviconHash
) {
const buf = Buffer.from(
serverRegistration.lastFavicon.split(",")[1],
"base64"
);
res
.writeHead(200, {
"Content-Type": "image/png",
"Content-Length": buf.length,
"Cache-Control": "public, max-age=604800", // Cache hashed favicon for 7 days
})
.end(buf);
return true;
}
}
return false;
};
createWebSocketServer() {
this._wss = new WebSocket.Server({
server: this._http,
});
this._wss.on("connection", (client, req) => {
logger.log(
"info",
"%s connected, total clients: %d",
getRemoteAddr(req),
this.getConnectedClients()
);
// Bind disconnect event for logging
client.on("close", () => {
logger.log(
"info",
"%s disconnected, total clients: %d",
getRemoteAddr(req),
this.getConnectedClients()
);
});
// Pass client off to proxy handler
this._app.handleClientConnection(client);
});
}
listen(host, port) {
this._http.listen(port, host);
logger.log("info", "Started on %s:%d", host, port);
}
broadcast(payload) {
this._wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(payload);
}
});
}
getConnectedClients() {
let count = 0;
this._wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
count++;
}
});
return count;
}
}
module.exports = Server;