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;