OpenTerminal/server/main.js

296 lines
7.7 KiB
JavaScript
Raw Normal View History

2023-09-30 06:58:03 +00:00
const fs = require('fs');
const http = require('http');
2023-09-30 06:58:03 +00:00
const path = require('path');
const Websocket = require('ws');
const VERSION = "1.1.0";
2023-09-30 06:58:03 +00:00
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,
version: 7,
};
const MOTDS = [
2023-09-30 06:58:03 +00:00
"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!",
2023-09-30 09:55:58 +00:00
"there is science to do...",
"now fully open-source!",
"somehow not the worst communication app!",
"\"oh this is like nano but multiplayer\"",
2023-10-02 13:06:50 +00:00
"there's no place like 127.0.0.1",
"it's no emacs, but it'll do",
2023-09-30 06:58:03 +00:00
];
const STATIC_PATH = path.join(process.cwd(), "public");
const CACHE_MAX_AGE = 14400 // 4 hours
2023-09-30 06:58:03 +00:00
const BANNER =
2023-09-30 09:55:58 +00:00
`Welcome to OpenTerminal!
2023-09-30 06:58:03 +00:00
`;
const FAKE_CRASH =
`
=========================================
This copy of OpenTerminal is not genuine.
Please acquire a genuine copy.
This connection will now terminate.
=========================================
`;
const VERSION_ERROR =
`Your client does not match this server's version of OpenTerminal (${VERSION})!
If you are connecting to this site's own OpenTerminal server, please refresh the page or clear your browser's cache to update.
`
console.log("using env vars: " + Object.keys(process.env).filter(value => { return value.startsWith("OPENTERM_") }).join(', '));
const HOST = process.env.OPENTERM_HOST || '0.0.0.0';
const PORT = process.env.OPENTERM_PORT || 8080;
const TRUSTED_PROXIES = process.env.OPENTERM_TRUSTED_PROXIES ? process.env.OPENTERM_TRUSTED_PROXIES.split(',') : [];
const PING_INTERVAL = 10000;
2023-09-30 06:58:03 +00:00
let sockets = [];
let buffer = "";
const MAX_BUFFER_SIZE = 1024 * 1000;
const MAX_MESSAGE_LENGTH = 1024;
2023-09-30 06:58:03 +00:00
/**
* simple file fetching for the HTTP server
*/
2023-09-30 06:58:03 +00:00
async function get_file(url) {
// ignore query params...not very helpful when getting files!
url = url.split("?")[0];
2023-09-30 06:58:03 +00:00
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.
2023-09-30 06:58:03 +00:00
const path_traversal = !file_path.startsWith(STATIC_PATH);
const exists = fs.existsSync(file_path) && fs.statSync(file_path).isFile();
2023-09-30 06:58:03 +00:00
if (path_traversal || !exists) return false;
const ext = path.extname(file_path).substring(1).toLowerCase();
try {
const data = await fs.promises.readFile(file_path, { encoding: 'utf8' });
return { data, ext };
} catch (error) {
console.error(error);
return false;
}
2023-09-30 06:58:03 +00:00
}
const server = http.createServer(async (req, res) => {
const request_time = new Date().getTime();
2023-09-30 06:58:03 +00:00
const file = await get_file(req.url);
if (file === false) {
2023-09-30 06:58:03 +00:00
res.writeHead(404);
res.end("404 not found!");
return log_request(req, res, request_time);
2023-09-30 06:58:03 +00:00
}
const mime_type = MIME_TYPES[file.ext] || MIME_TYPES.default;
2023-10-02 13:06:50 +00:00
res.writeHead(200, {
"Content-Type": mime_type,
"Cache-Control": `max-age=${CACHE_MAX_AGE}`,
"Server": "OpenTerminal",
});
res.write(file.data);
res.end();
return log_request(req, res, request_time);
2023-09-30 06:58:03 +00:00
});
function log_request(req, res, time) {
const elapsed = new Date().getTime() - time;
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${res.statusCode} - ${get_real_address(req)} in ${elapsed}ms`);
}
function get_real_address(req) {
2024-01-19 04:22:18 +00:00
if (TRUSTED_PROXIES.indexOf(req.connection.remoteAddress) !== -1 && req.headers['x-forwarded-for']) {
return req.headers['x-forwarded-for'];
}
2024-01-19 04:22:18 +00:00
return req.connection.remoteAddress;
}
2023-09-30 06:58:03 +00:00
const wss = new Websocket.Server({ server });
wss.on('connection', (socket, req) => {
console.log(`${new Date().toISOString()} - WS OPEN - ${get_real_address(req)} (active connections: ${sockets.length + 1})`);
/*
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,
}));
}
2023-09-30 06:58:03 +00:00
const ping_routine = setInterval(
function() {
socket.send(JSON.stringify({
type: DATA_TYPES.ping,
}))
}, PING_INTERVAL);
socket.ping_routine = ping_routine;
2023-09-30 06:58:03 +00:00
sockets.push(socket);
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);
}
});
2023-09-30 06:58:03 +00:00
socket.on('close', () => {
clearInterval(socket.ping_routine);
2023-09-30 06:58:03 +00:00
sockets = sockets.filter(s => s !== socket);
2023-09-30 09:55:58 +00:00
// console.log(`connection closed.\n\tcurrent connections: ${sockets.length}`);
2023-09-30 06:58:03 +00:00
});
});
/**
* handles parsed JSON data sent by the client.
*/
function handle_message(data, user) {
if (user.version === undefined) {
if (data.type !== DATA_TYPES.version) {
user.send(JSON.stringify({
type: DATA_TYPES.text,
text: VERSION_ERROR
}));
user.close();
return;
} else if (data.text !== VERSION) {
user.send(JSON.stringify({
type: DATA_TYPES.text,
text: VERSION_ERROR
}));
user.close();
return;
} else {
user.version = data.text;
return;
}
}
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));
2023-09-30 09:55:58 +00:00
}
2023-09-30 06:58:03 +00:00
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!`)
2023-09-30 06:58:03 +00:00
}
}
/**
* 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;
2023-09-30 06:58:03 +00:00
}
server.listen(PORT, HOST, () => {
console.log(`OpenTerminal is now LIVE on http://${HOST === '0.0.0.0' ? '127.0.0.1' : HOST}:${PORT}`);
if (TRUSTED_PROXIES.length > 0) console.log(`Using X-Forwarded-For headers for hosts: ${TRUSTED_PROXIES.join(", ")}`);
2023-09-30 07:25:29 +00:00
});
2023-09-30 06:58:03 +00:00
/**
* 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));
2023-09-30 06:58:03 +00:00
}