const fs = require('fs'); const https = require('https'); const path = require('path'); const Websocket = require('ws'); const config = { cert: fs.readFileSync(process.env.SSL_CERT || './certs/cert.crt'), key: fs.readFileSync(process.env.SSL_KEY || './certs/cert.key'), } const MIME_TYPES = { default: "application/octet-stream", html: "text/html; charset=UTF-8", js: "application/javascript", css: "text/css", png: "image/png", jpg: "image/jpg", gif: "image/gif", ico: "image/x-icon", svg: "image/svg+xml", }; const DATA_TYPES = { ping: 0, text: 1, colour: 2, buffer: 3, backspace: 4, backword: 5, arrow: 6, }; const MOTDS = [ "hello, world!", "all your TTY are belong to us.", "TIP: got a linux system low on storage? try running `sudo rm -rf /`!", "none of this is real! don't believe what they tell you.", "it's awfully cosy in here!", "how's the weather?", "with each web request, my server room grows hotter.", "mobile support coming later probably!", "there is science to do...", "now fully open-source!", "somehow not the worst communication app!", "\"oh this is like nano but multiplayer\"", "there's no place like 127.0.0.1", "it's no emacs, but it'll do", ]; const STATIC_PATH = path.join(process.cwd(), "public"); const CACHE_MAX_AGE = 86400 // 1 day const BANNER = `Welcome to OpenTerminal! `; const FAKE_CRASH = ` ========================================= This copy of OpenTerminal is not genuine. Please acquire a genuine copy. This connection will now terminate. ========================================= `; const PORT = process.env.PORT || 8443; const PING_INTERVAL = 10000; let sockets = []; let buffer = ""; const MAX_BUFFER_SIZE = 1024 * 1000; const MAX_MESSAGE_LENGTH = 1024; /** * simple file fetching for the HTTPS server */ async function get_file(url) { // ignore query params...not very helpful when getting files! url = url.split("?")[0]; const paths = [STATIC_PATH, url]; if (url.endsWith("/")) paths.push("index.html"); const file_path = path.join(...paths); // check for path traversal. path traversal is...bad. const path_traversal = !file_path.startsWith(STATIC_PATH); const exists = await fs.promises.access(file_path).then(...[() => true, () => false]); if (path_traversal || !exists) return false; const ext = path.extname(file_path).substring(1).toLowerCase(); const stream = fs.createReadStream(file_path); return { stream, ext }; } const server = https.createServer(config, async (req, res) => { const file = await get_file(req.url); if (!file) { res.writeHead(404); res.end(); return; } const mime_type = MIME_TYPES[file.ext] || MIME_TYPES.default; res.writeHead(200, { "Content-Type": mime_type, "Cache-Control": `max-age=${CACHE_MAX_AGE}`, "Server": "OpenTerminal", }); file.stream.pipe(res); // console.log(`${req.method} - ${req.url}`); }); const wss = new Websocket.Server({ server }); wss.on('connection', socket => { /* socket.colour = generate_colour(); socket.send(JSON.stringify({ type: DATA_TYPES.colour, colour: socket.colour, })); */ socket.send(JSON.stringify({ type: DATA_TYPES.text, text: `${BANNER}/* ${MOTDS[Math.floor(Math.random() * MOTDS.length)]} */\n\n`, colour: false, sticky: true, })); if (buffer.length > 0) { socket.send(JSON.stringify({ type: DATA_TYPES.buffer, data: buffer, })); } const ping_interval = setInterval( function() { socket.send(JSON.stringify({ type: DATA_TYPES.ping, })) }, PING_INTERVAL); socket.ping_interval = ping_interval; sockets.push(socket); // console.log(`new connection.\n\tcurrent connections: ${sockets.length}`); socket.on('message', event => { try { handle_message(JSON.parse(event), socket) } catch (error) { socket.send(JSON.stringify({ type: DATA_TYPES.text, text: FAKE_CRASH, })); console.error(error); } }); socket.on('close', () => { clearInterval(socket.ping_interval); sockets = sockets.filter(s => s !== socket); // console.log(`connection closed.\n\tcurrent connections: ${sockets.length}`); }); }); /** * handles parsed JSON data sent by the client. */ function handle_message(data, user) { switch (data.type) { case DATA_TYPES.backword: var break_point = buffer.lastIndexOf(" "); const last_newline = buffer.lastIndexOf("\n"); if (last_newline > break_point) break_point = last_newline; buffer = buffer.substring(0, break_point); for (var i = 0; i < buffer.length - break_point; i++) { broadcast(JSON.stringify({ type: DATA_TYPES.backspace, })); } case DATA_TYPES.backspace: buffer = buffer.substring(0, buffer.length - 1); broadcast(JSON.stringify({ type: DATA_TYPES.backspace, })); return; case DATA_TYPES.text: if (buffer.length >= MAX_BUFFER_SIZE) { return; } if (data.text.length > MAX_MESSAGE_LENGTH) { user.send(JSON.stringify({ type: DATA_TYPES.text, text: "bleeeehhhh :P\n(message too long!)\n", })) user.close(); return; } block = { type: DATA_TYPES.text, text: data.text, colour: user.colour, }; buffer += data.text; broadcast(JSON.stringify(block)); } if (buffer.length > MAX_BUFFER_SIZE) { broadcast_as_server(`\n\nSERVER: This channel's maximum buffer length has been hit (${MAX_BUFFER_SIZE}).\n` + `You will need to make more room, or the server will have to be restarted.\n` + `Apologies for the inconvenience!`) } } /** * generates a random hexadecimal colour value (ex. #ff00ff) */ function generate_colour() { let result = '#'; let hexref = '0123456789abcdef'; for (let i = 0; i < 6; i++) { result += hexref.charAt(Math.floor(Math.random() * hexref.length * .75) + 4); } return result; } server.listen(PORT, () => { console.log(`OpenTerminal is now LIVE on https://127.0.0.1:${PORT}!`); }); /** * sends a server-wide message to all connected clients. */ function broadcast_as_server(message) { broadcast(JSON.stringify({ type: DATA_TYPES.text, text: message, colour: "#ffffff", })); } /** * sends raw data to all connected clients. */ function broadcast(data) { sockets.forEach(s => s.send(data)); }