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/html/') 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