diff --git a/public/main.js b/public/main.js index 23beafe..e2c5589 100644 --- a/public/main.js +++ b/public/main.js @@ -2,7 +2,7 @@ let configEditor; $(async () => { const gswFeatures = await fetchApi('GET', 'features'); if (gswFeatures) { - $('#versions').html('gsw v' + gswFeatures.features.version + '
gamedig v' + gswFeatures.features.gamedig); + $('#versions').html('gsw v' + gswFeatures.versions.gsw + '
gamedig v' + gswFeatures.versions.gamedig); await setRandomBg().catch(() => { }); setInterval(setRandomBg, 60000); @@ -46,7 +46,7 @@ $(async () => { "ajax": true, "ajaxCredentials": false, "ajax_cache_responses": !Boolean(gswFeatures.debug), - "ajax_cache_buster": 'gsw-v' + gswFeatures.features.version + '+gamedig-v' + gswFeatures.features.gamedig, + "ajax_cache_buster": 'gsw-v' + gswFeatures.versions.gsw + '+gamedig-v' + gswFeatures.versions.gamedig, "disable_edit_json": false, "disable_collapse": true, "disable_properties": false, diff --git a/src/server.ts b/src/server.ts index acd733f..8cc6dfc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -28,9 +28,11 @@ interface ApiResponse extends FeaturesResponse, ConfigResponse { } interface FeaturesResponse{ - features?: { - version: string; + versions?: { + gsw: string; gamedig: string; + } + services?: { steam: boolean; discord: boolean; telegram: boolean; @@ -66,110 +68,127 @@ watcher.start(); createServer(async (req, res) => { if (DBG) console.log('DBG: %j %j', (new Date()), req.url); - const reqUrl = new URL(req.url || '', 'http://localhost'); - const p = reqUrl.pathname === '/' ? 'index.html' : reqUrl.pathname.slice(1); - const ext = path.extname(p).slice(1); + try { + const reqUrl = new URL(req.url || '', 'http://localhost'); + const p = req.url === '/' ? 'index.html' : reqUrl.pathname.slice(1); + const ext = path.extname(p).slice(1); + + if (ext in EXT_MIME && !p.includes('/') && !p.includes('\\')) { + if (SECRET !== '') { + const filePath = path.resolve('./public/', p); + if (fs.existsSync(filePath)) { + res.writeHead(200, { + 'Content-Type': EXT_MIME[ext] || 'plain/text' + }); + fs.createReadStream(filePath).on('error', (err: any) => { + console.error(err?.message || err); + res.end(); + }).pipe(res); + } else { + res.writeHead(404, { 'Content-Type': 'text/html' }); + res.end('404 💢'); + } + } else { + res.end('Configure the `SECRET` env var to enable the web UI!'); + } + } else if (p === 'ping') { + if (DBG) console.log('ping'); + res.end('pong'); + } else if (p === 'gamedig-games') { + const gdProtocols = Object.keys(protocols).map(p => `protocol-${p}`); + const gdGameTypes = []; + const gdGamesNames = []; + + for (const [type, g] of Object.entries(games)) { + gdGameTypes.push(type); + gdGamesNames.push(g.name + ' (' + g.release_year + ')'); + } - if (ext in EXT_MIME && !p.includes('/') && !p.includes('\\')) { - if (SECRET !== '') { res.writeHead(200, { - 'Content-Type': EXT_MIME[ext] || 'plain/text' + 'Content-Type': 'application/json', + 'Cache-Control': 'max-age=0' }); - fs.createReadStream(path.resolve('./public/', p)).pipe(res); - } else { - res.end('Configure the `SECRET` env var to enable the web UI!'); - } - } else if (p === 'ping') { - if (DBG) console.log('ping'); - res.end('pong'); - } else if (p === 'gamedig-games') { - const gdProtocols = Object.keys(protocols).map(p => `protocol-${p}`); - const gdGameTypes = []; - const gdGamesNames = []; - - for (const [type, g] of Object.entries(games)) { - gdGameTypes.push(type); - gdGamesNames.push(g.name + ' (' + g.release_year + ')'); - } - res.writeHead(200, { - 'Content-Type': 'application/json', - 'Cache-Control': 'max-age=0' - }); - - res.end(JSON.stringify({ - enum: [...gdGameTypes, ...gdProtocols], - options: { - enum_titles: [...gdGamesNames, ...gdProtocols] - } - } as SelectOptionsResponse, null, DBG ? 2 : 0)); - } else if (SECRET !== '' && req.headers['x-btoken']) { - let status = 200; - let re: ApiResponse = {}; - - if (DBG) re.debug = true; - - if (validateBearerToken(String(req.headers['x-btoken']))) { - const reqPath = p.split('/'); - try { - if (reqPath[0] === 'features') { - re.features = { - version: String(gswVersion), - gamedig: String(gamedigVersion), - steam: Boolean(process.env.STEAM_WEB_API_KEY), - discord: Boolean(process.env.DISCORD_BOT_TOKEN), - telegram: Boolean(process.env.TELEGRAM_BOT_TOKEN), - slack: Boolean(process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) - }; - } else if (reqPath[0] === 'config') { - if (req.method === 'GET') { - re.config = await readConfig(); - } else if (req.method === 'POST') { - const body = await new Promise(resolve => { - let body = ''; - req.on('data', (chunk: string) => { - body += chunk; - }); - req.on('end', () => { - resolve(body); + res.end(JSON.stringify({ + enum: [...gdGameTypes, ...gdProtocols], + options: { + enum_titles: [...gdGamesNames, ...gdProtocols] + } + } as SelectOptionsResponse, null, DBG ? 2 : 0)); + } else if (SECRET !== '' && req.headers['x-btoken']) { + let status = 200; + let re: ApiResponse = {}; + + if (validateBearerToken(String(req.headers['x-btoken']))) { + const reqPath = p.split('/'); + try { + if (reqPath[0] === 'features') { + if (DBG) re.debug = true; + re.versions = { + gsw: String(gswVersion), + gamedig: String(gamedigVersion) + }; + re.services = { + steam: Boolean(process.env.STEAM_WEB_API_KEY), + discord: Boolean(process.env.DISCORD_BOT_TOKEN), + telegram: Boolean(process.env.TELEGRAM_BOT_TOKEN), + slack: Boolean(process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) + }; + } else if (reqPath[0] === 'config') { + if (req.method === 'GET') { + re.config = await readConfig(); + } else if (req.method === 'POST') { + const body = await new Promise(resolve => { + let body = ''; + req.on('data', (chunk: string) => { + body += chunk; + }); + req.on('end', () => { + resolve(body); + }); }); - }); - - // TODO: validate (ajv) - await updateConfig(JSON.parse(String(body)) || [] as GameServerConfig[]); - await watcher.restart(); - re.message = 'Configuration updated. Watcher restarted.'; + // TODO: validate (ajv) + await updateConfig(JSON.parse(String(body)) || [] as GameServerConfig[]); + await watcher.restart(); + + re.message = 'Configuration updated. Watcher restarted.'; + } else { + status = 400; + re.error = 'Invalid Request'; + } + } else if (reqPath[0] === 'flush' && ['servers', 'discord', 'telegram', 'slack'].includes(reqPath[1])) { + //TODO: check for and append host:port if available + await watcher.restart(reqPath[1]); + re.message = '🗑️ ' + reqPath[1].slice(0, 1).toUpperCase() + reqPath[1].slice(1) + ' data flushed.'; } else { status = 400; re.error = 'Invalid Request'; } - } else if (reqPath[0] === 'flush' && ['servers', 'discord', 'telegram', 'slack'].includes(reqPath[1])) { - //TODO: check for and append host:port if available - await watcher.restart(reqPath[1]); - re.message = '🗑️ ' + reqPath[1].slice(0, 1).toUpperCase() + reqPath[1].slice(1) + ' data flushed.'; - } else { - status = 400; - re.error = 'Invalid Request'; + } catch (err: any) { + status = 500; + re.error = err.message || String(err); } - } catch (err: any) { - status = 500; - re.error = err.message || String(err); + } else { + status = 401; + re.error = 'Unauthorized'; } - } else { - status = 401; - re.error = 'Unauthorized'; - } - res.writeHead(status, { - 'Content-Type': 'application/json', - 'Cache-Control': 'max-age=0' - }); + res.writeHead(status, { + 'Content-Type': 'application/json', + 'Cache-Control': 'max-age=0' + }); - res.end(JSON.stringify(re, null, DBG ? 2 : 0)); - } else { - res.writeHead(404, { 'Content-Type': 'text/html' }); - res.end('404 💢'); + res.end(JSON.stringify(re, null, DBG ? 2 : 0)); + } else { + res.writeHead(400, { 'Content-Type': 'text/html' }); + res.end('400 💢'); + } + } catch (err: any) { + if (DBG) console.error(err?.message || err); + const code = err.message === 'Invalid URL' ? 400 : 500; + res.writeHead(code, { 'Content-Type': 'text/html' }); + res.end(`${code} 💢`); } }).listen(PORT, HOST, () => { console.log('GSW Control Panel service started %s:%s', HOST, PORT);